polish: improve atmosphere effects and fix fog opacity jump
- Fire: reworked colour ramp (dark red → orange → amber → warm yellow core), per-column height variation via colFireH for jagged silhouette, ember glow at base, taller overall height, fire/rain combined in screenshot - Rain: more translucent (0.15–0.40 opacity), halved speed range, direction tilts 90°→70° as intensity rises - Embers: smaller particles (0.8–2.2px), more numerous (50–150) - Fog: fix opacity jump at ~3s — CSS keyframe animation was overriding the inline intensity-based opacity value; replaced with CSS transition via React-managed mounted state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3059fbaedf
commit
1e881764e3
6 changed files with 72 additions and 50 deletions
|
|
@ -4,19 +4,9 @@
|
||||||
z-index: 9998;
|
z-index: 9998;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
animation: fadeIn 3s ease-in;
|
|
||||||
filter: var(--fog-filter);
|
filter: var(--fog-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer1,
|
.layer1,
|
||||||
.layer2,
|
.layer2,
|
||||||
.layer3 {
|
.layer3 {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import styles from "./FogOverlay.module.css";
|
import styles from "./FogOverlay.module.css";
|
||||||
|
|
||||||
interface FogOverlayProps {
|
interface FogOverlayProps {
|
||||||
|
|
@ -6,13 +7,23 @@ interface FogOverlayProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FogOverlay({ active, intensity }: FogOverlayProps) {
|
export default function FogOverlay({ active, intensity }: FogOverlayProps) {
|
||||||
|
// One-frame delay lets the browser paint opacity:0 first so the CSS transition fires.
|
||||||
|
// Without this the element mounts at full opacity with no fade-in.
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) { setVisible(false); return; }
|
||||||
|
const id = requestAnimationFrame(() => setVisible(true));
|
||||||
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
if (!active) return null;
|
if (!active) return null;
|
||||||
|
|
||||||
// Map 0–100 to 0.15–1.0 so fog is always at least faintly visible when active
|
// Map 0–100 to 0.15–1.0 so fog is always at least faintly visible when active
|
||||||
const opacity = 0.15 + (intensity / 100) * 0.85;
|
const opacity = visible ? 0.15 + (intensity / 100) * 0.85 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.overlay} style={{ opacity }}>
|
<div className={styles.overlay} style={{ opacity, transition: "opacity 3s ease-in" }}>
|
||||||
<div className={styles.layer1} />
|
<div className={styles.layer1} />
|
||||||
<div className={styles.layer2} />
|
<div className={styles.layer2} />
|
||||||
<div className={styles.layer3} />
|
<div className={styles.layer3} />
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,14 @@ const FRAG = /* glsl */ `
|
||||||
return v / 0.9375;
|
return v / 0.9375;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire colour ramp: black → dark red → orange → yellow → pale yellow
|
// Fire colour ramp: black → deep red tips → orange → amber → warm yellow core
|
||||||
vec3 fireRamp(float t) {
|
vec3 fireRamp(float t) {
|
||||||
t = clamp(t, 0.0, 1.0);
|
t = clamp(t, 0.0, 1.0);
|
||||||
vec3 c0 = vec3(0.00, 0.00, 0.00);
|
vec3 c0 = vec3(0.00, 0.00, 0.00); // black / transparent
|
||||||
vec3 c1 = vec3(0.65, 0.04, 0.00);
|
vec3 c1 = vec3(0.68, 0.05, 0.00); // deep red (cool tips)
|
||||||
vec3 c2 = vec3(1.00, 0.28, 0.00);
|
vec3 c2 = vec3(1.00, 0.32, 0.00); // rich orange (mid flame)
|
||||||
vec3 c3 = vec3(1.00, 0.76, 0.05);
|
vec3 c3 = vec3(1.00, 0.56, 0.02); // amber-orange (inner flame)
|
||||||
vec3 c4 = vec3(1.00, 1.00, 0.55);
|
vec3 c4 = vec3(1.00, 0.84, 0.28); // warm yellow (dense core, not pure white)
|
||||||
if (t < 0.25) return mix(c0, c1, t * 4.0);
|
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.50) return mix(c1, c2, (t - 0.25) * 4.0);
|
||||||
if (t < 0.75) return mix(c2, c3, (t - 0.50) * 4.0);
|
if (t < 0.75) return mix(c2, c3, (t - 0.50) * 4.0);
|
||||||
|
|
@ -58,19 +58,25 @@ const FRAG = /* glsl */ `
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
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;
|
float t = uTime * 0.45;
|
||||||
|
|
||||||
// Noise coords: aspect-corrected X, upward-scrolling Y
|
// Base fire height (fraction of screen height) — taller overall
|
||||||
vec2 p = vec2(vUv.x * uAspect * 1.6, vUv.y / fireH * 1.4 - t);
|
float fireH = 0.42 + uIntensity * 0.28; // 0.42–0.70
|
||||||
|
|
||||||
|
// Per-column height variation: slow wide FBM humps create distinct tall/short columns.
|
||||||
|
// Varying colFireH changes where yN=1 lands on screen, so the early discard below
|
||||||
|
// produces a naturally jagged silhouette rather than a flat top.
|
||||||
|
float heightVar = 0.52 * fbm(vec2(vUv.x * uAspect * 1.1 + t * 0.07, t * 0.05));
|
||||||
|
float colFireH = fireH * (1.0 + heightVar); // per-column effective height
|
||||||
|
|
||||||
|
// Normalised height inside this column's fire zone (0=ground, 1=column top)
|
||||||
|
float yN = vUv.y / colFireH;
|
||||||
|
|
||||||
|
// Discard pixels above the fire zone — jagged because colFireH varies per column
|
||||||
|
if (yN > 1.6) { gl_FragColor = vec4(0.0); return; }
|
||||||
|
|
||||||
|
// Noise coords: aspect-corrected X, upward-scrolling Y scaled to column height
|
||||||
|
vec2 p = vec2(vUv.x * uAspect * 1.6, vUv.y / colFireH * 1.4 - t);
|
||||||
|
|
||||||
// Layered FBM — coarse structure + fine flicker
|
// Layered FBM — coarse structure + fine flicker
|
||||||
float n = fbm(p * 1.8);
|
float n = fbm(p * 1.8);
|
||||||
|
|
@ -78,12 +84,12 @@ const FRAG = /* glsl */ `
|
||||||
n += 0.20 * fbm(p * 7.0 - vec2(0.0, t * 0.6));
|
n += 0.20 * fbm(p * 7.0 - vec2(0.0, t * 0.6));
|
||||||
n /= 1.75;
|
n /= 1.75;
|
||||||
|
|
||||||
// Vary flame height horizontally for ragged top edge
|
// Fine ragged tips on top of the broad column silhouette
|
||||||
float crest = 0.25 * noise(vec2(vUv.x * uAspect * 2.0 + t * 0.18, 0.3));
|
float crest = 0.16 * noise(vec2(vUv.x * uAspect * 4.0 + t * 0.22, 1.1));
|
||||||
float yMod = yN + crest;
|
float yMod = yN + crest;
|
||||||
|
|
||||||
// Core fire value
|
// Core fire value — 0.88 lets noise breathe at ground level
|
||||||
float fire = (1.0 - yMod) * 1.1 + n * 0.75 - 0.05;
|
float fire = (1.0 - yMod) * 0.88 + n * 0.92 - 0.05;
|
||||||
fire *= 1.0 + uIntensity * 0.35;
|
fire *= 1.0 + uIntensity * 0.35;
|
||||||
fire = clamp(fire, 0.0, 1.0);
|
fire = clamp(fire, 0.0, 1.0);
|
||||||
|
|
||||||
|
|
@ -92,7 +98,10 @@ const FRAG = /* glsl */ `
|
||||||
fire *= 0.72 + edge * 0.28;
|
fire *= 0.72 + edge * 0.28;
|
||||||
|
|
||||||
vec3 col = fireRamp(fire);
|
vec3 col = fireRamp(fire);
|
||||||
float alpha = fire * fire; // quadratic → softer halo falloff
|
// Deep red/ember tint near the base — coals and heat where flame meets ground
|
||||||
|
float emberBase = smoothstep(0.30, 0.0, yN);
|
||||||
|
col = mix(col, vec3(0.70, 0.08, 0.00), emberBase * 0.45);
|
||||||
|
float alpha = pow(fire, 1.6);
|
||||||
|
|
||||||
gl_FragColor = vec4(col * alpha, alpha);
|
gl_FragColor = vec4(col * alpha, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,26 @@
|
||||||
import type { ISourceOptions } from "@tsparticles/engine";
|
import type { ISourceOptions } from "@tsparticles/engine";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rain — intensity 0–100 → count 200–600, speed 15–30
|
* Rain — intensity 0–100 → count 200–600, speed 15–22.5
|
||||||
* Fast-falling blue-white circles. At this speed the motion itself
|
* More translucent, half speed range, direction tilts from 90° (vertical)
|
||||||
* reads as rain streaks — no elongated shape needed.
|
* to 70° (wind-driven) as intensity rises. In tsparticles degrees: 90=down, 70=bottom-right lean.
|
||||||
*/
|
*/
|
||||||
export function getRainConfig(intensity: number): ISourceOptions {
|
export function getRainConfig(intensity: number): ISourceOptions {
|
||||||
const count = Math.round(200 + (intensity / 100) * 400);
|
const count = Math.round(200 + (intensity / 100) * 400);
|
||||||
const speed = 15 + (intensity / 100) * 15;
|
const speed = 15 + (intensity / 100) * 7.5; // half the previous speed range
|
||||||
|
const direction = 90 - (intensity / 100) * 20; // 90° straight down → 70° angled
|
||||||
return {
|
return {
|
||||||
fullScreen: { enable: true, zIndex: 9997 },
|
fullScreen: { enable: true, zIndex: 9997 },
|
||||||
particles: {
|
particles: {
|
||||||
number: { value: count, density: { enable: false } },
|
number: { value: count, density: { enable: false } },
|
||||||
color: { value: "#aad4f5" },
|
color: { value: "#aad4f5" },
|
||||||
shape: { type: "circle" },
|
shape: { type: "circle" },
|
||||||
opacity: { value: { min: 0.4, max: 0.75 } },
|
opacity: { value: { min: 0.15, max: 0.40 } },
|
||||||
size: { value: { min: 1, max: 2.5 } },
|
size: { value: { min: 1, max: 2.5 } },
|
||||||
move: {
|
move: {
|
||||||
enable: true,
|
enable: true,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
direction: "bottom",
|
direction: direction as never,
|
||||||
straight: true,
|
straight: true,
|
||||||
outModes: { default: "out" },
|
outModes: { default: "out" },
|
||||||
},
|
},
|
||||||
|
|
@ -29,12 +30,11 @@ export function getRainConfig(intensity: number): ISourceOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embers — intensity 0–100 → count 20–80, speed 0.5–2
|
* Embers — intensity 0–100 → count 50–150, speed 0.5–2
|
||||||
* Slow-drifting warm amber dots with flickering opacity.
|
* Smaller, more numerous slow-drifting sparks. Flickering opacity.
|
||||||
* Clearly slower and dimmer than fire — different mood entirely.
|
|
||||||
*/
|
*/
|
||||||
export function getEmbersConfig(intensity: number): ISourceOptions {
|
export function getEmbersConfig(intensity: number): ISourceOptions {
|
||||||
const count = Math.round(20 + (intensity / 100) * 60);
|
const count = Math.round(50 + (intensity / 100) * 100);
|
||||||
const speed = 0.5 + (intensity / 100) * 1.5;
|
const speed = 0.5 + (intensity / 100) * 1.5;
|
||||||
return {
|
return {
|
||||||
fullScreen: { enable: true, zIndex: 9997 },
|
fullScreen: { enable: true, zIndex: 9997 },
|
||||||
|
|
@ -46,7 +46,7 @@ export function getEmbersConfig(intensity: number): ISourceOptions {
|
||||||
value: { min: 0.15, max: 0.9 },
|
value: { min: 0.15, max: 0.9 },
|
||||||
animation: { enable: true, speed: 0.6, sync: false, startValue: "random" },
|
animation: { enable: true, speed: 0.6, sync: false, startValue: "random" },
|
||||||
},
|
},
|
||||||
size: { value: { min: 1.5, max: 4 } },
|
size: { value: { min: 0.8, max: 2.2 } },
|
||||||
move: {
|
move: {
|
||||||
enable: true,
|
enable: true,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1,018 KiB After Width: | Height: | Size: 1.1 MiB |
|
|
@ -198,13 +198,25 @@ async function captureAtmosphere(page) {
|
||||||
await page.click('button[title="Atmosphere effects"]');
|
await page.click('button[title="Atmosphere effects"]');
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
// Toggle Fog on — the toggle button has aria-label="Toggle Fog"
|
// Toggle Fog on
|
||||||
await page.click('button[aria-label="Toggle Fog"]');
|
await page.click('button[aria-label="Toggle Fog"]');
|
||||||
await page.waitForTimeout(800); // wait for fog to render
|
await page.waitForTimeout(400);
|
||||||
// Screenshot captures fog effect over the campaign view
|
|
||||||
|
|
||||||
// Close the atmosphere panel after so it doesn't clutter other captures
|
// Toggle Fire on and set intensity to 25%
|
||||||
await page.keyboard.press('Escape');
|
// Effects order: fog(0), fire(1), rain(2), embers(3) — fire is 2nd slider
|
||||||
|
await page.click('button[aria-label="Toggle Fire"]');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const sliders = document.querySelectorAll('input[type="range"]');
|
||||||
|
const fireSlider = sliders[1];
|
||||||
|
fireSlider.value = '25';
|
||||||
|
fireSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
fireSlider.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(1200); // wait for both effects to render
|
||||||
|
|
||||||
|
// Close the atmosphere panel so it doesn't clutter the screenshot
|
||||||
|
await page.click('button[title="Atmosphere effects"]');
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue