Noise Flow Field
Hundreds of thin lines stream across a section along the currents of a slowly changing noise field, leaving silky trails that fade behind them, like wind or water made visible.
Follow the current
How to use it
- Paste the snippet just before
</head>. - Add
data-noise-flow-fieldto a section, hero or card. It looks best on a dark background.
Options
Add any of these attributes to the same element.
data-colors | Comma-separated colours, spread from left to right. Default violet, pink and sky blue. |
data-density | Lines per 100 × 100 px. Default 12. |
data-speed | Speed multiplier. Default 1. |
data-scale | Size of the swirls. Default 1. |
data-trail | How long the trails are, from 0 to 1. Default 0.5. |
It’s drawn on a transparent canvas behind your content and paused when off screen. With reduced motion it draws one still picture of the currents.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Noise Flow Field: hundreds of thin lines stream across a section along
// the currents of a slowly changing noise field, leaving silky trails that
// fade behind them, like wind or water made visible. Drawn on a
// transparent canvas behind your content, and paused when off screen.
// Usage: <section data-noise-flow-field style="background:#0b0618">…</section>
// Optional: data-colors (comma-separated, spread from left to right,
// default violet, pink and sky blue), data-density (lines per 100 × 100 px,
// default 12), data-speed (default 1), data-scale (size of the swirls,
// default 1), data-trail (0 to 1, how long the trails are, default 0.5).
var DEFAULTS = { colors: ['#a855f7', '#ec4899', '#38bdf8'], density: 12, speed: 1, scale: 1, trail: 0.5 };
var css =
'[data-noise-flow-field]{position:relative;isolation:isolate;overflow:hidden}' +
'.nf-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// Smooth 3D value noise (x, y across the section, z through time), 0–1.
var perm = new Uint8Array(512), vals = new Float32Array(256);
for (var i = 0; i < 256; i++) { perm[i] = i; vals[i] = Math.random(); }
for (i = 255; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)), t = perm[i]; perm[i] = perm[j]; perm[j] = t; }
for (i = 0; i < 256; i++) perm[i + 256] = perm[i];
function smooth(t) { return t * t * (3 - 2 * t); }
function lerp(a, b, t) { return a + (b - a) * t; }
function noise(x, y, z) {
var X = Math.floor(x), Y = Math.floor(y), Z = Math.floor(z);
var u = smooth(x - X), v = smooth(y - Y), w = smooth(z - Z);
X &= 255; Y &= 255; Z &= 255;
var c = function (a, b, d) { return vals[perm[perm[perm[(X + a) & 255] + ((Y + b) & 255)] + ((Z + d) & 255)]]; };
return lerp(
lerp(lerp(c(0, 0, 0), c(1, 0, 0), u), lerp(c(0, 1, 0), c(1, 1, 0), u), v),
lerp(lerp(c(0, 0, 1), c(1, 0, 1), u), lerp(c(0, 1, 1), c(1, 1, 1), u), v), w);
}
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'nf-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
var density = +d.density || DEFAULTS.density;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var scale = 0.0045 / (+d.scale || DEFAULTS.scale);
var tail = Math.round(6 + 34 * Math.min(1, Math.max(0, d.trail !== undefined ? +d.trail : DEFAULTS.trail)));
var w = 0, h = 0, dpr = 1, z = Math.random() * 100;
var lines = [];
var R = anime.utils.random;
// Each line remembers its last few points: its trail.
function spawn(p) {
p.x = R(0, w, 1);
p.y = R(0, h, 1);
p.age = 0;
p.life = R(80, 240);
p.pts = [p.x, p.y];
// Colours run across the section from the first to the last.
p.c = Math.max(0, Math.min(colors.length - 1, Math.floor((p.x / Math.max(1, w)) * colors.length + R(-0.4, 0.4, 2))));
return p;
}
function resize() {
dpr = Math.min(2, window.devicePixelRatio || 1);
w = el.clientWidth;
h = el.clientHeight;
canvas.width = w * dpr;
canvas.height = h * dpr;
var n = Math.round(Math.min(1200, Math.max(60, (w * h * density) / 10000)));
lines = [];
for (var k = 0; k < n; k++) {
var p = spawn({});
// Start part-way through life, so they don't all fade in at once.
p.age = R(0, p.life);
lines.push(p);
}
}
// Move every line one step along the field.
function advance(dt) {
var move = dt * 0.09 * speed;
for (var k = 0; k < lines.length; k++) {
var p = lines[k];
var a = noise(p.x * scale, p.y * scale, z) * Math.PI * 4;
p.x += Math.cos(a) * move;
p.y += Math.sin(a) * move;
p.pts.push(p.x, p.y);
if (p.pts.length > tail * 2) p.pts.splice(0, 2);
if (++p.age > p.life || p.x < -4 || p.y < -4 || p.x > w + 4 || p.y > h + 4) spawn(p);
}
}
// Draw the trails, batched by colour and brightness: each fades in at
// birth and out at the end of its life, and its older half is dimmer.
function draw(strength) {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
ctx.lineWidth = 1.1;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
for (var c = 0; c < colors.length; c++) {
ctx.strokeStyle = colors[c];
for (var b = 1; b <= 4; b++) {
for (var half = 0; half < 2; half++) {
ctx.globalAlpha = strength * (b / 4) * (half ? 0.9 : 0.35);
ctx.beginPath();
for (var k = 0; k < lines.length; k++) {
var p = lines[k];
if (p.c !== c) continue;
var fade = Math.min(1, p.age / 30, (p.life - p.age) / 30);
if (Math.max(1, Math.ceil(fade * 4)) !== b) continue;
var pts = p.pts, n = pts.length / 2, mid = Math.floor(n / 2);
var from = half ? mid : 0, to = half ? n - 1 : mid;
if (to <= from) continue;
ctx.moveTo(pts[from * 2], pts[from * 2 + 1]);
for (var i = from + 1; i <= to; i++) ctx.lineTo(pts[i * 2], pts[i * 2 + 1]);
}
ctx.stroke();
}
}
}
ctx.globalAlpha = 1;
}
resize();
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// A still picture of the currents: long, quiet trails.
tail = 60;
var still = function () {
for (var s = 0; s < 60; s++) advance(16);
draw(0.6);
};
still();
if (window.ResizeObserver) new ResizeObserver(function () { resize(); still(); }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
var dt = last ? Math.min(50, now - last) : 16;
last = now;
z += dt * 0.00012 * speed;
advance(dt);
draw(1);
}
});
// Pause while off screen to save battery.
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-noise-flow-field]').forEach(function (el) {
if (el._nf) return;
el._nf = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-noise-flow-field style="background: #0b0618; color: #fff">
<h1>Follow the current</h1>
</section>