feat: replace particle fire with Three.js FBM shader overlay
Switch fire effect from tsParticles to a full-screen Three.js WebGL shader using layered FBM noise for volumetric-style flames rising from the bottom of the screen. Also fix rain/embers canvas banding by switching them to tsParticles fullScreen mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bcf118093b
commit
e2ce57527f
6 changed files with 276 additions and 71 deletions
81
client/package-lock.json
generated
81
client/package-lock.json
generated
|
|
@ -13,11 +13,13 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/three": "^0.183.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
|
|
@ -47,6 +49,12 @@
|
|||
"three": "^0.143.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@3d-dice/dice-box-threejs/node_modules/three": {
|
||||
"version": "0.143.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz",
|
||||
"integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
|
|
@ -354,6 +362,13 @@
|
|||
"@babylonjs/core": "^5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
|
|
@ -1673,6 +1688,13 @@
|
|||
"@tsparticles/engine": "3.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -1753,6 +1775,36 @@
|
|||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.183.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz",
|
||||
"integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
|
|
@ -1774,6 +1826,13 @@
|
|||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.69",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/babylonjs-gltf2interface": {
|
||||
"version": "5.57.1",
|
||||
"resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-5.57.1.tgz",
|
||||
|
|
@ -2004,6 +2063,13 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -2083,6 +2149,13 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||
"integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -2352,9 +2425,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.143.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz",
|
||||
"integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==",
|
||||
"version": "0.183.2",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz",
|
||||
"integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@
|
|||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"three": "^0.183.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/three": "^0.183.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||
import { loadSlim } from "@tsparticles/slim";
|
||||
import type { AtmosphereState } from "../lib/atmosphereTypes";
|
||||
import { getFireConfig, getRainConfig, getEmbersConfig } from "../lib/particleConfigs";
|
||||
import { getRainConfig, getEmbersConfig } from "../lib/particleConfigs";
|
||||
|
||||
// Module-level singleton so the engine is only initialised once
|
||||
let enginePromise: Promise<void> | null = null;
|
||||
|
|
@ -16,18 +15,6 @@ function ensureEngine(): Promise<void> {
|
|||
return enginePromise;
|
||||
}
|
||||
|
||||
const overlayStyle: CSSProperties = {
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 9997,
|
||||
pointerEvents: "none",
|
||||
};
|
||||
|
||||
const instanceStyle: CSSProperties = {
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
};
|
||||
|
||||
interface ParticleOverlayProps {
|
||||
atmosphere: AtmosphereState;
|
||||
}
|
||||
|
|
@ -41,26 +28,15 @@ export default function ParticleOverlay({ atmosphere }: ParticleOverlayProps) {
|
|||
|
||||
if (!ready) return null;
|
||||
|
||||
const { fire, rain, embers } = atmosphere;
|
||||
const anyActive = fire.active || rain.active || embers.active;
|
||||
if (!anyActive) return null;
|
||||
const { rain, embers } = atmosphere;
|
||||
|
||||
return (
|
||||
<div style={overlayStyle}>
|
||||
{fire.active && (
|
||||
<Particles
|
||||
key={`fire-${fire.intensity}`}
|
||||
id="particles-fire"
|
||||
options={getFireConfig(fire.intensity)}
|
||||
style={instanceStyle}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
{rain.active && (
|
||||
<Particles
|
||||
key={`rain-${rain.intensity}`}
|
||||
id="particles-rain"
|
||||
options={getRainConfig(rain.intensity)}
|
||||
style={instanceStyle}
|
||||
/>
|
||||
)}
|
||||
{embers.active && (
|
||||
|
|
@ -68,9 +44,8 @@ export default function ParticleOverlay({ atmosphere }: ParticleOverlayProps) {
|
|||
key={`embers-${embers.intensity}`}
|
||||
id="particles-embers"
|
||||
options={getEmbersConfig(embers.intensity)}
|
||||
style={instanceStyle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
186
client/src/components/ThreeFireOverlay.tsx
Normal file
186
client/src/components/ThreeFireOverlay.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
// Full-screen quad trick — bypasses the camera entirely
|
||||
const VERT = /* glsl */ `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position.xy, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
// Gradient-noise FBM fire shader
|
||||
// vUv.y = 0 at screen bottom, 1 at screen top (Three.js PlaneGeometry convention)
|
||||
const FRAG = /* glsl */ `
|
||||
uniform float uTime;
|
||||
uniform float uIntensity; // 0–1
|
||||
uniform float uAspect;
|
||||
varying vec2 vUv;
|
||||
|
||||
// Gradient noise ─────────────────────────────────────────────────────
|
||||
vec2 hash2(vec2 p) {
|
||||
p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));
|
||||
return -1.0 + 2.0 * fract(sin(p) * 43758.5453);
|
||||
}
|
||||
float noise(vec2 p) {
|
||||
vec2 i = floor(p);
|
||||
vec2 f = fract(p);
|
||||
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||
return mix(
|
||||
mix(dot(hash2(i), f),
|
||||
dot(hash2(i+vec2(1,0)), f-vec2(1,0)), u.x),
|
||||
mix(dot(hash2(i+vec2(0,1)), f-vec2(0,1)),
|
||||
dot(hash2(i+vec2(1,1)), f-vec2(1,1)), u.x),
|
||||
u.y);
|
||||
}
|
||||
float fbm(vec2 p) {
|
||||
float v = 0.0;
|
||||
v += 0.5000 * noise(p); p = p * 2.03 + vec2(0.31, 0.12);
|
||||
v += 0.2500 * noise(p); p = p * 1.97 + vec2(0.24, 0.35);
|
||||
v += 0.1250 * noise(p); p = p * 2.01 + vec2(0.17, 0.28);
|
||||
v += 0.0625 * noise(p);
|
||||
return v / 0.9375;
|
||||
}
|
||||
|
||||
// Fire colour ramp: black → dark red → orange → yellow → pale yellow
|
||||
vec3 fireRamp(float t) {
|
||||
t = clamp(t, 0.0, 1.0);
|
||||
vec3 c0 = vec3(0.00, 0.00, 0.00);
|
||||
vec3 c1 = vec3(0.65, 0.04, 0.00);
|
||||
vec3 c2 = vec3(1.00, 0.28, 0.00);
|
||||
vec3 c3 = vec3(1.00, 0.76, 0.05);
|
||||
vec3 c4 = vec3(1.00, 1.00, 0.55);
|
||||
if (t < 0.25) return mix(c0, c1, t * 4.0);
|
||||
if (t < 0.50) return mix(c1, c2, (t - 0.25) * 4.0);
|
||||
if (t < 0.75) return mix(c2, c3, (t - 0.50) * 4.0);
|
||||
return mix(c3, c4, (t - 0.75) * 4.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// How tall the fire reaches (fraction of screen height)
|
||||
float fireH = 0.28 + uIntensity * 0.24; // 0.28–0.52
|
||||
|
||||
// Normalised height inside fire zone (0=ground, 1=top of flames)
|
||||
float yN = vUv.y / fireH;
|
||||
|
||||
// Discard pixels comfortably above the fire zone
|
||||
if (yN > 1.6) { gl_FragColor = vec4(0.0); return; }
|
||||
|
||||
float t = uTime * 0.45;
|
||||
|
||||
// Noise coords: aspect-corrected X, upward-scrolling Y
|
||||
vec2 p = vec2(vUv.x * uAspect * 1.6, vUv.y / fireH * 1.4 - t);
|
||||
|
||||
// Layered FBM — coarse structure + fine flicker
|
||||
float n = fbm(p * 1.8);
|
||||
n += 0.55 * fbm(p * 3.6 + vec2(t * 0.25, 0.0));
|
||||
n += 0.20 * fbm(p * 7.0 - vec2(0.0, t * 0.6));
|
||||
n /= 1.75;
|
||||
|
||||
// Vary flame height horizontally for ragged top edge
|
||||
float crest = 0.25 * noise(vec2(vUv.x * uAspect * 2.0 + t * 0.18, 0.3));
|
||||
float yMod = yN + crest;
|
||||
|
||||
// Core fire value
|
||||
float fire = (1.0 - yMod) * 1.1 + n * 0.75 - 0.05;
|
||||
fire *= 1.0 + uIntensity * 0.35;
|
||||
fire = clamp(fire, 0.0, 1.0);
|
||||
|
||||
// Soft edge fade at left/right screen edges
|
||||
float edge = smoothstep(0.0, 0.07, vUv.x) * smoothstep(1.0, 0.93, vUv.x);
|
||||
fire *= 0.72 + edge * 0.28;
|
||||
|
||||
vec3 col = fireRamp(fire);
|
||||
float alpha = fire * fire; // quadratic → softer halo falloff
|
||||
|
||||
gl_FragColor = vec4(col * alpha, alpha);
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
active: boolean;
|
||||
intensity: number; // 0–100
|
||||
}
|
||||
|
||||
export default function ThreeFireOverlay({ active, intensity }: Props) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: false });
|
||||
renderer.setClearColor(0x000000, 0);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(w, h, false);
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
// Camera irrelevant for full-screen quad, but required by renderer
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
const uniforms = {
|
||||
uTime: { value: 0 },
|
||||
uIntensity: { value: intensity / 100 },
|
||||
uAspect: { value: w / h },
|
||||
};
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: VERT,
|
||||
fragmentShader: FRAG,
|
||||
transparent: true,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
|
||||
scene.add(mesh);
|
||||
|
||||
let rafId: number;
|
||||
const startTime = performance.now();
|
||||
|
||||
function animate() {
|
||||
rafId = requestAnimationFrame(animate);
|
||||
uniforms.uTime.value = (performance.now() - startTime) / 1000;
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
function onResize() {
|
||||
const nw = window.innerWidth;
|
||||
const nh = window.innerHeight;
|
||||
renderer.setSize(nw, nh, false);
|
||||
uniforms.uAspect.value = nw / nh;
|
||||
}
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.removeEventListener("resize", onResize);
|
||||
material.dispose();
|
||||
renderer.dispose();
|
||||
};
|
||||
}, [active, intensity]);
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
pointerEvents: "none",
|
||||
zIndex: 9997,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,38 +1,5 @@
|
|||
import type { ISourceOptions } from "@tsparticles/engine";
|
||||
|
||||
/**
|
||||
* Fire — intensity 0–100 → count 60–200, speed 4–10
|
||||
* Small-to-medium orange/red/yellow particles rising upward fast.
|
||||
* outModes "out" respawns particles at the bottom when they exit the top.
|
||||
* Noticeably faster and brighter than embers.
|
||||
*/
|
||||
export function getFireConfig(intensity: number): ISourceOptions {
|
||||
const count = Math.round(60 + (intensity / 100) * 140);
|
||||
const speed = 4 + (intensity / 100) * 6;
|
||||
return {
|
||||
fullScreen: { enable: false },
|
||||
background: { opacity: 0 },
|
||||
particles: {
|
||||
number: { value: count, density: { enable: false } },
|
||||
color: { value: ["#ff2200", "#ff5500", "#ff8800", "#ffbb00", "#ffee00"] },
|
||||
shape: { type: "circle" },
|
||||
opacity: {
|
||||
value: { min: 0.6, max: 1 },
|
||||
animation: { enable: true, speed: 3, sync: false, startValue: "random" },
|
||||
},
|
||||
size: { value: { min: 1, max: 5 } },
|
||||
move: {
|
||||
enable: true,
|
||||
speed: { min: speed * 0.6, max: speed },
|
||||
direction: "top",
|
||||
random: true,
|
||||
straight: false,
|
||||
outModes: { default: "out" },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rain — intensity 0–100 → count 200–600, speed 15–30
|
||||
* Fast-falling blue-white circles. At this speed the motion itself
|
||||
|
|
@ -42,8 +9,7 @@ export function getRainConfig(intensity: number): ISourceOptions {
|
|||
const count = Math.round(200 + (intensity / 100) * 400);
|
||||
const speed = 15 + (intensity / 100) * 15;
|
||||
return {
|
||||
fullScreen: { enable: false },
|
||||
background: { opacity: 0 },
|
||||
fullScreen: { enable: true, zIndex: 9997 },
|
||||
particles: {
|
||||
number: { value: count, density: { enable: false } },
|
||||
color: { value: "#aad4f5" },
|
||||
|
|
@ -58,6 +24,7 @@ export function getRainConfig(intensity: number): ISourceOptions {
|
|||
outModes: { default: "out" },
|
||||
},
|
||||
},
|
||||
interactivity: { events: { onClick: { enable: false }, onHover: { enable: false } } },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -70,8 +37,7 @@ export function getEmbersConfig(intensity: number): ISourceOptions {
|
|||
const count = Math.round(20 + (intensity / 100) * 60);
|
||||
const speed = 0.5 + (intensity / 100) * 1.5;
|
||||
return {
|
||||
fullScreen: { enable: false },
|
||||
background: { opacity: 0 },
|
||||
fullScreen: { enable: true, zIndex: 9997 },
|
||||
particles: {
|
||||
number: { value: count, density: { enable: false } },
|
||||
color: { value: ["#ff8800", "#ffaa00", "#ffcc44", "#ff6600"] },
|
||||
|
|
@ -90,5 +56,6 @@ export function getEmbersConfig(intensity: number): ISourceOptions {
|
|||
outModes: { default: "out" },
|
||||
},
|
||||
},
|
||||
interactivity: { events: { onClick: { enable: false }, onHover: { enable: false } } },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import DiceTray from "../components/DiceTray";
|
|||
import FogOverlay from "../components/FogOverlay";
|
||||
import AtmospherePanel from "../components/AtmospherePanel";
|
||||
import ParticleOverlay from "../components/ParticleOverlay";
|
||||
import ThreeFireOverlay from "../components/ThreeFireOverlay";
|
||||
import type { AtmosphereState } from "../lib/atmosphereTypes";
|
||||
import { defaultAtmosphere } from "../lib/atmosphereTypes";
|
||||
import SelectDropdown from "../components/SelectDropdown";
|
||||
|
|
@ -484,6 +485,7 @@ export default function CampaignView() {
|
|||
<RollLog campaignId={campaignId} rolls={rolls} freshIds={freshIds} />
|
||||
<DiceTray roll={diceRoll} onAnimationComplete={handleDiceComplete} />
|
||||
<FogOverlay active={atmosphere.fog.active} />
|
||||
<ThreeFireOverlay active={atmosphere.fire.active} intensity={atmosphere.fire.intensity} />
|
||||
<ParticleOverlay atmosphere={atmosphere} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue