Ombre delle fronde al Bosco ai Frati, bandiere del campeggio al vento, fascio di luce nella Pieve; nel viale semi e foglioline verdi invece delle foglie d'autunno

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-25 00:38:08 +02:00
co-authored by Claude Opus 5.5
parent d6de63d344
commit 55bb814cc0
2 changed files with 47 additions and 2 deletions
+43 -1
View File
@@ -411,6 +411,48 @@ export const EFFECTS = {
lc.globalCompositeOperation = 'source-over'; lc.globalCompositeOperation = 'source-over';
ctx.drawImage(layer, 0, 0); ctx.drawImage(layer, 0, 0);
}, },
// Le ombre delle fronde sul terreno: macchie scure e chiare che ondeggiano piano col vento, come sotto un albero.
leafshadow(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, t = time / 1000;
for (let i = 0; i < (e.count ?? 14); i++) {
const bx = ax + hash(i * 7) * aw, by = ay + hash(i * 13) * ah, r = 3 + hash(i * 3) * (e.size ?? 6);
const x = bx + Math.sin(t * 0.9 + i) * 2 + Math.sin(t * 0.37 + i * 2) * 2, y = by + Math.sin(t * 0.7 + i * 3) * 1;
ctx.fillStyle = i % 3 ? 'rgba(30, 20, 45, 0.24)' : 'rgba(255, 235, 170, 0.18)';
for (let dy = -r; dy <= r; dy += 1) {
const half = Math.round(Math.sqrt(r * r - dy * dy) * 1.6);
ctx.fillRect(Math.round(x - half), Math.round(y + dy / 2), half * 2, 1);
}
}
},
// 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.
cloth(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()) {
for (let c = 0; c < w; c++) {
const free = e.pole === 'left' ? c / w : 1 - c / w;
const dy = Math.sin(time / 260 - free * 5 + i) * free * (e.amount ?? 2.2);
ctx.drawImage(bg, (x + c) * k, y * k, k, h * k, x + c, y + dy, 1, h);
}
}
},
// Un fascio di luce da una finestra: un cono chiaro che sfuma fino al pavimento, appena pulsante.
beam(ctx, e, time) {
const [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] = e.poly, pulse = 0.85 + 0.15 * Math.sin(time / 2300);
const g = ctx.createLinearGradient(x1, y1, (x3 + x4) / 2, (y3 + y4) / 2);
g.addColorStop(0, `rgba(255, 236, 180, ${0.42 * pulse})`);
g.addColorStop(1, 'rgba(255, 236, 180, 0)');
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4);
ctx.closePath();
ctx.fill();
ctx.restore();
},
// 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();
@@ -426,7 +468,7 @@ export const EFFECTS = {
ctx.restore(); ctx.restore();
}, },
}; };
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds']); const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow']);
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);
+4 -1
View File
@@ -522,6 +522,7 @@ export const story = {
name: 'Dentro la Pieve', name: 'Dentro la Pieve',
map: [56, 39], map: [56, 39],
background: 'assets/scenes/pieve-interno.png', background: 'assets/scenes/pieve-interno.png',
effects: [{ type: 'beam', phase: ['pomeriggio', 'tramonto'], poly: [[22, 122], [32, 118], [250, 360], [150, 360]] }, { type: 'dust', phase: ['pomeriggio', 'tramonto'], area: [60, 200, 90, 110], count: 9 }],
// Penombra, e le sei candele dell'altare accese (Andrea, 24 ottobre). // Penombra, e le sei candele dell'altare accese (Andrea, 24 ottobre).
indoor: true, indoor: true,
shade: 'rgba(14, 8, 24, 0.62)', shade: 'rgba(14, 8, 24, 0.62)',
@@ -695,6 +696,7 @@ export const story = {
name: 'Ingresso del campeggio', name: 'Ingresso del campeggio',
map: [12, 84], map: [12, 84],
background: 'assets/scenes/campeggio.png', background: 'assets/scenes/campeggio.png',
effects: [{ type: 'cloth', pole: 'right', rects: [[28, 108, 26, 44], [84, 92, 36, 72], [229, 104, 34, 30], [548, 92, 58, 64]] }],
walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]], walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]],
depth: [[262, 0.6], [360, 1.15]], depth: [[262, 0.6], [360, 1.15]],
entry: [200, 345], entry: [200, 345],
@@ -726,6 +728,7 @@ export const story = {
name: 'Bosco ai Frati', name: 'Bosco ai Frati',
map: [12, 96], map: [12, 96],
background: 'assets/scenes/bosco-ai-frati.png', background: 'assets/scenes/bosco-ai-frati.png',
effects: [{ type: 'leafshadow', phase: ['pomeriggio', 'tramonto'], area: [0, 296, 230, 50], count: 16 }],
walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]], walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]],
depth: [[292, 0.78], [360, 1.1]], depth: [[292, 0.78], [360, 1.1]],
entry: [320, 345], entry: [320, 345],
@@ -1123,7 +1126,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: 'clouds', area: [300, 10, 220, 120], count: 2 }, { 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: 4, colors: ['#a8c44e', '#efe8c8', '#7fa83a'] }],
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],