Glyph dust
The word FORM is built from hundreds of tiny monospace glyphs that drift in, sweep into place left to right, flash with a bright wave while locked, then scatter again on a 9-second loop with a live phase readout.
Code: MIT license.
<!doctype html>
<html lang="en">
<head
><script>
/* 6to9 pattern harness */ (function () {
var parentPaused = false,
hidden = document.hidden,
p = (window.__pattern = { paused: false, reducedMotion: false, maxDpr: 2 });
function sync() {
p.paused = parentPaused || hidden;
}
sync();
try {
p.reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
} catch (e) {}
document.addEventListener("visibilitychange", function () {
hidden = document.hidden;
sync();
});
addEventListener("message", function (e) {
var d = e.data || {};
if (d.pattern === "pause") {
parentPaused = true;
sync();
}
if (d.pattern === "play") {
parentPaused = false;
sync();
}
});
})();
</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FORM — glyph dust</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
background: #050807;
overflow: hidden;
}
body {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
color: #d4ffec;
}
.hero {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
background:
radial-gradient(ellipse 60% 50% at 50% 48%, rgba(70, 200, 150, 0.12), transparent 70%),
linear-gradient(rgba(150, 255, 210, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
linear-gradient(90deg, rgba(150, 255, 210, 0.035) 1px, transparent 1px) 0 0 / 48px 48px,
#050807;
}
.scan {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(160, 255, 215, 0.35), transparent);
animation: scan 6s linear infinite;
z-index: 0;
pointer-events: none;
}
.still .scan {
animation: none;
top: 30%;
}
@keyframes scan {
from {
transform: translateY(0);
}
to {
transform: translateY(100vh);
}
}
.tag {
position: absolute;
top: 22px;
left: 24px;
right: 24px;
display: flex;
justify-content: space-between;
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(190, 255, 225, 0.55);
z-index: 3;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.stage {
position: relative;
flex: none;
z-index: 1;
}
.g {
position: absolute;
display: block;
text-align: center;
white-space: nowrap;
font-weight: 700;
color: #d4ffec;
will-change: transform, opacity;
pointer-events: none;
}
.copy {
position: relative;
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-top: clamp(28px, 6vh, 56px);
padding: 0 24px;
text-align: center;
}
.readout {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: rgba(190, 255, 225, 0.6);
min-height: 14px;
}
.sub {
max-width: 460px;
font-size: clamp(14px, 1.3vw, 17px);
line-height: 1.55;
color: #b5e8d0;
}
.btn {
font-family: inherit;
font-size: 13px;
letter-spacing: 0.16em;
text-transform: uppercase;
padding: 15px 30px;
color: #04100b;
background: #bfffe0;
border: 1px solid #bfffe0;
border-radius: 2px;
cursor: pointer;
transition:
background 0.25s,
color 0.25s;
}
.btn:hover {
background: transparent;
color: #bfffe0;
}
.fallback {
display: none;
font-family: "Arial Black", "Helvetica Neue", Arial, sans-serif;
font-weight: 900;
font-size: clamp(80px, 24vw, 300px);
line-height: 1;
color: #d4ffec;
letter-spacing: -0.02em;
}
.nofx .fallback {
display: block;
}
.nofx .stage {
display: none;
}
</style>
</head>
<body>
<main class="hero" id="hero">
<div class="scan"></div>
<div class="tag"><span>form/engine</span><span>v0.4 · generative</span></div>
<h1 class="sr-only">FORM</h1>
<div class="fallback" aria-hidden="true">FORM</div>
<div class="stage" id="stage" aria-hidden="true"></div>
<div class="copy">
<div class="readout" id="readout">-- glyphs · --</div>
<p class="sub"
>Hundreds of tiny particles find their place, hold for a beat, then scatter. Precision, on
a loop.</p
>
<button class="btn" type="button">Start building</button>
</div>
</main>
<script>
(function () {
var P = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
var hero = document.getElementById("hero");
var stage = document.getElementById("stage");
var readout = document.getElementById("readout");
var CYCLE = 9000;
var CHARS = "01234567890·:;+*=/<>[]{}()#%&abcdefxyz_-~^|!?";
var FONT = "'Arial Black','Helvetica Neue',Arial,sans-serif";
var DUST = "#3f9a7c";
var LOCK = "#d4ffec";
var ACCENT = "#8dffd0";
var anims = [];
var count = 0;
var clock = CYCLE * 0.08;
var lastNow = performance.now();
var lastText = "";
var resizeTimer = 0;
if (P.reducedMotion) {
hero.classList.add("still");
}
function rand(a, b) {
return a + Math.random() * (b - a);
}
function tf(x, y, rot, sc) {
return (
"translate(-50%,-50%) translate(" +
x.toFixed(1) +
"px," +
y.toFixed(1) +
"px) rotate(" +
rot.toFixed(1) +
"deg) scale(" +
sc +
")"
);
}
// Measure the word and decide the size of the stage.
function layout() {
var vw = window.innerWidth;
var vh = window.innerHeight;
var stacked = vw / vh < 0.8;
var lines = stacked ? ["FO", "RM"] : ["FORM"];
var c = document.createElement("canvas");
var ctx = c.getContext("2d");
ctx.font = "900 100px " + FONT;
var wm = 0;
lines.forEach(function (l) {
wm = Math.max(wm, ctx.measureText(l).width);
});
var asc = ctx.measureText("FORM").actualBoundingBoxAscent || 72;
var gap = 1.16;
var hUnits = asc * (1 + (lines.length - 1) * gap);
var maxW = Math.min(vw * 0.88, 1180);
var k = Math.min(maxW / wm, (vh * (stacked ? 0.34 : 0.4)) / hUnits);
return {
vw: vw,
vh: vh,
lines: lines,
wm: wm,
asc: asc,
gap: gap,
k: k,
W: Math.ceil(wm * k),
H: Math.ceil(hUnits * k),
};
}
// Sample the letter shapes from an offscreen canvas into target points.
function samplePoints(L) {
var c = document.createElement("canvas");
c.width = L.W;
c.height = L.H;
var ctx = c.getContext("2d", { willReadFrequently: true });
ctx.fillStyle = "#fff";
ctx.font = "900 " + 100 * L.k + "px " + FONT;
ctx.textAlign = "center";
ctx.textBaseline = "alphabetic";
L.lines.forEach(function (line, i) {
ctx.fillText(line, L.W / 2, L.asc * L.k * (1 + i * L.gap));
});
var data = ctx.getImageData(0, 0, L.W, L.H).data;
var step = Math.max(6.5, L.W / 70);
var pts = [];
var rows = Math.floor(L.H / step);
var cols = Math.floor(L.W / step);
for (var r = 0; r < rows; r++) {
var off = (r % 2) * step * 0.5;
for (var q = 0; q < cols; q++) {
var x = (q + 0.5) * step + off;
var y = (r + 0.5) * step;
var ix = Math.min(L.W - 1, Math.floor(x));
var iy = Math.min(L.H - 1, Math.floor(y));
if (data[(iy * L.W + ix) * 4 + 3] > 110) {
pts.push([x, y]);
}
}
}
return { pts: pts, step: step };
}
// Every animation is kept paused; the rAF loop scrubs currentTime from one shared clock.
function applyClock() {
var t = clock % CYCLE;
for (var i = 0; i < anims.length; i++) {
anims[i].currentTime = t;
}
}
function build() {
anims.forEach(function (a) {
a.cancel();
});
anims = [];
stage.textContent = "";
var L = layout();
stage.style.width = L.W + "px";
stage.style.height = L.H + "px";
var S = samplePoints(L);
var rect = stage.getBoundingClientRect();
var frag = document.createDocumentFragment();
var fontSize = Math.max(8, S.step * 1.05);
count = S.pts.length;
S.pts.forEach(function (p) {
var tx = p[0];
var ty = p[1];
var el = document.createElement("span");
el.className = "g";
el.textContent = CHARS.charAt(Math.floor(Math.random() * CHARS.length));
el.style.left = tx.toFixed(1) + "px";
el.style.top = ty.toFixed(1) + "px";
el.style.width = S.step + "px";
el.style.lineHeight = S.step + "px";
el.style.fontSize = fontSize + "px";
var lockColor = Math.random() < 0.18 ? ACCENT : LOCK;
var lockT = tf(0, 0, 0, 1);
if (P.reducedMotion) {
el.style.transform = lockT;
el.style.color = lockColor;
frag.appendChild(el);
return;
}
// Scatter positions are relative to the glyph's own target.
var bx = rand(0, L.vw) - (rect.left + tx);
var by = rand(0, L.vh) - (rect.top + ty);
var ax = bx + rand(-60, 60);
var ay = by + rand(-60, 60);
var a2x = ax + rand(-70, 70);
var a2y = ay + rand(-70, 70);
var startRot = rand(-90, 90);
var startT = tf(ax, ay, startRot, 0.7);
// Assemble sweeps left to right with noise, dissolve radiates from the centre.
var s = (tx / L.W) * 0.6 + Math.random() * 0.4;
var dx = (tx / L.W - 0.5) * 2;
var dy = (ty / L.H - 0.5) * 2;
var rr = Math.min(1, Math.sqrt(dx * dx + dy * dy) / 1.4) * 0.6 + Math.random() * 0.4;
var a0 = 0.03 + s * 0.13;
var a1 = a0 + 0.2;
var d0 = 0.64 + rr * 0.06;
var d1 = d0 + 0.18;
// A bright wave sweeps across the locked word so the hold is never frozen.
var w = a1 + (d0 - a1) * (0.15 + 0.7 * (tx / L.W));
var wHalf = (d0 - a1) * 0.12;
var frames = [
{ offset: 0, transform: startT, opacity: 0.35, color: DUST, easing: "linear" },
{
offset: a0,
transform: tf(a2x, a2y, rand(-180, 180), 0.8),
opacity: 0.55,
color: DUST,
easing: "cubic-bezier(.16,1,.3,1)",
},
{ offset: a1, transform: lockT, opacity: 1, color: lockColor, easing: "linear" },
{
offset: w - wHalf,
transform: lockT,
opacity: 1,
color: lockColor,
easing: "ease-in-out",
},
{
offset: w,
transform: tf(0, 0, 0, 1.18),
opacity: 1,
color: "#ffffff",
easing: "ease-in-out",
},
{
offset: w + wHalf,
transform: lockT,
opacity: 1,
color: lockColor,
easing: "linear",
},
{
offset: d0,
transform: lockT,
opacity: 1,
color: lockColor,
easing: "cubic-bezier(.6,0,.9,.5)",
},
{
offset: d1,
transform: tf(bx, by, rand(-180, 180), 0.7),
opacity: 0.4,
color: DUST,
easing: "linear",
},
{ offset: 1, transform: startT, opacity: 0.35, color: DUST },
];
var anim = el.animate(frames, { duration: CYCLE, iterations: Infinity, fill: "both" });
anim.pause();
anims.push(anim);
frag.appendChild(el);
});
stage.appendChild(frag);
applyClock();
lastText = "";
if (P.reducedMotion) {
readout.textContent = count + " glyphs · locked";
}
}
function phaseName(p) {
if (p < 0.03) return "drifting";
if (p < 0.36) return "assembling";
if (p < 0.64) return "locked";
if (p < 0.86) return "dissolving";
return "drifting";
}
function tick(now) {
requestAnimationFrame(tick);
var dt = Math.min(100, now - lastNow);
lastNow = now;
if (P.reducedMotion || !anims.length || P.paused) {
return;
}
clock += dt;
applyClock();
var p = (clock % CYCLE) / CYCLE;
var pct = Math.floor(p * 100);
var text = count + " glyphs · " + phaseName(p) + " · " + (pct < 10 ? "0" : "") + pct;
if (text !== lastText) {
lastText = text;
readout.textContent = text;
}
}
function start() {
try {
if (!stage.animate && !P.reducedMotion) {
throw new Error("no WAAPI");
}
build();
} catch (e) {
hero.classList.add("nofx");
return;
}
window.addEventListener("resize", function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
try {
build();
} catch (e) {
hero.classList.add("nofx");
}
}, 160);
});
requestAnimationFrame(tick);
}
start();
})();
</script>
</body>
</html>
Made by 6to9. Back to all motion patterns.