diff --git a/src/engine/render.js b/src/engine/render.js index 18c5fa1..ae076cd 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -411,6 +411,48 @@ 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. + leafshadow(ctx, e, time) { + const [ax, ay, aw, ah] = e.area, t = time / 1000; + 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); + } + } + }, + // Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte + // dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero. + cloth(ctx, e, time, game) { + const bg = backgrounds.get(game.scene.background); + if (!bg) return; + const k = bg.width / WIDTH; + for (const [i, [x, y, w, h]] of e.rects.entries()) { + for (let c = 0; c < w; c++) { + const free = e.pole === 'left' ? c / w : 1 - c / w; + const dy = Math.sin(time / 260 - free * 5 + i) * free * (e.amount ?? 2.2); + ctx.drawImage(bg, (x + c) * k, y * k, k, h * k, x + c, y + dy, 1, h); + } + } + }, + // Un fascio di luce da una finestra: un cono chiaro che sfuma fino al pavimento, appena pulsante. + beam(ctx, e, time) { + const [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] = e.poly, pulse = 0.85 + 0.15 * Math.sin(time / 2300); + const g = ctx.createLinearGradient(x1, y1, (x3 + x4) / 2, (y3 + y4) / 2); + g.addColorStop(0, `rgba(255, 236, 180, ${0.42 * pulse})`); + g.addColorStop(1, 'rgba(255, 236, 180, 0)'); + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = g; + ctx.beginPath(); + ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3); ctx.lineTo(x4, y4); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + }, // Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`). lamps(ctx, e, time) { ctx.save(); @@ -426,7 +468,7 @@ export const EFFECTS = { ctx.restore(); }, }; -const BACKGROUND_EFFECTS = new Set(['sway', 'clouds']); +const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow']); 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); diff --git a/src/story/story.js b/src/story/story.js index dcc049f..b529f1c 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -522,6 +522,7 @@ export const story = { name: 'Dentro la Pieve', map: [56, 39], background: 'assets/scenes/pieve-interno.png', + effects: [{ type: 'beam', phase: ['pomeriggio', 'tramonto'], poly: [[22, 122], [32, 118], [250, 360], [150, 360]] }, { type: 'dust', phase: ['pomeriggio', 'tramonto'], area: [60, 200, 90, 110], count: 9 }], // Penombra, e le sei candele dell'altare accese (Andrea, 24 ottobre). indoor: true, shade: 'rgba(14, 8, 24, 0.62)', @@ -695,6 +696,7 @@ export const story = { name: 'Ingresso del campeggio', map: [12, 84], background: 'assets/scenes/campeggio.png', + effects: [{ type: 'cloth', pole: 'right', rects: [[28, 108, 26, 44], [84, 92, 36, 72], [229, 104, 34, 30], [548, 92, 58, 64]] }], walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]], depth: [[262, 0.6], [360, 1.15]], entry: [200, 345], @@ -726,6 +728,7 @@ export const story = { name: 'Bosco ai Frati', map: [12, 96], background: 'assets/scenes/bosco-ai-frati.png', + effects: [{ type: 'leafshadow', phase: ['pomeriggio', 'tramonto'], area: [0, 296, 230, 50], count: 16 }], walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]], depth: [[292, 0.78], [360, 1.1]], entry: [320, 345], @@ -1123,7 +1126,7 @@ export const story = { name: 'Via Provinciale, il viale alberato', map: [72, 68], background: 'assets/scenes/provinciale-viale.png', - effects: [{ type: 'clouds', area: [300, 10, 220, 120], count: 2 }, { type: 'leaves', area: [20, 40, 360, 250], count: 7 }], + effects: [{ type: 'clouds', area: [300, 10, 220, 120], count: 2 }, { type: 'leaves', area: [20, 40, 360, 250], count: 4, colors: ['#a8c44e', '#efe8c8', '#7fa83a'] }], walk: [[0, 360], [0, 330], [400, 232], [462, 232], [620, 360]], depth: [[232, 0.4], [360, 1.7]], entry: [300, 350],