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 = \`\`; + if (params["Background"]) { + svg += \`\`; + } + 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 = \`\`; + svg += \`\`; + svg += \`\`; + + return svg; +} `; export default {