Fasi del giorno legate alla storia (pomeriggio, tramonto, sera) ed effetti animati: lucine, lampioni, acqua, fumo, bandiere, foglie, pulviscolo, occhi che sbattono

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:13:13 +02:00
co-authored by Claude Opus 5.5
parent 9cb026074b
commit ab2bfff1ad
5 changed files with 177 additions and 8 deletions
+119 -5
View File
@@ -161,7 +161,9 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
}
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
drawApparitions(ctx, game, time);
drawLights(ctx, scene, time);
drawPhase(ctx, game);
drawEffects(ctx, game, time);
drawLights(ctx, game, time);
for (const shape of art) if (shape.top) drawShape(ctx, shape);
if (spots) drawSpots(ctx, game);
if (debug) drawDebug(ctx, game, debug);
@@ -196,6 +198,117 @@ function drawApparitions(ctx, game, time) {
}
}
// Un effetto o una luce vale adesso? `when` (condizione sui flag) e `phase` (una fase del giorno o un elenco).
function onNow(e, game) {
if (!check(e.when, game.state)) return false;
if (!e.phase) return true;
const now = game.phase()?.id;
return Array.isArray(e.phase) ? e.phase.includes(now) : e.phase === now;
}
// La luce della fase del giorno sulle scene all'aperto: un velo di colore (`tint` della fase), non sugli interni.
function drawPhase(ctx, game) {
const tint = game.phase()?.tint;
if (!tint || game.scene.indoor) return;
ctx.fillStyle = tint;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
}
// 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;
// Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo.
export const EFFECTS = {
// Lucine dei festoni: ognuna si accende e si spegne per conto suo, coi suoi colori.
bulbs(ctx, e, time) {
const colors = e.colors ?? ['#ff5a5a', '#ffd25a', '#5ad2ff', '#7dff6a'];
e.points.forEach(([x, y], i) => {
const on = hash(Math.floor(time / (e.every ?? 450)) * 7 + i * 13) < 0.7;
if (!on) return;
const c = colors[i % colors.length];
ctx.fillStyle = c;
ctx.globalAlpha = 0.35;
ctx.fillRect(x - 2, y - 2, 5, 5);
ctx.globalAlpha = 1;
ctx.fillRect(x - 1, y - 1, 2, 2);
ctx.fillStyle = '#fff';
ctx.fillRect(x - 1, y - 1, 1, 1);
});
},
// Luccichii sull'acqua: punti chiari che compaiono e spariscono in giro per l'area.
sparkle(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, step = e.every ?? 260;
for (let i = 0; i < (e.count ?? 6); i++) {
const t = Math.floor(time / step + i * 0.37);
const x = ax + Math.floor(hash(t * 31 + i) * aw), y = ay + Math.floor(hash(t * 17 + i * 5) * ah);
ctx.fillStyle = e.fill ?? 'rgba(255, 255, 240, 0.9)';
ctx.fillRect(x, y, hash(t + i) < 0.5 ? 2 : 1, 1);
}
},
// Una bandiera che sventola: a strisce (`colors`, verticali), ogni colonna sale e scende con l'onda.
flag(ctx, e, time) {
const [x0, y0] = e.at, w = e.w ?? 12, h = e.h ?? 8, colors = e.colors;
for (let c = 0; c < w; c++) {
const dy = Math.round(Math.sin(time / 180 - c * 0.7) * (c / w) * 1.6);
ctx.fillStyle = colors[Math.min(colors.length - 1, Math.floor(c * colors.length / w))];
ctx.fillRect(x0 + c, y0 + dy, 1, h);
}
},
// Il fumo da un comignolo: sbuffi che salgono, crescono e svaniscono.
smoke(ctx, e, time) {
const [x, y] = e.at, life = 2600;
for (let i = 0; i < 4; i++) {
const t = ((time + i * life / 4) % life) / life;
const r = 1 + t * 3, px = x + Math.sin(t * 5 + i) * 2 + t * 6, py = y - t * 22;
ctx.fillStyle = `rgba(225, 225, 230, ${0.5 * (1 - t)})`;
ctx.fillRect(Math.round(px - r), Math.round(py - r), Math.round(r * 2), Math.round(r * 2));
}
},
// Foglie che cadono ondeggiando.
leaves(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['#d7b23a', '#b88a2a', '#8fa83a'];
for (let i = 0; i < (e.count ?? 6); i++) {
const fall = 5200 + hash(i * 3) * 3000, t = ((time + hash(i) * fall) % fall) / fall;
const x = ax + hash(i * 7) * aw + Math.sin(t * 9 + i) * 6, y = ay + t * ah;
ctx.fillStyle = colors[i % colors.length];
ctx.fillRect(Math.round(x), Math.round(y), 2, Math.sin(t * 20 + i) > 0 ? 1 : 2);
}
},
// Pulviscolo che galleggia in un fascio di luce.
dust(ctx, e, time) {
const [ax, ay, aw, ah] = e.area;
for (let i = 0; i < (e.count ?? 8); i++) {
const t = time / 3000 + hash(i * 11);
const x = ax + ((hash(i * 5) * aw + Math.sin(t * 2 + i) * 3) % aw + aw) % aw, y = ay + ((hash(i * 9) * ah - t * 4) % ah + ah) % ah;
ctx.fillStyle = 'rgba(255, 245, 200, 0.75)';
ctx.fillRect(Math.round(x), Math.round(y), 1, 1);
}
},
// Qualcosa che sbatte le palpebre (gli occhi del Regolo): aperto, poi chiuso per un attimo, ogni tanto.
blink(ctx, e, time) {
const every = e.every ?? 1400, closed = (time % every) < (e.closed ?? 140);
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);
},
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
lamps(ctx, e, time) {
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (const [i, [x, y]] of e.points.entries()) {
const r = (e.glow ?? 16) * (0.94 + 0.06 * Math.sin(time / 500 + i));
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, 'rgba(255, 210, 120, 0.55)');
g.addColorStop(1, 'rgba(255, 170, 70, 0)');
ctx.fillStyle = g;
ctx.fillRect(x - r, y - r, r * 2, r * 2);
}
ctx.restore();
},
};
function drawEffects(ctx, game, time) {
for (const e of game.scene.effects ?? []) if (onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time);
}
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
// Ogni fiamma ha due disegni che si alternano a caso, e intorno un alone caldo che pulsa piano.
const FLAME_STEP = 140;
@@ -203,15 +316,16 @@ export function flameFrame(time, i) {
const n = Math.floor(time / FLAME_STEP) * 31 + i * 17;
return ((n * 2654435761) >>> 0) % 7 < 3 ? 1 : 0;
}
function drawLights(ctx, scene, time) {
function drawLights(ctx, game, time) {
const scene = game.scene, lights = (scene.lights ?? []).filter(l => onNow(l, game));
if (scene.shade) {
ctx.fillStyle = scene.shade;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
}
if (!scene.lights?.length) return;
if (!lights.length) return;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
scene.lights.forEach(({ at: [x, y], glow = 30 }, i) => {
lights.forEach(({ at: [x, y], glow = 30 }, i) => {
const pulse = 0.85 + 0.15 * Math.sin(time / 260 + i * 1.7);
const g = ctx.createRadialGradient(x, y - 2, 0, x, y - 2, glow * pulse);
g.addColorStop(0, 'rgba(255, 170, 70, 0.4)');
@@ -220,7 +334,7 @@ function drawLights(ctx, scene, time) {
ctx.fillRect(x - glow, y - 2 - glow, glow * 2, glow * 2);
});
ctx.restore();
scene.lights.forEach(({ at: [x, y] }, i) => {
lights.forEach(({ at: [x, y] }, i) => {
const tall = flameFrame(time, i);
ctx.fillStyle = '#ff9a2e';
ctx.fillRect(x - 1, y - 3 - tall, 2, 3 + tall);