2024-08-29 15:22:54 -05:00
|
|
|
|
/** SPDX-License-Identifier: MIT
|
|
|
|
|
******************************************************************************
|
|
|
|
|
* QRSVG
|
2024-09-11 17:43:48 -05:00
|
|
|
|
* Version 1.0.1
|
2024-08-29 15:22:54 -05:00
|
|
|
|
* https://fietkau.software/qr
|
|
|
|
|
* Copyright (c) Julian Fietkau
|
|
|
|
|
*
|
|
|
|
|
* This is a small JavaScript project to render a two-dimensional bitmask
|
|
|
|
|
* (mostly assumed to be a QR code) with a fixed width and height to an SVG
|
|
|
|
|
* element as a collection of SVG paths with defined purposes. The code
|
|
|
|
|
* analyzes the bitmask geometrically and traces the contours of contiguous
|
|
|
|
|
* shapes. It allows rendering QR codes in several stylized ways. Note that
|
|
|
|
|
* this code does not contain an actual QR code creator – it expects to receive
|
|
|
|
|
* the 2D QR code as a bitmask for its input. See the project website for a
|
|
|
|
|
* demo and more information.
|
|
|
|
|
******************************************************************************
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the “Software”), to
|
|
|
|
|
* deal in the Software without restriction, including without limitation the
|
|
|
|
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
|
|
|
* sell copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in
|
|
|
|
|
* all copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
|
* DEALINGS IN THE SOFTWARE.
|
|
|
|
|
******************************************************************************
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
var qrsvg;
|
|
|
|
|
(function (qrsvg) {
|
|
|
|
|
|
|
|
|
|
// Data class holding four SVG pathspecs that make up a QR code pattern.
|
|
|
|
|
// There are separate properties for the inner and outer parts of the position
|
|
|
|
|
// detection pattern, for 1x1 blocks in the pattern and for all larger shapes.
|
|
|
|
|
// This separation is for later ease in distinct coloration.
|
|
|
|
|
// The pathspecs are held as arrays instead of strings so their component parts
|
|
|
|
|
// can be more easily iterated and manipulated. This is necessary for the
|
|
|
|
|
// application of shape styles.
|
|
|
|
|
class Contour {
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.pdpOuter = [];
|
|
|
|
|
this.pdpInner = [];
|
|
|
|
|
this.dots = [];
|
|
|
|
|
this.shapes = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Data class holding a rectangular bitmask, accessible as x/y coordinates
|
|
|
|
|
// returning boolean values.
|
|
|
|
|
class Bitmask {
|
|
|
|
|
|
|
|
|
|
constructor(width, height) {
|
|
|
|
|
if(!Number.isInteger(width) || !Number.isInteger(height)) {
|
|
|
|
|
throw Error('Bitmask: width and height must be integers: ' + width + ', ' + height);
|
|
|
|
|
}
|
|
|
|
|
this.width = width;
|
|
|
|
|
this.height = height;
|
|
|
|
|
this._array = new Array(width * height);
|
|
|
|
|
this.wipe(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get(x, y) {
|
|
|
|
|
if(!Number.isInteger(x) || !Number.isInteger(y)) {
|
|
|
|
|
throw Error('Bitmask: x and y must be integers: ' + x + ', ' + y);
|
|
|
|
|
}
|
|
|
|
|
if(x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return this._array[y * this.width + x];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set(x, y, value) {
|
|
|
|
|
if(!Number.isInteger(x) || !Number.isInteger(y)) {
|
|
|
|
|
throw Error('Bitmask: x and y must be integers: ' + x + ', ' + y);
|
|
|
|
|
}
|
|
|
|
|
if(x < 0 || x >= this.width) {
|
|
|
|
|
throw Error('Bitmask: x must be at least 0 and less than width: ' + x);
|
|
|
|
|
}
|
|
|
|
|
if(y < 0 || y >= this.height) {
|
|
|
|
|
throw Error('Bitmask: y must be at least 0 and less than height: ' + y);
|
|
|
|
|
}
|
|
|
|
|
this._array[y * this.width + x] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fully overwrite the current data with a sequence of boolean
|
|
|
|
|
// values. In the simplest case, call wipe(false) to set all
|
|
|
|
|
// coordinates to false, or provide more values. They will be
|
|
|
|
|
// repeated in sequence as often as needed.
|
|
|
|
|
wipe(...pattern) {
|
|
|
|
|
for(let i = 0; i < this._array.length; i++) {
|
|
|
|
|
this._array[i] = pattern[i % pattern.length];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mini pseudo-random number generator. Used because rerendering
|
|
|
|
|
// jittered bitmasks is easier when we can do it deterministically.
|
|
|
|
|
class PRNG {
|
|
|
|
|
constructor(seed) {
|
|
|
|
|
// LCG using GCC's constants
|
|
|
|
|
this.m = 0x80000000;
|
|
|
|
|
this.a = 1103515245;
|
|
|
|
|
this.c = 12345;
|
|
|
|
|
this.state = seed ? seed : Math.floor(Math.random() * (this.m - 1));
|
|
|
|
|
}
|
|
|
|
|
next() {
|
|
|
|
|
this.state = (this.a * this.state + this.c) % this.m;
|
|
|
|
|
return this.state / (this.m - 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Take an existing segmented pathspec and round its corners.
|
|
|
|
|
// Used by `rounded` and `dots` styles.
|
|
|
|
|
function makePathSpecRound(oldPathSpec) {
|
|
|
|
|
let isInnerContour;
|
|
|
|
|
let newPathSpec = new Array();
|
|
|
|
|
for(let i = 0; i < oldPathSpec.length; i++) {
|
|
|
|
|
if(oldPathSpec[i].startsWith('M')) {
|
|
|
|
|
let coords = oldPathSpec[i].substring(1).split(' ').map(c => parseInt(c, 10));
|
|
|
|
|
if(oldPathSpec[i+1].startsWith('h')) {
|
|
|
|
|
coords[0] += 0.5;
|
|
|
|
|
isInnerContour = false;
|
|
|
|
|
} else if(oldPathSpec[i+1].startsWith('v')) {
|
|
|
|
|
coords[1] += 0.5;
|
|
|
|
|
isInnerContour = true;
|
|
|
|
|
}
|
|
|
|
|
newPathSpec.push('M' + coords[0] + ' ' + coords[1]);
|
|
|
|
|
i++; // Skip the first horizontal line segment
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'z') {
|
|
|
|
|
if(isInnerContour) {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 0.5');
|
|
|
|
|
} else {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
|
|
|
|
}
|
|
|
|
|
newPathSpec.push('z');
|
|
|
|
|
// End this loop iteration here because (a) if this is the last path
|
|
|
|
|
// segment, trying to access i+1 further down would cause errors,
|
|
|
|
|
// and (b) because we might as well.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'h1') {
|
|
|
|
|
newPathSpec.push('h1');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'h-1') {
|
|
|
|
|
newPathSpec.push('h-1');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'v1') {
|
|
|
|
|
newPathSpec.push('v1');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'v-1') {
|
|
|
|
|
newPathSpec.push('v-1');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'v1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h1' && oldPathSpec[i+1] == 'v-1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 0 0.5 -0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'v1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'h-1' && oldPathSpec[i+1] == 'v-1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 -0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'h1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 0 0.5 0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v1' && oldPathSpec[i+1] == 'h-1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'h1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
|
|
|
|
}
|
|
|
|
|
if(oldPathSpec[i] == 'v-1' && oldPathSpec[i+1] == 'h-1') {
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 0 -0.5 -0.5');
|
|
|
|
|
}
|
|
|
|
|
let len = newPathSpec.length;
|
|
|
|
|
if(len >= 2 && newPathSpec[len-1][0] == newPathSpec[len-2][0] && ['h', 'v'].includes(newPathSpec[len-1][0])) {
|
|
|
|
|
let command = newPathSpec[len-1][0];
|
|
|
|
|
let delta1 = parseInt(newPathSpec.pop().slice(1), 10);
|
|
|
|
|
let delta2 = parseInt(newPathSpec.pop().slice(1), 10);
|
|
|
|
|
newPathSpec.push(command + (delta1 + delta2));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return newPathSpec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addJitterToPathSpec(oldPathSpec, jitterValue, prng) {
|
|
|
|
|
let newPathSpec = []
|
|
|
|
|
let currentPos = [null, null];
|
|
|
|
|
for(let step of oldPathSpec) {
|
|
|
|
|
if(step.startsWith('M')) {
|
|
|
|
|
currentPos = step.slice(1).split(' ').map(c => parseInt(c, 10));
|
|
|
|
|
newPathSpec.push(step);
|
|
|
|
|
} else if(step.startsWith('h') || step.startsWith('v')) {
|
|
|
|
|
let posIndex = 0; // default: h
|
|
|
|
|
if(step.startsWith('v')) {
|
|
|
|
|
posIndex = 1;
|
|
|
|
|
}
|
|
|
|
|
let distance = parseInt(step.slice(1), 10);
|
|
|
|
|
currentPos[posIndex] += distance;
|
|
|
|
|
let jitteredPos = currentPos.map(c => c + (prng.next() * 2 - 1) * jitterValue);
|
|
|
|
|
newPathSpec.push('L' + jitteredPos[0] + ' ' + jitteredPos[1]);
|
|
|
|
|
} else {
|
|
|
|
|
newPathSpec.push(step);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return newPathSpec;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactPathSpec(oldPathSpec) {
|
|
|
|
|
let newPathSpec = [];
|
|
|
|
|
if(oldPathSpec.length == 0) {
|
|
|
|
|
return newPathSpec;
|
|
|
|
|
}
|
|
|
|
|
newPathSpec.push(oldPathSpec[0]);
|
|
|
|
|
for(let step of oldPathSpec.slice(1)) {
|
|
|
|
|
let prev = newPathSpec[newPathSpec.length - 1];
|
|
|
|
|
if((step[0] == 'h' || step[0] == 'v') && step[0] == prev[0]) {
|
|
|
|
|
let distance = parseInt(prev.substring(1), 10) + parseInt(step.substring(1), 10);
|
|
|
|
|
newPathSpec[newPathSpec.length - 1] = step[0] + distance;
|
2024-09-12 07:56:49 -05:00
|
|
|
|
} else if(step[0] == 'm' && (prev[0] == 'm' || prev[0] == 'M')) {
|
|
|
|
|
let prevPos = prev.substring(1).split(' ').map(c => parseFloat(c));
|
|
|
|
|
let newDelta = step.substring(1).split(' ').map(c => parseFloat(c));
|
|
|
|
|
newPathSpec[newPathSpec.length - 1] = prev[0] + (prevPos[0] + newDelta[0]).toPrecision(3) + ' ' + (prevPos[1] + newDelta[1]).toPrecision(3);
|
2024-08-29 15:22:54 -05:00
|
|
|
|
} else {
|
|
|
|
|
newPathSpec.push(step);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return newPathSpec;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 05:02:24 -05:00
|
|
|
|
// Special case method to calculate contours for the styles
|
2024-08-29 15:22:54 -05:00
|
|
|
|
// where shapes are not contiguous. This also skips the PDP.
|
2024-09-12 05:02:24 -05:00
|
|
|
|
function calculateTileStyleContour(bitmask, margin, style) {
|
|
|
|
|
if(!['dots', 'mosaic', 'confetti'].includes(style)) {
|
|
|
|
|
throw Error('Unsupported tiled render style: ' + style);
|
2024-08-29 15:22:54 -05:00
|
|
|
|
}
|
2024-09-12 07:56:49 -05:00
|
|
|
|
let confettiShapes = [
|
|
|
|
|
[ // circle
|
|
|
|
|
'M 0.5 0.1',
|
|
|
|
|
'A 0.4 0.4 0 0 1 0.5 0.9',
|
|
|
|
|
'A 0.4 0.4 0 0 1 0.5 0.1',
|
|
|
|
|
],
|
|
|
|
|
[ // rectangle
|
|
|
|
|
'M 0.1 0.15',
|
|
|
|
|
'L 0.9 0.15',
|
|
|
|
|
'L 0.9 0.85',
|
|
|
|
|
'L 0.1 0.85',
|
|
|
|
|
'L 0.1 0.15',
|
|
|
|
|
],
|
|
|
|
|
[ // star
|
|
|
|
|
'M 0.68 0.254',
|
|
|
|
|
'Q 1.21 0.284 0.786 0.58',
|
|
|
|
|
'Q 0.9275 1.0925 0.498 0.801',
|
|
|
|
|
'Q 0.0545 1.1 0.212 0.606',
|
|
|
|
|
'Q -0.208 0.269 0.312 0.259',
|
|
|
|
|
'Q 0.5 -0.25 0.68 0.254',
|
|
|
|
|
],
|
|
|
|
|
[ // heart
|
|
|
|
|
'M 0.5 0.3',
|
|
|
|
|
'C 1.2 -0.1 1 0.7 0.5 1',
|
|
|
|
|
'C 0 0.7 -0.2 -0.1 0.5 0.3',
|
|
|
|
|
],
|
|
|
|
|
[ // diamond
|
|
|
|
|
'M 0.5 0',
|
|
|
|
|
'Q 0.65 0.35 1 0.5',
|
|
|
|
|
'Q 0.65 0.65 0.5 1',
|
|
|
|
|
'Q 0.35 0.65 0 0.5',
|
|
|
|
|
'Q 0.35 0.35 0.5 0',
|
|
|
|
|
],
|
|
|
|
|
[ // shamrock
|
|
|
|
|
'M 0.45 0.45',
|
|
|
|
|
'A 0.23 0.23 0 1 1 0.55 0.45',
|
|
|
|
|
'A 0.23 0.23 0 1 1 0.54 0.48',
|
|
|
|
|
'L 0.7 0.95',
|
|
|
|
|
'L 0.3 0.95',
|
|
|
|
|
'L 0.46 0.48',
|
|
|
|
|
'A 0.23 0.23 0 1 1 0.45 0.45',
|
|
|
|
|
],
|
|
|
|
|
[ // lemon
|
|
|
|
|
'M 0.5 0',
|
|
|
|
|
'Q 1.5 0.5 0.5 1',
|
|
|
|
|
'Q -0.5 0.5 0.5 0',
|
|
|
|
|
],
|
|
|
|
|
];
|
2024-08-29 15:22:54 -05:00
|
|
|
|
let contour = new Contour();
|
|
|
|
|
let prng = new PRNG(1);
|
2024-09-12 07:56:49 -05:00
|
|
|
|
// Rotate a point around (0.5, 0.5) by angle in radians
|
|
|
|
|
let rotatePoint = (x, y, angle) => {
|
|
|
|
|
let rotatedX = 0.5 + (x - 0.5) * Math.cos(angle) - (y - 0.5) * Math.sin(angle);
|
|
|
|
|
let rotatedY = 0.5 + (x - 0.5) * Math.sin(angle) + (y - 0.5) * Math.cos(angle);
|
|
|
|
|
return [rotatedX, rotatedY];
|
|
|
|
|
};
|
2024-08-29 15:22:54 -05:00
|
|
|
|
for(let y = 0; y < bitmask.height; y++) {
|
|
|
|
|
for(let x = 0; x < bitmask.width; x++) {
|
|
|
|
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
|
|
|
|
// Check if we are inside a PDP area, because they have already been handled separately.
|
2024-09-11 17:43:48 -05:00
|
|
|
|
if((x < 8 && y < 8) ||
|
|
|
|
|
(x < 8 && y > bitmask.height - 8) ||
|
|
|
|
|
(x > bitmask.width - 8 && y < 8)) {
|
2024-08-29 15:22:54 -05:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(bitmask.get(x, y)) {
|
|
|
|
|
let newPathSpec = new Array();
|
|
|
|
|
if(style == 'dots') {
|
|
|
|
|
newPathSpec.push('M' + (x + margin + 0.5) + ' ' + (y + margin));
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 0.5');
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 0.5');
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 -0.5 -0.5');
|
|
|
|
|
newPathSpec.push('a0.5 0.5 0 0 1 0.5 -0.5');
|
|
|
|
|
newPathSpec.push('z');
|
2024-09-12 05:02:24 -05:00
|
|
|
|
} else if(style == 'mosaic') {
|
2024-08-29 15:22:54 -05:00
|
|
|
|
// For the mosaic style, we jury-rig a pseudo-random rotation for each pixel.
|
|
|
|
|
let size = 0.9; // relative to grid size
|
|
|
|
|
let maxAngle = Math.PI * 0.03;
|
|
|
|
|
let angle = (prng.next() * 2 - 1) * maxAngle;
|
2024-09-12 07:56:49 -05:00
|
|
|
|
newPathSpec.push('M' + (x + 1) + ' ' + (y + 1));
|
|
|
|
|
let tileCorners = [
|
|
|
|
|
rotatePoint(0.5 - (size / 2), 0.5 - (size / 2), angle),
|
|
|
|
|
rotatePoint(0.5 + (size / 2), 0.5 - (size / 2), angle),
|
|
|
|
|
rotatePoint(0.5 + (size / 2), 0.5 + (size / 2), angle),
|
|
|
|
|
rotatePoint(0.5 - (size / 2), 0.5 + (size / 2), angle),
|
|
|
|
|
];
|
|
|
|
|
newPathSpec.push('m' + tileCorners[0][0].toPrecision(3) + ' ' + tileCorners[0][1].toPrecision(3));
|
|
|
|
|
newPathSpec.push('l' + (tileCorners[1][0] - tileCorners[0][0]).toPrecision(3) + ' ' + (tileCorners[1][1] - tileCorners[0][1]).toPrecision(3));
|
|
|
|
|
newPathSpec.push('l' + (tileCorners[2][0] - tileCorners[1][0]).toPrecision(3) + ' ' + (tileCorners[2][1] - tileCorners[1][1]).toPrecision(3));
|
|
|
|
|
newPathSpec.push('l' + (tileCorners[3][0] - tileCorners[2][0]).toPrecision(3) + ' ' + (tileCorners[3][1] - tileCorners[2][1]).toPrecision(3));
|
2024-08-29 15:22:54 -05:00
|
|
|
|
newPathSpec.push('z');
|
2024-09-12 05:02:24 -05:00
|
|
|
|
} else if(style == 'confetti') {
|
2024-09-12 07:56:49 -05:00
|
|
|
|
newPathSpec.push('M' + (x + margin) + ' ' + (y + margin));
|
|
|
|
|
let currentShape = confettiShapes[Math.floor(prng.next() * confettiShapes.length)];
|
|
|
|
|
let previousPoint = [0, 0];
|
|
|
|
|
let angle = prng.next() * Math.PI * 2;
|
|
|
|
|
for(let segment of currentShape) {
|
|
|
|
|
segment = segment.split(' ');
|
|
|
|
|
let formatRotatedPoint = (i, j) => {
|
|
|
|
|
let rotated = rotatePoint(segment[i], segment[j], angle);
|
|
|
|
|
return (rotated[0] - previousPoint[0]).toPrecision(3) + ' ' + (rotated[1] - previousPoint[1]).toPrecision(3);
|
|
|
|
|
}
|
|
|
|
|
if(segment[0] == 'M') {
|
|
|
|
|
let rotated = rotatePoint(segment[1], segment[2], angle);
|
|
|
|
|
newPathSpec.push('m' + formatRotatedPoint(1, 2));
|
|
|
|
|
previousPoint = rotated;
|
|
|
|
|
} else if(segment[0] == 'L') {
|
|
|
|
|
let rotated = rotatePoint(segment[1], segment[2], angle);
|
|
|
|
|
newPathSpec.push('l' + formatRotatedPoint(1, 2));
|
|
|
|
|
previousPoint = rotated;
|
|
|
|
|
} else if(segment[0] == 'Q') {
|
|
|
|
|
newPathSpec.push('q' + formatRotatedPoint(1, 2) + ' ' + formatRotatedPoint(3, 4));
|
|
|
|
|
previousPoint = rotatePoint(segment[3], segment[4], angle)
|
|
|
|
|
} else if(segment[0] == 'C') {
|
|
|
|
|
newPathSpec.push('c' + formatRotatedPoint(1, 2) + ' ' + formatRotatedPoint(3, 4) + ' ' + formatRotatedPoint(5, 6));
|
|
|
|
|
previousPoint = rotatePoint(segment[5], segment[6], angle);
|
|
|
|
|
} else if(segment[0] == 'A') {
|
|
|
|
|
newPathSpec.push('a' + segment.slice(1, 6).join(' ') + ' ' + formatRotatedPoint(6, 7));
|
|
|
|
|
previousPoint = rotatePoint(segment[6], segment[7], angle);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-12 05:02:24 -05:00
|
|
|
|
newPathSpec.push('z');
|
2024-08-29 15:22:54 -05:00
|
|
|
|
}
|
|
|
|
|
if(!bitmask.get(x - 1, y) && !bitmask.get(x + 1, y) && !bitmask.get(x, y - 1) && !bitmask.get(x, y + 1)) {
|
|
|
|
|
contour.dots = contour.dots.concat(newPathSpec);
|
|
|
|
|
} else {
|
|
|
|
|
contour.shapes = contour.shapes.concat(newPathSpec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return contour;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-12 05:02:24 -05:00
|
|
|
|
// For styles that do not use individual tiles, this method traces along
|
2024-08-29 15:22:54 -05:00
|
|
|
|
// contiguous shapes in the bitmask and builds a contour. Still skips
|
|
|
|
|
// the PDP and assumes it is handled separately.
|
|
|
|
|
function calculateShapeContour(bitmask, margin, style) {
|
|
|
|
|
let contour = new Contour();
|
|
|
|
|
let corners = new Array();
|
|
|
|
|
let width = bitmask.width + 1;
|
|
|
|
|
let height = bitmask.height + 1;
|
|
|
|
|
for(let y = 0; y < height; y++) {
|
|
|
|
|
for(let x = 0; x < width; x++) {
|
|
|
|
|
corners.push({});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(let y = 0; y < height; y++) {
|
|
|
|
|
for(let x = 0; x < width; x++) {
|
|
|
|
|
if(Object.keys(corners[y * width + x]).includes('e')) continue;
|
|
|
|
|
if(bitmask.get(x, y) == bitmask.get(x - 1, y) && bitmask.get(x, y) == bitmask.get(x, y - 1)
|
|
|
|
|
&& bitmask.get(x, y) == bitmask.get(x - 1, y - 1)) continue; // This corner is not part of any edge.
|
|
|
|
|
if(bitmask.get(x, y - 1) || !bitmask.get(x, y)) continue;
|
|
|
|
|
let contourX = x;
|
|
|
|
|
let contourY = y;
|
|
|
|
|
let direction = 'e';
|
|
|
|
|
while(!corners[contourY * width + contourX][direction]) {
|
|
|
|
|
let prevDirection = direction;
|
|
|
|
|
if(direction == 'n') {
|
|
|
|
|
if(bitmask.get(contourX, contourY - 1) && !bitmask.get(contourX - 1, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
|
|
|
|
} else if(!bitmask.get(contourX, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
|
|
|
|
direction = 'e';
|
|
|
|
|
} else if(bitmask.get(contourX - 1, contourY - 1) && bitmask.get(contourX, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
|
|
|
|
direction = 'w';
|
|
|
|
|
}
|
|
|
|
|
} else if(direction == 'e') {
|
|
|
|
|
if(bitmask.get(contourX, contourY) && !bitmask.get(contourX, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
|
|
|
|
} else if(!bitmask.get(contourX, contourY)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
|
|
|
|
direction = 's';
|
|
|
|
|
} else if(bitmask.get(contourX, contourY) && bitmask.get(contourX, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
|
|
|
|
direction = 'n';
|
|
|
|
|
}
|
|
|
|
|
} else if(direction == 's') {
|
|
|
|
|
if(bitmask.get(contourX - 1, contourY) && !bitmask.get(contourX, contourY)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
|
|
|
|
} else if(!bitmask.get(contourX - 1, contourY)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
|
|
|
|
direction = 'w';
|
|
|
|
|
} else if(bitmask.get(contourX, contourY) && bitmask.get(contourX - 1, contourY)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX + 1, contourY];
|
|
|
|
|
direction = 'e';
|
|
|
|
|
}
|
|
|
|
|
} else if(direction == 'w') {
|
|
|
|
|
if(bitmask.get(contourX - 1, contourY - 1) && !bitmask.get(contourX - 1, contourY)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX - 1, contourY];
|
|
|
|
|
} else if(!bitmask.get(contourX - 1, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY - 1];
|
|
|
|
|
direction = 'n';
|
|
|
|
|
} else if(bitmask.get(contourX - 1, contourY) && bitmask.get(contourX - 1, contourY - 1)) {
|
|
|
|
|
corners[contourY * width + contourX][direction] = [contourX, contourY + 1];
|
|
|
|
|
direction = 's';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let next = corners[contourY * width + contourX][prevDirection];
|
|
|
|
|
if(!next) break;
|
|
|
|
|
contourX = next[0];
|
|
|
|
|
contourY = next[1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(let y = 0; y < height; y++) {
|
|
|
|
|
for(let x = 0; x < width; x++) {
|
|
|
|
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
|
|
|
|
// Check if we are inside a PDP area, because they have already been handled separately.
|
2024-09-11 17:43:48 -05:00
|
|
|
|
if((x < 8 && y < 8) ||
|
|
|
|
|
(x < 8 && y > bitmask.height - 8) ||
|
|
|
|
|
(x > bitmask.width - 8 && y < 8)) {
|
2024-08-29 15:22:54 -05:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(Object.keys(corners[y * width + x]).length == 0) continue;
|
|
|
|
|
let direction = Object.keys(corners[y * width + x])[0];
|
|
|
|
|
let newPathSpec = new Array();
|
|
|
|
|
newPathSpec.push('M' + (x + margin) + ' ' + (y + margin));
|
|
|
|
|
let contourX = x;
|
|
|
|
|
let contourY = y;
|
|
|
|
|
while(corners[contourY * width + contourX][direction]) {
|
|
|
|
|
let next = corners[contourY * width + contourX][direction];
|
|
|
|
|
let prevSpecStep = newPathSpec[newPathSpec.length - 1];
|
|
|
|
|
delete corners[contourY * width + contourX][direction];
|
|
|
|
|
let pathCommand, pathDelta;
|
|
|
|
|
if(next[0] > contourX) {
|
|
|
|
|
direction = 'e';
|
|
|
|
|
pathCommand = 'h';
|
|
|
|
|
pathDelta = 1;
|
|
|
|
|
} else if(next[0] < contourX) {
|
|
|
|
|
direction = 'w';
|
|
|
|
|
pathCommand = 'h';
|
|
|
|
|
pathDelta = -1;
|
|
|
|
|
} else if(next[1] > contourY) {
|
|
|
|
|
direction = 's';
|
|
|
|
|
pathCommand = 'v';
|
|
|
|
|
pathDelta = 1;
|
|
|
|
|
} else if(next[1] < contourY) {
|
|
|
|
|
direction = 'n';
|
|
|
|
|
pathCommand = 'v';
|
|
|
|
|
pathDelta = -1;
|
|
|
|
|
}
|
|
|
|
|
newPathSpec.push(pathCommand + pathDelta);
|
|
|
|
|
contourX = next[0];
|
|
|
|
|
contourY = next[1];
|
|
|
|
|
}
|
|
|
|
|
// Skip non-shaped paths for serialization
|
|
|
|
|
if(newPathSpec.length <= 2) continue;
|
|
|
|
|
// Avoid double-pathing the initial segment
|
|
|
|
|
if(newPathSpec.length % 2 == 0) {
|
|
|
|
|
newPathSpec.pop();
|
|
|
|
|
}
|
|
|
|
|
// Technically at this point we should have already returned to the start,
|
|
|
|
|
// but adding an explicit `z` anyway helps with rendering under some
|
|
|
|
|
// circumstances. For pure line segment paths we could use `z` to jump
|
|
|
|
|
// back instead of making the last step before here explicit, but that can
|
|
|
|
|
// interfere with the way we round corners in some styles.
|
|
|
|
|
newPathSpec.push('z');
|
|
|
|
|
if(newPathSpec.length == 6 && !newPathSpec[1].startsWith('v')) {
|
|
|
|
|
contour.dots = contour.dots.concat(newPathSpec);
|
|
|
|
|
} else {
|
|
|
|
|
contour.shapes = contour.shapes.concat(newPathSpec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return contour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Overarching method that turns a 2D bitmask into a set of contour pathspecs.
|
|
|
|
|
// `margin` is an offset that is added to all x and y coordinates in the output.
|
2024-09-12 05:02:24 -05:00
|
|
|
|
// It defaults to 1 to accommodate jitter and mosaic styles that have elements
|
|
|
|
|
// randomly extending slightly outside of the basic QR code area.
|
2024-08-29 15:22:54 -05:00
|
|
|
|
function calculateContour(bitmask, margin = 1, style = 'basic') {
|
|
|
|
|
let contour = new Contour();
|
|
|
|
|
if(bitmask.width > 16 && bitmask.height > 16) {
|
|
|
|
|
// This is where we build the PDP contours, regardless of style. Skipped for
|
|
|
|
|
// bitmasks below a size threshold - those are assumed to not be valid QR codes.
|
|
|
|
|
// I also tried rendering the PDP paths as individual "pixels" in the dots and
|
|
|
|
|
// mosaic styles, but that led to bad scanning compatibility, so we take care
|
|
|
|
|
// to keep those more solid than the rest of the code.
|
|
|
|
|
for(let offset of [[margin, margin], [bitmask.width + margin - 7, margin], [margin, bitmask.height + margin - 7]]) {
|
|
|
|
|
contour.pdpOuter.push('M' + offset[0] + ' ' + offset[1]);
|
|
|
|
|
contour.pdpOuter.push(...Array(7).fill('h1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(7).fill('v1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(7).fill('h-1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(7).fill('v-1'));
|
|
|
|
|
contour.pdpOuter.push('z');
|
|
|
|
|
contour.pdpOuter.push('M' + (offset[0] + 1) + ' ' + (offset[1] + 1));
|
|
|
|
|
contour.pdpOuter.push(...Array(5).fill('v1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(5).fill('h1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(5).fill('v-1'));
|
|
|
|
|
contour.pdpOuter.push(...Array(5).fill('h-1'));
|
|
|
|
|
contour.pdpOuter.push('z');
|
|
|
|
|
contour.pdpInner.push('M' + (offset[0] + 2) + ' ' + (offset[1] + 2));
|
|
|
|
|
contour.pdpInner.push(...Array(3).fill('h1'));
|
|
|
|
|
contour.pdpInner.push(...Array(3).fill('v1'));
|
|
|
|
|
contour.pdpInner.push(...Array(3).fill('h-1'));
|
|
|
|
|
contour.pdpInner.push(...Array(3).fill('v-1'));
|
|
|
|
|
contour.pdpInner.push('z');
|
|
|
|
|
}
|
2024-09-12 07:56:49 -05:00
|
|
|
|
if(['dots', 'rounded', 'confetti'].includes(style)) {
|
2024-08-29 15:22:54 -05:00
|
|
|
|
contour.pdpInner = makePathSpecRound(contour.pdpInner);
|
|
|
|
|
contour.pdpOuter = makePathSpecRound(contour.pdpOuter);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let newContour;
|
2024-09-12 05:02:24 -05:00
|
|
|
|
if(['dots', 'mosaic', 'confetti'].includes(style)) {
|
|
|
|
|
newContour = calculateTileStyleContour(bitmask, margin, style);
|
2024-08-29 15:22:54 -05:00
|
|
|
|
} else {
|
|
|
|
|
newContour = calculateShapeContour(bitmask, margin, style);
|
|
|
|
|
}
|
|
|
|
|
contour.dots = newContour.dots;
|
|
|
|
|
contour.shapes = newContour.shapes;
|
|
|
|
|
if(style == 'rounded') {
|
|
|
|
|
contour.shapes = makePathSpecRound(contour.shapes);
|
|
|
|
|
contour.dots = makePathSpecRound(contour.dots);
|
|
|
|
|
}
|
|
|
|
|
if(style.startsWith('jitter-')) {
|
|
|
|
|
let jitterValue = 0.0;
|
|
|
|
|
// Suitable jitter values that still lead to good scanning compatibility
|
|
|
|
|
// have been derived experimentally. Customize as you see fit.
|
|
|
|
|
if(style == 'jitter-heavy') {
|
|
|
|
|
jitterValue = 0.15;
|
|
|
|
|
} else if (style == 'jitter-light') {
|
|
|
|
|
jitterValue = 0.07;
|
|
|
|
|
}
|
|
|
|
|
let prng = new PRNG(1);
|
|
|
|
|
// It's important to jitterize the shapes and dots first, as they
|
|
|
|
|
// are different for every QR code. Reusing the same PRNG afterwards
|
|
|
|
|
// for the PDP paths ensures that they get different jitter values
|
|
|
|
|
// for different QR codes and thus not look the same every time.
|
|
|
|
|
contour.shapes = addJitterToPathSpec(contour.shapes, jitterValue, prng);
|
|
|
|
|
contour.dots = addJitterToPathSpec(contour.dots, jitterValue, prng);
|
|
|
|
|
contour.pdpInner = addJitterToPathSpec(contour.pdpInner, jitterValue, prng);
|
|
|
|
|
contour.pdpOuter = addJitterToPathSpec(contour.pdpOuter, jitterValue, prng);
|
|
|
|
|
} else {
|
|
|
|
|
contour.shapes = compactPathSpec(contour.shapes);
|
|
|
|
|
contour.dots = compactPathSpec(contour.dots);
|
|
|
|
|
contour.pdpInner = compactPathSpec(contour.pdpInner);
|
|
|
|
|
contour.pdpOuter = compactPathSpec(contour.pdpOuter);
|
|
|
|
|
}
|
|
|
|
|
return contour;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// This is the main callable method to render a bitmask into an SVG
|
|
|
|
|
// element using a specific render style. The SVG element will be
|
|
|
|
|
// cleared, the viewBox will be adjusted as needed, and four path
|
|
|
|
|
// elements will be created within containing the PDP inner and
|
|
|
|
|
// outer parts, dots, and other shapes. See a few lines below for
|
|
|
|
|
// the list of valid styles.
|
|
|
|
|
function render(bitmask, renderTarget, style = 'basic') {
|
2024-09-12 05:02:24 -05:00
|
|
|
|
if(!['basic', 'rounded', 'dots', 'mosaic', 'confetti', 'jitter-light', 'jitter-heavy'].includes(style)) {
|
2024-08-29 15:22:54 -05:00
|
|
|
|
throw Error('Unsupported render style: ' + style);
|
|
|
|
|
}
|
|
|
|
|
renderTarget.setAttribute('viewBox', '0 0 ' + (bitmask.width + 2) + ' ' + (bitmask.height + 2));
|
|
|
|
|
while(renderTarget.firstChild) {
|
|
|
|
|
renderTarget.firstChild.remove();
|
|
|
|
|
}
|
|
|
|
|
let contours = qrsvg.calculateContour(bitmask, 1, style);
|
|
|
|
|
for(let contourType in contours) {
|
|
|
|
|
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
|
|
|
path.setAttribute('d', contours[contourType].join(''));
|
|
|
|
|
// Add your customizations - e.g. `fill` colors, custom classes... - here.
|
|
|
|
|
renderTarget.appendChild(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qrsvg['Bitmask'] = Bitmask;
|
|
|
|
|
qrsvg['Contour'] = Contour;
|
|
|
|
|
qrsvg['PRNG'] = PRNG;
|
|
|
|
|
qrsvg['makePathSpecRound'] = makePathSpecRound;
|
|
|
|
|
qrsvg['addJitterToPathSpec'] = addJitterToPathSpec;
|
|
|
|
|
qrsvg['compactPathSpec'] = compactPathSpec;
|
2024-09-12 05:02:24 -05:00
|
|
|
|
qrsvg['calculateTileStyleContour'] = calculateTileStyleContour;
|
2024-08-29 15:22:54 -05:00
|
|
|
|
qrsvg['calculateShapeContour'] = calculateShapeContour;
|
|
|
|
|
qrsvg['calculateContour'] = calculateContour;
|
|
|
|
|
qrsvg['render'] = render;
|
|
|
|
|
|
|
|
|
|
})(qrsvg || (qrsvg = {}));
|