Moiré
Dustin Adkison · 2 hours ago · v1
Code written by a VibePen user, not by VibePen. It runs sandboxed on an isolated origin with no network access. Nobody at VibePen has reviewed it — treat it like any page from a stranger, and never enter a password or payment details.
Two grids of concentric rings, slowly sliding past each other. Nothing here is drawn — every band and rosette you see is interference between the two layers. Pure canvas, roughly forty lines of actual drawing code.
Source
3.2 KB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Moiré</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #08080c; }
canvas { display: block; width: 100%; height: 100%; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
(() => {
"use strict";
var cv = document.getElementById("c");
var ctx = cv.getContext("2d", { alpha: false });
var SPACING = 13; // ring pitch, px
var RINGS = 190; // rings per layer
var t = 0;
var w = 0, h = 0, dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 1.5);
var nw = Math.max(1, Math.floor(cv.clientWidth * dpr));
var nh = Math.max(1, Math.floor(cv.clientHeight * dpr));
if (nw === w && nh === h) return;
w = nw; h = nh;
cv.width = w; cv.height = h;
}
function rings(cx, cy, pitch, stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = dpr;
ctx.beginPath();
for (var i = 1; i <= RINGS; i++) {
var r = i * pitch;
// cheap cull — skip rings entirely outside the canvas
if (r - pitch > Math.hypot(Math.max(cx, w - cx), Math.max(cy, h - cy))) break;
ctx.moveTo(cx + r, cy);
ctx.arc(cx, cy, r, 0, Math.PI * 2);
}
ctx.stroke();
}
function frame() {
resize();
ctx.fillStyle = "#08080c";
ctx.fillRect(0, 0, w, h);
var pitch = SPACING * dpr;
var mx = w / 2, my = h / 2;
// the two centres orbit each other on slightly different periods,
// so the interference pattern never settles into a loop
var ax = mx + Math.cos(t * 0.21) * w * 0.16;
var ay = my + Math.sin(t * 0.17) * h * 0.16;
var bx = mx + Math.cos(t * 0.13 + 2.1) * w * 0.16;
var by = my + Math.sin(t * 0.29 + 0.7) * h * 0.16;
ctx.globalCompositeOperation = "lighter";
rings(ax, ay, pitch, "rgba(90, 200, 255, 0.55)");
rings(bx, by, pitch * 1.035, "rgba(255, 110, 160, 0.55)");
ctx.globalCompositeOperation = "source-over";
// vignette — pulls the eye to the middle where the rosettes form
var g = ctx.createRadialGradient(mx, my, Math.min(w, h) * 0.15, mx, my, Math.max(w, h) * 0.72);
g.addColorStop(0, "rgba(8, 8, 12, 0)");
g.addColorStop(1, "rgba(8, 8, 12, 0.9)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
t += 0.016;
}
var raf = 0, running = false;
function tick() { frame(); raf = requestAnimationFrame(tick); }
function play() { if (!running) { running = true; raf = requestAnimationFrame(tick); } }
function stop() { running = false; cancelAnimationFrame(raf); }
resize();
var reduced = typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduced) {
t = 1.4;
frame();
} else {
if (typeof IntersectionObserver === "function") {
new IntersectionObserver(function (e) {
e[0].isIntersecting ? play() : stop();
}, { threshold: 0 }).observe(cv);
}
document.addEventListener("visibilitychange", function () {
document.hidden ? stop() : play();
});
play();
}
window.addEventListener("resize", resize);
})();
</script>
</body>
</html>