// L'incantesimo (story.closeups, type 'incanto'; Andrea, 30 ottobre: «Quanto darei per essere all'Ingorgo stasera?!? // Effetto speciale e compare»). Sopra la scena di adesso, con Andrea dentro: i fasci delle luci da palco che girano // sempre più svelti, i colpi di cassa che fanno pulsare l'aria, il lampo bianco; nel bianco cambia tutto (`swap`, di // solito il flag della festa: lo sfondo diventa quello della notte), poi il bianco sfuma e piovono i coriandoli. // Unità: i pixel della scena 640×360. // I tempi, in secondi: i fasci, il lampo che sale, il cambio (a bianco pieno), il bianco che sfuma, la fine. export const T = { beams: 0.2, flash: 1.9, swap: 2.2, clear: 3.0, end: 3.8 }; const COLORS = ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255']; const clamp01 = k => Math.max(0, Math.min(1, k)); // Un numero a caso ma sempre lo stesso per lo stesso indice: i coriandoli non tremano da un fotogramma all'altro. const hash = n => { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); }; export function newIncanto(id) { return { id, t: 0, age: 0, events: [], beats: 0, swapped: false }; } // Il tempo che passa e i suoni (un colpo di cassa a ogni battito, sempre più fitti; il lampo). Torna 'swap' nel // momento del cambio, 'end' alla fine. export function updateIncanto(c, dt) { c.t += dt; const beats = Math.floor(beatAt(Math.min(c.t, T.flash))); for (; c.beats < beats; c.beats++) c.events.push({ kind: 'incanto-cassa', power: 0.5 + 0.5 * c.beats / 8 }); if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'incanto-lampo', power: 1 }); } if (!c.swapped && c.t >= T.swap) { c.swapped = true; return 'swap'; } return c.t >= T.end ? 'end' : null; } // Quanti battiti sono passati: accelerano, da uno ogni mezzo secondo a uno ogni decimo. export const beatAt = t => t <= 0 ? 0 : 2 * t + 1.6 * t * t; // Quanto è bianco lo schermo: sale fino al cambio, resta pieno un attimo, poi sfuma. export function whiteness(t) { if (t < T.flash) return 0; if (t < T.swap) return clamp01((t - T.flash) / (T.swap - T.flash)); return clamp01(1 - (t - T.swap) / (T.clear - T.swap)); } export function drawIncanto(ctx, c) { const t = c.t, before = t < T.swap; ctx.save(); // Il colpo di cassa: un velo scuro che si apre e si chiude, l'aria che pulsa. if (before) { const b = beatAt(t), pulse = 1 - (b - Math.floor(b)); ctx.fillStyle = `rgba(10,0,30,${0.25 * clamp01(t / 0.6) * pulse})`; ctx.fillRect(0, 0, 640, 360); } // I fasci: partono da sotto, ai lati, e spazzano il cielo; più passa il tempo, più sono e più girano svelti. const strength = before ? clamp01((t - T.beams) / 0.5) : clamp01(1 - (t - T.swap) / 0.6); if (strength > 0) { ctx.globalCompositeOperation = 'lighter'; const n = 2 + Math.min(6, Math.floor(t * 3)); for (let i = 0; i < n; i++) { const from = [i % 2 ? 560 : 80, 372], speed = 0.9 + i * 0.35 + t * 0.9; const a = -Math.PI / 2 + Math.sin(t * speed + i * 1.7) * 0.9, len = 520, half = 0.07; ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.22 * strength})`; ctx.beginPath(); ctx.moveTo(...from); ctx.lineTo(from[0] + Math.cos(a - half) * len, from[1] + Math.sin(a - half) * len); ctx.lineTo(from[0] + Math.cos(a + half) * len, from[1] + Math.sin(a + half) * len); ctx.closePath(); ctx.fill(); } // Le scintille che salgono da terra, a quadratini. for (let i = 0; i < 40; i++) { const life = (t * (0.5 + hash(i)) + hash(i + 50)) % 1, x = 20 + hash(i + 100) * 600, y = 360 - life * 300; ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.9 * strength * (1 - life)})`; ctx.fillRect(Math.round(x), Math.round(y), 2, 2); } ctx.globalCompositeOperation = 'source-over'; } // Il lampo. const white = whiteness(t); if (white > 0) { ctx.fillStyle = `rgba(255,255,250,${white})`; ctx.fillRect(0, 0, 640, 360); } // Dopo il cambio, i coriandoli che cadono sulla festa. if (!before) { const k = (t - T.swap) / (T.end - T.swap); for (let i = 0; i < 70; i++) { const x = (hash(i) * 640 + Math.sin(t * 3 + i) * 8) % 640, y = -10 + (hash(i + 7) * 120 + k * 420 * (0.6 + hash(i + 3) * 0.6)); if (y > 360) continue; ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${clamp01(1.3 - k)})`; ctx.fillRect(Math.round(x), Math.round(y), 3, 2); } } ctx.restore(); }