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