// qr.jsx — real QR generation (qrcode-generator) + custom SVG render + downloads. // Exports to window: buildQRPayload, QRPreview, downloadQRPng, downloadQRSvg, qrContentLabel // Build the string that gets encoded based on the chosen content type. function buildQRPayload(form) { if (!form) return " "; const type = form.contentType || "contact"; if (type === "url") { return (form.url || "").trim() || "https://example.com"; } if (type === "text") { return (form.text || form.message || "").trim() || " "; } // contact -> vCard 3.0 const fn = `${form.firstName || ""} ${form.lastName || ""}`.trim(); const lines = [ "BEGIN:VCARD", "VERSION:3.0", `N:${form.lastName || ""};${form.firstName || ""};;;`, `FN:${fn || "Contact"}`, ]; if (form.phone) lines.push(`TEL;TYPE=CELL:${form.phone}`); if (form.email) lines.push(`EMAIL;TYPE=INTERNET:${form.email}`); if (form.message) lines.push(`NOTE:${form.message.replace(/\n/g, "\\n")}`); const handle = (v) => (v || "").trim().replace(/^@+/, ""); if (handle(form.instagram)) lines.push(`URL;TYPE=Instagram:https://instagram.com/${handle(form.instagram)}`); if (handle(form.snapchat)) lines.push(`URL;TYPE=Snapchat:https://snapchat.com/add/${handle(form.snapchat)}`); if (handle(form.tiktok)) lines.push(`URL;TYPE=TikTok:https://tiktok.com/@${handle(form.tiktok)}`); lines.push("END:VCARD"); return lines.join("\n"); } function qrContentLabel(form, t) { const type = form.contentType || "contact"; if (type === "url") return form.url || "—"; if (type === "text") return form.text || form.message || "—"; const fn = `${form.firstName || ""} ${form.lastName || ""}`.trim(); const bits = [fn, form.phone, form.email].filter(Boolean); return bits.join(" · ") || "—"; } // Compute boolean matrix from text using global `qrcode` (qrcode-generator). function qrMatrix(text, ecc) { try { const qr = window.qrcode(0, ecc || "M"); qr.addData(text && text.length ? text : " "); qr.make(); const n = qr.getModuleCount(); const m = []; for (let r = 0; r < n; r++) { const row = []; for (let c = 0; c < n; c++) row.push(qr.isDark(r, c)); m.push(row); } return m; } catch (e) { return null; } } // Is this module part of one of the 3 finder (corner) eyes? function inFinder(r, c, n) { const f = 7; return (r < f && c < f) || (r < f && c >= n - f) || (r >= n - f && c < f); } // React component: renders a styled, scannable SVG QR. function QRPreview(props) { const { text, plate = "#FFFFFF", code = "#101114", size = 240, pad = 4, // quiet zone in modules rounded = true, showPlate = true, ecc = "M", style, id, } = props; const matrix = React.useMemo(() => qrMatrix(text, ecc), [text, ecc]); // Geometry is built ONCE per code content/shape and reused across color changes. // Colors are applied purely via CSS vars on the (code = currentColor, // plate = --qp), so tapping a swatch only updates one style attribute — no // rebuild/diff of the hundreds of module rects. Keeps recoloring instant. const geom = React.useMemo(() => { if (!matrix) return null; const n = matrix.length; const total = n + pad * 2; const dotSize = rounded ? 0.88 : 1; const dotOff = (1 - dotSize) / 2; const rects = []; for (let r = 0; r < n; r++) { for (let c = 0; c < n; c++) { if (!matrix[r][c]) continue; if (inFinder(r, c, n)) continue; rects.push(React.createElement("rect", { key: `${r}-${c}`, x: c + pad + dotOff, y: r + pad + dotOff, width: dotSize, height: dotSize, rx: rounded ? dotSize * 0.42 : 0, fill: "currentColor", })); } } const eye = (cx, cy, key) => React.createElement("g", { key }, React.createElement("rect", { x: cx, y: cy, width: 7, height: 7, rx: rounded ? 1.8 : 0, fill: "currentColor" }), React.createElement("rect", { x: cx + 1, y: cy + 1, width: 5, height: 5, rx: rounded ? 1.2 : 0, fill: "var(--qp)" }), React.createElement("rect", { x: cx + 2, y: cy + 2, width: 3, height: 3, rx: rounded ? 0.8 : 0, fill: "currentColor" }), ); const eyes = [eye(pad, pad, "tl"), eye(pad + n - 7, pad, "tr"), eye(pad, pad + n - 7, "bl")]; const plateRect = React.createElement("rect", { key: "plate", x: 0, y: 0, width: total, height: total, rx: 2.2, fill: "var(--qp)" }); return { total, rects, eyes, plateRect }; }, [matrix, pad, rounded]); if (!geom) { return React.createElement("div", { style: { width: size, height: size } }); } return React.createElement( "svg", { id, width: size, height: size, viewBox: `0 0 ${geom.total} ${geom.total}`, style: { display: "block", borderRadius: 18, color: code, "--qp": plate, ...(style || {}) }, shapeRendering: "geometricPrecision", role: "img", }, showPlate ? geom.plateRect : null, geom.rects, geom.eyes ); } // Render the QR to a canvas at hi-res and trigger a PNG download. function downloadQRPng(text, opts) { const o = opts || {}; const plate = o.plate || "#FFFFFF"; const code = o.code || "#101114"; const ecc = o.ecc || "M"; const filename = o.filename || "qrcode.png"; const px = o.px || 1024; const pad = o.pad != null ? o.pad : 4; const matrix = qrMatrix(text, ecc); if (!matrix) return; const n = matrix.length; const total = n + pad * 2; const scale = px / total; const canvas = document.createElement("canvas"); canvas.width = px; canvas.height = px; const ctx = canvas.getContext("2d"); // plate ctx.fillStyle = plate; ctx.fillRect(0, 0, px, px); ctx.fillStyle = code; const radius = (v) => Math.max(0, v); function roundRect(x, y, w, h, r) { r = Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); ctx.fill(); } for (let r = 0; r < n; r++) { for (let c = 0; c < n; c++) { if (!matrix[r][c]) continue; if (inFinder(r, c, n)) continue; const s = 0.88 * scale; const x = (c + pad + 0.06) * scale; const y = (r + pad + 0.06) * scale; roundRect(x, y, s, s, s * 0.42); } } // finder eyes with standard 1:1:3:1:1 proportions (scannable) function eye(cx, cy) { const x = (cx + pad) * scale, y = (cy + pad) * scale; ctx.fillStyle = code; roundRect(x, y, 7 * scale, 7 * scale, 1.8 * scale); ctx.fillStyle = plate; roundRect(x + 1 * scale, y + 1 * scale, 5 * scale, 5 * scale, 1.2 * scale); ctx.fillStyle = code; roundRect(x + 2 * scale, y + 2 * scale, 3 * scale, 3 * scale, 0.8 * scale); } eye(0, 0); eye(n - 7, 0); eye(0, n - 7); canvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 2000); }, "image/png"); } function downloadQRSvg(text, opts) { const o = opts || {}; const plate = o.plate || "#FFFFFF"; const code = o.code || "#101114"; const pad = o.pad != null ? o.pad : 4; const matrix = qrMatrix(text, o.ecc || "M"); if (!matrix) return; const n = matrix.length; const total = n + pad * 2; // No background: only the QR shapes, ready for 3D-print extrusion. let parts = ""; for (let r = 0; r < n; r++) { for (let c = 0; c < n; c++) { if (!matrix[r][c] || inFinder(r, c, n)) continue; parts += ``; } } // rounded-rect subpath (for even-odd ring cutouts) function rr(x, y, w, h, r) { return `M${x + r},${y} h${w - 2 * r} a${r},${r} 0 0 1 ${r},${r} v${h - 2 * r} a${r},${r} 0 0 1 -${r},${r} h-${w - 2 * r} a${r},${r} 0 0 1 -${r},-${r} v-${h - 2 * r} a${r},${r} 0 0 1 ${r},-${r} Z`; } // Finder eye = hollow ring (outer minus middle, true hole) + solid center. function eye(cx, cy) { const x = cx + pad, y = cy + pad; return `` + ``; } parts += eye(0, 0) + eye(n - 7, 0) + eye(0, n - 7); const svg = `${parts}`; const blob = new Blob([svg], { type: "image/svg+xml" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = o.filename || "qrcode.svg"; a.click(); setTimeout(() => URL.revokeObjectURL(url), 2000); } const QRPreviewMemo = React.memo(QRPreview); Object.assign(window, { buildQRPayload, QRPreview: QRPreviewMemo, downloadQRPng, downloadQRSvg, qrContentLabel, });