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:
+119
-5
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user