diff --git a/examples/RoundQuietzoneOptions.js b/examples/RoundQuietzoneOptions.js new file mode 100644 index 0000000..3cea630 --- /dev/null +++ b/examples/RoundQuietzoneOptions.js @@ -0,0 +1,73 @@ +/** + * @created 10.06.2024 + * @author smiley + * @copyright 2024 smiley + * @license MIT + */ + +import {QROptions} from '../src/index.js'; + +/** + * The options class for RoundQuietzoneSVGoutput + */ +export default class RoundQuietzoneOptions extends QROptions{ + + /** + * we need to add the constructor with a parent call here, + * otherwise the additional properties will not be recognized + * + * @inheritDoc + */ + constructor($options = null){ + super(); +// this.__workaround__.push('myMagicProp'); + this._fromIterable($options) + } + + /** + * The amount of additional modules to be used in the circle diameter calculation + * + * Note that the middle of the circle stroke goes through the (assumed) outer corners + * or centers of the QR Code (excluding quiet zone) + * + * Example: + * + * - a value of -1 would go through the center of the outer corner modules of the finder patterns + * - a value of 0 would go through the corner of the outer modules of the finder patterns + * - a value of 3 would go through the center of the module outside next to the finder patterns, in a 45-degree angle + * + * @type {number|int} + */ + additionalModules = 0; + + /** + * the logo as SVG string (e.g. from simple-icons) + * + * @type {string} + */ + svgLogo = ''; + + /** + * an optional css class for the logo container + * + * @type {string} + */ + svgLogoCssClass = ''; + + /** + * logo scale in % of QR Code size, internally clamped to 5%-25% + * + * @type {number|float} + */ + svgLogoScale = 0.20; + + /** + * the IDs for the several colored layers, translates to css class "qr-123" which can be used in the stylesheet + * + * note that the layer id has to be an integer value, ideally outside the several bitmask values + * + * @type {int[]} + * @see QRMarkupSVG.getCssClass() + */ + dotColors = []; +} diff --git a/examples/RoundQuietzoneSVGoutput.js b/examples/RoundQuietzoneSVGoutput.js new file mode 100644 index 0000000..28e49dc --- /dev/null +++ b/examples/RoundQuietzoneSVGoutput.js @@ -0,0 +1,266 @@ +/** + * @created 10.06.2024 + * @author smiley + * @copyright 2024 smiley + * @license MIT + */ + +import {IS_DARK, M_DATA, M_LOGO, M_QUIETZONE, M_QUIETZONE_DARK, QRMarkupSVG} from '../src/index.js'; + +/** + * A custom SVG output class + * + * @see https://github.com/chillerlan/php-qrcode/discussions/137 + */ +export default class RoundQuietzoneSVGoutput extends QRMarkupSVG{ + + radius; + center; + logoScale; + + /** + * @inheritDoc + */ + createMarkup($saveToFile){ + + // some Pythagorean magick + let $diameter = Math.sqrt(2 * Math.pow((this.moduleCount + this.options.additionalModules), 2)); + this.radius = ($diameter / 2).toFixed(3); + + // clamp the logo scale + this.logoScale = Math.max(0.05, Math.min(0.25, this.options.svgLogoScale)); + + // calculate the quiet zone size, add 1 to it as the outer circle stroke may go outside of it + let $quietzoneSize = (Math.ceil(($diameter - this.moduleCount) / 2) + 1); + + // add the quiet zone to fill the circle + this.matrix.setQuietZone($quietzoneSize); + + // update the matrix dimensions to avoid errors in subsequent calculations + // the moduleCount is now QR Code matrix + 2x quiet zone + this.setMatrixDimensions(); + this.center = (this.moduleCount / 2); + + // clear the logo space + this.clearLogoSpace(); + + // color the quiet zone + this.colorQuietzone($quietzoneSize, this.radius); + + // start SVG output + let $svg = this.header(); + let $eol = this.options.eol; + + if(this.options.svgDefs !== ''){ + $svg += `${this.options.svgDefs}${$eol}${$eol}`; + } + + $svg += this.paths(); + $svg += this.addCircle(this.radius); + $svg += this.getLogo(); + + // close svg + $svg += `${$eol}${$eol}`; + + return $svg; + } + + /** + * Clears a circular area for the logo + */ + clearLogoSpace(){ + let $logoSpaceSize = (Math.ceil(this.moduleCount * this.logoScale) + 1); + // set a rectangular space instead +// this.matrix.setLogoSpace($logoSpaceSize); + + let $r = ($logoSpaceSize / 2) + this.options.circleRadius; + + for(let $y = 0; $y < this.moduleCount; $y++){ + for(let $x = 0; $x < this.moduleCount; $x++){ + + if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){ + this.matrix.set($x, $y, false, M_LOGO); + + } + + } + } + + } + + /** + * Sets random modules of the quiet zone to dark + * + * @param {number|int} $quietzoneSize + * @param {number|float} $radius + */ + colorQuietzone($quietzoneSize, $radius){ + let $l1 = ($quietzoneSize - 1); + let $l2 = (this.moduleCount - $quietzoneSize); + // substract 1/2 stroke width and module radius from the circle radius to not cut off modules + let $r = ($radius - this.options.circleRadius * 2); + + for(let $y = 0; $y < this.moduleCount; $y++){ + for(let $x = 0; $x < this.moduleCount; $x++){ + + // skip anything that's not quiet zone + if(!this.matrix.checkType($x, $y, M_QUIETZONE)){ + continue; + } + + // leave one row of quiet zone around the matrix + if( + ($x === $l1 && $y >= $l1 && $y <= $l2) + || ($x === $l2 && $y >= $l1 && $y <= $l2) + || ($y === $l1 && $x >= $l1 && $x <= $l2) + || ($y === $l2 && $x >= $l1 && $x <= $l2) + ){ + continue; + } + + // we need to add 0.5 units to the check values since we're calculating the element centers + // ($x/$y is the element's assumed top left corner) + if(this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, $r)){ + let randomBoolean = (Math.random() < 0.5); + + this.matrix.set($x, $y, randomBoolean, M_QUIETZONE); + } + + } + } + } + + /** + * @see https://stackoverflow.com/a/7227057 + * + * @param {number|float} $x + * @param {number|float} $y + * @param {number|float} $centerX + * @param {number|float} $centerY + * @param {number|float} $radius + */ + checkIfInsideCircle($x, $y, $centerX, $centerY, $radius){ + let $dx = Math.abs($x - $centerX); + let $dy = Math.abs($y - $centerY); + + if(($dx + $dy) <= $radius){ + return true; + } + + if($dx > $radius || $dy > $radius){ + return false; + } + + return (Math.pow($dx, 2) + Math.pow($dy, 2)) <= Math.pow($radius, 2); + } + + /** + * add a solid circle around the matrix + * + * @param {number|float} $radius + */ + addCircle($radius){ + let pos = this.center.toFixed(3); + let stroke = (this.options.circleRadius * 2).toFixed(3); + + return `${this.options.eol}`; + } + + /** + * returns the SVG logo wrapped in a container with a transform that scales it proportionally + */ + getLogo(){ + let eol = this.options.eol; + let pos = (this.moduleCount - this.moduleCount * this.logoScale) / 2; + + return `${eol}${this.options.svgLogo}${eol}` + } + + /** + * @inheritDoc + */ + collectModules($transform){ + let $paths = {}; + let $matrix = this.matrix.getMatrix(); + let $y = 0; + + // collect the modules for each type + for(let $row of $matrix){ + let $x = 0; + + for(let $M_TYPE of $row){ + let $M_TYPE_LAYER = $M_TYPE; + + if(this.options.connectPaths && !this.matrix.checkTypeIn($x, $y, this.options.excludeFromConnect)){ + // to connect paths we'll redeclare the $M_TYPE_LAYER to data only + $M_TYPE_LAYER = M_DATA; + + if(this.matrix.check($x, $y)){ + $M_TYPE_LAYER |= IS_DARK; + } + } + + // randomly assign another $M_TYPE_LAYER for the given types + if($M_TYPE_LAYER === M_QUIETZONE_DARK){ + let key = Math.floor(Math.random() * this.options.dotColors.length); + + $M_TYPE_LAYER = this.options.dotColors[key]; + } + + // collect the modules per $M_TYPE + let $module = $transform($x, $y, $M_TYPE, $M_TYPE_LAYER); + + if($module){ + if(!$paths[$M_TYPE_LAYER]){ + $paths[$M_TYPE_LAYER] = []; + } + + $paths[$M_TYPE_LAYER].push($module); + } + $x++; + } + $y++; + } + + // beautify output + + + return $paths; + } + + /** + * @inheritDoc + */ + module($x, $y, $M_TYPE){ + + // we'll ignore anything outside the circle + if(!this.checkIfInsideCircle(($x + 0.5), ($y + 0.5), this.center, this.center, this.radius)){ + return ''; + } + + if((!this.options.drawLightModules && !this.matrix.check($x, $y))){ + return ''; + } + + if(this.options.drawCircularModules && !this.matrix.checkTypeIn($x, $y, this.options.keepAsSquare)){ + let r = parseFloat(this.options.circleRadius); + let d = (r * 2); + let ix = ($x + 0.5 - r); + let iy = ($y + 0.5); + + if(ix < 1){ + ix = ix.toPrecision(3); + } + + if(iy < 1){ + iy = iy.toPrecision(3); + } + + return `M${ix} ${iy} a${r} ${r} 0 1 0 ${d} 0 a${r} ${r} 0 1 0 -${d} 0Z`; + } + + return `M${$x} ${$y} h1 v1 h-1Z`; + } + +} + diff --git a/examples/browser-svg-round-quietzone.html b/examples/browser-svg-round-quietzone.html new file mode 100644 index 0000000..f783ee2 --- /dev/null +++ b/examples/browser-svg-round-quietzone.html @@ -0,0 +1,124 @@ + + + + + + QRCode SVG example + + + +
+ + + diff --git a/examples/node-svg-round-quietzone.mjs b/examples/node-svg-round-quietzone.mjs new file mode 100644 index 0000000..2e5d16b --- /dev/null +++ b/examples/node-svg-round-quietzone.mjs @@ -0,0 +1,92 @@ +/** + * @created 10.06.2024 + * @author smiley + * @copyright 2024 smiley + * @license MIT + */ + +import * as qrc from '../src/index.js'; +import RoundQuietzoneSVGoutput from './RoundQuietzoneSVGoutput.js'; +import RoundQuietzoneOptions from './RoundQuietzoneOptions.js'; +import * as fs from 'node:fs'; + +/* + * run the example + */ + +// use the extended options class +let options = new RoundQuietzoneOptions({ + // custom dot options (see extended options class) + additionalModules : 5, + // logo from: https://github.com/simple-icons/simple-icons + svgLogo : '', + svgLogoCssClass : 'logo', + svgLogoScale : 0.2, + dotColors : [111, 222, 333, 444, 555, 666], + // load our own output class + outputInterface : RoundQuietzoneSVGoutput, + version : 7, + eccLevel : qrc.ECC_H, + // we're not adding a quiet zone, this is done internally in our own module + addQuietzone : false, + // toggle base64 data URI + outputBase64 : false, + // DOM is not available here + returnAsDomElement : false, + svgUseFillAttributes: false, + // if set to false, the light modules won't be rendered + drawLightModules : false, + // draw the modules as circles isntead of squares + drawCircularModules : true, + circleRadius : 0.4, + // connect paths + connectPaths : true, + excludeFromConnect : [ + qrc.M_LOGO, + qrc.M_QUIETZONE, + ], + // keep modules of these types as square + keepAsSquare : [ + qrc.M_FINDER_DARK, + qrc.M_FINDER_DOT, + qrc.M_ALIGNMENT_DARK, + ], + svgDefs : + '\n\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '', +}); + + +let data = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; +let qrcode = (new qrc.QRCode(options)).render(data); + +// write the data to an svg file +fs.writeFile('./qrcode.svg', qrcode, (err) => { + if(err){ + console.error(err); + } +});