// 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); }, };