diff --git a/docs/STATO.md b/docs/STATO.md index 188e6e5..2dc07cf 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -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 diff --git a/src/engine/render.js b/src/engine/render.js index 8e790a7..b40d4cf 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -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`). diff --git a/src/story/story.js b/src/story/story.js index d1605ca..d50093a 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -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 },