Weight wave
A five-letter word drawn in stroked SVG paths whose line weight and width swell as a gaussian wave rolls through each letter, with a thin accent line and crest dot riding along beneath.
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>PULSE — Weight wave</title>
<style>
:root {
--cream: #f4ecdc;
--ink: #14120f;
--tomato: #e8432a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
background: var(--cream);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
overflow: hidden;
}
.hero {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: clamp(20px, 4vh, 44px);
padding: 0 4vw;
}
.tag {
position: absolute;
top: 28px;
left: 4vw;
right: 4vw;
display: flex;
justify-content: space-between;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
}
.tag span:last-child {
color: var(--tomato);
}
.word {
width: min(94vw, 1280px);
height: auto;
display: block;
overflow: visible;
}
.word path.glyph {
fill: none;
stroke: var(--ink);
stroke-linecap: butt;
stroke-linejoin: miter;
stroke-miterlimit: 3;
}
.copy {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
text-align: center;
}
.sub {
max-width: 34ch;
font-size: clamp(16px, 1.5vw, 20px);
line-height: 1.45;
font-weight: 500;
}
.btn {
appearance: none;
border: 0;
cursor: pointer;
background: var(--ink);
color: var(--cream);
font: inherit;
font-weight: 700;
font-size: 16px;
letter-spacing: 0.02em;
padding: 16px 32px;
border-radius: 999px;
transition:
background 0.25s ease,
transform 0.25s ease;
}
.btn:hover {
background: var(--tomato);
transform: translateY(-2px);
}
.btn:focus-visible {
outline: 3px solid var(--tomato);
outline-offset: 3px;
}
@media (max-width: 600px) {
.tag {
top: 20px;
font-size: 10px;
}
.word {
width: 96vw;
}
}
</style>
</head>
<body>
<main class="hero">
<div class="tag"><span>Oro Studio</span><span>Rhythm in type</span></div>
<svg class="word" id="word" viewBox="0 0 700 215" role="img" aria-label="PULSE">
<defs>
<linearGradient id="fade" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="700" y2="0">
<stop offset="0" stop-color="#e8432a" stop-opacity="0"></stop>
<stop offset="0.12" stop-color="#e8432a" stop-opacity="1"></stop>
<stop offset="0.88" stop-color="#e8432a" stop-opacity="1"></stop>
<stop offset="1" stop-color="#e8432a" stop-opacity="0"></stop>
</linearGradient>
</defs>
<g id="g0">
<path class="glyph" d="M14 126 V14 H54 C78 14 86 28 86 44 C86 60 78 72 54 72 H14"></path>
</g>
<g id="g1">
<path class="glyph" d="M14 14 V80 C14 112 30 126 50 126 C70 126 86 112 86 80 V14"></path>
</g>
<g id="g2"><path class="glyph" d="M14 14 V126 H86"></path></g>
<g id="g3">
<path
class="glyph"
d="M84 40 C82 24 68 14 50 14 C30 14 16 24 16 42 C16 60 34 66 50 70 C68 74 84 82 84 98 C84 116 70 126 50 126 C30 126 16 116 14 100"
></path>
</g>
<g id="g4"><path class="glyph" d="M86 14 H14 V126 H86 M14 70 H72"></path></g>
<path
id="rider"
d=""
fill="none"
stroke="url(#fade)"
stroke-width="1.6"
stroke-linecap="round"
></path>
<circle id="crest" r="4" fill="#e8432a" cx="0" cy="0" opacity="0"></circle>
</svg>
<div class="copy">
<p class="sub"
>A word that breathes. Weight rolls through every letter like a heartbeat, so your brand
never sits still.</p
>
<button class="btn" type="button">Start the rhythm</button>
</div>
</main>
<script>
(function () {
var state = window.__pattern || { paused: false, reducedMotion: false, maxDpr: 2 };
var COUNT = 5;
var CYCLE = 5.2; // seconds per wave pass
var TRAVEL = 0.86; // share of the cycle spent travelling; the rest is a hairline rest beat
var MIN_STROKE = 2.5; // hairline
var MAX_STROKE = 27; // black
var MIN_SCALE = 0.8;
var MAX_SCALE = 1.16;
var SIGMA = 0.85; // wave width, measured in letters
var GAP = 8;
var CELL = 100;
var VIEW_W = 700;
var BASE_Y = 10;
var LINE_Y = 190;
var LINE_AMP = 30;
var groups = [];
var paths = [];
for (var i = 0; i < COUNT; i++) {
var g = document.getElementById("g" + i);
groups.push(g);
paths.push(g.querySelector("path"));
}
var rider = document.getElementById("rider");
var crest = document.getElementById("crest");
function bump(distance) {
return Math.exp(-(distance * distance) / (2 * SIGMA * SIGMA));
}
function render(position) {
var weights = [];
var total = 0;
for (var i = 0; i < COUNT; i++) {
var w = bump(i - position);
weights.push(w);
total += CELL * (MIN_SCALE + (MAX_SCALE - MIN_SCALE) * w);
}
total += GAP * (COUNT - 1);
var x = (VIEW_W - total) / 2;
var centers = [];
for (var j = 0; j < COUNT; j++) {
var sx = MIN_SCALE + (MAX_SCALE - MIN_SCALE) * weights[j];
groups[j].setAttribute(
"transform",
"translate(" + x.toFixed(2) + " " + BASE_Y + ") scale(" + sx.toFixed(4) + " 1)",
);
paths[j].setAttribute(
"stroke-width",
(MIN_STROKE + (MAX_STROKE - MIN_STROKE) * weights[j]).toFixed(2),
);
centers.push(x + (CELL * sx) / 2);
x += CELL * sx + GAP;
}
// the accent line: a thin swell that travels with the wave, then settles flat
var step = 8;
var d = "";
var spacing = total / COUNT;
var cx = VIEW_W / 2;
var waveX = cx + (position - (COUNT - 1) / 2) * spacing;
for (var px = 0; px <= VIEW_W; px += step) {
var u = (px - waveX) / spacing;
var y = LINE_Y - LINE_AMP * Math.exp(-(u * u) / (2 * 0.9 * 0.9));
d += (px === 0 ? "M" : "L") + px + " " + y.toFixed(2) + " ";
}
rider.setAttribute("d", d);
var visible = Math.max(0, Math.min(1, 1 - Math.abs(position - (COUNT - 1) / 2) / 4.2));
crest.setAttribute("cx", waveX.toFixed(2));
crest.setAttribute("cy", (LINE_Y - LINE_AMP).toFixed(2));
crest.setAttribute("opacity", visible.toFixed(3));
}
var time = 0;
var last = null;
function positionAt(t) {
var phase = (t % CYCLE) / CYCLE;
var k = Math.min(1, phase / TRAVEL);
// wave starts and ends well outside the word, so the loop is seamless
return -2.4 + k * (COUNT - 1 + 4.8);
}
function frame(now) {
requestAnimationFrame(frame);
if (last === null) {
last = now;
}
var dt = Math.min(0.05, (now - last) / 1000);
last = now;
if (state.paused || state.reducedMotion) {
return;
}
time += dt;
render(positionAt(time));
}
try {
if (state.reducedMotion) {
render(1.6);
} else {
render(positionAt(0.9));
}
requestAnimationFrame(frame);
} catch (e) {
render(1.6);
}
})();
</script>
</body>
</html>
Made by 6to9. Back to all motion patterns.