Components
Loading preview...
import React, { useEffect, useRef } from "react";
/**
* ProceduralFireBackground
* High-performance cinematic fire background (slow & smooth).
* Production-safe WebGL implementation.
*/
const ProceduralFireBackground: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const gl = canvas.getContext("webgl", { antialias: false });
if (!gl) {
console.warn("WebGL not supported");
return;
}
/* ===================== SHADERS ===================== */
const vertexShaderSource = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragmentShaderSource = `
precision highp float;
uniform float u_time;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(a, b, u.x) +
(c - a) * u.y * (1.0 - u.x) +
(d - b) * u.x * u.y;
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 6; i++) {
v += a * noise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
float aspect = u_resolution.x / u_resolution.y;
vec2 pos = uv;
pos.x *= aspect;
vec2 m = u_mouse;
m.x *= aspect;
float dist = distance(pos, m);
float mouseInfluence = smoothstep(0.35, 0.0, dist) * 0.35;
vec2 q = uv;
q.y -= u_time * 0.18; // 🔥 slower vertical movement
q.x += sin(u_time * 0.15) * 0.03;
float baseNoise = fbm(q * 3.0);
float detailNoise = fbm(q * 6.0 + baseNoise);
float flameShape = pow(1.1 - uv.y, 2.2);
float strength = (baseNoise * 0.6 + detailNoise * 0.4 + mouseInfluence) * flameShape;
vec3 col = vec3(0.0);
col = mix(vec3(0.08, 0.01, 0.0), vec3(0.8, 0.15, 0.0), smoothstep(0.1, 0.45, strength));
col = mix(col, vec3(1.0, 0.45, 0.0), smoothstep(0.45, 0.75, strength));
col = mix(col, vec3(1.0, 0.95, 0.7), smoothstep(0.75, 1.0, strength));
gl_FragColor = vec4(col * strength, 1.0);
}
`;
/* ===================== HELPERS ===================== */
const createShader = (type: number, source: string) => {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
};
const program = gl.createProgram();
if (!program) return;
const vs = createShader(gl.VERTEX_SHADER, vertexShaderSource);
const fs = createShader(gl.FRAGMENT_SHADER, fragmentShaderSource);
if (!vs || !fs) return;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program);
/* ===================== GEOMETRY ===================== */
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]),
gl.STATIC_DRAW
);
const posLoc = gl.getAttribLocation(program, "position");
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
const uTime = gl.getUniformLocation(program, "u_time");
const uRes = gl.getUniformLocation(program, "u_resolution");
const uMouse = gl.getUniformLocation(program, "u_mouse");
/* ===================== RESIZE (RETINA SAFE) ===================== */
const resize = () => {
const dpr = window.devicePixelRatio || 1;
const width = window.innerWidth;
const height = window.innerHeight;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
gl.viewport(0, 0, canvas.width, canvas.height);
};
resize();
window.addEventListener("resize", resize);
/* ===================== MOUSE ===================== */
let mouse = [0.5, 0.5];
const onMouseMove = (e: MouseEvent) => {
const rect = canvas.getBoundingClientRect();
mouse = [
(e.clientX - rect.left) / rect.width,
1.0 - (e.clientY - rect.top) / rect.height,
];
};
window.addEventListener("mousemove", onMouseMove);
/* ===================== RENDER LOOP ===================== */
let raf = 0;
const render = (time: number) => {
gl.uniform1f(uTime, time * 0.0009); // 🐢 slower global time
gl.uniform2f(uRes, canvas.width, canvas.height);
gl.uniform2f(uMouse, mouse[0], mouse[1]);
gl.drawArrays(gl.TRIANGLES, 0, 6);
raf = requestAnimationFrame(render);
};
raf = requestAnimationFrame(render);
/* ===================== CLEANUP ===================== */
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("resize", resize);
gl.deleteBuffer(buffer);
gl.deleteProgram(program);
gl.deleteShader(vs);
gl.deleteShader(fs);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: "fixed",
inset: 0,
background: "#000",
zIndex: -1,
display: "block",
}}
/>
);
};
export default ProceduralFireBackground;