Ornament — stepped bloom
Variant: Stepped bloom
A ripple in masonry: stepped-diamond rings born at a summit a quarter-cycle apart, spreading to the rim and thinning out as they go. One decelerating curve covers each ring’s whole life, because a wave sheds energy as it travels — quick out of the centre, slow at the edge — and the birth is at opacity zero so a ring grows out of the summit rather than appearing on it. The stepped outline is what keeps this carving instead of water; a circle doing exactly this would be a pond, and the library already has a pond. Stroke weight is pinned by non-scaling-stroke from birth to rim — let it scale and young rings read bold, old rings faint, a depth cue the piece is not claiming. Negative delays mean the field is populated on the first frame: ambient motion must never appear to have started. Where the beacon says “look here now”, this says nothing at all — it is emission as decoration, for surfaces that should feel alive rather than watched.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ornament — stepped bloom</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--round: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — one ring's whole life, birth to rim. Four rings ride it a
quarter-cycle apart on negative delays, so the field is already
populated on the first frame: ambient motion must not appear to
have started. */
--cycle: 7.2s;
--t: calc(var(--cycle) / var(--speed));
--ease-accent: cubic-bezier(0.32, 0.72, 0, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.panel {
position: relative;
overflow: hidden;
width: min(392px, calc(100% - 26px));
height: clamp(150px, 46vw, 214px);
border: 1px solid var(--oat);
border-radius: calc(16px * var(--round));
background: var(--paper);
}
.field {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
/* Faded across the bottom third rather than stopped above the copy, so
the rings read as the surface the text is printed on. */
-webkit-mask-image: linear-gradient(var(--slate) 46%, transparent 92%);
mask-image: linear-gradient(var(--slate) 46%, transparent 92%);
}
/* The line weight never grows with the ring: non-scaling-stroke pins it
at 1.4px from birth to rim. Let the stroke scale and the young rings
read as bold and the old ones as faint — a depth cue this piece is
not claiming. */
.r {
fill: none;
stroke: var(--clay-d);
stroke-width: 1.4;
vector-effect: non-scaling-stroke;
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation: bloom var(--t) var(--ease-accent) infinite;
animation-delay: calc(var(--_k) * var(--t) / -4);
}
/* One decelerating segment for the whole travel: a wave sheds energy as
it spreads, so the ring is quick out of the summit and slow at the
rim. The stepped edge is what keeps this carving rather than water —
a circle doing exactly this would be a pond. */
@keyframes bloom {
0% {
transform: scale(0.22);
opacity: 0;
}
14% {
opacity: 0.46;
}
82% {
opacity: 0.16;
}
100% {
transform: scale(1.16);
opacity: 0;
}
}
.summit {
fill: var(--clay);
opacity: 0.55;
}
.copy {
position: absolute;
right: clamp(16px, 4.6vw, 24px);
bottom: clamp(14px, 4.2vw, 22px);
left: clamp(16px, 4.6vw, 24px);
}
.head {
font-size: clamp(15px, 4.4vw, 19px);
font-weight: 600;
letter-spacing: -0.01em;
}
.sub {
margin-top: 3px;
font-size: clamp(11.5px, 3.3vw, 13px);
color: color-mix(in srgb, var(--slate) 55%, transparent);
}
@media (prefers-reduced-motion: reduce) {
/* Growth is travel, so it goes: the four rings park at the sizes the
ripple would have passed through, and what survives is brightness
walking outward through them on the doubled cycle. The nesting is
the piece's actual subject, and it is better legible standing
still. */
.r {
animation: glow calc(var(--t) * 2) ease infinite;
animation-delay: calc(var(--_k) * var(--t) / 2 - var(--t) * 2);
transform: scale(calc(0.28 + var(--_k) * 0.28));
}
@keyframes glow {
0%,
100% {
opacity: 0.16;
}
25% {
opacity: 0.44;
}
50% {
opacity: 0.16;
}
}
}
</style>
</head>
<body>
<div class="panel">
<svg
class="field"
viewBox="0 0 400 210"
preserveAspectRatio="xMidYMid slice"
aria-hidden="true"
>
<defs>
<!-- A stepped diamond: three treads per quadrant, long flats top and
bottom. The union-of-squares outline is what makes the growth
read as masonry rather than ripples in water. -->
<path
id="sd"
d="M-64,-8 V8 H-48 V24 H-32 V40 H-16 V56 H16 V40 H32 V24 H48 V8 H64 V-8 H48 V-24 H32 V-40 H16 V-56 H-16 V-40 H-32 V-24 H-48 V-8 Z"
/>
</defs>
<g transform="translate(200 92)">
<use class="r" style="--_k: 0" href="#sd" />
<use class="r" style="--_k: 1" href="#sd" />
<use class="r" style="--_k: 2" href="#sd" />
<use class="r" style="--_k: 3" href="#sd" />
<use class="summit" href="#sd" transform="scale(0.1)" />
</g>
</svg>
<div class="copy">
<p class="head">Ripples in stone</p>
<p class="sub">Ambient ornament. Nothing here reports state.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.