Files
leggenda-del-regolo/src/engine/risveglio.js
T

91 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
}