Back to the library

Ornament — ring clockwork

Variant: Ring clockwork

Three tick wheels turning about one hub at 67, 43 and 53 seconds a revolution, neighbours running counter. The eye reads relative motion, so the shear between adjacent rims moves at the sum of their speeds while no wheel itself exceeds 9px/s — the panel visibly turns and never appears to hurry. The periods are pairwise coprime, and even granting that every wheel is invariant under a one-tick turn, the visible arrangement does not recur for about twenty-one hours; an ambient loop is judged on whether you can say where it was ten seconds ago, and this one cannot. Two register circles hold still between the moving three, which is what makes it a built mechanism rather than three dashed strokes sharing a centre. Linear throughout — constant motion is the one place linear is correct, and a wheel that eased would be a wheel starting and stopping. Reach for it in hero corners and empty states that want machinery rather than weather.

ambientloopsvgspin

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 — ring clockwork</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 — the middle wheel's revolution. The three periods are pairwise
           coprime (67, 43, 53 are prime); each wheel is invariant under a
           one-tick turn, so the *visible* arrangement recurs every
           67 x 43 x 53 / gcd(48, 36, 26) seconds — about 21 hours. An ambient
           loop is judged on whether you can tell where it was ten seconds ago,
           and this one cannot even tell you itself. */
        --cycle: 43s;
        --t: calc(var(--cycle) / var(--speed));
        --_t-outer: calc(67s / var(--speed));
        --_t-inner: calc(53s / var(--speed));
      }

      * {
        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 mechanism reads 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%);
      }

      /* Every wheel turns about its own centre. fill-box excludes the stroke,
         so for a circle the origin is exact. */
      .w {
        fill: none;
        transform-box: fill-box;
        transform-origin: center;
        animation: cw var(--t) linear infinite;
      }

      /* Ticks are counted, not measured: pathLength normalises each ring, so
         the dash arithmetic is "48 blocks" rather than a circumference carried
         to three decimals, and the pattern tiles with no odd gap at the seam. */
      .w.blocks {
        stroke: var(--clay-d);
        stroke-width: 6;
        stroke-dasharray: 0.34 0.66;
        opacity: 0.3;
        animation-duration: var(--_t-outer);
      }

      /* Adjacent wheels counter-rotate. The eye reads relative motion, so the
         shear between neighbouring rims moves at the sum of their speeds while
         no single wheel exceeds 9px/s — the panel turns without ever appearing
         to hurry. */
      .w.segments {
        stroke: var(--slate);
        stroke-width: 1.4;
        stroke-dasharray: 0.55 0.45;
        opacity: 0.26;
        animation-name: ccw;
      }

      .w.beads {
        stroke: var(--clay);
        stroke-width: 3;
        stroke-linecap: round;
        stroke-dasharray: 0 1;
        opacity: 0.45;
        animation-duration: var(--_t-inner);
      }

      /* The register lines do not turn. Two still circles between three moving
         ones are what make this read as a built mechanism rather than three
         dashed strokes that happen to share a centre. */
      .rule {
        fill: none;
        stroke: var(--slate);
        stroke-width: 1;
        opacity: 0.15;
      }

      .hub {
        fill: var(--clay);
        opacity: 0.6;
      }

      @keyframes cw {
        to {
          transform: rotate(1turn);
        }
      }

      @keyframes ccw {
        to {
          transform: rotate(-1turn);
        }
      }

      .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) {
        /* Rotation is travel, so it goes. What survives is a slow brightness
           round passing outward wheel to wheel — the mechanism stops turning
           and keeps ticking, which on a Low Power Mode phone is the difference
           between a resting card and a dead one. */
        .w.blocks,
        .w.segments,
        .w.beads {
          animation: hold calc(var(--t) / 4) ease infinite;
          transform: none;
        }

        .w.blocks {
          animation-delay: calc(var(--t) / -12);
        }

        .w.segments {
          animation-delay: calc(var(--t) / -6);
        }

        @keyframes hold {
          0%,
          100% {
            opacity: 0.22;
          }
          30% {
            opacity: 0.42;
          }
          70% {
            opacity: 0.22;
          }
        }
      }
    </style>
  </head>
  <body>
    <div class="panel">
      <svg
        class="field"
        viewBox="0 0 400 210"
        preserveAspectRatio="xMidYMid slice"
        aria-hidden="true"
      >
        <circle class="rule" cx="200" cy="94" r="68" />
        <circle class="rule" cx="200" cy="94" r="47" />
        <circle class="w blocks" cx="200" cy="94" r="78" pathLength="48" />
        <circle class="w segments" cx="200" cy="94" r="57" pathLength="36" />
        <circle class="w beads" cx="200" cy="94" r="38" pathLength="26" />
        <circle class="hub" cx="200" cy="94" r="3" />
      </svg>
      <div class="copy">
        <p class="head">Wheels within wheels</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.