Nuvole a pixel che scorrono dietro alberi e tetti; cipressi che ondeggiano di più; rondini più grandi e più spesso

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:53:34 +02:00
co-authored by Claude Opus 5.5
parent 86f8df16b1
commit fd0fddae5d
2 changed files with 96 additions and 22 deletions
+88 -14
View File
@@ -200,6 +200,55 @@ function drawApparitions(ctx, game, time) {
} }
} }
// Il cielo di uno sfondo: i pixel azzurri (blu ben più forte del rosso), come maschera; si calcola una volta sola.
const skyMasks = new WeakMap();
function skyMask(bg) {
if (skyMasks.has(bg)) return skyMasks.get(bg);
let mask = null;
try {
const c = document.createElement('canvas');
c.width = WIDTH; c.height = HEIGHT;
const x = c.getContext('2d');
x.drawImage(bg, 0, 0, WIDTH, HEIGHT);
const img = x.getImageData(0, 0, WIDTH, HEIGHT), d = img.data;
for (let p = 0; p < d.length; p += 4) {
const sky = d[p + 2] > 150 && d[p + 2] - d[p] > 60 && d[p + 2] - d[p + 1] > 15;
d[p + 3] = sky ? 255 : 0;
}
x.putImageData(img, 0, 0);
mask = c;
} catch { /* senza canvas (test) niente nuvole */ }
skyMasks.set(bg, mask);
return mask;
}
let scratchCanvas = null;
const scratch = () => {
if (!scratchCanvas) { scratchCanvas = document.createElement('canvas'); scratchCanvas.width = WIDTH; scratchCanvas.height = HEIGHT; }
return scratchCanvas;
};
// Una nuvola a pixel: qualche cerchio pieno uno accanto all'altro, la cima color panna e la pancia azzurrina.
const cloudSprites = new Map();
function cloudSprite(n) {
if (cloudSprites.has(n)) return cloudSprites.get(n);
const w = 34 + Math.floor(hash(n * 13 + 1) * 30), h = 18, c = document.createElement('canvas');
c.width = w; c.height = h;
const x = c.getContext('2d'), blobs = [];
const count = 3 + Math.floor(hash(n * 3) * 3);
for (let b = 0; b < count; b++) {
const r = 4 + hash(n * 17 + b) * 5, cx = 5 + (w - 10) * (b + 0.5) / count + (hash(n + b * 3) - 0.5) * 4;
blobs.push([cx, h - 4 - r * 0.8, r]);
}
for (let py = 0; py < h; py++) for (let px = 0; px < w; px++) {
const inside = blobs.some(([cx, cy, r]) => (px - cx) ** 2 + (py - cy) ** 2 <= r * r) && py <= h - 3;
if (!inside) continue;
const top = !blobs.some(([cx, cy, r]) => (px - cx) ** 2 + (py - 2 - cy) ** 2 <= r * r);
x.fillStyle = py >= h - 6 ? '#c7d4ec' : top ? '#ffffff' : py < h - 9 ? '#fbf3dc' : '#eae6dc';
x.fillRect(px, py, 1, 1);
}
cloudSprites.set(n, c);
return c;
}
// Un effetto o una luce vale adesso? `when` (condizione sui flag) e `phase` (una fase del giorno o un elenco). // Un effetto o una luce vale adesso? `when` (condizione sui flag) e `phase` (una fase del giorno o un elenco).
function onNow(e, game) { function onNow(e, game) {
if (!check(e.when, game.state)) return false; if (!check(e.when, game.state)) return false;
@@ -316,19 +365,25 @@ export const EFFECTS = {
ctx.fillRect(x + 1, y + 1, Math.max(1, w - 2), Math.max(1, Math.floor(h / 3))); 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. // Rondini: ogni pochi secondi un gruppetto attraversa il cielo in diagonale, a «V» con le ali che battono.
birds(ctx, e, time) { birds(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, every = e.every ?? 9000, t = (time % every) / every, run = Math.floor(time / every); const [ax, ay, aw, ah] = e.area, every = e.every ?? 6000, run = Math.floor(time / every), t = (time % every) / every;
if (t > 0.45) return; if (t > 0.6) return;
const k = t / 0.45; const k = t / 0.6, rtl = hash(run * 13) < 0.5;
for (let i = 0; i < (e.count ?? 3); i++) { for (let i = 0; i < (e.count ?? 4); i++) {
const x = Math.round(ax + aw - k * (aw + 40) + i * 9 + hash(run * 5 + i) * 10); const lag = i * 0.05, kk = k - lag;
const y = Math.round(ay + hash(run * 3 + i) * ah + Math.sin(k * 12 + i) * 3); if (kk < 0) continue;
const up = Math.floor(time / 120 + i) % 2; const x0 = rtl ? ax + aw + 10 - kk * (aw + 30) : ax - 10 + kk * (aw + 30);
ctx.fillStyle = '#1e1a24'; const x = Math.round(x0 + (hash(run * 5 + i) - 0.5) * 16);
ctx.fillRect(x, y, 2, 1); const y = Math.round(ay + hash(run * 3 + i) * ah - kk * 20 + Math.sin(kk * 14 + i) * 4);
ctx.fillRect(x - 2, y - up, 2, 1); const up = Math.floor(time / 90 + i * 2) % 3;
ctx.fillRect(x + 2, y - up, 2, 1); ctx.fillStyle = '#1b1722';
ctx.fillRect(x, y, 2, 2);
const wy = up === 0 ? -2 : up === 1 ? -1 : 1;
ctx.fillRect(x - 2, y + (wy < 0 ? -1 : 0), 2, 1);
ctx.fillRect(x - 4, y + wy, 2, 1);
ctx.fillRect(x + 2, y + (wy < 0 ? -1 : 0), 2, 1);
ctx.fillRect(x + 4, y + wy, 2, 1);
} }
}, },
// Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con // Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con
@@ -336,7 +391,7 @@ export const EFFECTS = {
sway(ctx, e, time, game) { sway(ctx, e, time, game) {
const bg = backgrounds.get(game.scene.background); const bg = backgrounds.get(game.scene.background);
if (!bg) return; if (!bg) return;
const k = bg.width / WIDTH, amount = e.amount ?? 1.2; const k = bg.width / WIDTH, amount = e.amount ?? 3.2;
for (const [i, [x, y, w, h]] of e.rects.entries()) { for (const [i, [x, y, w, h]] of e.rects.entries()) {
const a = amount * (0.65 * Math.sin(time / 1900 + i * 1.7) + 0.35 * Math.sin(time / 830 + i * 2.9)); const a = amount * (0.65 * Math.sin(time / 1900 + i * 1.7) + 0.35 * Math.sin(time / 830 + i * 2.9));
ctx.save(); ctx.save();
@@ -347,6 +402,25 @@ export const EFFECTS = {
ctx.restore(); ctx.restore();
} }
}, },
// Nuvole a pixel che attraversano il cielo, lentissime, ognuna a una sua altezza e velocità; uscita da un lato
// rientra dall'altro. Si vedono solo dove lo sfondo è cielo (skyMask): passano dietro alberi, torri e tetti.
clouds(ctx, e, time, game) {
const bg = backgrounds.get(game.scene.background), mask = bg && skyMask(bg);
if (!mask) return;
const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area;
lc.clearRect(0, 0, WIDTH, HEIGHT);
for (let i = 0; i < (e.count ?? 3); i++) {
const sprite = cloudSprite(i + (e.seed ?? 0)), speed = (e.speed ?? 3) * (0.6 + hash(i * 7) * 0.8);
const span = aw + sprite.width + 20, x = ax - sprite.width - 10 + ((hash(i * 11) * span + time / 1000 * speed) % span);
lc.drawImage(sprite, Math.round(x), ay + Math.floor(hash(i * 5 + 2) * ah));
}
lc.globalCompositeOperation = 'destination-in';
lc.drawImage(mask, 0, 0);
lc.globalCompositeOperation = 'source-over';
ctx.globalAlpha = e.alpha ?? 0.92;
ctx.drawImage(layer, 0, 0);
ctx.globalAlpha = 1;
},
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`). // Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
lamps(ctx, e, time) { lamps(ctx, e, time) {
ctx.save(); ctx.save();
@@ -362,7 +436,7 @@ export const EFFECTS = {
ctx.restore(); ctx.restore();
}, },
}; };
const BACKGROUND_EFFECTS = new Set(['sway']); const BACKGROUND_EFFECTS = new Set(['sway', 'clouds']);
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)) EFFECTS[e.type]?.(ctx, e, time, game); if (BACKGROUND_EFFECTS.has(e.type) === background && onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time, game);
+8 -8
View File
@@ -249,7 +249,7 @@ export const story = {
name: 'Piazza Colonna', name: 'Piazza Colonna',
map: [32, 45], map: [32, 45],
background: 'assets/scenes/piazza-colonna.png', 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] }], effects: [{ type: 'clouds', area: [180, 10, 420, 90], count: 3 }, { 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],
@@ -1107,7 +1107,7 @@ export const story = {
name: 'Via Provinciale, il viale alberato', name: 'Via Provinciale, il viale alberato',
map: [72, 68], map: [72, 68],
background: 'assets/scenes/provinciale-viale.png', background: 'assets/scenes/provinciale-viale.png',
effects: [{ type: 'leaves', area: [20, 40, 360, 250], count: 7 }], effects: [{ type: 'clouds', area: [300, 10, 220, 120], count: 2 }, { type: 'leaves', area: [20, 40, 360, 250], count: 7 }],
walk: [[0, 360], [0, 330], [400, 232], [462, 232], [620, 360]], walk: [[0, 360], [0, 330], [400, 232], [462, 232], [620, 360]],
depth: [[232, 0.4], [360, 1.7]], depth: [[232, 0.4], [360, 1.7]],
entry: [300, 350], entry: [300, 350],
@@ -1144,7 +1144,7 @@ export const story = {
name: 'La Provinciale lungo la Carza', name: 'La Provinciale lungo la Carza',
map: [66, 73], map: [66, 73],
background: 'assets/scenes/verso-luna.png', background: 'assets/scenes/verso-luna.png',
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]] }], effects: [{ type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [200, 30, 400, 90] }, { type: 'clouds', area: [180, 10, 460, 120], count: 3 }, { 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]], walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]],
depth: [[192, 0.45], [360, 1.7]], depth: [[192, 0.45], [360, 1.7]],
entry: [150, 206], entry: [150, 206],
@@ -1278,7 +1278,7 @@ export const story = {
name: 'Campomigliaio, il ponte vecchio', name: 'Campomigliaio, il ponte vecchio',
map: [50, 86], map: [50, 86],
background: 'assets/scenes/campomigliaio-ponte.png', background: 'assets/scenes/campomigliaio-ponte.png',
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] }], effects: [{ type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [100, 10, 300, 60] }, { type: 'clouds', area: [80, 5, 300, 50], count: 2 }, { 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]], walk: [[0, 360], [0, 232], [100, 225], [140, 240], [170, 275], [260, 330], [300, 360]],
depth: [[225, 0.6], [360, 1.5]], depth: [[225, 0.6], [360, 1.5]],
entry: [80, 330], entry: [80, 330],
@@ -1434,7 +1434,7 @@ export const story = {
name: 'Villa Adami', name: 'Villa Adami',
map: [46, 32], map: [46, 32],
background: 'assets/scenes/villa-adami.png', background: 'assets/scenes/villa-adami.png',
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'] }], effects: [{ type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [300, 20, 330, 60] }, { type: 'clouds', area: [300, 5, 340, 60], count: 2 }, { 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]], 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]], depth: [[244, 0.5], [360, 1.2]],
entry: [300, 345], entry: [300, 345],
@@ -1695,7 +1695,7 @@ export const story = {
map: [14, 19], map: [14, 19],
background: 'assets/scenes/scuola.png', background: 'assets/scenes/scuola.png',
// Il tricolore che sventola in cima all'asta. // Il tricolore che sventola in cima all'asta.
effects: [{ type: 'flag', at: [122, 117], w: 13, h: 9, colors: ['#2f9a4a', '#f4f1e8', '#d2393a'] }], effects: [{ type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 30, 400, 70] }, { type: 'clouds', area: [130, 10, 380, 100], count: 3 }, { type: 'flag', at: [122, 117], w: 13, h: 9, colors: ['#2f9a4a', '#f4f1e8', '#d2393a'] }],
walk: [[110, 360], [118, 226], [140, 222], [268, 220], [290, 232], [470, 330], [620, 300], [640, 300], [640, 360]], walk: [[110, 360], [118, 226], [140, 222], [268, 220], [290, 232], [470, 330], [620, 300], [640, 300], [640, 360]],
depth: [[220, 0.42], [360, 1.32]], depth: [[220, 0.42], [360, 1.32]],
entry: [300, 350], entry: [300, 350],
@@ -1740,7 +1740,7 @@ export const story = {
name: 'Viale dei cipressi', name: 'Viale dei cipressi',
map: [60, 6], map: [60, 6],
background: 'assets/scenes/viale-cipressi.png', 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]] }], effects: [{ type: 'clouds', area: [230, 10, 140, 120], count: 2 }, { 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]], walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
depth: [[250, 0.16], [360, 1.2]], depth: [[250, 0.16], [360, 1.2]],
entry: [300, 350], entry: [300, 350],
@@ -1764,7 +1764,7 @@ export const story = {
name: 'Castello del Trebbio', name: 'Castello del Trebbio',
map: [84, 6], map: [84, 6],
background: 'assets/scenes/trebbio.png', 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 }], effects: [{ type: 'clouds', area: [200, 10, 440, 120], count: 3 }, { 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, 30, 400, 80] }],
music: 'assets/audio/ricordi.mp3', music: 'assets/audio/ricordi.mp3',
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]], walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],
depth: [[306, 0.75], [360, 1.05]], depth: [[306, 0.75], [360, 1.05]],