Lucciole, finestre che si accendono di sera, rondini, cipressi al vento e dissolvenza fra le scene

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 23:37:23 +02:00
co-authored by Claude Opus 5.5
parent f64051b5df
commit 6a96611a1b
4 changed files with 76 additions and 6 deletions
+1
View File
@@ -374,6 +374,7 @@ export class Game {
go(scene, at, face) {
if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene);
this.state.scene = scene;
this.enteredAt = this.time; // per gli effetti che cominciano all'ingresso (le finestre che si accendono)
[this.state.x, this.state.y] = at ?? this.scene.entry;
this.state.facing = FACING[face] ?? this.state.facing;
this.path = [];
+58 -2
View File
@@ -145,6 +145,8 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
// Gli effetti che muovono pezzi di sfondo (i cipressi al vento) vanno subito sopra lo sfondo, sotto tutto il resto.
drawEffects(ctx, game, time, true);
for (const shape of scene.art ?? []) drawShape(ctx, shape);
// Una forma può avere la sua condizione (`when`): la chiavetta nella griglia sparisce quando la si prende.
// I primi piani (`top`) invece vanno sopra a tutto, anche al buio e alle candele: si disegnano dopo.
@@ -290,6 +292,58 @@ export const EFFECTS = {
ctx.fillStyle = e.fill;
for (const [x, y, w, h] of e.rects) closed ? ctx.fillRect(x, y + Math.floor(h / 2), w, 1) : ctx.fillRect(x, y, w, h);
},
// Lucciole di sera: puntini gialli che vagano piano e si accendono e si spengono.
fireflies(ctx, e, time) {
const [ax, ay, aw, ah] = e.area;
for (let i = 0; i < (e.count ?? 8); i++) {
const t = time / 1000, glow = Math.sin(t * (1.3 + hash(i) * 1.2) + i * 2);
if (glow < 0.15) continue;
const x = ax + aw * (0.5 + 0.45 * Math.sin(t * 0.21 + i * 2.3) * Math.cos(t * 0.13 + i)), y = ay + ah * (0.5 + 0.45 * Math.sin(t * 0.17 + i * 1.7));
ctx.fillStyle = `rgba(255, 240, 120, ${0.25 * glow})`;
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3);
ctx.fillStyle = `rgba(230, 255, 140, ${glow})`;
ctx.fillRect(Math.round(x), Math.round(y), 1, 1);
}
},
// Le finestre che si accendono di sera, una dopo l'altra, appena si entra nella scena.
windows(ctx, e, time, game) {
const since = ((game.time ?? 0) - (game.enteredAt ?? 0)) * 1000;
e.rects.forEach(([x, y, w, h], i) => {
if (since < 400 + hash(i * 7 + 3) * (e.over ?? 3500)) return;
ctx.fillStyle = 'rgba(255, 196, 100, 0.72)';
ctx.fillRect(x, y, w, h);
ctx.fillStyle = 'rgba(255, 236, 170, 0.5)';
ctx.fillRect(x + 1, y + 1, Math.max(1, w - 2), Math.max(1, Math.floor(h / 3)));
});
},
// Rondini: ogni tanto un gruppetto attraversa il cielo, da destra a sinistra.
birds(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, every = e.every ?? 9000, t = (time % every) / every, run = Math.floor(time / every);
if (t > 0.45) return;
const k = t / 0.45;
for (let i = 0; i < (e.count ?? 3); i++) {
const x = Math.round(ax + aw - k * (aw + 40) + i * 9 + hash(run * 5 + i) * 10);
const y = Math.round(ay + hash(run * 3 + i) * ah + Math.sin(k * 12 + i) * 3);
const up = Math.floor(time / 120 + i) % 2;
ctx.fillStyle = '#1e1a24';
ctx.fillRect(x, y, 2, 1);
ctx.fillRect(x - 2, y - up, 2, 1);
ctx.fillRect(x + 2, y - up, 2, 1);
}
},
// Alberi al vento (i cipressi): un pezzo di sfondo ridisegnato inclinato appena, più in cima che alla base.
sway(ctx, e, time, game) {
const bg = backgrounds.get(game.scene.background);
if (!bg) return;
const k = bg.width / WIDTH;
for (const [i, [x, y, w, h]] of e.rects.entries()) {
const a = Math.sin(time / (e.speed ?? 900) + i * 1.3) * (e.amount ?? 1.6);
for (let r = 0; r < h; r += 2) {
const dx = Math.round(a * (1 - r / h));
if (dx) ctx.drawImage(bg, x * k, (y + r) * k, w * k, 2 * k, x + dx, y + r, w, 2);
}
}
},
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
lamps(ctx, e, time) {
ctx.save();
@@ -305,8 +359,10 @@ export const EFFECTS = {
ctx.restore();
},
};
function drawEffects(ctx, game, time) {
for (const e of game.scene.effects ?? []) if (onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time);
const BACKGROUND_EFFECTS = new Set(['sway']);
function drawEffects(ctx, game, time, background = false) {
for (const e of game.scene.effects ?? [])
if (BACKGROUND_EFFECTS.has(e.type) === background && onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time, game);
}
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
+9 -1
View File
@@ -538,7 +538,8 @@ function updateUi() {
}
}
let last = performance.now(), drawnStill = false;
let last = performance.now(), drawnStill = false, fade = 0, fadeScene = null;
const FADE = 0.4; // secondi della dissolvenza fra le scene
function frame(now) {
// Il primo timestamp di requestAnimationFrame può precedere `last`: un dt negativo darebbe alla musica un volume
// sotto zero, il browser lancerebbe un errore e il ciclo dei fotogrammi si fermerebbe (gioco congelato).
@@ -556,6 +557,13 @@ function frame(now) {
if (game && (!still || still !== drawnStill)) {
if (running) game.update(dt);
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() });
// Cambiando scena, un attimo di nero che sfuma: la dissolvenza dei vecchi giochi, invece del taglio netto.
if (game.state.scene !== fadeScene) { if (fadeScene) fade = 1; fadeScene = game.state.scene; }
if (fade > 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${fade})`;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
fade = Math.max(0, fade - dt / FADE);
}
updateUi();
}
drawnStill = still;
+8 -3
View File
@@ -249,6 +249,7 @@ export const story = {
name: 'Piazza Colonna',
map: [32, 45],
background: 'assets/scenes/piazza-colonna.png',
effects: [{ 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],
@@ -1141,7 +1142,7 @@ export const story = {
name: 'La Provinciale lungo la Carza',
map: [66, 73],
background: 'assets/scenes/verso-luna.png',
effects: [{ type: 'sparkle', area: [380, 288, 200, 30], count: 5 }, { type: 'sparkle', area: [430, 210, 40, 30], count: 2 }, { type: 'smoke', at: [113, 116] }, { type: 'lamps', phase: 'sera', points: [[165, 93]] }],
effects: [{ type: 'fireflies', phase: 'sera', area: [200, 210, 220, 80], count: 8 }, { type: 'sparkle', area: [380, 288, 200, 30], count: 5 }, { type: 'sparkle', area: [430, 210, 40, 30], count: 2 }, { type: 'smoke', at: [113, 116] }, { type: 'lamps', phase: 'sera', points: [[165, 93]] }],
walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]],
depth: [[192, 0.45], [360, 1.7]],
entry: [150, 206],
@@ -1220,6 +1221,7 @@ export const story = {
name: 'Campomigliaio, il circolino',
map: [55, 82],
background: 'assets/scenes/campomigliaio-circolo.png',
effects: [{ type: 'windows', phase: 'sera', rects: [[55, 186, 50, 28], [110, 186, 50, 28], [165, 186, 36, 28], [316, 180, 16, 44]] }],
walk: [[0, 360], [0, 335], [60, 300], [400, 280], [640, 250], [640, 360]],
depth: [[250, 1.0], [360, 1.5]],
entry: [320, 330],
@@ -1274,7 +1276,7 @@ export const story = {
name: 'Campomigliaio, il ponte vecchio',
map: [50, 86],
background: 'assets/scenes/campomigliaio-ponte.png',
effects: [{ type: 'sparkle', area: [150, 205, 160, 90], count: 7 }, { type: 'sparkle', area: [330, 300, 110, 40], count: 3 }, { type: 'smoke', at: [605, 50] }],
effects: [{ type: 'fireflies', phase: 'sera', area: [0, 220, 150, 120], count: 7 }, { type: 'sparkle', area: [150, 205, 160, 90], count: 7 }, { type: 'sparkle', area: [330, 300, 110, 40], count: 3 }, { type: 'smoke', at: [605, 50] }],
walk: [[0, 360], [0, 232], [100, 225], [140, 240], [170, 275], [260, 330], [300, 360]],
depth: [[225, 0.6], [360, 1.5]],
entry: [80, 330],
@@ -1430,7 +1432,7 @@ export const story = {
name: 'Villa Adami',
map: [46, 32],
background: 'assets/scenes/villa-adami.png',
effects: [{ type: 'flag', at: [251, 4], w: 9, h: 6, colors: ['#f4f1e8', '#2a2a2a'] }],
effects: [{ type: 'windows', phase: 'sera', rects: [[148, 104, 10, 24], [205, 110, 10, 22], [300, 120, 12, 26], [372, 128, 10, 20], [412, 130, 10, 20], [448, 134, 10, 18], [150, 186, 12, 22], [205, 190, 12, 22], [372, 190, 10, 20], [412, 190, 10, 20], [448, 192, 10, 18]] }, { type: 'flag', at: [251, 4], w: 9, h: 6, colors: ['#f4f1e8', '#2a2a2a'] }],
walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]],
depth: [[244, 0.5], [360, 1.2]],
entry: [300, 345],
@@ -1528,6 +1530,7 @@ export const story = {
name: 'Il posticino della Carza',
map: [60, 58],
background: 'assets/scenes/carza.png',
effects: [{ type: 'fireflies', phase: 'sera', area: [80, 200, 480, 100], count: 9 }],
walk: [[60, 360], [250, 262], [300, 218], [350, 218], [420, 262], [600, 360]],
depth: [[218, 0.55], [360, 1.65]],
entry: [300, 350],
@@ -1735,6 +1738,7 @@ export const story = {
name: 'Viale dei cipressi',
map: [60, 6],
background: 'assets/scenes/viale-cipressi.png',
effects: [{ type: 'sway', rects: [[322, 58, 30, 150], [346, 8, 32, 190], [312, 108, 18, 110], [244, 118, 26, 100]] }],
walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
depth: [[250, 0.16], [360, 1.2]],
entry: [300, 350],
@@ -1758,6 +1762,7 @@ export const story = {
name: 'Castello del Trebbio',
map: [84, 6],
background: 'assets/scenes/trebbio.png',
effects: [{ type: 'sway', rects: [[237, 140, 20, 60], [165, 160, 20, 48], [305, 132, 14, 30], [560, 120, 22, 80], [600, 120, 20, 80], [620, 100, 20, 100], [530, 160, 20, 40]] }, { type: 'fireflies', phase: 'sera', area: [80, 290, 440, 60], count: 10 }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [200, 20, 400, 90], every: 12000 }],
music: 'assets/audio/ricordi.mp3',
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],
depth: [[306, 0.75], [360, 1.05]],