Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
84 lines
4.5 KiB
JavaScript
84 lines
4.5 KiB
JavaScript
// 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();
|
||
}
|