// 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(); }