/* Turbo Race - canvas painter (neon synthwave) */ window.TurboPainter = (function () { 'use strict'; const ROAD_W = 420; // wereldbreedte (px) const K = 0.32; // verticale schaal: px per eenheid afstand const C = { cyan: '#00f0ff', pink: '#ff3fa4', gold: '#ffd93d', green: '#3dff88', red: '#ff4d6d', }; function rgba(hex, a) { const h = hex.replace('#', ''); return `rgba(${parseInt(h.slice(0, 2), 16)},${parseInt(h.slice(2, 4), 16)},${parseInt(h.slice(4, 6), 16)},${a})`; } function clamp(n, lo, hi) { return n < lo ? lo : (n > hi ? hi : n); } function rrect(ctx, 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(); } // werelds-afstand -> scherm-y function syFor(v, d) { return v.carY - (d - v.me.d) * K; } /* ---------------- ACHTERGROND ---------------- */ function drawBackground(ctx, W, H, t) { const g = ctx.createLinearGradient(0, 0, 0, H); g.addColorStop(0, '#070a20'); g.addColorStop(0.45, '#120c30'); g.addColorStop(1, '#1b0f38'); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); const hx = W / 2 + Math.sin(t * 0.5) * 8; const rad = ctx.createRadialGradient(hx, H * 0.42, 40, hx, H * 0.42, W * 0.75); rad.addColorStop(0, 'rgba(255,63,164,0.20)'); rad.addColorStop(0.5, 'rgba(0,240,255,0.10)'); rad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = rad; ctx.fillRect(0, 0, W, H); // synthwave grid const gridY = H * 0.5; ctx.lineWidth = 1; for (let i = 1; i < 10; i++) { const y = gridY + (H - gridY) * Math.pow(i / 10, 1.7); ctx.strokeStyle = rgba(C.cyan, 0.06 + i * 0.006); ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } for (let i = -7; i <= 7; i++) { const sway = Math.sin(t * 0.7 + i) * (W * 0.008); ctx.strokeStyle = rgba(C.pink, 0.08); ctx.beginPath(); ctx.moveTo(hx + i * 22 + sway, gridY); ctx.lineTo(hx + i * (W / 6), H); ctx.stroke(); } } /* ---------------- WEG ---------------- */ function drawRoad(ctx, v) { const { H, roadLeft, roadW, me, trackLength } = v; const rl = roadLeft, rr = roadLeft + roadW; const ag = ctx.createLinearGradient(rl, 0, rr, 0); ag.addColorStop(0, '#181e38'); ag.addColorStop(0.18, '#222a4e'); ag.addColorStop(0.5, '#2a3460'); ag.addColorStop(0.82, '#202747'); ag.addColorStop(1, '#171c34'); ctx.fillStyle = ag; ctx.fillRect(rl, 0, roadW, H); // horizontale textuur-stroken (scollen) const stripeH = 26; const off = me.d * K; ctx.fillStyle = 'rgba(255,255,255,0.025)'; for (let y = -(off % stripeH); y < H; y += stripeH) { ctx.fillRect(rl + 5, y, roadW - 10, stripeH * 0.55); } // midden-streepjes const period = 74, dashLen = 32; const cOff = (me.d * K) % period; ctx.fillStyle = rgba(C.gold, 0.55); for (let y = -period + cOff; y < H; y += period) { rrect(ctx, rl + roadW / 2 - 2.5, y, 5, dashLen, 2); ctx.fill(); } ctx.globalAlpha = 1; // kantlijnen neon ctx.save(); ctx.shadowBlur = 14; ctx.shadowColor = C.cyan; ctx.strokeStyle = C.cyan; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(rl + 2, 0); ctx.lineTo(rl + 2, H); ctx.stroke(); ctx.shadowColor = C.pink; ctx.strokeStyle = C.pink; ctx.beginPath(); ctx.moveTo(rr - 2, 0); ctx.lineTo(rr - 2, H); ctx.stroke(); ctx.restore(); // stroken net buiten de weg ctx.fillStyle = 'rgba(255,255,255,0.04)'; ctx.fillRect(rl - 14, 0, 12, H); ctx.fillRect(rr + 2, 0, 12, H); // start- en finishlijn roadLine(ctx, v, 0, false); roadLine(ctx, v, trackLength, true); } function roadLine(ctx, v, worldD, checkered) { const sy = syFor(v, worldD); if (sy < -16 || sy > v.H + 16) return; const { roadLeft, roadW } = v; const h = 16; if (checkered) { const sq = 8; for (let x = 0; x * sq < roadW; x++) { for (let y = 0; y * sq < h; y++) { ctx.fillStyle = ((x + y) % 2 === 0) ? '#fff' : '#0b0f2a'; ctx.fillRect(roadLeft + x * sq, sy + y * sq, sq, sq); } } } else { ctx.fillStyle = 'rgba(255,255,255,0.75)'; ctx.fillRect(roadLeft, sy, roadW, h); } } /* ---------------- AUTO'S ---------------- */ function drawCar(ctx, v, p, t) { const w = Math.max(20, v.roadW * 0.062); const h = w * 1.42; const sx = v.roadLeft + p.x * v.scaleX; const sy = syFor(v, p.d); if (sy < -90 || sy > v.H + 90) return; ctx.save(); ctx.translate(sx, sy); if (p.slip) ctx.rotate(Math.sin(t * 40 + p.x) * 0.08); else ctx.rotate((p.steer || 0) * -0.08); // boost-vlam if (p.boost) { const fl = 8 + Math.random() * 10; const fg = ctx.createLinearGradient(0, h * 0.4, 0, h * 0.4 + fl); fg.addColorStop(0, rgba(C.cyan, 0.9)); fg.addColorStop(1, rgba(C.cyan, 0)); ctx.fillStyle = fg; ctx.beginPath(); ctx.moveTo(-w * 0.28, h * 0.38); ctx.lineTo(0, h * 0.38 + fl); ctx.lineTo(w * 0.28, h * 0.38); ctx.closePath(); ctx.fill(); } // schild-bubbel if (p.shield) { ctx.strokeStyle = rgba(C.green, 0.65); ctx.lineWidth = 2; ctx.shadowBlur = 12; ctx.shadowColor = C.green; ctx.beginPath(); ctx.ellipse(0, 0, w * 0.8, h * 0.75, 0, 0, 7); ctx.stroke(); ctx.shadowBlur = 0; } // schaduw ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.ellipse(2, 2, w * 0.55, h * 0.55, 0, 0, 7); ctx.fill(); // carrosserie met glow ctx.shadowColor = p.color; ctx.shadowBlur = p.isMe ? 22 : 12; const body = ctx.createLinearGradient(0, -h / 2, w, h / 2); body.addColorStop(0, lighten(p.color, 18)); body.addColorStop(0.5, p.color); body.addColorStop(1, darken(p.color, 12)); ctx.fillStyle = body; rrect(ctx, -w / 2, -h / 2, w, h, w * 0.32); ctx.fill(); ctx.shadowBlur = 0; ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1; rrect(ctx, -w / 2, -h / 2, w, h, w * 0.32); ctx.stroke(); // voorruit ctx.fillStyle = 'rgba(10,16,46,0.9)'; rrect(ctx, -w * 0.34, -h * 0.16, w * 0.68, h * 0.34, 3); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.25)'; rrect(ctx, -w * 0.34, -h * 0.16, w * 0.68, 2.5, 2); ctx.fill(); // zijwielen ctx.fillStyle = '#0a0d1c'; rrect(ctx, -w * 0.52, -h * 0.34, w * 0.16, h * 0.28, 2); ctx.fill(); rrect(ctx, w * 0.36, -h * 0.34, w * 0.16, h * 0.28, 2); ctx.fill(); rrect(ctx, -w * 0.52, h * 0.1, w * 0.16, h * 0.3, 2); ctx.fill(); rrect(ctx, w * 0.36, h * 0.1, w * 0.16, h * 0.3, 2); ctx.fill(); // vlag bij gefinisht if (p.finished) { ctx.fillStyle = '#dfe7ff'; ctx.fillRect(w * 0.3, -h * 0.72, 3, h * 0.7); const sq = 3; for (let i = 0; i < 3; i++) { for (let j = 0; j < 2; j++) { ctx.fillStyle = ((i + j) % 2 === 0) ? '#fff' : '#111'; ctx.fillRect(w * 0.3 + 3 + i * sq, -h * 0.72 + j * sq, sq, sq); } } } // naamlabel ctx.font = '600 10px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.fillStyle = p.isMe ? '#fff' : rgba('#eaf4ff', 0.92); ctx.shadowColor = '#000'; ctx.shadowBlur = 4; ctx.fillText(p.name + (p.isMe ? ' • jij' : ''), 0, -h * 0.62); ctx.restore(); } function hexToRgb(hex) { const h = hex.replace('#', ''); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; } function lighten(hex, amt) { const [r, g, b] = hexToRgb(hex); const f = (x) => Math.round(clamp(x + amt, 0, 255)); return `rgb(${f(r)},${f(g)},${f(b)})`; } function darken(hex, amt) { const [r, g, b] = hexToRgb(hex); const f = (x) => Math.round(clamp(x - amt, 0, 255)); return `rgb(${f(r)},${f(g)},${f(b)})`; } /* ---------------- FX / DEELTJES ---------------- */ function drawFx(ctx, fx, t) { for (let i = fx.length - 1; i >= 0; i--) { const f = fx[i]; if (!f) continue; const lifeF = 1 - f.life / f.maxLife; if (f.kind === 'pulse') { ctx.strokeStyle = rgba(f.color, lifeF * 0.7); ctx.lineWidth = 2.5; ctx.beginPath(); ctx.arc(f.sx, f.sy, f.size * (1 + (1 - lifeF) * 3), 0, 7); ctx.stroke(); } else { ctx.globalAlpha = lifeF; ctx.fillStyle = f.color; ctx.shadowBlur = f.size * 2; ctx.shadowColor = f.color; ctx.beginPath(); ctx.arc(f.sx, f.sy, f.size * (0.3 + lifeF * 0.7), 0, 7); ctx.fill(); ctx.shadowBlur = 0; } } ctx.globalAlpha = 1; } function drawPylon(ctx, v, sx, sy, t) { const s = Math.max(13, v.roadW * 0.05); ctx.save(); ctx.translate(sx, sy); ctx.shadowBlur = 14; ctx.shadowColor = C.gold; const grad = ctx.createLinearGradient(0, -s, 0, s); grad.addColorStop(0, '#ff7a1a'); grad.addColorStop(0.5, '#20264a'); grad.addColorStop(1, '#ffd93d'); ctx.fillStyle = grad; ctx.beginPath(); ctx.moveTo(-s * 0.42, -s); ctx.lineTo(s * 0.42, -s); ctx.lineTo(s * 0.2, s); ctx.lineTo(-s * 0.2, s); ctx.closePath(); ctx.fill(); ctx.fillStyle = C.red; ctx.shadowColor = C.red; ctx.shadowBlur = 8 + Math.sin(t * 10) * 5; ctx.beginPath(); ctx.arc(0, -s - 4, 3, 0, 7); ctx.fill(); ctx.restore(); } function drawOil(ctx, v, sx, sy) { const rx = Math.max(24, v.roadW * 0.10); ctx.save(); ctx.translate(sx, sy); const og = ctx.createRadialGradient(0, 0, 2, 0, 0, rx); og.addColorStop(0, 'rgba(40,20,60,0.9)'); og.addColorStop(0.7, 'rgba(60,30,80,0.75)'); og.addColorStop(1, 'rgba(80,40,110,0.45)'); ctx.fillStyle = og; ctx.beginPath(); ctx.ellipse(0, 0, rx, rx * 0.42, 0, 0, 7); ctx.fill(); ctx.strokeStyle = rgba(C.pink, 0.35); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.ellipse(0, 0, rx * 0.6, rx * 0.24, 0, 0, 7); ctx.stroke(); ctx.restore(); } function drawPickup(ctx, v, sx, sy, kind, t) { const s = Math.max(11, v.roadW * 0.042); const pulse = 1 + Math.sin(t * 4 + sx * 0.01) * 0.12; ctx.save(); ctx.translate(sx, sy + Math.sin(t * 3) * 4); ctx.scale(pulse, pulse); const col = kind === 'boost' ? C.cyan : C.green; ctx.shadowBlur = 18; ctx.shadowColor = col; ctx.strokeStyle = col; ctx.fillStyle = rgba(col, 0.18); ctx.lineWidth = 3; ctx.rotate(t * 1.5); ctx.beginPath(); if (kind === 'boost') { ctx.moveTo(0, -s); ctx.lineTo(s, 0); ctx.lineTo(0, s); ctx.lineTo(-s, 0); ctx.closePath(); } else { for (let i = 0; i < 6; i++) { const a = (i / 6) * Math.PI * 2; const r = (i % 2 === 0) ? s : s * 0.62; ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); if (i === 0) { ctx.moveTo(Math.cos(a) * r, Math.sin(a) * r); ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r); } } ctx.closePath(); } ctx.fill(); ctx.stroke(); ctx.rotate(-t * 1.5); ctx.fillStyle = '#fff'; if (kind === 'boost') { ctx.beginPath(); ctx.moveTo(s * 0.12, -s * 0.55); ctx.lineTo(-s * 0.45, s * 0.25); ctx.lineTo(-s * 0.02, s * 0.25); ctx.lineTo(-s * 0.12, s * 0.55); ctx.lineTo(s * 0.45, -s * 0.25); ctx.lineTo(s * 0.02, -s * 0.25); ctx.closePath(); ctx.fill(); } else { ctx.beginPath(); ctx.moveTo(0, -s * 0.55); ctx.lineTo(s * 0.5, -s * 0.3); ctx.lineTo(s * 0.5, s * 0.15); ctx.lineTo(0, s * 0.55); ctx.lineTo(-s * 0.5, s * 0.15); ctx.lineTo(-s * 0.5, -s * 0.3); ctx.closePath(); ctx.fill(); ctx.strokeStyle = '#0b0f2a'; ctx.stroke(); } ctx.restore(); } /* ---------------- HOOFD-RENDER ---------------- */ function render(ctx, o) { const t = o.t / 1000; const W = o.W, H = o.H; const roadW = Math.min(W * 0.88, 460); const roadLeft = (W - roadW) / 2; const scaleX = roadW / ROAD_W; const carY = Math.min(H * 0.62, H - 140); const v = { W, H, roadLeft, roadW, scaleX, carY, t, me: { d: o.me.d }, trackLength: o.trackLength, }; ctx.save(); if (o.shake) { ctx.translate((Math.random() - 0.5) * o.shake, (Math.random() - 0.5) * o.shake); } drawBackground(ctx, W, H, t); drawRoad(ctx, v); // obstakels + power-ups (alleen zichtbare) const top = -80, bot = H + 80; if (o.track) { for (const ob of o.track.obstacles) { const sy = syFor(v, ob.d); if (sy < top || sy > bot) continue; const sx = roadLeft + ob.x * scaleX; if (ob.type === 'oil') drawOil(ctx, v, sx, sy); else drawPylon(ctx, v, sx, sy, t); } for (const pu of o.track.powerups) { const sy = syFor(v, pu.d); if (sy < top || sy > bot) continue; const sx = roadLeft + pu.x * scaleX; drawPickup(ctx, v, sx, sy, pu.kind, t); } } // auto's (achterste eerst) const cars = (o.cars || []).slice().sort((a, b) => a.d - b.d); for (const p of cars) drawCar(ctx, v, p, t); drawFx(ctx, o.fx || [], t); ctx.restore(); // snelheidsstrepen bij boost if (o.speedFactor > 1.15) { ctx.strokeStyle = 'rgba(255,255,255,0.12)'; ctx.lineWidth = 2; for (let i = 0; i < 8; i++) { const x = (Math.sin(t * 3 + i * 1.7) * 0.5 + 0.5) * W; const len = 40 + ((t * 300) % 40); ctx.beginPath(); ctx.moveTo(x, H); ctx.lineTo(x, H - len - ((t * 400) % 60)); ctx.stroke(); } } } return { render }; })();