// 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;