/** * Minimal QR Code generator — no dependencies * Encodes a URL into a canvas element * Based on QR Code Model 2, Version 3 (29x29), Error Correction Level L */ (function () { 'use strict'; // Generate QR code on a canvas for a given URL function generateQR(canvas, url) { // Use the Google Charts API as a simple fallback-free approach: // Draw QR as an image loaded from a data URL generated client-side. // For a truly zero-dependency approach, we use a proven minimal encoder. const size = canvas.width; const ctx = canvas.getContext('2d'); // Try using the minimal encoder try { const modules = encode(url); const moduleCount = modules.length; const cellSize = Math.floor((size - 16) / moduleCount); // 8px padding each side const offset = Math.floor((size - cellSize * moduleCount) / 2); ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, size, size); ctx.fillStyle = '#051229'; // Navy from brand for (let r = 0; r < moduleCount; r++) { for (let c = 0; c < moduleCount; c++) { if (modules[r][c]) { ctx.fillRect( offset + c * cellSize, offset + r * cellSize, cellSize, cellSize ); } } } } catch (e) { // Fallback: show URL text ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, size, size); ctx.fillStyle = '#051229'; ctx.font = '10px monospace'; ctx.textAlign = 'center'; ctx.fillText(url, size / 2, size / 2); } } // ---- Minimal QR Encoder (byte mode, ECC L) ---- // GF(256) math const EXP = new Uint8Array(512); const LOG = new Uint8Array(256); (function initGF() { let x = 1; for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x = (x << 1) ^ (x & 128 ? 0x11d : 0); } for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; })(); function gfMul(a, b) { return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]]; } function polyMul(a, b) { const r = new Uint8Array(a.length + b.length - 1); for (let i = 0; i < a.length; i++) for (let j = 0; j < b.length; j++) r[i + j] ^= gfMul(a[i], b[j]); return r; } function polyRemainder(data, gen) { const r = new Uint8Array(data.length + gen.length - 1); r.set(data); for (let i = 0; i < data.length; i++) { if (r[i] === 0) continue; for (let j = 0; j < gen.length; j++) r[i + j] ^= gfMul(gen[j], r[i]); } return r.slice(data.length); } function generatorPoly(n) { let g = new Uint8Array([1]); for (let i = 0; i < n; i++) g = polyMul(g, new Uint8Array([1, EXP[i]])); return g; } // Version info: [version, size, dataBytes, eccPerBlock, numBlocks] // We pick the smallest version that fits const VERSIONS = [ [1, 21, 19, 7, 1], [2, 25, 34, 10, 1], [3, 29, 55, 15, 1], [4, 33, 80, 20, 1], [5, 37, 108, 26, 1], [6, 41, 136, 18, 2], [7, 45, 156, 20, 2], [8, 49, 194, 24, 2], [9, 53, 232, 30, 2], [10, 57, 274, 18, 4], ]; function pickVersion(dataLen) { // Byte mode: 4 bits mode + char count bits + data + terminator for (const v of VERSIONS) { const charCountBits = v[0] <= 9 ? 8 : 16; const totalBits = 4 + charCountBits + dataLen * 8; const capacity = v[2] * 8; if (totalBits <= capacity) return v; } return VERSIONS[VERSIONS.length - 1]; // fallback to largest } function encode(text) { const data = new TextEncoder().encode(text); const [version, size, dataBytes, eccPerBlock, numBlocks] = pickVersion(data.length); const charCountBits = version <= 9 ? 8 : 16; // Build data bitstream const bits = []; function pushBits(val, len) { for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1); } pushBits(0b0100, 4); // Byte mode pushBits(data.length, charCountBits); for (const b of data) pushBits(b, 8); pushBits(0, Math.min(4, dataBytes * 8 - bits.length)); // terminator // Pad to byte boundary while (bits.length % 8 !== 0) bits.push(0); // Pad bytes const padBytes = [0xEC, 0x11]; let padIdx = 0; while (bits.length < dataBytes * 8) { pushBits(padBytes[padIdx % 2], 8); padIdx++; } // Convert to bytes const dataArr = new Uint8Array(dataBytes); for (let i = 0; i < dataBytes; i++) { let byte = 0; for (let b = 0; b < 8; b++) byte = (byte << 1) | (bits[i * 8 + b] || 0); dataArr[i] = byte; } // Split into blocks and generate ECC const blockSize = Math.floor(dataBytes / numBlocks); const gen = generatorPoly(eccPerBlock); const dataBlocks = []; const eccBlocks = []; for (let b = 0; b < numBlocks; b++) { const start = b * blockSize; const end = b === numBlocks - 1 ? dataBytes : start + blockSize; const block = dataArr.slice(start, end); dataBlocks.push(block); eccBlocks.push(polyRemainder(block, gen)); } // Interleave const codewords = []; const maxDataLen = Math.max(...dataBlocks.map(b => b.length)); for (let i = 0; i < maxDataLen; i++) for (const block of dataBlocks) if (i < block.length) codewords.push(block[i]); for (let i = 0; i < eccPerBlock; i++) for (const block of eccBlocks) if (i < block.length) codewords.push(block[i]); // Create module matrix const modules = Array.from({ length: size }, () => new Uint8Array(size)); const reserved = Array.from({ length: size }, () => new Uint8Array(size)); // Finder patterns function drawFinder(row, col) { for (let r = -1; r <= 7; r++) { for (let c = -1; c <= 7; c++) { const rr = row + r, cc = col + c; if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue; reserved[rr][cc] = 1; if (r >= 0 && r <= 6 && c >= 0 && c <= 6) { modules[rr][cc] = (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4)) ? 1 : 0; } } } } drawFinder(0, 0); drawFinder(0, size - 7); drawFinder(size - 7, 0); // Alignment pattern (version >= 2) if (version >= 2) { const pos = alignmentPositions(version); for (const r of pos) { for (const c of pos) { if (reserved[r][c]) continue; for (let dr = -2; dr <= 2; dr++) { for (let dc = -2; dc <= 2; dc++) { const rr = r + dr, cc = c + dc; reserved[rr][cc] = 1; modules[rr][cc] = (Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0; } } } } } // Timing patterns for (let i = 8; i < size - 8; i++) { reserved[6][i] = 1; modules[6][i] = i % 2 === 0 ? 1 : 0; reserved[i][6] = 1; modules[i][6] = i % 2 === 0 ? 1 : 0; } // Dark module reserved[size - 8][8] = 1; modules[size - 8][8] = 1; // Reserve format info areas for (let i = 0; i < 8; i++) { reserved[8][i] = 1; reserved[8][size - 1 - i] = 1; reserved[i][8] = 1; reserved[size - 1 - i][8] = 1; } reserved[8][8] = 1; // Reserve version info (version >= 7) — not needed for our range // Place data let bitIdx = 0; const totalBitsToPlace = codewords.length * 8; let col = size - 1; while (col >= 0) { if (col === 6) col--; // skip timing column for (let dir = 0; dir < 2; dir++) { const cc = col - dir; const upward = ((size - 1 - col + (col > 6 ? 1 : 0)) / 2) % 2 === 0; for (let i = 0; i < size; i++) { const r = upward ? i : size - 1 - i; if (reserved[r][cc]) continue; if (bitIdx < totalBitsToPlace) { const byteIdx = Math.floor(bitIdx / 8); const bitOff = 7 - (bitIdx % 8); modules[r][cc] = (codewords[byteIdx] >> bitOff) & 1; bitIdx++; } } } col -= 2; } // Apply mask (mask 0: (row + col) % 2 === 0) for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (!reserved[r][c] && (r + c) % 2 === 0) modules[r][c] ^= 1; // Write format info (mask 0, ECC L) const formatBits = getFormatBits(0); // ECC L, mask 0 // Horizontal for (let i = 0; i < 6; i++) modules[8][i] = (formatBits >> (14 - i)) & 1; modules[8][7] = (formatBits >> 8) & 1; modules[8][8] = (formatBits >> 7) & 1; modules[7][8] = (formatBits >> 6) & 1; for (let i = 0; i < 6; i++) modules[5 - i][8] = (formatBits >> (5 - i)) & 1; // Vertical for (let i = 0; i < 7; i++) modules[size - 1 - i][8] = (formatBits >> (14 - i)) & 1; for (let i = 0; i < 8; i++) modules[8][size - 8 + i] = (formatBits >> (7 - i)) & 1; return modules; } function alignmentPositions(version) { if (version === 1) return []; const positions = [6]; const last = version * 4 + 10; const count = Math.floor(version / 7) + 2; const step = Math.ceil((last - 6) / (count - 1)); for (let i = 1; i < count; i++) positions.push(6 + i * step > last ? last : 6 + i * step); // Ensure last is exact positions[positions.length - 1] = last; return positions; } function getFormatBits(mask) { // Pre-computed format info for ECC Level L (01) with masks 0-7 const FORMAT_INFO = [ 0x77C4, 0x72F3, 0x7DAA, 0x789D, 0x662F, 0x6318, 0x6C41, 0x6976, ]; return FORMAT_INFO[mask]; } // Auto-init on load window.addEventListener('DOMContentLoaded', () => { const canvas = document.getElementById('qr-canvas'); if (canvas) { generateQR(canvas, 'https://monitor.proactys.swiss/opusbike/'); } }); })();