Skip to content

Commit 4a77d19

Browse files
committed
:octocat: +complex example
1 parent 19884a7 commit 4a77d19

File tree

4 files changed

+555
-0
lines changed

4 files changed

+555
-0
lines changed

examples/RoundQuietzoneOptions.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @created 10.06.2024
3+
* @author smiley <[email protected]>
4+
* @copyright 2024 smiley
5+
* @license MIT
6+
*/
7+
8+
import {QROptions} from '../src/index.js';
9+
10+
/**
11+
* The options class for RoundQuietzoneSVGoutput
12+
*/
13+
export default class RoundQuietzoneOptions extends QROptions{
14+
15+
/**
16+
* we need to add the constructor with a parent call here,
17+
* otherwise the additional properties will not be recognized
18+
*
19+
* @inheritDoc
20+
*/
21+
constructor($options = null){
22+
super();
23+
// this.__workaround__.push('myMagicProp');
24+
this._fromIterable($options)
25+
}
26+
27+
/**
28+
* The amount of additional modules to be used in the circle diameter calculation
29+
*
30+
* Note that the middle of the circle stroke goes through the (assumed) outer corners
31+
* or centers of the QR Code (excluding quiet zone)
32+
*
33+
* Example:
34+
*
35+
* - a value of -1 would go through the center of the outer corner modules of the finder patterns
36+
* - a value of 0 would go through the corner of the outer modules of the finder patterns
37+
* - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle
38+
*
39+
* @type {number|int}
40+
*/
41+
additionalModules = 0;
42+
43+
/**
44+
* the logo as SVG string (e.g. from simple-icons)
45+
*
46+
* @type {string}
47+
*/
48+
svgLogo = '';
49+
50+
/**
51+
* an optional css class for the logo <g> container
52+
*
53+
* @type {string}
54+
*/
55+
svgLogoCssClass = '';
56+
57+
/**
58+
* logo scale in % of QR Code size, internally clamped to 5%-25%
59+
*
60+
* @type {number|float}
61+
*/
62+
svgLogoScale = 0.20;
63+
64+
/**
65+
* the IDs for the several colored layers, translates to css class "qr-123" which can be used in the stylesheet
66+
*
67+
* note that the layer id has to be an integer value, ideally outside the several bitmask values
68+
*
69+
* @type {int[]}
70+
* @see QRMarkupSVG.getCssClass()
71+
*/
72+
dotColors = [];
73+
}

examples/RoundQuietzoneSVGoutput.js

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/**
2+
* @created 10.06.2024
3+
* @author smiley <[email protected]>
4+
* @copyright 2024 smiley
5+
* @license MIT
6+
*/
7+
8+
import {IS_DARK, M_DATA, M_LOGO, M_QUIETZONE, M_QUIETZONE_DARK, QRMarkupSVG} from '../src/index.js';
9+
10+
/**
11+
* A custom SVG output class
12+
*
13+
* @see https://github.com/chillerlan/php-qrcode/discussions/137
14+
*/
15+
export default class RoundQuietzoneSVGoutput extends QRMarkupSVG{
16+
17+
radius;
18+
center;
19+
logoScale;
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
createMarkup($saveToFile){
25+
26+
// some Pythagorean magick
27+
let $diameter = Math.sqrt(2 * Math.pow((this.moduleCount + this.options.additionalModules), 2));
28+
this.radius = ($diameter / 2).toFixed(3);
29+
30+
// clamp the logo scale
31+
this.logoScale = Math.max(0.05, Math.min(0.25, this.options.svgLogoScale));
32+
33+
// calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it
34+
let $quietzoneSize = (Math.ceil(($diameter - this.moduleCount) / 2) + 1);
35+
36+
// add the quiet zone to fill the circle
37+
this.matrix.setQuietZone($quietzoneSize);
38+
39+
// update the matrix dimensions to avoid errors in subsequent calculations
40+
// the moduleCount is now QR Code matrix + 2x quiet zone
41+
this.setMatrixDimensions();
42+
this.center = (this.moduleCount / 2);
43+
44+
// clear the logo space
45+
this.clearLogoSpace();
46+
47+
// color the quiet zone
48+
this.colorQuietzone($quietzoneSize, this.radius);
49+
50+
// start SVG output
51+
let $svg = this.header();
52+
let $eol = this.options.eol;
53+
54+
if(this.options.svgDefs !== ''){
55+
$svg += `<defs>${this.options.svgDefs}${$eol}</defs>${$eol}`;
56+
}
57+
58+
$svg += this.paths();
59+
$svg += this.addCircle(this.radius);
60+
$svg += this.getLogo();
61+
62+
// close svg
63+
$svg += `${$eol}</svg>${$eol}`;
64+
65+
return $svg;
66+
}
67+
68+
/**
69+
* Clears a circular area for the logo
70+
*/
71+
clearLogoSpace(){
72+
let $logoSpaceSize = (Math.ceil(this.moduleCount * this.logoScale) + 1);
73+
// set a rectangular space instead
74+
// this.matrix.setLogoSpace($logoSpaceSize);
75+
76+
let $r = ($logoSpaceSize / 2) + this.options.circleRadius;
77+
78+
for(let $y = 0; $y < this.moduleCount; $y++){
79+
for(let $x = 0; $x < this.moduleCount; $x++){
80+
81+
if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){
82+
this.matrix.set($x, $y, false, M_LOGO);
83+
84+
}
85+
86+
}
87+
}
88+
89+
}
90+
91+
/**
92+
* Sets random modules of the quiet zone to dark
93+
*
94+
* @param {number|int} $quietzoneSize
95+
* @param {number|float} $radius
96+
*/
97+
colorQuietzone($quietzoneSize, $radius){
98+
let $l1 = ($quietzoneSize - 1);
99+
let $l2 = (this.moduleCount - $quietzoneSize);
100+
// substract 1/2 stroke width and module radius from the circle radius to not cut off modules
101+
let $r = ($radius - this.options.circleRadius * 2);
102+
103+
for(let $y = 0; $y < this.moduleCount; $y++){
104+
for(let $x = 0; $x < this.moduleCount; $x++){
105+
106+
// skip anything that's not quiet zone
107+
if(!this.matrix.checkType($x, $y, M_QUIETZONE)){
108+
continue;
109+
}
110+
111+
// leave one row of quiet zone around the matrix
112+
if(
113+
($x === $l1 && $y >= $l1 && $y <= $l2)
114+
|| ($x === $l2 && $y >= $l1 && $y <= $l2)
115+
|| ($y === $l1 && $x >= $l1 && $x <= $l2)
116+
|| ($y === $l2 && $x >= $l1 && $x <= $l2)
117+
){
118+
continue;
119+
}
120+
121+
// we need to add 0.5 units to the check values since we're calculating the element centers
122+
// ($x/$y is the element's assumed top left corner)
123+
if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){
124+
let randomBoolean = (Math.random() < 0.5);
125+
126+
this.matrix.set($x, $y, randomBoolean, M_QUIETZONE);
127+
}
128+
129+
}
130+
}
131+
}
132+
133+
/**
134+
* @see https://stackoverflow.com/a/7227057
135+
*
136+
* @param {number|float} $x
137+
* @param {number|float} $y
138+
* @param {number|float} $centerX
139+
* @param {number|float} $centerY
140+
* @param {number|float} $radius
141+
*/
142+
checkIfInsideCircle($x, $y, $centerX, $centerY, $radius){
143+
let $dx = Math.abs($x - $centerX);
144+
let $dy = Math.abs($y - $centerY);
145+
146+
if(($dx + $dy) <= $radius){
147+
return true;
148+
}
149+
150+
if($dx > $radius || $dy > $radius){
151+
return false;
152+
}
153+
154+
return (Math.pow($dx, 2) + Math.pow($dy, 2)) <= Math.pow($radius, 2);
155+
}
156+
157+
/**
158+
* add a solid circle around the matrix
159+
*
160+
* @param {number|float} $radius
161+
*/
162+
addCircle($radius){
163+
let pos = this.center.toFixed(3);
164+
let stroke = (this.options.circleRadius * 2).toFixed(3);
165+
166+
return `<circle class="qr-circle" cx="${pos}" cy="${pos}" r="${$radius}" stroke-width="${stroke}"/>${this.options.eol}`;
167+
}
168+
169+
/**
170+
* returns the SVG logo wrapped in a <g> container with a transform that scales it proportionally
171+
*/
172+
getLogo(){
173+
let eol = this.options.eol;
174+
let pos = (this.moduleCount - this.moduleCount * this.logoScale) / 2;
175+
176+
return `<g transform="translate(${pos} ${pos}) scale(${this.logoScale})" class="${this.options.svgLogoCssClass}">${eol}${this.options.svgLogo}${eol}</g>`
177+
}
178+
179+
/**
180+
* @inheritDoc
181+
*/
182+
collectModules($transform){
183+
let $paths = {};
184+
let $matrix = this.matrix.getMatrix();
185+
let $y = 0;
186+
187+
// collect the modules for each type
188+
for(let $row of $matrix){
189+
let $x = 0;
190+
191+
for(let $M_TYPE of $row){
192+
let $M_TYPE_LAYER = $M_TYPE;
193+
194+
if(this.options.connectPaths && !this.matrix.checkTypeIn($x, $y, this.options.excludeFromConnect)){
195+
// to connect paths we'll redeclare the $M_TYPE_LAYER to data only
196+
$M_TYPE_LAYER = M_DATA;
197+
198+
if(this.matrix.check($x, $y)){
199+
$M_TYPE_LAYER |= IS_DARK;
200+
}
201+
}
202+
203+
// randomly assign another $M_TYPE_LAYER for the given types
204+
if($M_TYPE_LAYER === M_QUIETZONE_DARK){
205+
let key = Math.floor(Math.random() * this.options.dotColors.length);
206+
207+
$M_TYPE_LAYER = this.options.dotColors[key];
208+
}
209+
210+
// collect the modules per $M_TYPE
211+
let $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER);
212+
213+
if($module){
214+
if(!$paths[$M_TYPE_LAYER]){
215+
$paths[$M_TYPE_LAYER] = [];
216+
}
217+
218+
$paths[$M_TYPE_LAYER].push($module);
219+
}
220+
$x++;
221+
}
222+
$y++;
223+
}
224+
225+
// beautify output
226+
227+
228+
return $paths;
229+
}
230+
231+
/**
232+
* @inheritDoc
233+
*/
234+
module($x, $y, $M_TYPE){
235+
236+
// we'll ignore anything outside the circle
237+
if(!this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, this.radius)){
238+
return '';
239+
}
240+
241+
if((!this.options.drawLightModules && !this.matrix.check($x, $y))){
242+
return '';
243+
}
244+
245+
if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){
246+
let r = parseFloat(this.options.circleRadius);
247+
let d = (r * 2);
248+
let ix = ($x + 0.5 - r);
249+
let iy = ($y + 0.5);
250+
251+
if(ix < 1){
252+
ix = ix.toPrecision(3);
253+
}
254+
255+
if(iy < 1){
256+
iy = iy.toPrecision(3);
257+
}
258+
259+
return `M${ix} ${iy} a${r} ${r} 0 1 0 ${d} 0 a${r} ${r} 0 1 0 -${d} 0Z`;
260+
}
261+
262+
return `M${$x} ${$y} h1 v1 h-1Z`;
263+
}
264+
265+
}
266+

0 commit comments

Comments
 (0)