Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
91 lines
4.3 KiB
JavaScript
91 lines
4.3 KiB
JavaScript
// Il risveglio (story.closeups, type 'risveglio'; Andrea, 31 ottobre: «si ritrova aprendo piano piano gli occhi su
|
||
// una panchina, come svegliato da una notte brava, con effetto annebbiato»). Sopra la scena di adesso, vista da
|
||
// dentro gli occhi: le palpebre si aprono a fatica, ricascano, si riaprono; tutto è sfocato e doppio, e ondeggia.
|
||
// Le battute (`lines`, ognuna col suo `at` in secondi) partono mentre gli occhi si aprono; mentre Andrea parla il
|
||
// tempo del risveglio si ferma (la battuta va letta), l'ondeggiare no. Unità: i pixel della scena 640×360.
|
||
|
||
// Quanto sono aperti gli occhi (0 chiusi, 1 aperti) nel tempo: tre tentativi, l'ultimo è quello buono.
|
||
const OPEN = [[0, 0], [1.4, 0], [2.1, 0.22], [2.5, 0.04], [2.9, 0.04], [3.8, 0.5], [4.15, 0.12], [4.45, 0.2], [5.6, 0.85], [5.85, 0.55], [6.9, 1]];
|
||
export const END = 8.2;
|
||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||
const smooth = k => k * k * (3 - 2 * k);
|
||
|
||
export function openness(t) {
|
||
for (let i = 1; i < OPEN.length; i++) {
|
||
const [t1, o1] = OPEN[i];
|
||
if (t <= t1) { const [t0, o0] = OPEN[i - 1]; return o0 + (o1 - o0) * smooth(clamp01((t - t0) / (t1 - t0))); }
|
||
}
|
||
return 1;
|
||
}
|
||
// La nebbia: sfocatura e vista doppia, che si diradano fino a sparire alla fine.
|
||
export const haze = t => 1 - smooth(clamp01((t - 1.4) / (END - 1.6)));
|
||
|
||
export function newRisveglio(id) { return { id, t: 0, age: 0, fired: 0 }; }
|
||
|
||
// Il tempo che passa: torna le battute da dire adesso, e 'end' alla fine.
|
||
export function updateRisveglio(c, def, dt) {
|
||
c.t += dt;
|
||
const lines = def.lines ?? [], due = [];
|
||
while (c.fired < lines.length && c.t >= lines[c.fired].at) { const { at, ...e } = lines[c.fired++]; due.push(e); }
|
||
return { due, end: c.t >= END && c.fired >= lines.length };
|
||
}
|
||
|
||
// La nebbia è lo sfondo rimpicciolito e riallargato morbido, sopra la scena: niente filtri. Sfocare con ctx.filter
|
||
// (rileggendo la tela) o col filter CSS sulla tela rallentava tutto il gioco anche a risveglio finito.
|
||
const SMALL = [80, 45];
|
||
let small = null, smallOf = null;
|
||
function blurred(bg) {
|
||
if (smallOf !== bg) {
|
||
small ??= Object.assign(document.createElement('canvas'), { width: SMALL[0], height: SMALL[1] });
|
||
const s = small.getContext('2d');
|
||
s.imageSmoothingEnabled = true;
|
||
s.drawImage(bg, 0, 0, ...SMALL);
|
||
smallOf = bg;
|
||
}
|
||
return small;
|
||
}
|
||
|
||
export function drawRisveglio(ctx, c, bg) {
|
||
const t = c.t, h = haze(t), open = openness(t), W = 640, H = 360;
|
||
ctx.save();
|
||
// La scena annebbiata (lo sfondo sfocato sopra) e la vista doppia (una seconda copia, spostata, che ondeggia).
|
||
if (h > 0.01 && bg && typeof document !== 'undefined') {
|
||
const fog = blurred(bg), sway = 14 * h, dx = Math.sin(c.age * 1.3) * sway, dy = Math.sin(c.age * 0.9 + 1) * sway * 0.35;
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.globalAlpha = 0.85 * h;
|
||
ctx.drawImage(fog, 0, 0, W, H);
|
||
ctx.globalAlpha = 0.4 * h;
|
||
ctx.drawImage(fog, dx, dy, W, H);
|
||
ctx.globalAlpha = 1;
|
||
ctx.imageSmoothingEnabled = false;
|
||
}
|
||
// La luce del mattino che abbaglia.
|
||
if (h > 0.01) {
|
||
ctx.fillStyle = `rgba(255,244,214,${0.22 * h})`;
|
||
ctx.fillRect(0, 0, W, H);
|
||
}
|
||
// Gli angoli scuri, che si aprono con gli occhi.
|
||
const v = ctx.createRadialGradient(W / 2, H / 2, 60 + 220 * open, W / 2, H / 2, 420);
|
||
v.addColorStop(0, 'rgba(10,2,4,0)');
|
||
v.addColorStop(1, `rgba(10,2,4,${0.85 * (1 - open * 0.75) * Math.max(h, 0.3)})`);
|
||
ctx.fillStyle = v;
|
||
ctx.fillRect(0, 0, W, H);
|
||
// Le palpebre: sopra e sotto, col bordo curvo; il bordo morbido sono quattro passate sempre più chiuse e più
|
||
// scure. Chiuse, si vede il rosso scuro della luce che passa.
|
||
if (open < 0.999) {
|
||
const mid = H / 2, lid = (gap, bulge, sign) => {
|
||
const edge = mid + sign * gap, side = edge - sign * bulge, far = sign < 0 ? -20 : H + 20;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-20, far); ctx.lineTo(W + 20, far); ctx.lineTo(W + 20, side);
|
||
ctx.quadraticCurveTo(W / 2, edge * 2 - side, -20, side);
|
||
ctx.closePath(); ctx.fill();
|
||
};
|
||
for (let i = 0; i < 4; i++) {
|
||
const gap = 200 * open + (3 - i) * 4, bulge = 70 + 60 * open;
|
||
ctx.fillStyle = `rgba(22,4,6,${i === 3 ? 1 : 0.3})`;
|
||
lid(gap, bulge, -1); lid(gap, bulge, 1);
|
||
}
|
||
}
|
||
ctx.restore();
|
||
}
|