// La macchina del tempo vista da fuori (story.closeups, type 'corsa'; Andrea, 30 ottobre). In partenza ('parti') va via // giù per la strada accelerando, il fulmine la prende, il bianco, e restano due sgommate di fuoco che bruciano e si // spengono. All'arrivo ('arriva') il fulmine in fondo alla strada, e lei viene avanti e ci passa accanto, lasciando le // stesse strisce (davanti a noi: dietro alla macchina non si vedrebbero). Sopra lo sfondo della scena (`scene`), coi fotogrammi di Codex (`frames`: dietro, dietro coi lampi, // davanti coi lampi). Unità: i pixel della scena 640×360. // I tempi, in secondi. Partenza: ferma che si carica, la corsa, il fulmine, il bianco, le fiamme, la fine. export const PARTI = { charge: 0.6, run: 2.2, bolt: 2.3, flash: 0.35, burn: 4.6, end: 5.2 }; // Arrivo: il fulmine in fondo, la corsa verso di noi fin fuori dal quadro, le fiamme, la fine. export const ARRIVA = { bolt: 0.3, flash: 0.35, run: 1.5, burn: 4, end: 4.6 }; const lerp = (a, b, k) => a + (b - a) * k; const clamp01 = k => Math.max(0, Math.min(1, k)); // Dove sta la macchina a un punto `k` del percorso (0 vicina, 1 in fondo): posizione delle ruote e grandezza. export function along(def, k) { const [a, b] = def.path; return { x: lerp(a[0], b[0], k), y: lerp(a[1], b[1], k), scale: lerp(a[2], b[2], k) }; } // Il punto del percorso al tempo t: in partenza accelera (da fermo); all'arrivo viene avanti dal fondo fin oltre il // punto 0 (fuori dal quadro, accanto a noi). export function progress(c, def) { if (def.mode === 'arriva') return 1 - clamp01((c.t - ARRIVA.bolt) / ARRIVA.run); const k = clamp01((c.t - PARTI.charge) / (PARTI.run - PARTI.charge)); return Math.min(0.92, k * k); } // Si vede la macchina? In partenza fino al fulmine; all'arrivo dal fulmine in poi. export function carShown(c, def) { return def.mode === 'arriva' ? c.t >= ARRIVA.bolt + 0.05 && c.t < ARRIVA.bolt + ARRIVA.run : c.t < PARTI.bolt + 0.05; } // Dove arriva il fulmine: sulla macchina in partenza, in fondo alla strada all'arrivo. export function boltAt(def) { return def.mode === 'arriva' ? along(def, 1) : along(def, progress({ t: PARTI.bolt }, def)); } const T = def => def.mode === 'arriva' ? ARRIVA : PARTI; export function newCorsa(id) { return { id, t: 0, age: 0, events: [] }; } // Il tempo che passa e i suoni; vero quando è finita. export function updateCorsa(c, def, dt) { c.t += dt; const times = T(def); if (def.mode !== 'arriva' && !c.charge && c.t >= 0.1) { c.charge = true; c.events.push({ kind: 'gara-turbo', power: 1 }); } if (!c.boom && c.t >= times.bolt) { c.boom = true; c.events.push({ kind: 'corsa-tuono', power: 1 }); } if (!c.fire && c.t >= (def.mode === 'arriva' ? ARRIVA.bolt + 0.2 : PARTI.charge + 0.3)) { c.fire = true; c.events.push({ kind: 'corsa-fuoco', power: 1 }); } return c.t >= times.end; } // Le due ruote posteriori (o anteriori) a un punto del percorso: una a sinistra e una a destra. function wheels(def, k) { const p = along(def, k), half = def.track * p.scale; return [[p.x - half, p.y], [p.x + half, p.y]]; } // Le sgommate di fuoco: da dove è partita a dove è arrivata, fiammelle che tremano, più piccole in fondo, che si // spengono dopo `burn`. function fire(ctx, c, def) { const times = T(def), shown = def.mode === 'arriva' ? [Math.max(progress(c, def), 0), 1] : [0, progress({ t: Math.min(c.t, times.bolt) }, def)]; const fade = c.t < times.burn ? 1 : clamp01(1 - (c.t - times.burn) / (times.end - times.burn)); if (fade <= 0 || shown[1] - shown[0] < 0.005) return; for (let side = 0; side < 2; side++) { // Prima il segno nero bruciato, poi le fiamme sopra. const [a, b] = [wheels(def, shown[0])[side], wheels(def, shown[1])[side]]; ctx.strokeStyle = `rgba(30,18,10,${0.7 * Math.max(fade, 0.5)})`; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke(); const n = 70; for (let i = 0; i <= n; i++) { const k = lerp(shown[0], shown[1], i / n), [x, y] = wheels(def, k)[side], s = Math.max(0.12, along(def, k).scale); // Lingue di fuoco di altezza diversa, che tremano: rosso fuori, arancio, il cuore giallo. const flick = 0.55 + 0.45 * Math.sin(c.t * 19 + i * 2.3 + side * 2) * Math.sin(c.t * 7 + i * 0.9); const h = (16 + 22 * flick) * s * fade, w = Math.max(2, 9 * s); if (h < 1.5) continue; ctx.fillStyle = `rgba(255,120,30,${0.18 * fade})`; ctx.fillRect(Math.round(x - w), Math.round(y - h * 1.2), Math.ceil(w * 2), Math.ceil(h * 1.2)); ctx.fillStyle = `rgba(220,50,20,${0.9 * fade})`; ctx.fillRect(Math.round(x - w / 2), Math.round(y - h), Math.ceil(w), Math.ceil(h)); ctx.fillStyle = `rgba(255,${Math.round(140 + 60 * flick)},30,${0.95 * fade})`; ctx.fillRect(Math.round(x - w / 3), Math.round(y - h * 0.7), Math.ceil(w * 0.66), Math.ceil(h * 0.7)); ctx.fillStyle = `rgba(255,245,170,${fade})`; ctx.fillRect(Math.round(x - w / 6), Math.round(y - h * 0.35), Math.max(1, Math.ceil(w / 3)), Math.ceil(h * 0.35)); // Un filo di fumo che sale quando le fiamme calano. if (c.t > times.burn - 1 && i % 8 === 0) { ctx.fillStyle = `rgba(90,90,96,${0.25 * fade})`; ctx.fillRect(Math.round(x - 2 * s), Math.round(y - h - 6 * s - ((c.t * 18 + i) % 14) * s), Math.ceil(4 * s), Math.ceil(4 * s)); } } } } // Il fulmine: una saetta spezzata dal cielo fino al punto, con due rami. function bolt(ctx, c, def) { const times = T(def), dt = c.t - times.bolt; if (dt < -0.12 || dt > 0.28) return; const p = boltAt(def), seed = Math.floor(c.t * 30); ctx.save(); for (const [width, color] of [[6, 'rgba(140,200,255,0.5)'], [2, '#ffffff']]) { ctx.strokeStyle = color; ctx.lineWidth = width; ctx.beginPath(); let x = p.x + 40, y = 0; ctx.moveTo(x, y); const steps = 9; for (let i = 1; i <= steps; i++) { x = lerp(p.x + 40, p.x, i / steps) + (i < steps ? ((seed * (i + 5) * 53) % 30) - 15 : 0); y = lerp(0, p.y - 12 * p.scale, i / steps); ctx.lineTo(x, y); } ctx.stroke(); } ctx.restore(); } export function drawCorsa(ctx, c, def, { bg, frames, tint }) { const times = T(def); ctx.save(); ctx.imageSmoothingEnabled = false; // La strada trema un po' quando la macchina si carica e quando arriva il fulmine. const shake = (def.mode !== 'arriva' && c.t < PARTI.charge + 0.4 ? 1.5 : 0) + (Math.abs(c.t - times.bolt) < 0.25 ? 3 : 0); ctx.translate(Math.round(Math.sin(c.t * 57) * shake), Math.round(Math.cos(c.t * 43) * shake)); if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4a4a50'; ctx.fillRect(0, 0, 640, 360); } if (tint) { ctx.fillStyle = tint; ctx.fillRect(0, 0, 640, 360); } fire(ctx, c, def); if (carShown(c, def)) { const k = progress(c, def), p = along(def, k); // Da dietro: coi lampi mentre si carica e quando corre forte; davanti (all'arrivo) coi lampi finché frena. const img = def.mode === 'arriva' ? frames?.davanti : c.t > PARTI.charge * 0.5 && Math.floor(c.t * 14) % 3 !== 0 ? frames?.lampi : frames?.dietro; if (img) { const w = def.width * p.scale, h = w * img.height / img.width; ctx.drawImage(img, Math.round(p.x - w / 2), Math.round(p.y - h), Math.round(w), Math.round(h)); } else { ctx.fillStyle = '#c8ccd2'; ctx.fillRect(Math.round(p.x - 40 * p.scale), Math.round(p.y - 40 * p.scale), Math.round(80 * p.scale), Math.round(40 * p.scale)); } } bolt(ctx, c, def); ctx.restore(); // Il bianco del fulmine, che sfuma. const f = c.t - times.bolt; if (f >= 0 && f < times.flash) { ctx.fillStyle = `rgba(255,255,255,${1 - f / times.flash})`; ctx.fillRect(0, 0, 640, 360); } }