mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
The shared "juice" layer, pure-canvas (CSP-safe, no WebGL/shaders), driven off a clock so both renderers light a scene the same way: - html/js/render/fxLayer.js (NEW): RK.fx — layered-sine flame flicker, additive warm glow, a dancing two-layer canvas flame, and pulsing hazard overlays (lava/poison/spikes) with a footprint-clip callback so it fits square cells and iso diamonds alike. - Tavern now has FIRE: 4 corner braziers + a hearth on the left wall throw real flickering light. Placed along walls, clear of spawn/aisles/tables. - Both renderers: torch/hearth/hazard tiles draw only their FLOOR base in the static pass (no more solid amber block), then RK.fx paints the animated flame / hazard pulse in the live loop. Tiled caches emitter positions and draws them over the vignette so light punches through the dark edges; iso adds them as depth-sorted fx items and keeps its RAF alive while any fire/hazard is onscreen. - Dungeon hazard zones wired end-to-end in the scene model (lava L / poison P / spikes ^ chars -> pulsing tiles) — they render the moment the generator emits them; no client-authoritative fakes. - fxLayer.js included in tavern.html + match.html. Suite 58/419 green; render files node --check clean.
119 lines
5.3 KiB
JavaScript
119 lines
5.3 KiB
JavaScript
// fxLayer.js — the shared "juice" engine (RK.fx). Pure-canvas animated FX (no WebGL, no shaders,
|
|
// CSP-safe) used by BOTH the top-down tiled renderer and the iso renderer so a scene lights up the
|
|
// same way in either projection: dancing torch/hearth flame + warm glow, and pulsing hazard tiles
|
|
// (lava / poison / spikes) for the dungeon. Everything is a function of `now` (ms) so callers just
|
|
// keep a RAF loop alive and pass the clock — no per-particle state to thread through.
|
|
(function (root) {
|
|
'use strict';
|
|
var RK = root.RK = root.RK || {};
|
|
var fx = RK.fx = RK.fx || {};
|
|
|
|
// Fire flicker → 0..1. Layered sines at incommensurate rates so it reads as a living flame
|
|
// rather than an obvious pulse. `seed` decorrelates neighbouring torches.
|
|
fx.flicker = function (now, seed) {
|
|
seed = seed || 0;
|
|
var t = now / 1000;
|
|
var v = 0.62
|
|
+ 0.20 * Math.sin(t * 11.3 + seed * 1.7)
|
|
+ 0.11 * Math.sin(t * 24.1 + seed * 4.3)
|
|
+ 0.07 * Math.sin(t * 3.7 + seed * 0.9);
|
|
return v < 0 ? 0 : (v > 1 ? 1 : v);
|
|
};
|
|
|
|
// Smooth 0..1 pulse for hazards (slower, sinusoidal breathing).
|
|
fx.pulse = function (now, period, seed) {
|
|
period = period || 1400;
|
|
return 0.5 + 0.5 * Math.sin((now / period) * Math.PI * 2 + (seed || 0));
|
|
};
|
|
|
|
// Additive warm/tinted glow disc centred on (cx, cy).
|
|
fx.glow = function (ctx, cx, cy, radius, opts) {
|
|
opts = opts || {};
|
|
if (radius <= 0) return;
|
|
var rgb = opts.rgb || '255,180,90';
|
|
var a = opts.intensity == null ? 0.5 : opts.intensity;
|
|
var g = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
|
|
g.addColorStop(0, 'rgba(' + rgb + ',' + a + ')');
|
|
g.addColorStop(0.45, 'rgba(' + rgb + ',' + (a * 0.35) + ')');
|
|
g.addColorStop(1, 'rgba(' + rgb + ',0)');
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'lighter';
|
|
ctx.fillStyle = g;
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
};
|
|
|
|
function flamePath(ctx, cx, cy, w, h, sway) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx - w / 2, cy);
|
|
ctx.quadraticCurveTo(cx - w * 0.5 + sway, cy - h * 0.55, cx + sway, cy - h);
|
|
ctx.quadraticCurveTo(cx + w * 0.5 + sway, cy - h * 0.55, cx + w / 2, cy);
|
|
ctx.quadraticCurveTo(cx, cy + h * 0.14, cx - w / 2, cy);
|
|
ctx.closePath();
|
|
}
|
|
|
|
// A dancing flame whose BASE sits at (cx, cy) and licks upward. `scale` ≈ flame width.
|
|
fx.flame = function (ctx, cx, cy, scale, now, seed) {
|
|
seed = seed || 0;
|
|
var f = fx.flicker(now, seed);
|
|
var sway = Math.sin(now / 150 + seed) * scale * 0.16;
|
|
var h = scale * (1.5 + f * 0.7);
|
|
var w = scale * 0.95;
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = 'lighter';
|
|
// Outer flame (orange envelope).
|
|
var og = ctx.createRadialGradient(cx, cy - h * 0.45, 0, cx, cy - h * 0.4, h);
|
|
og.addColorStop(0, 'rgba(255,225,150,0.95)');
|
|
og.addColorStop(0.4, 'rgba(255,140,40,0.72)');
|
|
og.addColorStop(1, 'rgba(190,40,10,0)');
|
|
ctx.fillStyle = og;
|
|
flamePath(ctx, cx, cy, w, h, sway);
|
|
ctx.fill();
|
|
// Inner core (white-hot).
|
|
var ig = ctx.createRadialGradient(cx + sway * 0.5, cy - h * 0.32, 0, cx + sway * 0.5, cy - h * 0.3, h * 0.55);
|
|
ig.addColorStop(0, 'rgba(255,255,238,0.95)');
|
|
ig.addColorStop(1, 'rgba(255,205,95,0)');
|
|
ctx.fillStyle = ig;
|
|
flamePath(ctx, cx, cy, w * 0.52, h * 0.62, sway * 0.6);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
};
|
|
|
|
// A fire emitter = flame + a soft warm ground glow. Used for torches (small) and hearths
|
|
// (large) alike; `scale` drives both.
|
|
fx.fire = function (ctx, cx, cy, scale, now, seed) {
|
|
var f = fx.flicker(now, seed || 0);
|
|
fx.glow(ctx, cx, cy - scale * 0.2, scale * (3.4 + f * 0.6), { rgb: '255,170,80', intensity: 0.16 + f * 0.12 });
|
|
fx.flame(ctx, cx, cy, scale, now, seed || 0);
|
|
};
|
|
|
|
// Hazard palettes. `rgb` = the emissive heat colour; `base` = the flat tint painted into the
|
|
// tile footprint; `period` = pulse speed (ms).
|
|
var HAZARD = {
|
|
lava: { rgb: '255,110,30', base: 'rgba(70,15,5,0.55)', period: 1500 },
|
|
poison: { rgb: '90,230,90', base: 'rgba(15,50,20,0.50)', period: 2200 },
|
|
spikes: { rgb: '170,190,215', base: 'rgba(20,24,30,0.35)', period: 1000 }
|
|
};
|
|
fx.isHazard = function (kind) { return !!HAZARD[kind]; };
|
|
fx.hazardKinds = function () { return Object.keys(HAZARD); };
|
|
|
|
// Pulsing hazard overlay. The renderer passes a `drawFootprint(ctx)` callback that traces the
|
|
// tile shape (a square cell top-down, a diamond in iso) so the flat tint clips correctly; the
|
|
// heat glow rides on top additively.
|
|
fx.hazard = function (ctx, kind, cx, cy, radius, now, drawFootprint) {
|
|
var s = HAZARD[kind];
|
|
if (!s) return;
|
|
var p = fx.pulse(now, s.period, cx * 0.13 + cy * 0.07);
|
|
if (drawFootprint) {
|
|
ctx.save();
|
|
drawFootprint(ctx);
|
|
ctx.fillStyle = s.base;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
fx.glow(ctx, cx, cy, radius * (1.0 + p * 0.25), { rgb: s.rgb, intensity: 0.16 + p * 0.4 });
|
|
};
|
|
})(typeof window !== 'undefined' ? window : this);
|