From 59c0d2d5cd73b363886c95a3b128af79b882fa23 Mon Sep 17 00:00:00 2001 From: Julian Fietkau Date: Thu, 12 Sep 2024 14:56:49 +0200 Subject: [PATCH] Add confetti shape style --- qrsvg-v1.0.1.js | 114 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 15 deletions(-) diff --git a/qrsvg-v1.0.1.js b/qrsvg-v1.0.1.js index 48e4450..9d91515 100644 --- a/qrsvg-v1.0.1.js +++ b/qrsvg-v1.0.1.js @@ -230,6 +230,10 @@ function compactPathSpec(oldPathSpec) { 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; + } 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); } else { newPathSpec.push(step); } @@ -243,8 +247,62 @@ function calculateTileStyleContour(bitmask, margin, style) { if(!['dots', 'mosaic', 'confetti'].includes(style)) { throw Error('Unsupported tiled render style: ' + style); } + 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', + ], + ]; let contour = new Contour(); let prng = new PRNG(1); + // 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]; + }; for(let y = 0; y < bitmask.height; y++) { for(let x = 0; x < bitmask.width; x++) { if(bitmask.width > 16 && bitmask.height > 16) { @@ -269,22 +327,48 @@ function calculateTileStyleContour(bitmask, margin, style) { let size = 0.9; // relative to grid size let maxAngle = Math.PI * 0.03; let angle = (prng.next() * 2 - 1) * maxAngle; - // |------ middle of the pixel ------| |-north displacement-| |-west displacement-| - let topLeftX = x + margin + 0.5 + ((1 - size) / 2) - 0.5 * Math.cos(angle) + 0.5 * Math.sin(angle); - let topLeftY = y + margin + 0.5 + ((1 - size) / 2) + 0.5 * Math.cos(angle) - 0.5 * Math.sin(angle) - 1; - newPathSpec.push('M' + topLeftX.toPrecision(3) + ' ' + topLeftY.toPrecision(3)); - newPathSpec.push('l' + (size * Math.cos(angle)).toPrecision(3) + ' ' + (size * Math.sin(angle)).toPrecision(3)); - newPathSpec.push(('l-' + (size * Math.sin(angle)).toPrecision(3) + ' ' + (size * Math.cos(angle)).toPrecision(3)).replaceAll('--', '')); - newPathSpec.push(('l-' + (size * Math.cos(angle)).toPrecision(3) + ' -' + (size * Math.sin(angle)).toPrecision(3)).replaceAll('--', '')); - newPathSpec.push(('l' + (size * Math.sin(angle)).toPrecision(3) + ' -' + (size * Math.cos(angle)).toPrecision(3)).replaceAll('--', '')); + 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)); newPathSpec.push('z'); } else if(style == 'confetti') { - // TODO - 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('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); + } + } newPathSpec.push('z'); } if(!bitmask.get(x - 1, y) && !bitmask.get(x + 1, y) && !bitmask.get(x, y - 1) && !bitmask.get(x, y + 1)) { @@ -466,7 +550,7 @@ function calculateContour(bitmask, margin = 1, style = 'basic') { contour.pdpInner.push(...Array(3).fill('v-1')); contour.pdpInner.push('z'); } - if(style == 'dots' || style == 'rounded') { + if(['dots', 'rounded', 'confetti'].includes(style)) { contour.pdpInner = makePathSpecRound(contour.pdpInner); contour.pdpOuter = makePathSpecRound(contour.pdpOuter); }