Effetti ambientali nelle altre 35 scene (lavoro di Codex): piccioni, merli, gatto, farfalle, libellula, falena, acqua che scorre, fontanella, tende, ombre delle fronde e delle nuvole
Controllato: test, partita completa e il giro di schermate degli effetti (npm run browser:effects). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
// Piccoli movimenti ambientali: nessun timer, stato di gioco o asset aggiuntivo.
|
||||
// L'orologio arriva dal renderer; lo stesso istante produce sempre lo stesso fotogramma.
|
||||
const TAU = Math.PI * 2;
|
||||
export const ambientHash = n => {
|
||||
let x = Math.imul((n | 0) ^ 0x9e3779b9, 0x21f0aaad);
|
||||
x = Math.imul(x ^ (x >>> 16), 0x735a2d97);
|
||||
return ((x ^ (x >>> 15)) >>> 0) / 4294967296;
|
||||
};
|
||||
const hash = ambientHash;
|
||||
const mix = (a, b, t) => a + (b - a) * t;
|
||||
|
||||
// Un evento breve seguito da una vera pausa. Gli offset evitano che tutti gli animali
|
||||
// entrino insieme; la dissolvenza serve solo ai bordi della loro piccola area.
|
||||
export function ambientMoment(time, { every = 32000, duration = 10000, offset = 0 } = {}) {
|
||||
const elapsed = ((time + offset) % every + every) % every;
|
||||
if (elapsed >= duration) return null;
|
||||
const t = elapsed / duration;
|
||||
return { t, alpha: Math.min(1, t * 12, (1 - t) * 12) };
|
||||
}
|
||||
|
||||
function pixel(ctx, x, y, w = 1, h = 1) { ctx.fillRect(Math.round(x), Math.round(y), w, h); }
|
||||
|
||||
export const ambientEffects = {
|
||||
// Tratti orizzontali morbidi che seguono la corrente. Il poligono `clip` della
|
||||
// scena lascia asciutti sassi, rive e tronchi: nessun luccichio da oggetto raccoglibile.
|
||||
ripples(ctx, e, time) {
|
||||
const [ax, ay, w, h] = e.area, life = e.life ?? 4200;
|
||||
for (let i = 0; i < (e.count ?? 10); i++) {
|
||||
const t = ((time + hash(i * 7 + 3) * life) % life) / life;
|
||||
const x = ax + hash(i * 13 + 5) * w + t * (e.flow?.[0] ?? 6);
|
||||
const y = ay + hash(i * 19 + 1) * h + t * (e.flow?.[1] ?? 3);
|
||||
const length = 3 + hash(i * 11) * 10;
|
||||
ctx.globalAlpha = Math.sin(t * Math.PI) * 0.32;
|
||||
ctx.fillStyle = '#d3e4d1';
|
||||
pixel(ctx, x, y, Math.round(length));
|
||||
ctx.fillStyle = '#344c40';
|
||||
pixel(ctx, x + 2, y + 1, Math.max(2, Math.round(length - 3)));
|
||||
}
|
||||
},
|
||||
// Farfalla, libellula o falena: una sola alla volta, pochi pixel, con soste lunghe.
|
||||
insect(ctx, e, time) {
|
||||
const moment = ambientMoment(time, e);
|
||||
if (!moment) return;
|
||||
const { t, alpha } = moment, [ax, ay, w, h] = e.area;
|
||||
const moth = e.kind === 'moth', dragonfly = e.kind === 'dragonfly';
|
||||
const x = ax + w * (moth ? 0.5 + 0.34 * Math.sin(t * TAU * 3) : 0.08 + t * 0.84);
|
||||
const y = ay + h * (0.5 + 0.3 * Math.sin(t * TAU * (moth ? 2 : 1.5)));
|
||||
const wing = Math.sin(time / (dragonfly ? 24 : moth ? 38 : 85));
|
||||
ctx.globalAlpha = alpha * (moth ? 0.7 : 0.9);
|
||||
ctx.fillStyle = e.fill ?? (moth ? '#d6bd8a' : dragonfly ? '#9cb6ac' : '#e5cb80');
|
||||
if (dragonfly) {
|
||||
pixel(ctx, x - 3, y + (wing > 0 ? -1 : 1), 7);
|
||||
ctx.fillStyle = '#416c6b';
|
||||
pixel(ctx, x, y - 1, 1, 4);
|
||||
} else {
|
||||
const span = wing > 0 ? 2 : 1;
|
||||
pixel(ctx, x - span, y - 1, span, 2);
|
||||
pixel(ctx, x + 1, y - 1, span, 2);
|
||||
ctx.fillStyle = '#60513b';
|
||||
pixel(ctx, x, y, 1, 2);
|
||||
}
|
||||
},
|
||||
// Piccioni/merli: pochi passi, una beccata, una sosta. La dimensione resta quella
|
||||
// di un animale lontano, senza trasformarlo in un nuovo personaggio interattivo.
|
||||
groundbird(ctx, e, time) {
|
||||
const moment = ambientMoment(time, e);
|
||||
if (!moment) return;
|
||||
const [ax, ay, w, h] = e.area, { t, alpha } = moment;
|
||||
const step = t * 8, segment = Math.floor(step), within = step % 1;
|
||||
const walk = Math.min(1, within / 0.42), peck = within > 0.65 && within < 0.85;
|
||||
const x = ax + w * (0.1 + (segment + walk) / 10), y = ay + h * 0.55;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.translate(Math.round(x), Math.round(y));
|
||||
ctx.scale(e.facing ?? 1, 1);
|
||||
ctx.fillStyle = 'rgba(25,25,20,0.18)';
|
||||
ctx.fillRect(-4, 0, 9, 2);
|
||||
ctx.fillStyle = e.kind === 'blackbird' ? '#393c32' : '#69717b';
|
||||
ctx.fillRect(-3, -5, 6, 4);
|
||||
ctx.fillRect(-5, -5, 3, 1);
|
||||
ctx.fillRect(1, peck ? -3 : -8, 3, 3);
|
||||
ctx.fillStyle = e.kind === 'blackbird' ? '#bca455' : '#b5b5a6';
|
||||
ctx.fillRect(4, peck ? -1 : -6, 2, 1);
|
||||
ctx.fillStyle = e.kind === 'blackbird' ? '#53564b' : '#9299a0';
|
||||
ctx.fillRect(-2, -4, 3, 2);
|
||||
ctx.fillStyle = '#665249';
|
||||
const stride = within < 0.42 ? Math.round(Math.sin(time / 95)) : 0;
|
||||
ctx.fillRect(-2 + stride, -1, 1, 2);
|
||||
ctx.fillRect(2 - stride, -1, 1, 2);
|
||||
ctx.restore();
|
||||
},
|
||||
// Il gatto attraversa il fondo del vicolo, poi lascia la scena vuota per quasi un minuto.
|
||||
cat(ctx, e, time) {
|
||||
const moment = ambientMoment(time, e);
|
||||
if (!moment) return;
|
||||
const { t, alpha } = moment, [a, b] = e.points;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.translate(Math.round(mix(a[0], b[0], t)), Math.round(mix(a[1], b[1], t)));
|
||||
ctx.scale(b[0] < a[0] ? -1 : 1, 1);
|
||||
ctx.fillStyle = 'rgba(20,20,20,0.16)';
|
||||
ctx.fillRect(-8, 0, 18, 2);
|
||||
ctx.fillStyle = '#50493e';
|
||||
ctx.fillRect(-7, -7, 13, 5);
|
||||
ctx.fillRect(4, -10, 5, 5);
|
||||
ctx.fillRect(4, -12, 1, 3); ctx.fillRect(8, -12, 1, 3);
|
||||
const step = Math.round(Math.sin(time / 110) * 2);
|
||||
ctx.fillRect(-5 + step, -3, 2, 4); ctx.fillRect(4 - step, -3, 2, 4);
|
||||
ctx.fillRect(-10, -9, 2, 5); ctx.fillRect(-9, -6, 3, 2);
|
||||
ctx.fillStyle = '#8a7d65'; ctx.fillRect(-5, -7, 8, 1);
|
||||
ctx.restore();
|
||||
},
|
||||
// Una goccia dalla fontanella, poi due piccoli archi nel punto d'impatto.
|
||||
drip(ctx, e, time) {
|
||||
const [x, y] = e.at, height = e.height ?? 20, t = (time % (e.every ?? 2600)) / (e.every ?? 2600);
|
||||
ctx.fillStyle = 'rgba(190,219,217,0.8)';
|
||||
if (t < 0.5) pixel(ctx, x, y + height * (t * 2) ** 2, 1, 2);
|
||||
else if (t < 0.8) {
|
||||
const k = (t - 0.5) / 0.3;
|
||||
ctx.globalAlpha = 1 - k;
|
||||
pixel(ctx, x - 1 - k * 3, y + height, 2);
|
||||
pixel(ctx, x + k * 3, y + height + 1, 2);
|
||||
}
|
||||
},
|
||||
// Tende dentro il vano della finestra: il clip le tiene lontane da cornici e persiane.
|
||||
curtain(ctx, e, time) {
|
||||
const [x, y, w, h] = e.area, wave = Math.sin(time / 1600 + x) * 1.2;
|
||||
ctx.fillStyle = 'rgba(235,223,192,0.52)';
|
||||
for (let row = 0; row < h; row++) {
|
||||
const hem = Math.sin(row / h * Math.PI) * wave;
|
||||
pixel(ctx, x, y + row, Math.max(1, Math.round(w * 0.2 + hem)));
|
||||
pixel(ctx, x + w * 0.78 + hem, y + row, Math.max(1, Math.round(w * 0.22 - hem)));
|
||||
}
|
||||
},
|
||||
// Vita dietro un vetro: luce tenue e un passaggio occasionale, sempre ritagliato
|
||||
// all'interno della vetrata. Nessun nuovo personaggio o interazione nella storia.
|
||||
windowlife(ctx, e, time) {
|
||||
const [x, y, w, h] = e.area;
|
||||
ctx.fillStyle = e.fill ?? 'rgba(255,192,105,0.12)';
|
||||
ctx.fillRect(x, y, w, h);
|
||||
const moment = ambientMoment(time, e);
|
||||
if (!moment) return;
|
||||
const px = x - 8 + moment.t * (w + 16), py = y + h * 0.3;
|
||||
ctx.globalAlpha = moment.alpha * 0.3;
|
||||
ctx.fillStyle = '#382e28';
|
||||
pixel(ctx, px, py, 4, 5);
|
||||
pixel(ctx, px - 2, py + 5, 8, Math.round(h * 0.6));
|
||||
},
|
||||
// Un'ombra larga di nuvola che attraversa lentamente il sentiero, con margini sfumati.
|
||||
cloudshadow(ctx, e, time) {
|
||||
const [x, y, w, h] = e.area, t = ((time + (e.offset ?? 0)) % 52000) / 52000;
|
||||
const cx = x - w * 0.5 + t * w * 2;
|
||||
const g = ctx.createRadialGradient(cx, y + h * 0.5, 0, cx, y + h * 0.5, w * 0.55);
|
||||
g.addColorStop(0, 'rgba(30,37,48,0.13)');
|
||||
g.addColorStop(1, 'rgba(30,37,48,0)');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
},
|
||||
};
|
||||
+39
-13
@@ -1,6 +1,7 @@
|
||||
// Disegno della scena su un canvas 640×360, ingrandito dal CSS senza sfocatura.
|
||||
// Le forme provvisorie (`art`) verranno sostituite da sfondi e sprite in pixel art.
|
||||
import { check } from './game.js';
|
||||
import { ambientEffects, ambientHash } from './ambient.js';
|
||||
export const WIDTH = 640, HEIGHT = 360;
|
||||
|
||||
function drawShape(ctx, s) {
|
||||
@@ -258,6 +259,7 @@ 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 = {
|
||||
...ambientEffects,
|
||||
// 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'];
|
||||
@@ -345,6 +347,8 @@ export const EFFECTS = {
|
||||
// Le finestre che si accendono di sera, una dopo l'altra, appena si entra nella scena.
|
||||
windows(ctx, e, time, game) {
|
||||
const since = ((game.time ?? 0) - (game.enteredAt ?? 0)) * 1000;
|
||||
ctx.globalAlpha = e.opacity ?? 1;
|
||||
ctx.globalCompositeOperation = e.blend ?? 'source-over';
|
||||
e.rects.forEach(([x, y, w, h], i) => {
|
||||
if (since < 400 + hash(i * 7 + 3) * (e.over ?? 3500)) return;
|
||||
ctx.fillStyle = 'rgba(255, 196, 100, 0.72)';
|
||||
@@ -355,9 +359,10 @@ export const EFFECTS = {
|
||||
},
|
||||
// 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 ?? 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;
|
||||
const [ax, ay, aw, ah] = e.area, every = e.every ?? 6000, duration = e.duration ?? every * 0.6;
|
||||
const elapsed = (time + (e.offset ?? 0)) % every, run = Math.floor((time + (e.offset ?? 0)) / every);
|
||||
if (elapsed >= duration) return;
|
||||
const k = elapsed / duration, 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;
|
||||
@@ -411,16 +416,23 @@ export const EFFECTS = {
|
||||
lc.globalCompositeOperation = 'source-over';
|
||||
ctx.drawImage(layer, 0, 0);
|
||||
},
|
||||
// Le ombre delle fronde sul terreno: macchie scure e chiare che ondeggiano piano col vento, come sotto un albero.
|
||||
// Ombre di foglie in piccoli gruppi irregolari. Il seme mescolato evita file di
|
||||
// ovali equidistanti (che sembravano sassi); niente rettangoli di sfondo deformati.
|
||||
leafshadow(ctx, e, time) {
|
||||
const [ax, ay, aw, ah] = e.area, t = time / 1000;
|
||||
ctx.globalAlpha = e.opacity ?? 1;
|
||||
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);
|
||||
const bx = ax + ambientHash(i * 7 + ax) * aw, by = ay + ambientHash(i * 13 + ay) * ah;
|
||||
const x = bx + Math.sin(t * 0.9 + i) * 2 + Math.sin(t * 0.37 + i * 2), y = by + Math.sin(t * 0.7 + i * 3);
|
||||
for (let leaf = 0; leaf < 4; leaf++) {
|
||||
const n = i * 31 + leaf * 7, dx = (ambientHash(n + 3) - 0.5) * 22, dy = (ambientHash(n + 9) - 0.5) * 9;
|
||||
const w = 3 + ambientHash(n + 21) * (e.size ?? 7), h = 1.5 + ambientHash(n + 4) * 2.5;
|
||||
const px = x + dx, py = y + dy;
|
||||
ctx.fillStyle = leaf === 3 ? 'rgba(255,235,170,0.10)' : 'rgba(30,29,40,0.15)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px - w, py); ctx.lineTo(px - w * 0.2, py - h);
|
||||
ctx.lineTo(px + w * 0.6, py - h * 0.5); ctx.lineTo(px + w, py + h * 0.3);
|
||||
ctx.lineTo(px, py + h); ctx.closePath(); ctx.fill();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -468,10 +480,24 @@ export const EFFECTS = {
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow']);
|
||||
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', ...Object.keys(ambientEffects)]);
|
||||
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);
|
||||
for (const e of game.scene.effects ?? []) {
|
||||
if (BACKGROUND_EFFECTS.has(e.type) !== background || !onNow(e, game)) continue;
|
||||
ctx.save();
|
||||
// Acqua solo nell'acqua, tende e sagome solo dietro il vetro; il ritaglio può
|
||||
// seguire una riva obliqua senza toccare il terreno o gli oggetti degli enigmi.
|
||||
if (e.clip) {
|
||||
ctx.beginPath();
|
||||
e.clip.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
||||
ctx.closePath();
|
||||
ctx.clip();
|
||||
}
|
||||
// Gli eventi nuovi seguono il tempo giocato: pausa e scheda nascosta non li fanno saltare.
|
||||
const at = ambientEffects[e.type] ? (game.time ?? time / 1000) * 1000 : time;
|
||||
EFFECTS[e.type]?.(ctx, e, at, game);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
|
||||
|
||||
Reference in New Issue
Block a user