From b9bd50c8c330b70d6f82dffe901bcd5c68e8d491 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sat, 31 Oct 2026 10:59:03 +0100 Subject: [PATCH] =?UTF-8?q?Il=20risveglio=20non=20rallenta=20pi=C3=B9=20il?= =?UTF-8?q?=20gioco:=20la=20nebbia=20=C3=A8=20lo=20sfondo=20sfocato=20dise?= =?UTF-8?q?gnato=20sopra,=20senza=20filtri=20(ctx.filter=20e=20il=20filter?= =?UTF-8?q?=20CSS=20sulla=20tela=20facevano=20andare=20a=20passo=20d'uomo?= =?UTF-8?q?=20tutto,=20anche=20dopo),=20e=20l'iframe=20del=20muro=20c'?= =?UTF-8?q?=C3=A8=20solo=20a=20muro=20aperto;=20le=20prove=20nel=20browser?= =?UTF-8?q?=20aspettano=20la=20fine=20del=20risveglio=20e=20l'avvio=20del?= =?UTF-8?q?=20gioco?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T --- index.html | 1 - src/engine/render.js | 2 +- src/engine/risveglio.js | 70 +++++++++++++++++++++--------------- src/main.js | 16 +++++---- style.css | 1 - tests/browser.mjs | 2 ++ tests/multimedia.browser.mjs | 7 ++-- tests/telefono.browser.mjs | 2 +- 8 files changed, 60 insertions(+), 41 deletions(-) diff --git a/index.html b/index.html index 885e5cb..fd58a96 100644 --- a/index.html +++ b/index.html @@ -15,7 +15,6 @@
-
diff --git a/src/engine/render.js b/src/engine/render.js index 1ca7f93..6e1cdbe 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -924,7 +924,7 @@ function drawCloseup(ctx, game, time) { // L'incantesimo va sopra la scena (con Andrea dentro), già disegnata. if (def.type === 'incanto') { drawIncanto(ctx, c); return; } // Il risveglio, visto da dentro gli occhi: anche lui sopra la scena già disegnata. - if (def.type === 'risveglio') { drawRisveglio(ctx, c); return; } + if (def.type === 'risveglio') { drawRisveglio(ctx, c, backgrounds.get(game.backdrop())); return; } // Il telefonino del Barone: sale come il libro, e il sito nello schermo si allarga a tutta la scena. if (def.type === 'telefono') { const pic = src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); }; diff --git a/src/engine/risveglio.js b/src/engine/risveglio.js index 032b44f..8c60365 100644 --- a/src/engine/risveglio.js +++ b/src/engine/risveglio.js @@ -30,24 +30,37 @@ export function updateRisveglio(c, def, dt) { return { due, end: c.t >= END && c.fired >= lines.length }; } -let scratch = null; -export function drawRisveglio(ctx, c) { +// 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: sfocata, e una seconda copia spostata che ondeggia (la vista doppia). - if (h > 0.01 && typeof document !== 'undefined') { - if (!scratch) { scratch = document.createElement('canvas'); scratch.width = W; scratch.height = H; } - const s = scratch.getContext('2d'); - s.clearRect(0, 0, W, H); - s.drawImage(ctx.canvas, 0, 0, W, H); - const sway = 14 * h, dx = Math.sin(c.age * 1.3) * sway, dy = Math.sin(c.age * 0.9 + 1) * sway * 0.35; - ctx.filter = `blur(${(5 * h).toFixed(2)}px)`; - ctx.drawImage(scratch, 0, 0); - ctx.globalAlpha = 0.45 * h; - ctx.drawImage(scratch, dx, dy); + // 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.filter = 'none'; - // La luce del mattino che abbaglia. + 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); } @@ -57,20 +70,21 @@ export function drawRisveglio(ctx, c) { 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 e morbido; chiuse, si vede il rosso scuro della luce che passa. + // 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 gap = 200 * open, mid = H / 2, bulge = 70 + 60 * open; - ctx.filter = 'blur(6px)'; - ctx.fillStyle = 'rgb(22,4,6)'; - ctx.beginPath(); - ctx.moveTo(-20, -20); ctx.lineTo(W + 20, -20); ctx.lineTo(W + 20, mid - gap + bulge); - ctx.quadraticCurveTo(W / 2, mid - gap - bulge, -20, mid - gap + bulge); - ctx.closePath(); ctx.fill(); - ctx.beginPath(); - ctx.moveTo(-20, H + 20); ctx.lineTo(W + 20, H + 20); ctx.lineTo(W + 20, mid + gap - bulge); - ctx.quadraticCurveTo(W / 2, mid + gap + bulge, -20, mid + gap - bulge); - ctx.closePath(); ctx.fill(); - ctx.filter = 'none'; + 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(); } diff --git a/src/main.js b/src/main.js index 7738de0..e5b6985 100644 --- a/src/main.js +++ b/src/main.js @@ -1098,12 +1098,16 @@ function updateUi() { setProp($('spots-button'), 'hidden', !running || !!game.closeup); setProp($('notebook-button'), 'hidden', !running || !story.notebook || !!game.closeup); setProp($('closeup-close'), 'hidden', !running || !game.closeup); - // Il Muro del Sanpierino: a muro aperto sul telefonino del Barone, la pagina vera sopra la scena. Ogni volta che si - // riapre si ricarica (i messaggi nuovi della storia). - const muro = $('muro'), wall = running && c && story.closeups[c.id]?.type === 'telefono' && c.step === 'wall'; - if (wall && muro.hidden) muro.src = versioned('assets/muro/index.html'); - if (!wall && !muro.hidden) muro.src = 'about:blank'; - setProp(muro, 'hidden', !wall); + // Il Muro del Sanpierino: a muro aperto sul telefonino del Barone, la pagina vera sopra la scena. L'iframe c'è solo + // allora (sempre presente, anche nascosto, rallentava la pagina): ogni volta che si riapre si ricarica, coi + // messaggi nuovi della storia. + const wall = running && c && story.closeups[c.id]?.type === 'telefono' && c.step === 'wall'; + let muro = $('muro'); + if (wall && !muro) { + muro = Object.assign(document.createElement('iframe'), { id: 'muro', title: 'Il Muro del Sanpierino', src: versioned('assets/muro/index.html') }); + canvas.after(muro); + } + if (!wall && muro) muro.remove(); if (wall) muro.style.setProperty('--muro-scale', String(canvas.clientWidth / 1067)); setText($('closeup-close'), torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : mg ? c.step === 'result' ? mg.back : 'Lascia stare' : c && story.closeups[c.id]?.type === 'risveglio' ? 'Salta' : 'Chiudi'); setProp($('panel'), 'inert', !!c); diff --git a/style.css b/style.css index e5c7ef3..a436ada 100644 --- a/style.css +++ b/style.css @@ -86,7 +86,6 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h sito, rimpicciolita sulla scena da main.js (--muro-scale). */ #muro { position: absolute; left: 0; top: 0; width: 1067px; height: 600px; border: 0; z-index: 1; background: #f7e1c0; transform: scale(var(--muro-scale, 0.6)); transform-origin: 0 0; } -#muro[hidden] { display: none; } /* Il primo piano del libro si chiude da qui (o con Esc). */ #closeup-close { position: absolute; right: 10px; bottom: 10px; z-index: 2; font: bold clamp(10px, 1.5vw, 15px) var(--font); padding: .45em 1.2em; border-radius: 6px; border: 0; cursor: pointer; color: var(--bg); background: var(--accent); } diff --git a/tests/browser.mjs b/tests/browser.mjs index 01594e4..cdf1cf7 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -56,6 +56,8 @@ try { // Il cartello d'apertura, poi da solo il titolo. assert.match(await page.textContent('#splash'), /Manzolo\s*\(Poca\)\s*Art\s*Presents/); assert.ok(await page.isHidden('#title'), 'il titolo aspetta la fine del cartello'); + // Il gioco parte con un import() dinamico, che l'evento load della pagina non aspetta. + await page.waitForFunction(() => window.jingle, null, { timeout: 10000 }); assert.match(await page.evaluate(() => jingle.src), /assets\/audio\/cartello\.mp3\?v=\d+\.\d+\.\d+$/, 'la musichetta del cartello'); assert.equal(await page.evaluate(() => music.want), null, 'la musica del titolo aspetta il titolo'); await page.waitForTimeout(2800); diff --git a/tests/multimedia.browser.mjs b/tests/multimedia.browser.mjs index 9e365d5..018d0c5 100644 --- a/tests/multimedia.browser.mjs +++ b/tests/multimedia.browser.mjs @@ -8,9 +8,10 @@ export async function checkMultimedia(browser, port, out, errors) { await page.click('#splash'); await page.click('#new-game'); await page.click('[data-rione="campomigliaio"]'); await page.click('#skip-intro'); const settle = async () => { - for (let i = 0; i < 200; i++) { + for (let i = 0; i < 600; i++) { if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space'); - if (await page.evaluate(() => game.closeup || game.idle)) return; + // Il risveglio dell'inizio è un primo piano che va avanti da solo: si aspetta che finisca. + if (await page.evaluate(() => (game.closeup && game.story.closeups[game.closeup.id].type !== 'risveglio') || game.idle)) return; await page.waitForTimeout(30); } throw new Error('la sequenza non si ferma'); @@ -56,10 +57,10 @@ export async function checkMultimedia(browser, port, out, errors) { await page.waitForFunction(() => game.closeup?.id === 'fessura'); }; await openTorch(); - const saved = await page.evaluate(() => game.savable()); await page.keyboard.press('Escape'); await settle(); assert.equal(await page.evaluate(() => game.state.ended), false, 'uscire presto non vince'); await openTorch(); + const saved = await page.evaluate(() => game.savable()); await page.click('#pause-button'); const age = await page.evaluate(() => game.closeup.age); await page.waitForTimeout(150); diff --git a/tests/telefono.browser.mjs b/tests/telefono.browser.mjs index 3199e71..4d6f50d 100644 --- a/tests/telefono.browser.mjs +++ b/tests/telefono.browser.mjs @@ -51,7 +51,7 @@ try { // «Home» torna alla home del sito sul telefonino. await frame.locator('.pagerbutton', { hasText: 'Home' }).first().click(); await page.waitForFunction(() => game.closeup.step === 'site', null, { timeout: 5000 }); - await page.waitForSelector('#muro', { state: 'hidden', timeout: 3000 }); + await page.waitForSelector('#muro', { state: 'detached', timeout: 3000 }); assert.deepEqual(errors, []); console.log(`ok: schermate in ${out}`); } finally { await browser?.close(); server.kill(); }