From 4fe6bf464928592ae569dd4c3e5c015cacef8023 Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Sat, 31 Oct 2026 13:42:55 +0100 Subject: [PATCH] All'Ingorgo la folla dipinta salta a tempo: la fascia della gente tagliata in strisce che sobbalzano sul battito, due fasce per scena, dietro piano e davanti forte (Andrea) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T --- docs/STATO.md | 8 ++++++++ src/engine/render.js | 18 +++++++++++++++++- src/story/story.js | 10 +++++----- tests/ingorgo.test.mjs | 17 +++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/STATO.md b/docs/STATO.md index 54ce5ed..b00237a 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -18,6 +18,14 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, ## Dove siamo arrivati +- **La folla dell'Ingorgo che salta (31 ottobre)**: nelle cinque scene della festa la gente dipinta nello sfondo va su + e giù a tempo (effetto `crowd` in render.js: la fascia della folla tagliata in strisce verticali, ognuna sobbalza sul + battito con una fase sua; due fasce per scena, dietro piano e davanti forte, al `bpm` delle luci della scena). +- **Il risveglio senza arretrati (31 ottobre)**: le palpebre si disegnano nette su una tela 128×72 e si allargano + con l'interpolazione (bordo morbido senza filtri né passate), tutto sulla tela principale, una versione sola. Con + la grafica software delle prove (swiftshader) ogni lavoro grafico si paga alla prima lettura dei pixel: dopo il + risveglio l'arretrato era di 24 s (filtro) o 14 s (sedici passate), ora un decimo di secondo; la 0.1.62 stava a 2,5. + Da ricordare per gli effetti futuri: niente `ctx.filter` e niente riempimenti trasparenti a tutto schermo ripetuti. - **La Smarronata del 1993 (31 ottobre)**: la festa delle castagne degli alpini di San Piero, sotto il ponte sulla Sieve fra il fiume e la strada del campo. Al Ponte sulla Sieve, sul lampione di destra, un vecchio cartello: si apre ingrandito (primo piano `manifesto_smarronata`, Codex dal manifesto vero, senza lo sponsor), e toccandolo i ricordi di diff --git a/src/engine/render.js b/src/engine/render.js index f61eb47..e827da9 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -518,6 +518,22 @@ export const EFFECTS = { }, // Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte // dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero. + // La folla dell'Ingorgo che salta a tempo (Andrea, 31 ottobre): la fascia di sfondo `area` tagliata in strisce + // verticali larghe `step`, ognuna che sobbalza sul battito (`bpm`) con una fase sua, come teste che vanno su e giù; + // ogni striscia salta un battito sì e uno no, a turno, con un colpo secco in su e una ricaduta. + crowd(ctx, e, time, game) { + const bg = backgrounds.get(game.backdrop()); + if (!bg) return; + const k = bg.width / WIDTH, [ax, ay, aw, ah] = e.area, step = e.step ?? 5, amount = e.amount ?? 3; + const beats = time / 60000 * (e.bpm ?? 120); + for (let x = ax; x < ax + aw; x += step) { + const i = (x - ax) / step, b = (beats / 2 + hash(i * 17 + ay)) % 1, jump = b < 0.5 ? Math.sin(b * Math.PI) : 0; + const dy = Math.round(amount * (0.4 + 0.6 * hash(i * 31 + 5)) * jump); + if (!dy) continue; + const w = Math.min(step, ax + aw - x); + ctx.drawImage(bg, x * k, ay * k, w * k, ah * k, x, ay - dy, w, ah); + } + }, cloth(ctx, e, time, game) { const bg = backgrounds.get(game.backdrop()); if (!bg) return; @@ -747,7 +763,7 @@ export const EFFECTS = { ctx.restore(); }, }; -const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', ...Object.keys(ambientEffects)]); +const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', 'crowd', ...Object.keys(ambientEffects)]); function drawEffects(ctx, game, time, background = false) { for (const e of game.scene.effects ?? []) { if (BACKGROUND_EFFECTS.has(e.type) !== background || !onNow(e, game)) continue; diff --git a/src/story/story.js b/src/story/story.js index 44eed65..ab1ff90 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -704,7 +704,7 @@ export const story = { { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 }, ], - effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146], [196, 140]], wash: [200, 215, 170], bpm: 124, beams: 3, strobe: 4, pulse: 0.08 }, { type: 'lasers', phase: 'ingorgo', at: [196, 150], count: 7, color: '255,60,200', len: 360 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 124, count: 7 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }], + effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 205, 640, 70], bpm: 124, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 275, 640, 85], bpm: 124, amount: 5 }, { type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146], [196, 140]], wash: [200, 215, 170], bpm: 124, beams: 3, strobe: 4, pulse: 0.08 }, { type: 'lasers', phase: 'ingorgo', at: [196, 150], count: 7, color: '255,60,200', len: 360 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 124, count: 7 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }], walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]], depth: [[236, 0.5], [360, 1.35]], entry: [345, 240], @@ -1741,7 +1741,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // La tecno davanti alla casetta bianca; l'underground arriva da giù, da Via dell'Antica Posta. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 13, len: 460 }, { type: 'lasers', phase: 'ingorgo', at: [370, 200], count: 7, color: '60,220,255' }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192], [408, 186]], wash: [408, 225, 180], bpm: 132, beams: 3, strobe: 2, pulse: 0.1, colors: ['90,255,110', '255,255,255', '60,220,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 132, count: 8, colors: ['90,255,110', '255,255,255', '60,220,255'] }], + effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 215, 640, 65], bpm: 132, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 280, 640, 80], bpm: 132, amount: 5 }, { type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 13, len: 460 }, { type: 'lasers', phase: 'ingorgo', at: [370, 200], count: 7, color: '60,220,255' }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192], [408, 186]], wash: [408, 225, 180], bpm: 132, beams: 3, strobe: 2, pulse: 0.1, colors: ['90,255,110', '255,255,255', '60,220,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 132, count: 8, colors: ['90,255,110', '255,255,255', '60,220,255'] }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 }, @@ -1787,7 +1787,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // Il bar della birra sui gradini; il latino dal Comune a sinistra, la house giù dalla piazza, l'underground su in cima. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 130], bpm: 98, colors: ['255,170,60', '255,90,160'], pulse: 0.05 }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205], [130, 150]], beams: 2, len: 260, bpm: 98 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 230, 560, 120], bpm: 98, count: 5 }], + effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 190, 640, 70], bpm: 98, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 260, 640, 100], bpm: 98, amount: 5 }, { type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 130], bpm: 98, colors: ['255,170,60', '255,90,160'], pulse: 0.05 }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205], [130, 150]], beams: 2, len: 260, bpm: 98 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 230, 560, 120], bpm: 98, count: 5 }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] }, @@ -1836,7 +1836,7 @@ export const story = { // Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza] // La console del latino davanti al Comune; la house arriva da giù, dalla piazza. // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). - effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212], [282, 205]], wash: [282, 228, 170], bpm: 98, beams: 3, strobe: 8, pulse: 0.08, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 98, count: 7, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }], + effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 240, 640, 50], bpm: 98, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 290, 640, 70], bpm: 98, amount: 5 }, { type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212], [282, 205]], wash: [282, 228, 170], bpm: 98, beams: 3, strobe: 8, pulse: 0.08, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 98, count: 7, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }], stages: [ { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 }, @@ -1894,7 +1894,7 @@ export const story = { // Le luci da palco della festa (render.js: stagelights, lasers, mirrorball). // Qui le luci sono quelle di Codex (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): teste sulle casse a // destra, la palla a specchi che proietta sui due muri e sulla folla, le lampade a inseguimento sulla traversa. - effects: [ + effects: [{ type: 'crowd', phase: 'ingorgo', area: [0, 185, 640, 75], bpm: 122, amount: 3 }, { type: 'crowd', phase: 'ingorgo', area: [0, 260, 640, 100], bpm: 122, amount: 5 }, { type: 'pixelbeams', phase: 'ingorgo', from: [[499, 177], [620, 153]], wash: [568, 220, 70], bpm: 122, aim: -0.35, spread: 0.55, sweep: 8, power: 0.28, colors: ['155,65,255', '65,110,255'] }, { type: 'mirrorball', phase: 'ingorgo', at: [550, 131], hang: 91, from: [499, 177], surfaces: [[430, 60, 205, 140], [18, 105, 155, 85], [35, 245, 570, 105]], bpm: 122, count: 72 }, { type: 'stagechase', phase: 'ingorgo', points: [[533, 210], [557, 210], [583, 209], [609, 208]], bpm: 122, colors: ['155,65,255', '65,110,255'] }, diff --git a/tests/ingorgo.test.mjs b/tests/ingorgo.test.mjs index e83fca9..49b9760 100644 --- a/tests/ingorgo.test.mjs +++ b/tests/ingorgo.test.mjs @@ -154,3 +154,20 @@ test('le musiche e gli sfondi della festa esistono', () => { assert.ok(files.size >= 10); for (const f of files) assert.ok(existsSync(new URL('../' + f, import.meta.url)), `manca ${f}`); }); + +test('alla festa la folla dipinta salta a tempo: l\'effetto crowd in tutte e cinque le scene, dietro piano e davanti forte', async () => { + const { EFFECTS } = await import('../src/engine/render.js'); + assert.equal(typeof EFFECTS.crowd, 'function'); + for (const id of ZONE.scenes) { + const crowds = (story.scenes[id].effects ?? []).filter(e => e.type === 'crowd'); + assert.equal(crowds.length, 2, `${id}: due fasce di folla`); + assert.ok(crowds.every(e => e.phase === 'ingorgo' && e.bpm > 0 && e.area[1] + e.area[3] <= 360), id); + assert.ok(crowds[0].amount < crowds[1].amount, `${id}: quella davanti salta di più`); + } + // Il salto: ogni striscia va su e giù nel tempo; senza sfondo l'effetto non disegna niente. + const calls = []; + const ctx = { drawImage: (...a) => calls.push(a) }; + const game = { backdrop: () => 'x.png' }; + EFFECTS.crowd(ctx, { area: [0, 200, 640, 100], bpm: 120 }, 0, game); + assert.equal(calls.length, 0, 'senza sfondo caricato non disegna'); +});