library / backgrounds / liquid-ether
live - 60fps 1920 x 1080
liquid-ether.html - self-contained
<!DOCTYPE html>
<html>
<head>
<title>Liquid Ether</title>
<style>
    :root {
        --bg-color: #04030d;
        --ink-1: #04030d;
        --ink-2: #d946ef;
        --ink-3: #22d3ee;
    }
    * { margin: 0; padding: 0; }
    body { height: 100vh; background: var(--bg-color); overflow: hidden; }
    canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
    <canvas id="c"></canvas>
<script>
    const cv = document.getElementById('c');
    const ctx = cv.getContext('2d');
    const css = getComputedStyle(document.documentElement);
    function hex(h) {
        h = (h || '').trim().replace('#', '');
        if (h.length === 3) h = h.split('').map(c => c + c).join('');
        const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    }
    const p1 = hex(css.getPropertyValue('--ink-1') || '#04030d');
    const p2 = hex(css.getPropertyValue('--ink-2') || '#d946ef');
    const p3 = hex(css.getPropertyValue('--ink-3') || '#22d3ee');
    const hi = [p3[0] + (255 - p3[0]) * 0.7, p3[1] + (255 - p3[1]) * 0.7, p3[2] + (255 - p3[2]) * 0.7];
    function hash(x, y) {
        let n = (x * 374761393 + y * 668265263) | 0;
        n = (n ^ (n >> 13)) * 1274126177 | 0;
        return ((n ^ (n >> 16)) >>> 0) / 4294967295;
    }
    function noise(x, y) {
        const ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
        const ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
        const a = hash(ix, iy), b = hash(ix + 1, iy), c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
        return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;
    }
    function fbm(x, y) {
        let v = 0, amp = 0.5;
        for (let i = 0; i < 4; i++) { v += amp * noise(x, y); x = x * 2.03 + 1.7; y = y * 2.03 - 1.1; amp *= 0.5; }
        return v;
    }
    const ss = (a, b, x) => { x = Math.max(0, Math.min(1, (x - a) / (b - a))); return x * x * (3 - 2 * x); };
    const SCALE = 5;
    let lw, lh, img, buf;
    function build() {
        cv.width = Math.ceil(innerWidth / SCALE);
        cv.height = Math.ceil(innerHeight / SCALE);
        lw = cv.width; lh = cv.height;
        img = ctx.createImageData(lw, lh); buf = img.data;
        ctx.imageSmoothingEnabled = true;
    }
    addEventListener('resize', build);
    build();
    function step(t) {
        const sc = 4.2, aspect = lw / lh;
        for (let y = 0; y < lh; y++) {
            for (let x = 0; x < lw; x++) {
                const nx = (x / lw) * sc * aspect, ny = (y / lh) * sc;
                // iterated domain warping -> tight marbled ink swirls
                const qx = fbm(nx, ny + t * 0.6);
                const qy = fbm(nx + 5.2, ny + 1.3 - t * 0.4);
                const rx = fbm(nx + 5 * qx + 1.7, ny + 5 * qy + t * 0.5);
                const ry = fbm(nx + 5 * qx - 3.1, ny + 5 * qy + 9.2);
                const v = fbm(nx + 6 * rx, ny + 6 * ry);
                const n = Math.max(0, Math.min(1, (v - 0.28) * 2.4));
                // base dark -> magenta -> cyan ramp
                let r0 = p1[0] + (p2[0] - p1[0]) * ss(0, 0.6, n);
                let g0 = p1[1] + (p2[1] - p1[1]) * ss(0, 0.6, n);
                let b0 = p1[2] + (p2[2] - p1[2]) * ss(0, 0.6, n);
                const hcy = ss(0.5, 1, n);
                r0 += (p3[0] - r0) * hcy; g0 += (p3[1] - g0) * hcy; b0 += (p3[2] - b0) * hcy;
                // bright filaments where the warp curls hardest
                const len = Math.hypot(rx - 0.5, ry - 0.5) * 2;
                const veins = ss(0.52, 0.9, len);
                r0 += (hi[0] - r0) * veins * 0.7; g0 += (hi[1] - g0) * veins * 0.7; b0 += (hi[2] - b0) * veins * 0.7;
                const i = (y * lw + x) * 4;
                buf[i] = r0; buf[i + 1] = g0; buf[i + 2] = b0; buf[i + 3] = 255;
            }
        }
        ctx.putImageData(img, 0, 0);
    }
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        step(1.0);
    } else {
        let t = 0;
        (function loop() { t += 0.007; step(t); requestAnimationFrame(loop); })();
    }
</script>
</body>
</html>

About this animation

Marbled ink swirls and diffuses like a drop of paint blooming in water, black tendrils and magenta veins folding through a pale medium.

Source Code
Copied to clipboard