diff --git a/src/lib/presets/Camo.ts b/src/lib/presets/Camo.ts
index da19462..2d6d677 100644
--- a/src/lib/presets/Camo.ts
+++ b/src/lib/presets/Camo.ts
@@ -156,9 +156,114 @@ const code = `export const paramsSchema = ${objString(paramsSchema)};
const Module = ${objString(Module)};
-${splitmix32.toString()}
+function splitmix32(a) {
+ return function () {
+ a |= 0;
+ a = (a + 0x9e3779b9) | 0;
+ let t = a ^ (a >>> 16);
+ t = Math.imul(t, 0x21f0aaad);
+ t = t ^ (t >>> 15);
+ t = Math.imul(t, 0x735a2d97);
+ return ((t = t ^ (t >>> 15)) >>> 0) / 4294967296;
+ };
+}
-export ${renderCanvas.toString()}
+export function renderCanvas(qr, params, ctx) {
+ const seededRand = splitmix32(params["Seed"]);
+ const margin = params["Margin"];
+ const quietZone = params["Quiet zone"];
+
+ const pixelSize = 10;
+ const radius = pixelSize / 2;
+ const qrWidth = qr.version * 4 + 17;
+ const matrixWidth = qrWidth + 2 * margin;
+ const canvasSize = matrixWidth * pixelSize;
+
+ const newMatrix = Array(matrixWidth * matrixWidth).fill(Module.SeparatorOFF);
+
+ // Copy qr to matrix with margin and randomly set pixels in margin
+ for (let y = 0; y < margin - quietZone; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ if (seededRand() > 0.5) newMatrix[y * matrixWidth + x] = Module.DataON;
+ }
+ }
+ for (let y = margin - quietZone; y < margin + qrWidth + quietZone; y++) {
+ for (let x = 0; x < margin - quietZone; x++) {
+ if (seededRand() > 0.5) newMatrix[y * matrixWidth + x] = Module.DataON;
+ }
+ if (y >= margin && y < margin + qrWidth) {
+ for (let x = margin; x < matrixWidth - margin; x++) {
+ newMatrix[y * matrixWidth + x] =
+ qr.matrix[(y - margin) * qrWidth + x - margin];
+ }
+ }
+ for (let x = margin + qrWidth + quietZone; x < matrixWidth; x++) {
+ if (seededRand() > 0.5) newMatrix[y * matrixWidth + x] = Module.DataON;
+ }
+ }
+ for (let y = margin + qrWidth + quietZone; y < matrixWidth; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ if (seededRand() > 0.5) newMatrix[y * matrixWidth + x] = Module.DataON;
+ }
+ }
+
+ const fg = "rgb(40, 70, 10)";
+ const bg = "rgb(200, 200, 100)";
+
+ ctx.canvas.width = canvasSize;
+ ctx.canvas.height = canvasSize;
+
+ ctx.fillStyle = bg;
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+ const xMax = matrixWidth - 1;
+ const yMax = matrixWidth - 1;
+
+ for (let y = 0; y < matrixWidth; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ const module = newMatrix[y * matrixWidth + x];
+
+ const top = y > 0 && newMatrix[(y - 1) * matrixWidth + x] & 1;
+ const bottom = y < yMax && newMatrix[(y + 1) * matrixWidth + x] & 1;
+ const left = x > 0 && newMatrix[y * matrixWidth + x - 1] & 1;
+ const right = x < xMax && newMatrix[y * matrixWidth + x + 1] & 1;
+
+ ctx.fillStyle = fg;
+
+ if (module & 1) {
+ ctx.beginPath();
+ ctx.roundRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize, [
+ (!left && !top && radius) || 0,
+ (!top && !right && radius) || 0,
+ (!right && !bottom && radius) || 0,
+ (!bottom && !left && radius) || 0,
+ ]);
+ ctx.fill();
+ } else {
+ // Draw rounded concave corners
+ const topLeft =
+ y > 0 && x > 0 && newMatrix[(y - 1) * matrixWidth + x - 1] & 1;
+ const topRight =
+ y > 0 && x < xMax && newMatrix[(y - 1) * matrixWidth + x + 1] & 1;
+ const bottomRight =
+ y < yMax && x < xMax && newMatrix[(y + 1) * matrixWidth + x + 1] & 1;
+ const bottomLeft =
+ y < yMax && x > 0 && newMatrix[(y + 1) * matrixWidth + x - 1] & 1;
+ ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
+
+ ctx.beginPath();
+ ctx.fillStyle = bg;
+ ctx.roundRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize, [
+ (left && top && topLeft && radius) || 0,
+ (top && right && topRight && radius) || 0,
+ (right && bottom && bottomRight && radius) || 0,
+ (bottom && left && bottomLeft && radius) || 0,
+ ]);
+ ctx.fill();
+ }
+ }
+ }
+}
`;
export default {
diff --git a/src/lib/presets/Circle.ts b/src/lib/presets/Circle.ts
index 9574e9c..80e8494 100644
--- a/src/lib/presets/Circle.ts
+++ b/src/lib/presets/Circle.ts
@@ -181,7 +181,149 @@ const code = `export const paramsSchema = ${objString(paramsSchema)};
const Module = ${objString(Module)};
-export ${renderCanvas.toString()}
+export function renderCanvas(qr, params, ctx) {
+ const pixelSize = 10;
+ const margin = 2;
+ const matrixWidth = qr.version * 4 + 17;
+ const canvasSize = (matrixWidth + 2 * margin) * pixelSize;
+ ctx.canvas.width = canvasSize;
+ ctx.canvas.height = canvasSize;
+
+ ctx.fillStyle = "rgb(255, 255, 255)";
+ ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
+
+ const gradient = ctx.createRadialGradient(
+ ctx.canvas.width / 2,
+ ctx.canvas.height / 2,
+ 2 * pixelSize,
+ ctx.canvas.width / 2,
+ ctx.canvas.height / 2,
+ 20 * pixelSize
+ );
+
+ gradient.addColorStop(0, "red");
+ gradient.addColorStop(1, "blue");
+
+ ctx.fillStyle = gradient;
+
+ const radius = pixelSize / 2;
+
+ const finderPos = [
+ [margin, margin],
+ [margin + matrixWidth - 7, margin],
+ [margin, margin + matrixWidth - 7],
+ ];
+
+ if (params["Circular finder pattern"]) {
+ for (const [x, y] of finderPos) {
+ ctx.beginPath();
+ ctx.arc(
+ (x + 3.5) * pixelSize,
+ (y + 3.5) * pixelSize,
+ 3.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+
+ ctx.fillStyle = "rgb(255, 255, 255)";
+ ctx.beginPath();
+ ctx.arc(
+ (x + 3.5) * pixelSize,
+ (y + 3.5) * pixelSize,
+ 2.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+
+ ctx.fillStyle = gradient;
+ ctx.beginPath();
+ ctx.arc(
+ (x + 3.5) * pixelSize,
+ (y + 3.5) * pixelSize,
+ 1.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+ }
+ }
+
+ const xMid = matrixWidth / 2;
+ const yMid = matrixWidth / 2;
+ const maxDist = Math.sqrt(xMid * xMid + yMid * yMid);
+
+ for (let y = 0; y < matrixWidth; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ const module = qr.matrix[y * matrixWidth + x];
+
+ if (module & 1) {
+ if (params["Circular finder pattern"] && module === Module.FinderON)
+ continue;
+ if (
+ params["Circular alignment pattern"] &&
+ module === Module.AlignmentON
+ ) {
+ // Find top left corner of alignment square
+ if (
+ qr.matrix[(y - 1) * matrixWidth + x] !== Module.AlignmentON &&
+ qr.matrix[y * matrixWidth + x - 1] !== Module.AlignmentON &&
+ qr.matrix[y * matrixWidth + x + 1] === Module.AlignmentON
+ ) {
+ const xPos = x + 2.5 + margin;
+ const yPos = y + 2.5 + margin;
+
+ ctx.beginPath();
+ ctx.arc(
+ xPos * pixelSize,
+ yPos * pixelSize,
+ 2.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+
+ ctx.fillStyle = "rgb(255, 255, 255)";
+ ctx.beginPath();
+ ctx.arc(
+ xPos * pixelSize,
+ yPos * pixelSize,
+ 1.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+
+ ctx.fillStyle = gradient;
+ ctx.beginPath();
+ ctx.arc(
+ xPos * pixelSize,
+ yPos * pixelSize,
+ 0.5 * pixelSize,
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+ }
+ continue;
+ }
+
+ const xCenter = (x + margin) * pixelSize + radius;
+ const yCenter = (y + margin) * pixelSize + radius;
+
+ const xDist = Math.abs(xMid - x);
+ const yDist = Math.abs(yMid - y);
+ const scale =
+ (Math.sqrt(xDist * xDist + yDist * yDist) / maxDist) * 0.7 + 0.5;
+
+ ctx.beginPath();
+ ctx.arc(xCenter, yCenter, radius * scale, 0, 2 * Math.PI);
+ ctx.fill();
+ }
+ }
+ }
+}
`;
export default {
diff --git a/src/lib/presets/Halftone.ts b/src/lib/presets/Halftone.ts
index 3a60222..0272656 100644
--- a/src/lib/presets/Halftone.ts
+++ b/src/lib/presets/Halftone.ts
@@ -203,7 +203,138 @@ const code = `export const paramsSchema = ${objString(paramsSchema)};
const Module = ${objString(Module)};
-export ${renderCanvas.toString()}
+export async function renderCanvas(qr, params, ctx) {
+ const moduleSize = 3;
+ const pixelSize = 1;
+
+ const matrixWidth = qr.version * 4 + 17;
+ const margin = params["Margin"];
+ const fg = params["Foreground"];
+ const bg = params["Background"];
+ const alignment = params["Alignment pattern"];
+ const timing = params["Timing pattern"];
+ const file = params["Image"];
+
+ const pixelWidth = matrixWidth + 2 * margin;
+ const canvasSize = pixelWidth * moduleSize;
+ ctx.canvas.width = canvasSize;
+ ctx.canvas.height = canvasSize;
+
+ if (params["QR background"]) {
+ ctx.fillStyle = bg;
+ ctx.fillRect(0, 0, canvasSize, canvasSize);
+ ctx.fillStyle = fg;
+ for (let y = 0; y < matrixWidth; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ const module = qr.matrix[y * matrixWidth + x];
+ if (module & 1) {
+ ctx.fillRect(
+ (x + margin) * moduleSize,
+ (y + margin) * moduleSize,
+ moduleSize,
+ moduleSize
+ );
+ }
+ }
+ }
+ }
+
+ const image = new Image();
+
+ if (file != null) {
+ image.src = URL.createObjectURL(file);
+ } else {
+ // if canvas tainted, need to reload
+ // https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
+ image.crossOrigin = "anonymous";
+ image.src =
+ "https://upload.wikimedia.org/wikipedia/commons/1/14/The_Widow_%28Boston_Public_Library%29_%28cropped%29.jpg";
+ }
+ await image.decode();
+
+ ctx.filter = \`brightness(\${params["Brightness"]}) contrast(\${params["Contrast"]})\`;
+ ctx.drawImage(image, 0, 0, canvasSize, canvasSize);
+ ctx.filter = "none";
+
+ if (file != null) {
+ URL.revokeObjectURL(image.src);
+ }
+
+ const imageData = ctx.getImageData(0, 0, canvasSize, canvasSize);
+ const data = imageData.data;
+
+ for (let y = 0; y < canvasSize; y++) {
+ for (let x = 0; x < canvasSize; x++) {
+ const i = (y * canvasSize + x) * 4;
+
+ if (data[i + 3] === 0) continue;
+ // Convert to grayscale and normalize to 0-255
+ const oldPixel =
+ (data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114) | 0;
+
+ let newPixel;
+ if (oldPixel < 128) {
+ newPixel = 0;
+ ctx.fillStyle = fg;
+ } else {
+ newPixel = 255;
+ ctx.fillStyle = bg;
+ }
+ ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
+
+ data[i] = data[i + 1] = data[i + 2] = newPixel;
+ const error = oldPixel - newPixel;
+
+ // Distribute error to neighboring pixels
+ if (x < canvasSize - 1) {
+ data[i + 4] += (error * 7) / 16;
+ }
+ if (y < canvasSize - 1) {
+ if (x > 0) {
+ data[i + canvasSize * 4 - 4] += (error * 3) / 16;
+ }
+ data[i + canvasSize * 4] += (error * 5) / 16;
+ if (x < canvasSize - 1) {
+ data[i + canvasSize * 4 + 4] += (error * 1) / 16;
+ }
+ }
+ }
+ }
+
+ const dataOffset = (moduleSize - pixelSize) / 2;
+
+ for (let y = 0; y < matrixWidth; y++) {
+ for (let x = 0; x < matrixWidth; x++) {
+ const module = qr.matrix[y * matrixWidth + x];
+ if (module & 1) {
+ ctx.fillStyle = fg;
+ } else {
+ ctx.fillStyle = bg;
+ }
+
+ const type = module | 1;
+ if (
+ type === Module.FinderON ||
+ (alignment && type === Module.AlignmentON) ||
+ (timing && type === Module.TimingON)
+ ) {
+ ctx.fillRect(
+ (x + margin) * moduleSize,
+ (y + margin) * moduleSize,
+ moduleSize,
+ moduleSize
+ );
+ } else {
+ ctx.fillRect(
+ (x + margin) * moduleSize + dataOffset,
+ (y + margin) * moduleSize + dataOffset,
+ pixelSize,
+ pixelSize
+ );
+ }
+ }
+ }
+}
`;
export default {
diff --git a/src/lib/presets/Minimal.ts b/src/lib/presets/Minimal.ts
index 1da3918..86bde94 100644
--- a/src/lib/presets/Minimal.ts
+++ b/src/lib/presets/Minimal.ts
@@ -102,7 +102,67 @@ const code = `export const paramsSchema = ${objString(paramsSchema)};
const Module = ${objString(Module)};
-export ${renderSVG.toString()}
+export function renderSVG(qr, params) {
+ const matrixWidth = qr.version * 4 + 17;
+ const margin = params["Margin"];
+ const dataSize = params["Data pixel size"];
+ const moduleSize = 10;
+ const fg = "#000";
+ const bg = "#fff";
+
+ const finderPos = [
+ [margin, margin],
+ [matrixWidth + margin - 7, margin],
+ [margin, matrixWidth + margin - 7],
+ ];
+
+ const svgSize = (matrixWidth + 2 * margin) * moduleSize;
+
+ let svg = \`\`;
+
+ return svg;
+}
`;
export default {
diff --git a/src/lib/presets/Square.ts b/src/lib/presets/Square.ts
index a637c0b..7628185 100644
--- a/src/lib/presets/Square.ts
+++ b/src/lib/presets/Square.ts
@@ -47,7 +47,30 @@ function renderSVG(qr: OutputQr, params: Params) {
const code = `export const paramsSchema = ${objString(paramsSchema)};
-export ${renderSVG.toString()}
+export function renderSVG(qr, params) {
+ const matrixWidth = qr.version * 4 + 17;
+ const margin = params["Margin"];
+ const fg = params["Foreground"];
+ const bg = params["Background"];
+
+ const size = matrixWidth + 2 * margin;
+
+ let svg = \`\`;
+
+ return svg;
+}
`;
export default {