From f57a91ca74810c76955544204fdeab37eca5cc35 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sat, 31 Oct 2026 11:57:26 +0100 Subject: [PATCH] =?UTF-8?q?Il=20risveglio=20di=20nuovo=20come=20prima=20su?= =?UTF-8?q?l=20computer,=20con=20la=20sfocatura=20vera,=20e=20leggero=20su?= =?UTF-8?q?l=20telefono,=20senza=20filtri;=20tutte=20e=20due=20su=20una=20?= =?UTF-8?q?tela=20a=20parte=20che=20c'=C3=A8=20solo=20finch=C3=A9=20il=20r?= =?UTF-8?q?isveglio=20dura,=20cos=C3=AC=20la=20scena=20non=20ne=20risente?= =?UTF-8?q?=20(Andrea)?= 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 --- src/engine/render.js | 20 ++++++++++++++++- src/engine/risveglio.js | 45 ++++++++++++++++++++----------------- style.css | 2 ++ tests/browser.mjs | 3 ++- tests/risveglio.browser.mjs | 11 ++++++--- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/engine/render.js b/src/engine/render.js index 6e1cdbe..ae35dc9 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -182,6 +182,7 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now( : drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null); // Durante il risveglio si guarda dagli occhi di Andrea: lui non si vede. const pov = game.closeup && game.story.closeups?.[game.closeup.id]?.type === 'risveglio'; + if (!pov) risveglioLayer(ctx, false); const queue = pov ? [] : [{ y: s.y, draw: drawAndreaNow }]; for (const p of game.people()) { const sheet = sprites.get(p.who); @@ -886,6 +887,18 @@ function drawDebug(ctx, game, { mouse, points = [], help }) { ctx.restore(); } +// La tela del risveglio (risveglio.js): una tela a parte sopra la scena, che c'è solo finché il risveglio dura. +function risveglioLayer(ctx, on = true) { + if (typeof document === 'undefined' || !ctx.canvas?.after) return null; + let layer = document.getElementById('risveglio'); + if (!on) { layer?.remove(); return null; } + if (!layer) { + layer = Object.assign(document.createElement('canvas'), { id: 'risveglio', width: WIDTH, height: HEIGHT }); + ctx.canvas.after(layer); + } + return layer.getContext('2d'); +} + // --- Il primo piano di un libro (story.closeups, game.closeup): la scena si oscura, le mani alzano il libro dal basso. // Le immagini hanno etichetta e pagine vuote: i testi si scrivono qui, nitidi. Una pagina sola gira, a fisarmonica // sulla piega; sotto c'è già l'ultima pagina. @@ -924,7 +937,12 @@ 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, backgrounds.get(game.backdrop())); return; } + // Sul telefono (schermo touch) la versione leggera, senza filtri; sul computer quella con la sfocatura vera. + if (def.type === 'risveglio') { + const layer = risveglioLayer(ctx), light = matchMedia('(pointer: coarse)').matches; + if (layer) drawRisveglio(layer, c, backgrounds.get(game.backdrop()), { light }); + 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 fddbe00..72135b4 100644 --- a/src/engine/risveglio.js +++ b/src/engine/risveglio.js @@ -30,8 +30,10 @@ export function updateRisveglio(c, def, dt) { 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. +// Il risveglio si disegna su una tela sua, appoggiata sopra la scena solo finché dura (render.js, `#risveglio`). +// Due versioni (Andrea, 31 ottobre): sul computer quella bella, con la sfocatura vera (ctx.filter); sul telefono +// (`light`) una leggera senza filtri, perché senza una scheda video la sfocatura a ogni fotogramma pesa: la nebbia è +// lo sfondo rimpicciolito e riallargato morbido, il bordo delle palpebre tante passate trasparenti. const SMALL = [80, 45]; let small = null, smallOf = null; function blurred(bg) { @@ -45,22 +47,24 @@ function blurred(bg) { return small; } -export function drawRisveglio(ctx, c, bg) { +export function drawRisveglio(ctx, c, bg, { light = false } = {}) { 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.clearRect(0, 0, W, H); + // La scena annebbiata: lo sfondo sfocato sopra a quello nitido, più una seconda copia spostata che ondeggia (la + // vista doppia); tutto si dirada col passare del tempo. + if (h > 0.01 && bg) { + const sway = 14 * h, dx = Math.sin(c.age * 1.3) * sway, dy = Math.sin(c.age * 0.9 + 1) * sway * 0.35; + const fog = light ? blurred(bg) : bg; ctx.imageSmoothingEnabled = true; - ctx.globalAlpha = 0.85 * h; + if (!light) ctx.filter = `blur(${(5 * h).toFixed(2)}px)`; + ctx.globalAlpha = h; ctx.drawImage(fog, 0, 0, W, H); - ctx.globalAlpha = 0.4 * h; + ctx.globalAlpha = 0.45 * 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.filter = 'none'; + // La luce del mattino che abbaglia. ctx.fillStyle = `rgba(255,244,214,${0.22 * h})`; ctx.fillRect(0, 0, W, H); } @@ -70,22 +74,23 @@ export function drawRisveglio(ctx, c, bg) { 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 (prima era una sfocatura, che rallentava tutto) - // sono tante passate trasparenti che sporgono verso l'occhio aperto, un pixel di più l'una: sovrapponendosi - // scuriscono piano verso la palpebra piena. Chiuse, si vede il rosso scuro della luce che passa. + // Le palpebre: sopra e sotto, col bordo curvo e sfumato; chiuse, si vede il rosso scuro della luce che passa. if (open < 0.999) { - const mid = H / 2, lid = (gap, bulge, sign) => { + const mid = H / 2, bulge = 70 + 60 * open, lid = (gap, 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(); }; - const SOFT = 16, bulge = 70 + 60 * open; - ctx.fillStyle = 'rgba(22,4,6,0.11)'; - for (let i = SOFT; i > 0; i--) { lid(200 * open - i, bulge, -1); lid(200 * open - i, bulge, 1); } + if (light) { + // Il bordo morbido: passate trasparenti che sporgono verso l'occhio aperto, un pixel di più l'una. + ctx.fillStyle = 'rgba(22,4,6,0.11)'; + for (let i = 16; i > 0; i--) { lid(200 * open - i, -1); lid(200 * open - i, 1); } + } else ctx.filter = 'blur(6px)'; ctx.fillStyle = 'rgb(22,4,6)'; - lid(200 * open, bulge, -1); lid(200 * open, bulge, 1); + lid(200 * open, -1); lid(200 * open, 1); + ctx.filter = 'none'; } ctx.restore(); } diff --git a/style.css b/style.css index a436ada..ed2bb19 100644 --- a/style.css +++ b/style.css @@ -82,6 +82,8 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000, 2px 2px #000, -2px -2px #000; } #speech:empty { display: none; } #speech { z-index: 3; } +/* La tela del risveglio, sopra la scena solo finché dura (render.js). */ +#risveglio { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; } /* Il Muro del Sanpierino sul telefonino del Barone: la pagina vera (assets/muro) larga 1067 pixel come la home del 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; diff --git a/tests/browser.mjs b/tests/browser.mjs index cdf1cf7..1694187 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -398,8 +398,9 @@ try { assert.equal(await book.evaluate(() => game.closeup.step), 'turn', 'un altro gira la pagina'); await book.waitForFunction(() => game.closeup?.step === 'end'); assert.equal(await book.evaluate(() => game.speech), null, '«FINE» non lo dice nessuno'); - await book.screenshot({ path: `${OUT}/03b-libro-fine.png` }); + // Prima la battuta, poi la schermata: con swiftshader una schermata può durare più della battuta. await book.waitForFunction(() => game.speech?.text === 'Mi ricordavo un finale diverso.', null, { timeout: 5000 }); + await book.screenshot({ path: `${OUT}/03b-libro-fine.png` }); assert.deepEqual(await book.evaluate(() => [game.state.x, game.state.y]), where, 'i clic sul libro non muovono Andrea'); await book.keyboard.press('Space'); await book.keyboard.press('Escape'); diff --git a/tests/risveglio.browser.mjs b/tests/risveglio.browser.mjs index a12d9f1..e696c6d 100644 --- a/tests/risveglio.browser.mjs +++ b/tests/risveglio.browser.mjs @@ -12,7 +12,10 @@ let browser; const errors = []; try { browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); - const page = await browser.newPage({ viewport: { width: 1280, height: 860 } }); + // Sul computer la versione con la sfocatura vera, sul telefono (touch) quella leggera senza filtri. + for (const mobile of [false, true]) { + const page = await browser.newPage(mobile ? { viewport: { width: 844, height: 390 }, hasTouch: true, isMobile: true } : { viewport: { width: 1280, height: 860 } }); + const tag = mobile ? 'telefono' : 'computer'; page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); }); await page.goto(`http://127.0.0.1:${server.port}/`); await page.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro'); @@ -21,10 +24,12 @@ try { // Le battute si saltano solo per far correre il tempo fino ai momenti da fotografare. for (const [t, name] of [[0.5, '0-buio'], [2.1, '1-primo-tentativo'], [3.8, '2-secondo'], [5.6, '3-quasi'], [7.2, '4-aperti']]) { await page.waitForFunction(at => { if (game.speech && game.closeup) game.skip(); return !game.closeup || game.closeup.t >= at; }, t, { timeout: 30000, polling: 50 }); - await page.screenshot({ path: `${out}/${name}.png` }); + await page.screenshot({ path: `${out}/${tag}-${name}.png` }); } await page.waitForFunction(() => !game.closeup && game.state.flags.intro, null, { timeout: 15000 }); - await page.screenshot({ path: `${out}/5-vecchietti.png` }); + await page.screenshot({ path: `${out}/${tag}-5-vecchietti.png` }); + assert.equal(await page.locator('#risveglio').count(), 0, 'finito il risveglio, la sua tela se ne va'); + } assert.deepEqual(errors, []); console.log(`ok: schermate in ${out}`); } finally { await browser?.close(); server.kill(); }