// Icons + procedural key-art const Icon = ({ name, size = 16, stroke = 1.6 }) => { const common = { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round" }; switch (name) { case "ios": return ; case "android": return ; case "windows": return ; case "mac": return ; case "steam": return ; case "itch": return ; case "arrow-tr": return ; case "arrow-r": return ; case "arrow-l": return ; case "play": return ; case "x-twitter": return ; case "instagram": return ; case "github": return ; case "sliders": return ; case "download": return ; case "check": return ; default: return null; } }; function storeIcon(key) { if (key === 'appstore') return 'ios'; if (key === 'playstore') return 'android'; if (key === 'steam') return 'steam'; if (key === 'itch') return 'itch'; return 'download'; } // Procedural key-art — deterministic per project function drawKeyArt(canvas, project, variant = 0) { if (!canvas) return; const dpr = Math.min(window.devicePixelRatio || 1, 2); const rect = canvas.getBoundingClientRect(); const w = Math.max(400, rect.width) * dpr; const h = Math.max(250, rect.height) * dpr; canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); const [bg, mid, accent, light] = project.palette; // Bg const grad = ctx.createLinearGradient(0, 0, w, h); grad.addColorStop(0, bg); grad.addColorStop(1, mid); ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h); let seed = variant * 31 + 7; for (let i = 0; i < project.id.length; i++) seed += project.id.charCodeAt(i); const rnd = () => { seed = (seed * 9301 + 49297) % 233280; return seed / 233280; }; if (project.kind === 'game') { const horizonY = h * 0.62; // Sun const sunR = h * 0.28, sunX = w * 0.5, sunY = horizonY - sunR * 0.3; const sg = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunR); sg.addColorStop(0, light); sg.addColorStop(0.45, accent); sg.addColorStop(1, 'transparent'); ctx.fillStyle = sg; ctx.fillRect(sunX - sunR * 1.5, sunY - sunR * 1.5, sunR * 3, sunR * 3); // Sun bands ctx.fillStyle = bg; for (let i = 0; i < 5; i++) { const y = sunY + sunR * (0.2 + i * 0.15); ctx.fillRect(sunX - sunR, y, sunR * 2, h * 0.012); } // Horizon glow const hg = ctx.createLinearGradient(0, horizonY - h * 0.1, 0, horizonY); hg.addColorStop(0, 'transparent'); hg.addColorStop(1, accent + '66'); ctx.fillStyle = hg; ctx.fillRect(0, horizonY - h * 0.1, w, h * 0.1); // Horizon line ctx.strokeStyle = accent; ctx.lineWidth = 2 * dpr; ctx.shadowColor = accent; ctx.shadowBlur = 20 * dpr; ctx.beginPath(); ctx.moveTo(0, horizonY); ctx.lineTo(w, horizonY); ctx.stroke(); ctx.shadowBlur = 0; // Grid ctx.strokeStyle = accent; ctx.lineWidth = 1 * dpr; for (let i = 1; i < 14; i++) { const t = i / 14; const y = horizonY + Math.pow(t, 2.2) * (h - horizonY); ctx.globalAlpha = 0.15 + t * 0.55; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } const vpX = w * 0.5; for (let i = -8; i <= 8; i++) { ctx.globalAlpha = 0.3; ctx.beginPath(); ctx.moveTo(vpX, horizonY); ctx.lineTo(vpX + i * w * 0.14, h); ctx.stroke(); } ctx.globalAlpha = 1; // Stars ctx.fillStyle = light; for (let i = 0; i < 80; i++) { const x = rnd() * w, y = rnd() * (horizonY - 20); const s = rnd() * 1.6 * dpr; ctx.globalAlpha = 0.4 + rnd() * 0.6; ctx.fillRect(x, y, s, s); } ctx.globalAlpha = 1; } else { // App: floating cards const blob = ctx.createRadialGradient(w * 0.75, h * 0.2, 0, w * 0.75, h * 0.2, h * 0.95); blob.addColorStop(0, accent + 'bb'); blob.addColorStop(1, 'transparent'); ctx.fillStyle = blob; ctx.fillRect(0, 0, w, h); for (let i = 0; i < 5; i++) { const cw = w * (0.14 + rnd() * 0.1); const ch = cw * (1.3 + rnd() * 0.4); const cx = w * (0.15 + i * 0.16) + (rnd() - 0.5) * w * 0.04; const cy = h * (0.42 + rnd() * 0.32); const rot = (rnd() - 0.5) * 0.3; ctx.save(); ctx.translate(cx, cy); ctx.rotate(rot); ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(-cw / 2 + 6 * dpr, -ch / 2 + 14 * dpr, cw, ch); const cg = ctx.createLinearGradient(0, -ch / 2, 0, ch / 2); cg.addColorStop(0, light + 'ee'); cg.addColorStop(1, mid); ctx.fillStyle = cg; const r = 16 * dpr, x = -cw / 2, y = -ch / 2; ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + cw - r, y); ctx.arcTo(x + cw, y, x + cw, y + r, r); ctx.lineTo(x + cw, y + ch - r); ctx.arcTo(x + cw, y + ch, x + cw - r, y + ch, r); ctx.lineTo(x + r, y + ch); ctx.arcTo(x, y + ch, x, y + ch - r, r); ctx.lineTo(x, y + r); ctx.arcTo(x, y, x + r, y, r); ctx.closePath(); ctx.fill(); ctx.fillStyle = bg; for (let j = 0; j < 4; j++) { const rowW = cw * (0.35 + rnd() * 0.5); ctx.fillRect(-cw / 2 + 12 * dpr, -ch / 2 + (20 + j * 20) * dpr, rowW, 4 * dpr); } ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(-cw / 2 + cw * 0.15, -ch / 2 + cw * 0.15, 6 * dpr, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } } // Title ctx.font = `600 ${Math.floor(48 * dpr)}px 'Space Grotesk', sans-serif`; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.fillStyle = light; ctx.globalAlpha = 0.95; ctx.fillText(project.title, 32 * dpr, 28 * dpr); ctx.globalAlpha = 1; ctx.font = `500 ${Math.floor(12 * dpr)}px 'JetBrains Mono', monospace`; ctx.fillStyle = accent; ctx.fillText('// ' + project.tagline.en.toUpperCase(), 32 * dpr, 86 * dpr); // Scanline ctx.globalAlpha = 0.06; ctx.fillStyle = '#000'; for (let y = 0; y < h; y += 3 * dpr) ctx.fillRect(0, y, w, 1 * dpr); ctx.globalAlpha = 1; } window.Icon = Icon; window.storeIcon = storeIcon; window.drawKeyArt = drawKeyArt;