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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
2026-10-31 13:42:55 +01:00
co-authored by Claude Fable 5.1
parent b10f031133
commit 4fe6bf4649
4 changed files with 47 additions and 6 deletions
+8
View File
@@ -18,6 +18,14 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati ## 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 - **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 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 ingrandito (primo piano `manifesto_smarronata`, Codex dal manifesto vero, senza lo sponsor), e toccandolo i ricordi di
+17 -1
View File
@@ -518,6 +518,22 @@ export const EFFECTS = {
}, },
// Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte // 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. // 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) { cloth(ctx, e, time, game) {
const bg = backgrounds.get(game.backdrop()); const bg = backgrounds.get(game.backdrop());
if (!bg) return; if (!bg) return;
@@ -747,7 +763,7 @@ export const EFFECTS = {
ctx.restore(); 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) { function drawEffects(ctx, game, time, background = false) {
for (const e of game.scene.effects ?? []) { for (const e of game.scene.effects ?? []) {
if (BACKGROUND_EFFECTS.has(e.type) !== background || !onNow(e, game)) continue; if (BACKGROUND_EFFECTS.has(e.type) !== background || !onNow(e, game)) continue;
+5 -5
View File
@@ -704,7 +704,7 @@ export const story = {
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 }, { 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]], 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]], depth: [[236, 0.5], [360, 1.35]],
entry: [345, 240], 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] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 }, { 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] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] }, { 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] // 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. // 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). // 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: [ stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] }, { when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 }, { 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). // 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 // 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. // 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: '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: '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'] }, { type: 'stagechase', phase: 'ingorgo', points: [[533, 210], [557, 210], [583, 209], [609, 208]], bpm: 122, colors: ['155,65,255', '65,110,255'] },
+17
View File
@@ -154,3 +154,20 @@ test('le musiche e gli sfondi della festa esistono', () => {
assert.ok(files.size >= 10); assert.ok(files.size >= 10);
for (const f of files) assert.ok(existsSync(new URL('../' + f, import.meta.url)), `manca ${f}`); 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');
});