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).
function onNow(e, game) {
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)));
});
},
// 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) {
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);
const [ax, ay, aw, ah] = e.area, every = e.every ?? 6000, run = Math.floor(time / every), t = (time % every) / every;
if (t > 0.6) return;
const k = t / 0.6, rtl = hash(run * 13) < 0.5;
for (let i = 0; i < (e.count ?? 4); i++) {
const lag = i * 0.05, kk = k - lag;
if (kk < 0) continue;
const x0 = rtl ? ax + aw + 10 - kk * (aw + 30) : ax - 10 + kk * (aw + 30);
const x = Math.round(x0 + (hash(run * 5 + i) - 0.5) * 16);
const y = Math.round(ay + hash(run * 3 + i) * ah - kk * 20 + Math.sin(kk * 14 + i) * 4);
const up = Math.floor(time / 90 + i * 2) % 3;
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
@@ -336,7 +391,7 @@ export const EFFECTS = {
sway(ctx, e, time, game) {
const bg = backgrounds.get(game.scene.background);
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()) {
const a = amount * (0.65 * Math.sin(time / 1900 + i * 1.7) + 0.35 * Math.sin(time / 830 + i * 2.9));
ctx.save();
@@ -347,6 +402,25 @@ export const EFFECTS = {
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`).
lamps(ctx, e, time) {
ctx.save();
@@ -362,7 +436,7 @@ export const EFFECTS = {
ctx.restore();
},
};
const BACKGROUND_EFFECTS = new Set(['sway']);
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds']);
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);