live - 60fps 1920 x 1080
<!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.
Related