// Five card styles for QRJam. Each exports a single Card({track, side, size}) renderer // that draws either the 'front' (artist + title + year) or 'back' (QR + scan instruction). // Cards are square at any size; default 280px is the on-screen preview size for 70mm. // Seeded PRNG for deterministic decoration (confetti positions etc). function seededRand(seedStr) { let h = 2166136261; for (let i = 0; i < seedStr.length; i++) { h ^= seedStr.charCodeAt(i); h = Math.imul(h, 16777619); } return () => { h = Math.imul(h ^ (h >>> 15), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); h ^= h >>> 16; return ((h >>> 0) / 4294967296); }; } // ───────────────────────────────────────────────────────────────────── // Shared bits // ───────────────────────────────────────────────────────────────────── const CardFrame = ({ children, size = 280, bg, style = {}, className = "" }) => (
{children}
); const ScanBadge = ({ color = "#fff", bg = "rgba(0,0,0,.6)", text = "Scan met je camera" }) => (
{text}
); // ───────────────────────────────────────────────────────────────────── // 1. RETRO (70s warm) // ───────────────────────────────────────────────────────────────────── const RetroFront = ({ track, size }) => ( {/* Rounded inner frame */}
★ Side A ★
{track.year}
A.D.
{track.title}
{track.artist}
); const RetroBack = ({ track, size }) => (
✶ QRJam ✶
▸ Scan & Play ◂
); // ───────────────────────────────────────────────────────────────────── // 2. NEON // ───────────────────────────────────────────────────────────────────── const NeonFront = ({ track, size }) => ( {/* Grid lines */}
◢◤ TRACK
{track.year}
{track.title}
{track.artist}
); const NeonBack = ({ track, size }) => (
QRJAM
▸ SCAN ▸ PLAY ▸
); // ───────────────────────────────────────────────────────────────────── // 3. 80s MEMPHIS // ───────────────────────────────────────────────────────────────────── const MemphisDecorations = ({ size, seed }) => { // deterministic positions but mostly fixed for consistent look return ( {/* Squiggle top-right */} {/* Dots cluster top-left */} {/* Triangle bottom-left */} {/* Zigzag bottom-right */} {/* Big shape mid-left */} ); }; const MemphisFront = ({ track, size }) => (
✦ Hit Card ✦
{track.year}
{track.title}
{track.artist}
); const MemphisBack = ({ track, size }) => (
QRJam
▶ Scan & Play
); // ───────────────────────────────────────────────────────────────────── // 4. PARTY // ───────────────────────────────────────────────────────────────────── const Confetti = ({ size, seed }) => { const r = seededRand(seed + "c"); const bits = []; const colors = ["#ff3d8a", "#ffd83d", "#3dffaa", "#3d8aff", "#ff6b3d", "#c93dff"]; for (let i = 0; i < 26; i++) { bits.push({ x: r() * 100, y: r() * 100, rot: r() * 360, color: colors[Math.floor(r() * colors.length)], type: r() > 0.5 ? "rect" : "circle", w: 1.5 + r() * 2, }); } return ( {bits.map((b, i) => b.type === "rect" ? ( ) : ( ) )} ); }; const PartyFront = ({ track, size }) => (
🎉 PARTY HIT
{track.year}
{track.title}
{track.artist}
); const PartyBack = ({ track, size }) => (
QRJam!
📱 Scan & Dans
); // ───────────────────────────────────────────────────────────────────── // 5. VINYL (record label) // ───────────────────────────────────────────────────────────────────── const VinylFront = ({ track, size }) => { const cx = size / 2; const cy = size / 2; const id = `vinyl-${track.id}`; return ( {/* outer paper border */}
{/* curved text path */} {/* vinyl disc */} {/* grooves */} {[0.42, 0.40, 0.38, 0.36, 0.34, 0.32].map((r, i) => ( ))} {/* label */} {/* center hole */} {/* curved title around top */} {track.title.length > 28 ? track.title.slice(0, 26) + "…" : track.title} {/* curved year around bottom */} ★ ★ ★ {track.year} ★ ★ ★ {/* center artist */} {track.artist.length > 18 ? track.artist.slice(0, 16) + "…" : track.artist.toUpperCase()} a QRJam single ); }; const VinylBack = ({ track, size }) => (
Side B
▸ Drop the Needle ◂
); // ───────────────────────────────────────────────────────────────────── // 6. CUSTOM — fully user-defined via a config object // ───────────────────────────────────────────────────────────────────── const CUSTOM_FONTS = { serif: '"Fraunces", "DM Serif Display", Georgia, serif', sans: '"Space Grotesk", "Inter", system-ui, sans-serif', bold: '"Bricolage Grotesque", "Space Grotesk", sans-serif', mono: '"Space Mono", monospace', }; function isLightHex(hex) { const h = String(hex).replace("#", ""); const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0"); const n = parseInt(x.slice(0, 6), 16); if (Number.isNaN(n)) return true; const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; return r * 299 + g * 587 + b * 114 > 148000; } const CustomPattern = ({ pattern, size, color, isLight }) => { const tint = isLight ? "rgba(0,0,0,.06)" : "rgba(255,255,255,.08)"; if (pattern === "none") return null; if (pattern === "dots") return ( ); if (pattern === "lines") return (
); if (pattern === "grain") return (
); if (pattern === "corner-blob") return ( ); if (pattern === "stripes") return (
); return null; }; const CustomFront = ({ track, size, config }) => { const c = { bg:"#1a3a5c", ink:"#ffffff", accent:"#ffd83d", font:"bold", pattern:"dots", ...config }; const light = isLightHex(c.bg); return (
◆ Track
{track.year}
{track.title}
{track.artist}
); }; const CustomBack = ({ track, size, config }) => { const c = { bg:"#1a3a5c", ink:"#ffffff", accent:"#ffd83d", font:"bold", pattern:"dots", ...config }; const light = isLightHex(c.bg); return (
QRJam
▸ Scan & Play ◂
); }; // ───────────────────────────────────────────────────────────────────── // Style registry // ───────────────────────────────────────────────────────────────────── const CARD_STYLES = { retro: { label: "Retro", hint: "Jaren 70 — warme tinten", Front: RetroFront, Back: RetroBack }, neon: { label: "Neon", hint: "Donker met felle glow", Front: NeonFront, Back: NeonBack }, memphis: { label: "80's Memphis",hint: "Geometrisch & speels", Front: MemphisFront, Back: MemphisBack }, party: { label: "Party", hint: "Confetti & knal-kleuren", Front: PartyFront, Back: PartyBack }, vinyl: { label: "Vinyl", hint: "Plaat-label stijl", Front: VinylFront, Back: VinylBack }, custom: { label: "Custom", hint: "Mix je eigen kleuren", Front: CustomFront, Back: CustomBack, custom: true }, }; function Card({ track, style = "retro", side = "front", size = 280, customConfig }) { const S = CARD_STYLES[style] || CARD_STYLES.retro; const Comp = side === "back" ? S.Back : S.Front; return ; } Object.assign(window, { Card, CARD_STYLES, CUSTOM_FONTS });