live - 60fps 1920 x 1080
<!DOCTYPE html>
<html>
<head>
<title>Electric Arcs</title>
<style>
:root { --bg-color: #04060f; --bolt-color: #7dd3fc; --bolt-core: #ffffff; }
* { margin: 0; padding: 0; }
body { height: 100vh; background: var(--bg-color); overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const cv = document.getElementById('c'), ctx = cv.getContext('2d');
const css = getComputedStyle(document.documentElement);
const bg = (css.getPropertyValue('--bg-color') || '#04060f').trim();
const bolt = (css.getPropertyValue('--bolt-color') || '#7dd3fc').trim();
const core = (css.getPropertyValue('--bolt-core') || '#ffffff').trim();
let w, h; const arcs = [];
const mouse = { x: -1, y: -1 };
function build() { w = cv.width = innerWidth; h = cv.height = innerHeight; }
addEventListener('resize', build);
addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
build();
function makePath(x1, y1, x2, y2, disp, depth, branches, out) {
let pts = [[x1, y1], [x2, y2]];
for (let g = 0; g < depth; g++) {
const np = [];
for (let i = 0; i < pts.length - 1; i++) {
const a = pts[i], b = pts[i + 1];
const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2;
let nx = -(b[1] - a[1]), ny = (b[0] - a[0]); const len = Math.hypot(nx, ny) || 1;
const o = (Math.random() - 0.5) * disp;
np.push(a); np.push([mx + nx / len * o, my + ny / len * o]);
}
np.push(pts[pts.length - 1]); pts = np; disp *= 0.55;
}
out.push(pts);
if (branches > 0 && Math.random() < 0.9) {
const bi = (Math.random() * (pts.length - 2) | 0) + 1, from = pts[bi];
makePath(from[0], from[1], from[0] + (Math.random() - 0.5) * 320, from[1] + (Math.random() - 0.3) * 320, disp * 5, 4, branches - 1, out);
}
}
function spawn(x1, y1, x2, y2) {
const paths = []; makePath(x1, y1, x2, y2, Math.hypot(x2 - x1, y2 - y1) * 0.34, 6, 3, paths);
arcs.push({ paths, life: 1, end: [x2, y2] });
}
function strokePaths(a, width) { for (const p of a.paths) { ctx.beginPath(); ctx.moveTo(p[0][0], p[0][1]); for (let k = 1; k < p.length; k++) ctx.lineTo(p[k][0], p[k][1]); ctx.stroke(); } }
let acc = 0;
function step() {
ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
acc++;
if (acc % 16 === 0 || arcs.length === 0) {
const n = 1 + (Math.random() < 0.5 ? 1 : 0);
for (let b = 0; b < n; b++) {
const fromTop = Math.random() < 0.5;
const x1 = fromTop ? Math.random() * w : (Math.random() < 0.5 ? 0 : w), y1 = fromTop ? 0 : Math.random() * h;
const useM = mouse.x > 0 && Math.random() < 0.55;
spawn(x1, y1, useM ? mouse.x : Math.random() * w, useM ? mouse.y : Math.random() * h);
}
}
ctx.lineJoin = 'round'; ctx.lineCap = 'round';
for (let i = arcs.length - 1; i >= 0; i--) {
const a = arcs[i]; a.life -= 0.045;
if (a.life <= 0) { arcs.splice(i, 1); continue; }
// strike flash at the endpoint
const fg = ctx.createRadialGradient(a.end[0], a.end[1], 0, a.end[0], a.end[1], 90);
fg.addColorStop(0, bolt); fg.addColorStop(1, 'rgba(0,0,0,0)');
ctx.globalAlpha = a.life * 0.4; ctx.fillStyle = fg;
ctx.beginPath(); ctx.arc(a.end[0], a.end[1], 90, 0, Math.PI * 2); ctx.fill();
// glow pass
ctx.shadowBlur = 18; ctx.shadowColor = bolt;
ctx.strokeStyle = bolt; ctx.globalAlpha = a.life * 0.9; ctx.lineWidth = 3.4; strokePaths(a);
// bright core
ctx.shadowBlur = 6; ctx.shadowColor = core;
ctx.strokeStyle = core; ctx.globalAlpha = a.life; ctx.lineWidth = 1.6; strokePaths(a);
}
ctx.shadowBlur = 0; ctx.globalAlpha = 1;
}
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h); spawn(w * 0.3, 0, w * 0.6, h * 0.7); spawn(w * 0.7, 0, w * 0.4, h * 0.6); step(); }
else (function loop() { step(); requestAnimationFrame(loop); })();
</script>
</body>
</html> About this animation
Branching bolts of neon lightning crackle across the dark, striking toward your cursor with a glowing flash at every impact.