All'underground dell'Ingorgo le luci di Codex: la palla a specchi appesa e colpita dal faro, i riflessi a tempo sui muri e sulla folla, le lampade a inseguimento; altrove restano le nostre, esagerate (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
This commit is contained in:
2026-10-30 23:38:56 +01:00
co-authored by Claude Opus 5.5
parent aba2e7a0aa
commit d4b472119d
3 changed files with 137 additions and 14 deletions
+3 -1
View File
@@ -26,7 +26,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
del Comune, cima di Via dell'Antica Posta, Piazzetta Cambray Digny; uscendo (a piedi o con la mappa) si torna a maggio.
Alla festa si clicca solo per spostarsi (zona con `exitsOnly`: panchine, porte e statue non rispondono).
Le luci da discoteca animate solo alla festa (`stagelights`, `lasers`, `mirrorball`, `floorspots` in render.js), rinforzate
su richiesta di Andrea: colpo di colore a ogni battuta, strobo a raffica a fine frase, macchie che corrono sulla folla. Fasci che spazzano, bagliore a
su richiesta di Andrea: colpo di colore a ogni battuta, strobo a raffica a fine frase, macchie che corrono sulla folla.
In cima a Via dell'Antica Posta le luci sono quelle proposte da Codex (`pixelbeams`, `mirrorball`, `stagechase`,
`pixelspots`): Andrea ha tenuto la sua palla a specchi lì, e le nostre «caciarone» altrove. Fasci che spazzano, bagliore a
tempo, strobo, laser alla tecno, palla a specchi all'underground. Le console (`stages` delle scene, `music.js` `mix`): più brani insieme, ognuno più forte vicino alla sua console, anche
da fuori quadro (la piazza accanto). Generi provvisori: house in Piazza, latino al Comune, underground in cima a Via
dell'Antica Posta, tecno a Cambray Digny; i brani sono di Andrea (Suno), portati al volume del gioco. Gli sfondi sono
+126 -12
View File
@@ -279,6 +279,35 @@ function drawPhase(ctx, game) {
// Un numero «a caso» ma ripetibile: lo stesso istante dà sempre lo stesso disegno (e i test lo possono controllare).
const hash = n => (((n | 0) * 2654435761) >>> 0) / 4294967296;
// Fasci della festa: alone e nucleo sulla griglia dei pixel, attenuati in tre fasce nette lungo il percorso.
// Bastano sei poligoni per fascio, senza sfocature o letture del canvas.
function discoCone(ctx, x, y, angle, len, width, color, power) {
for (let layer = 0; layer < 2; layer++) {
const half = width * (layer ? 0.32 : 1);
const lx = Math.cos(angle - half), ly = Math.sin(angle - half), rx = Math.cos(angle + half), ry = Math.sin(angle + half);
for (let band = 0; band < 3; band++) {
const near = len * band / 3, far = len * (band + 1) / 3;
ctx.fillStyle = `rgba(${color},${power * (layer ? 0.8 : 0.45) * (1 - band * 0.36)})`;
ctx.beginPath();
ctx.moveTo(Math.round(x + lx * near), Math.round(y + ly * near));
ctx.lineTo(Math.round(x + lx * far), Math.round(y + ly * far));
ctx.lineTo(Math.round(x + rx * far), Math.round(y + ry * far));
ctx.lineTo(Math.round(x + rx * near), Math.round(y + ry * near));
ctx.closePath(); ctx.fill();
}
}
}
// Una chiazza schiacciata a scalini: cinque strisce bastano, anche sulla folla in primo piano.
function discoPool(ctx, x, y, rx, ry, color, power) {
ctx.fillStyle = `rgba(${color},${power})`;
for (let row = -2; row <= 2; row++) {
const w = Math.round(rx * (Math.abs(row) === 2 ? 0.6 : Math.abs(row) === 1 ? 0.88 : 1));
const top = Math.round(y + (row - 0.5) * ry * 0.4), bottom = Math.round(y + (row + 0.5) * ry * 0.4);
ctx.fillRect(Math.round(x) - w, top, w * 2, bottom - top);
}
}
// Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo.
export const EFFECTS = {
...ambientEffects,
@@ -571,22 +600,107 @@ export const EFFECTS = {
ctx.stroke();
ctx.restore();
},
// La palla a specchi: puntini di luce che girano sui muri intorno (`area`), più chiari vicino alla palla.
// La palla a specchi «di Codex» (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): appesa al suo filo, un faro
// la colpisce, e i riflessi girano a tempo sui muri e sulla folla (`surfaces`), con qualche scintilla.
mirrorball(ctx, e, time) {
const [bx, by] = e.at, [ax, ay, aw, ah] = e.area, turn = time / 4200;
const [bx, by] = e.at, beat = time * (e.bpm ?? 122) / 60000, turn = beat / 32;
const surfaces = e.surfaces ?? [e.area];
ctx.save();
ctx.fillStyle = '#71718d'; ctx.fillRect(bx, e.hang ?? by - 26, 1, by - (e.hang ?? by - 26) - 7);
ctx.globalCompositeOperation = 'lighter';
if (e.from) {
const [x, y] = e.from, dx = bx - x, dy = by - y;
discoCone(ctx, x, y, Math.atan2(dy, dx), Math.hypot(dx, dy), 0.035, '195,215,255', 0.32);
}
for (let i = 0; i < (e.count ?? 60); i++) {
const [ax, ay, aw, ah] = surfaces[i % surfaces.length];
const u = (hash(i * 13) + turn) % 1, v = 0.5 + 0.42 * Math.sin(hash(i * 7 + 3) * Math.PI * 2 + turn * Math.PI * 2);
const x = ax + Math.floor(u * (aw - 4)), y = ay + Math.floor(v * (ah - 3));
const glint = Math.max(0, Math.sin(turn * Math.PI * 4 + i * 2.3)) ** 4;
ctx.fillStyle = `rgba(${e.color ?? '200,215,255'},${0.13 + 0.65 * glint})`;
ctx.fillRect(x, y, glint > 0.7 ? 3 : 2, 2);
if (glint > 0.95) ctx.fillRect(x + 1, y - 1, 1, 4);
}
ctx.globalCompositeOperation = 'source-over';
for (let row = -3; row <= 3; row++) {
for (let col = -3; col <= 3; col++) {
if (row * row + col * col > 11) continue;
const light = Math.sin(col * 0.8 + turn * Math.PI * 2) * 0.65 - row * 0.12;
ctx.fillStyle = light > 0.55 ? '#f3f5ff' : light > 0.1 ? '#b9bcdf' : light > -0.4 ? '#6e78ab' : '#343856';
ctx.fillRect(bx + col * 2, by + row * 2, 2, 2);
}
}
ctx.fillStyle = '#ffffff'; ctx.fillRect(bx - 3, by - 4, 3, 1); ctx.fillRect(bx - 2, by - 5, 1, 3);
ctx.restore();
},
// Le teste mobili «di Codex» (30 ottobre, usate all'underground): incrocio, ventaglio, passaggio parallelo. Il programma cambia ogni otto battiti,
// sempre ricavato dal tempo: anche tornando indietro a un istante si ritrova esattamente la stessa luce.
pixelbeams(ctx, e, time) {
const colors = e.colors ?? ['255,45,170', '45,190,255'], beat = time * (e.bpm ?? 124) / 60000;
const hit = (1 - beat % 1) ** 3, bar = Math.floor(beat / 4), cue = Math.floor(beat / 8) % 3;
const from = e.from ?? [], swing = Math.sin(beat * Math.PI / (e.sweep ?? 4));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 40); i++) {
const u = (hash(i * 13) + turn) % 1, x = ax + Math.round(u * aw), y = ay + Math.round(hash(i * 7 + 3) * ah);
const glint = 0.5 + 0.5 * Math.sin(time / 160 + i * 2.3);
ctx.fillStyle = `rgba(${e.color ?? '220,200,255'},${0.35 + 0.5 * glint})`;
ctx.fillRect(x, y, 2, 2);
if (e.wash) {
const [x, y, r] = e.wash, c = colors[bar % colors.length];
discoPool(ctx, x, y, r, r * 0.32, c, 0.045 + 0.045 * hit);
discoPool(ctx, x, y, r * 0.55, r * 0.18, c, 0.04 + 0.07 * hit);
}
for (let k = 0; k < from.length; k++) {
const [x, y] = from[k], side = k % 2 ? 1 : -1;
const fan = from.length > 1 ? k / (from.length - 1) * 2 - 1 : 0;
const pan = cue === 0 ? side * swing : cue === 1 ? fan * (0.6 + 0.25 * swing) : swing;
const angle = -Math.PI / 2 + (e.aim ?? 0) + pan * (e.spread ?? 0.7);
const color = colors[(k + bar) % colors.length];
// Un breve buio di fine frase rende leggibile il rientro della cassa; il latino resta più continuo.
const power = (e.power ?? 0.38) * (0.65 + 0.35 * hit) * (e.shutter && beat % 16 > 15.5 ? 0.15 : 1);
for (let i = 0; i < (e.beams ?? 1); i++) {
const offset = (i - ((e.beams ?? 1) - 1) / 2) * 0.15;
discoCone(ctx, x, y, angle + offset, e.len ?? 270, e.width ?? 0.055, color, power);
}
ctx.fillStyle = `rgb(${color})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1);
}
ctx.restore();
},
// Lampade sulla traversa: inseguimento a ottavi, poi accensione insieme sul primo colpo della frase.
// I blinder illuminano solo davanti al palco: nessun velo bianco su tutta la scena.
stagechase(ctx, e, time) {
const beat = time * (e.bpm ?? 124) / 60000, step = Math.floor(beat * 2), points = e.points;
const colors = e.colors ?? ['255,45,170', '45,190,255'];
const flash = e.blinder && (beat % 16 < 0.14 || (Math.floor(beat) % 16 === 15 && beat * 2 % 1 < 0.16));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < points.length; i++) {
const [x, y] = points[i], on = step % points.length === i || beat % 8 < 0.22;
const color = flash ? '255,240,210' : colors[(i + Math.floor(beat / 4)) % colors.length];
if (on || flash) {
ctx.fillStyle = `rgba(${color},0.18)`; ctx.fillRect(x - 4, y - 3, 9, 7);
if (flash) discoCone(ctx, x, y, Math.PI / 2, e.len ?? 65, 0.3, color, 0.2);
}
ctx.fillStyle = `rgba(${color},${on || flash ? 0.95 : 0.15})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
if (on || flash) { ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1); }
}
ctx.restore();
},
// Gobo sulla folla «di Codex»: macchie a gradini, con tagli scuri, più grandi verso il primo piano. L'area le contiene tutte.
pixelspots(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,45,170', '45,190,255'];
const beat = time * (e.bpm ?? 124) / 60000, hit = (1 - beat % 1) ** 2;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 6); i++) {
const phase = beat * Math.PI / 8 + i * 2.4;
const v = 0.5 + 0.5 * Math.sin(phase * 0.7 + i), u = 0.5 + 0.5 * Math.sin(phase);
const rx = Math.min((e.size ?? 24) * (0.55 + 0.45 * v), aw / 2), ry = Math.min(rx * 0.3, ah / 2);
const x = ax + rx + u * (aw - rx * 2), y = ay + ry + v * (ah - ry * 2);
const c = colors[(i + Math.floor(beat / 4)) % colors.length];
// Le tre impronte separate ricordano un disco sagomato davanti al proiettore.
for (let part = -1; part <= 1; part++)
discoPool(ctx, x + part * rx * 0.65, y, rx * 0.25, ry * (part ? 0.7 : 1), c, 0.12 + 0.16 * hit);
}
const g = ctx.createRadialGradient(bx, by, 0, bx, by, 9);
g.addColorStop(0, 'rgba(255,255,255,0.8)');
g.addColorStop(1, 'rgba(200,180,255,0)');
ctx.fillStyle = g;
ctx.fillRect(bx - 9, by - 9, 18, 18);
ctx.restore();
},
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
+8 -1
View File
@@ -1779,7 +1779,14 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
// L'underground sotto il muro della villa; su, in fondo, la tecno di Cambray Digny; giù, lontano, la house.
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
effects: [{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], area: [0, 20, 640, 300], count: 110 }, { type: 'stagelights', phase: 'ingorgo', from: [[520, 200], [600, 200]], wash: [552, 205, 160], bpm: 122, beams: 2, strobe: 8, pulse: 0.07, colors: ['170,60,255', '90,40,220', '220,60,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 122, count: 6, colors: ['170,60,255', '60,120,255', '220,60,255'] }],
// 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: [
{ 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'] },
{ type: 'pixelspots', phase: 'ingorgo', area: [130, 242, 490, 108], bpm: 122, count: 5, size: 20, colors: ['155,65,255', '65,110,255'] },
],
stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },