Moiré Interference
Two sets of fine rings are laid over each other, and where they cross, broad ghostly bands appear that aren’t in either set. As the rings drift, the bands ripple and swing far faster than the rings themselves move. Move the pointer, or drag a finger, to steer the pink set.
Move or drag to steer the rings
How to use it
- Paste the snippet just before
</head>. - Add
data-moire-interferenceto a box with a height and a light background.
Options
Add any of these attributes to the same element.
data-pattern | rings, lines (two sets of straight lines at a slight angle) or mix (rings over lines). Default rings. |
data-spacing | Gap between lines in px. Default 8. |
data-colors | Colour of each set. Default #4c1d95,#db2777. |
data-speed | Speed multiplier. Default 1. |
data-pointer="false" | Ignore the pointer. |
The two sets are blended with multiply, so where they overlap they darken together. It’s paused when off screen, and with reduced motion it shows a still frame.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Moiré Interference: two sets of fine lines are laid over each other,
// and where they cross, big ghostly bands appear that aren't in either
// set. As one set drifts, turns or slides, the bands ripple and swing far
// faster than the lines themselves move. The pointer (or a finger) moves
// the second set; otherwise it drifts by itself.
// Usage: <div data-moire-interference style="height:360px;background:#fff"></div>
// Optional: data-pattern ("rings", "lines" or "mix": rings over straight
// lines; default rings), data-spacing (gap between lines in px, default
// 8), data-colors (colour of each set, default "#4c1d95,#db2777"),
// data-speed (speed multiplier, default 1), data-pointer="false" (ignore
// the pointer).
var DEFAULTS = { pattern: 'rings', spacing: 8, colors: '#4c1d95,#db2777', speed: 1 };
var css =
'[data-moire-interference]{position:relative;isolation:isolate;overflow:hidden}' +
'.moi-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function build(el) {
var d = el.dataset;
var canvas = document.createElement('canvas');
canvas.className = 'moi-canvas';
canvas.setAttribute('aria-hidden', 'true');
el.insertBefore(canvas, el.firstChild);
var ctx = canvas.getContext('2d');
var pattern = d.pattern || DEFAULTS.pattern;
var gap = +d.spacing || DEFAULTS.spacing;
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
if (colors.length < 2) colors.push(colors[0]);
var speed = +d.speed || DEFAULTS.speed;
var usePointer = d.pointer !== 'false';
var w = 0, h = 0, dpr = 1, t = 0, pointer = null, follow = null;
function resize() {
var nw = el.clientWidth, nh = el.clientHeight;
if (nw === w && nh === h) return false;
w = nw; h = nh;
dpr = Math.min(2, window.devicePixelRatio || 1);
canvas.width = Math.round(w * dpr);
canvas.height = Math.round(h * dpr);
return true;
}
function rings(cx, cy, color) {
var far = Math.sqrt(Math.max(cx, w - cx) * Math.max(cx, w - cx) + Math.max(cy, h - cy) * Math.max(cy, h - cy));
ctx.strokeStyle = color;
ctx.lineWidth = gap * 0.38;
ctx.beginPath();
for (var r = gap; r < far + gap; r += gap) {
ctx.moveTo(cx + r, cy);
ctx.arc(cx, cy, r, 0, Math.PI * 2);
}
ctx.stroke();
}
function lines(cx, cy, angle, color) {
var far = Math.sqrt(w * w + h * h);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.strokeStyle = color;
ctx.lineWidth = gap * 0.38;
ctx.beginPath();
for (var x = -far; x <= far; x += gap) {
ctx.moveTo(x, -far);
ctx.lineTo(x, far);
}
ctx.stroke();
ctx.restore();
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
var m = Math.min(w, h);
// The first set wanders slowly; the second follows the pointer, or a
// looping path of its own.
var ax = w / 2 + Math.cos(t * 0.21) * w * 0.12, ay = h / 2 + Math.sin(t * 0.17) * h * 0.1;
var bx = w / 2 + Math.sin(t * 0.33) * Math.min(w * 0.3, m * 0.5), by = h / 2 + Math.sin(t * 0.47 + 1) * h * 0.22;
if (follow) { bx = follow.x; by = follow.y; }
ctx.globalCompositeOperation = 'multiply';
if (pattern === 'lines') {
lines(w / 2, h / 2, 0.35, colors[0]);
lines(follow ? bx : w / 2, follow ? by : h / 2, 0.35 + Math.sin(t * 0.3) * 0.09 + (follow ? (bx / w - 0.5) * 0.3 : 0), colors[1]);
} else if (pattern === 'mix') {
lines(ax, ay, Math.sin(t * 0.13) * 0.4, colors[0]);
rings(bx, by, colors[1]);
} else {
rings(ax, ay, colors[0]);
rings(bx, by, colors[1]);
}
ctx.globalCompositeOperation = 'source-over';
}
function tick(dt) {
t += dt / 1000 * speed;
if (pointer) {
if (!follow) follow = { x: pointer.x, y: pointer.y };
follow.x += (pointer.x - follow.x) * 0.15;
follow.y += (pointer.y - follow.y) * 0.15;
} else follow = null;
}
resize();
t = 2.4;
draw();
if (reduced()) {
if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) draw(); }).observe(el);
return;
}
if (window.ResizeObserver) new ResizeObserver(function () { if (resize()) draw(); }).observe(el);
if (usePointer) {
var at = function (e) {
var b = el.getBoundingClientRect();
pointer = { x: e.clientX - b.left, y: e.clientY - b.top };
};
el.addEventListener('pointermove', function (e) { if (e.pointerType === 'mouse' || e.buttons) at(e); });
el.addEventListener('pointerdown', at);
el.addEventListener('pointerleave', function () { pointer = null; });
el.addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') pointer = null; });
el.addEventListener('pointercancel', function () { pointer = null; });
}
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;
tick(dt);
draw();
}
});
// 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-moire-interference]').forEach(function (el) {
if (el._moi) return;
el._moi = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-moire-interference style="height: 360px; background: #fff"></div>
<!-- straight lines: -->
<div data-moire-interference data-pattern="lines" style="height: 360px; background: #fff"></div>