// 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" }) => (
);
// ─────────────────────────────────────────────────────────────────────
// 1. RETRO (70s warm)
// ─────────────────────────────────────────────────────────────────────
const RetroFront = ({ track, size }) => (
{/* Rounded inner frame */}
★ Side A ★
{track.title}
{track.artist}
);
const RetroBack = ({ track, size }) => (
✶ QRJam ✶
▸ Scan & Play ◂
);
// ─────────────────────────────────────────────────────────────────────
// 2. NEON
// ─────────────────────────────────────────────────────────────────────
const NeonFront = ({ track, size }) => (
{/* Grid lines */}
◢◤ TRACK
{track.title}
{track.artist}
);
const NeonBack = ({ track, size }) => (
);
// ─────────────────────────────────────────────────────────────────────
// 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.title}
{track.artist}
);
const MemphisBack = ({ track, size }) => (
);
// ─────────────────────────────────────────────────────────────────────
// 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.title}
{track.artist}
);
const PartyBack = ({ track, size }) => (
);
// ─────────────────────────────────────────────────────────────────────
// 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.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 (
);
};
// ─────────────────────────────────────────────────────────────────────
// 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 });