Gallery
Sunset Embers
Canvas 2DMedium
slow embers burning out over a dark canvas. Canvas 2D.
#sparks#glow#canvas#embers#sunset
Live preview
1 file
// @ts-nocheck
import { useEffect, useRef } from "react";
// Render <Sparks /> inside a container with position: relative.
const opts = {"variant":"embers","bg":"#0a0505","colors":["#f59e0b","#f43f5e","#8b5cf6"],"light":{"bg":"#fdf5f2","colors":["#b45309","#be123c","#6d28d9"]},"speed":0.75,"density":0.9};
export function Sparks() {
const ref = useRef(null);
useEffect(() => {
const canvas = ref.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
/* Both schemes ship in opts; `light` is absent in the live preview, which
resolves the scheme itself, so nothing is observed there. */
const schemeMq =
opts.light && typeof matchMedia === "function"
? matchMedia("(prefers-color-scheme: light)")
: null;
const schemeNow = () => (schemeMq && schemeMq.matches ? opts.light : opts);
/** The scheme the scene or particle set was first built from. */
const SCHEME_BUILT = schemeNow();
let COLORS = SCHEME_BUILT.colors, BG = SCHEME_BUILT.bg;
const onScheme = () => {
const s = schemeNow();
COLORS = s.colors; BG = s.bg;
if (typeof seed === "function") seed();
if (typeof applyScheme === "function") applyScheme();
};
if (schemeMq) schemeMq.addEventListener("change", onScheme);
const SPEED = opts.speed || 1, DENSITY = opts.density || 1, V = opts.variant;
let w = 0, h = 0, dpr = 1, raf = 0, t = 0;
const mouse = { x: -9999, y: -9999, on: false };
const rnd = (a, b) => a + Math.random() * (b - a);
const pick = (a) => a[(Math.random() * a.length) | 0];
const TAU = Math.PI * 2;
let ps = [], bolts = [];
function seed() { ps = []; bolts = []; ctx.fillStyle = BG; ctx.fillRect(0, 0, w, h); }
function burst(x, y) {
const n = 34 + Math.floor(rnd(0, 22));
const c = pick(COLORS);
for (let i = 0; i < n; i++) {
const a = (i / n) * TAU + rnd(-0.08, 0.08);
const v = rnd(1.4, 4.4);
ps.push({ x, y, vx: Math.cos(a) * v, vy: Math.sin(a) * v, life: 1, c, r: rnd(1.2, 2.4) });
}
}
function boltPath(x1, y1, x2, y2, depth, out) {
if (depth === 0) { out.push([x2, y2]); return; }
const mx = (x1 + x2) / 2 + rnd(-1, 1) * 26 * depth * 0.4;
const my = (y1 + y2) / 2 + rnd(-1, 1) * 26 * depth * 0.4;
boltPath(x1, y1, mx, my, depth - 1, out);
boltPath(mx, my, x2, y2, depth - 1, out);
}
function draw() {
ctx.globalAlpha = V === "lightning" ? 0.35 : 0.18;
ctx.fillStyle = BG;
ctx.fillRect(0, 0, w, h);
ctx.globalAlpha = 1;
if (V === "fireworks") {
if (t - (draw.last || 0) > 0.9 / DENSITY) { draw.last = t; burst(rnd(w * 0.15, w * 0.85), rnd(h * 0.15, h * 0.6)); }
} else if (V === "embers") {
if (ps.length < 150 * DENSITY) {
for (let i = 0; i < 3; i++) {
ps.push({ x: rnd(0, w), y: h + 8, vx: rnd(-0.3, 0.3), vy: -rnd(0.5, 1.6), life: 1, c: pick(COLORS), r: rnd(1, 2.4) });
}
}
} else if (V === "meteors") {
if (t - (draw.last || 0) > 0.35 / DENSITY) {
draw.last = t;
const y = rnd(-h * 0.2, h * 0.7);
ps.push({ x: -30, y, vx: rnd(5, 9), vy: rnd(1.6, 3.2), life: 1, c: pick(COLORS), r: rnd(1.4, 2.6), trail: true });
}
} else if (t - (draw.last || 0) > 1.2 / DENSITY) {
draw.last = t;
const out = [[rnd(w * 0.15, w * 0.85), -10]];
boltPath(out[0][0], -10, rnd(w * 0.1, w * 0.9), h * rnd(0.6, 1.0), 6, out);
bolts.push({ pts: out, life: 1, c: pick(COLORS) });
}
for (let i = ps.length - 1; i >= 0; i--) {
const p = ps[i];
p.x += p.vx * SPEED; p.y += p.vy * SPEED;
if (V === "embers") { p.vy -= 0.004; p.vx += Math.sin(t * 2 + p.y * 0.02) * 0.02; p.life -= 0.006; }
else { p.vy += V === "meteors" ? 0.012 : 0.045; p.vx *= 0.992; p.vy *= 0.992; p.life -= 0.011; }
if (p.life <= 0 || p.x > w + 60 || p.y > h + 60) { ps.splice(i, 1); continue; }
ctx.globalAlpha = Math.max(0, p.life);
ctx.fillStyle = p.c;
ctx.shadowBlur = 10; ctx.shadowColor = p.c;
if (p.trail) {
ctx.strokeStyle = p.c; ctx.lineWidth = p.r;
ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - p.vx * 6, p.y - p.vy * 6); ctx.stroke();
} else {
ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, TAU); ctx.fill();
}
ctx.shadowBlur = 0;
}
for (let i = bolts.length - 1; i >= 0; i--) {
const b = bolts[i];
b.life -= 0.05 * SPEED;
if (b.life <= 0) { bolts.splice(i, 1); continue; }
ctx.globalAlpha = Math.max(0, b.life);
ctx.strokeStyle = b.c;
ctx.shadowBlur = 18; ctx.shadowColor = b.c;
ctx.lineWidth = 1.6;
ctx.beginPath();
for (let k = 0; k < b.pts.length; k++) {
const p = b.pts[k];
if (k) ctx.lineTo(p[0], p[1]); else ctx.moveTo(p[0], p[1]);
}
ctx.stroke();
ctx.shadowBlur = 0;
}
ctx.globalAlpha = 1;
if (mouse.on && V === "fireworks" && t - (draw.lastMouse || 0) > 0.5) {
draw.lastMouse = t; burst(mouse.x, mouse.y);
}
}
function resize() {
w = canvas.clientWidth; h = canvas.clientHeight;
dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
seed();
}
const onMove = (e) => { const r = canvas.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top; mouse.on = true; };
const onLeave = () => { mouse.on = false; };
const loop = () => { t += 0.016 * SPEED; draw(); raf = requestAnimationFrame(loop); };
const ro = new ResizeObserver(resize); ro.observe(canvas);
resize(); loop();
canvas.addEventListener("pointermove", onMove); canvas.addEventListener("pointerleave", onLeave);
return () => { cancelAnimationFrame(raf); ro.disconnect(); canvas.removeEventListener("pointermove", onMove); canvas.removeEventListener("pointerleave", onLeave); if (schemeMq) schemeMq.removeEventListener("change", onScheme); };
}, []);
return (
<canvas ref={ref} style={{ position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }} />
);
}Copy or download any file. The HTML build loads Three.js from a CDN via an import map, so it runs by just opening index.html. The React build is react-three-fiber; setup notes live at the top of each file.
Variants
40 of this pattern · page 1 of 4