Back to the library

Balance — the lever trade

Variant: Lever trade

A block slides out along a beam until one unit of mass at 80 balances four units at 20 — and the beam does nothing at all until the numbers cross. That hold is the piece: torque is a threshold, not a dimmer, so the beam sits on its stop through nearly the whole slide and then answers all at once, ringing level through shrinking swings on a constant half-period. The heavy block is twice the side of the light one, which is exactly four times the area, so the caption's arithmetic is drawn to scale. Reach for it when documenting leverage of any kind — gearing, margin, priority weights — anywhere position substitutes for size.

physicsdiagramloop

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>Balance — the lever trade</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 — every duration in the document runs off --t. */
        --cycle: 4.6s;
        --t: calc(var(--cycle) / var(--speed));

        --ease-in-out: cubic-bezier(0.77, 0, 0.175, 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);
      }

      .stage {
        display: grid;
        gap: 10px;
        width: 240px;
      }

      .cap {
        display: flex;
        justify-content: space-between;
        font-size: 11px;
        letter-spacing: 0.04em;
        color: color-mix(in srgb, var(--slate) 52%, transparent);
      }

      /* The verdict appears on the frame the beam is done ringing, not the
         frame the block reaches its mark — settling is what takes the time. */
      .level {
        font-variant-numeric: tabular-nums;
        color: var(--clay-d);
        animation: verdict var(--t) steps(1, end) infinite;
      }

      .scene {
        position: relative;
        width: 240px;
        height: 132px;
      }

      .ground {
        position: absolute;
        left: 0;
        right: 0;
        top: 124px;
        height: 2px;
        background: var(--oat);
      }

      .rig {
        position: absolute;
        inset: 0;
        animation: rig-cycle var(--t) linear infinite;
      }

      /* The stop the heavy side rests on while it is winning. The beam leaving
         it is the event the whole piece builds to. */
      .stop {
        position: absolute;
        left: 24px;
        top: 99px;
        width: 5px;
        height: 25px;
        background: var(--oat);
      }

      .pillar {
        position: absolute;
        left: 117px;
        top: 88px;
        width: 6px;
        height: 36px;
        background: var(--oat);
      }

      /* A scale beam hangs from a pin, and that is why level is where it wants
         to be: with the mass below the pivot, level is the stable pose once the
         torques cancel. A seesaw balanced on a point has no such preference. */
      .beam {
        position: absolute;
        left: 20px;
        top: 86.5px;
        width: 200px;
        height: 3px;
        background: color-mix(in srgb, var(--slate) 55%, transparent);
        transform: rotate(-6deg);
        animation: tilt var(--t) infinite;
      }

      .pin {
        position: absolute;
        left: 115px;
        top: 83px;
        width: 10px;
        height: 10px;
        border-radius: 50%;
        background: var(--clay-d);
      }

      /* Children of the beam, so they stay seated through every swing. The
         heavy block is 2x the side, which is exactly 4x the area — the sizes
         carry the arithmetic in the caption. */
      .heavy {
        position: absolute;
        left: 67px;
        top: -26px;
        width: 26px;
        height: 26px;
        border-radius: calc(4px * var(--round));
        background: var(--paper);
        border: 1.5px solid var(--oat);
        display: grid;
        place-items: center;
        font-size: 11px;
        color: color-mix(in srgb, var(--slate) 62%, transparent);
      }

      .light {
        position: absolute;
        left: 115.5px;
        top: -13px;
        width: 13px;
        height: 13px;
        border-radius: calc(3px * var(--round));
        background: var(--clay);
        animation: slide-out var(--t) infinite;
      }

      /* The block is repositioned deliberately, so it eases both ends; the beam
         does not move a degree of it. Torque is a threshold, not a dimmer —
         4x20 beats 1xd for every d short of 80, so the beam holds its stop
         until the very end of the slide and then answers all at once. */
      @keyframes slide-out {
        0%,
        10% {
          transform: translateX(0);
          animation-timing-function: var(--ease-in-out);
        }
        32%,
        93% {
          transform: translateX(58px);
        }
        94%,
        100% {
          transform: translateX(0);
        }
      }

      /* The settle rings at a constant half-period — 8% of the cycle a swing,
         departing on the frame the slide ends — while the amplitude decays by
         0.4 a swing: 6, 2.4, 1, 0.4. Shrinking the period with the swings is
         the one mistake that makes a settle read as speeding up. */
      @keyframes tilt {
        0%,
        32% {
          transform: rotate(-6deg);
          animation-timing-function: var(--ease-in-out);
        }
        40% {
          transform: rotate(2.4deg);
          animation-timing-function: var(--ease-in-out);
        }
        48% {
          transform: rotate(-1deg);
          animation-timing-function: var(--ease-in-out);
        }
        56% {
          transform: rotate(0.4deg);
          animation-timing-function: var(--ease-in-out);
        }
        64%,
        93% {
          transform: rotate(0deg);
        }
        94%,
        100% {
          transform: rotate(-6deg);
        }
      }

      @keyframes verdict {
        0%,
        63.9% {
          opacity: 0;
        }
        64%,
        100% {
          opacity: 1;
        }
      }

      /* Both resets cross between 93% and 94%, strictly inside the dark
         window, so the beam is never seen giving the balance back. */
      @keyframes rig-cycle {
        0% {
          opacity: 0;
        }
        3%,
        86% {
          opacity: 1;
        }
        92%,
        100% {
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        /* The swing goes; the state the piece argues for stays — the small
           mass parked far out, the beam level, the verdict breathing. */
        .rig {
          animation: none;
          opacity: 1;
        }

        .beam {
          animation: none;
          transform: rotate(0deg);
        }

        .light {
          animation: none;
          transform: translateX(58px);
        }

        .level {
          animation: rest-cue calc(var(--t) * 1.8) ease infinite;
        }

        @keyframes rest-cue {
          0%,
          100% {
            opacity: 0.45;
          }
          50% {
            opacity: 1;
          }
        }
      }
    </style>
  </head>
  <body>
    <div
      class="stage"
      role="img"
      aria-label="A small block slides outward along a beam until its longer arm balances a block four times its mass sitting close to the pivot; the beam lifts off its stop, rings briefly, and settles level"
    >
      <p class="cap"><span>mass &times; arm &middot; 4 &times; 20 = 1 &times; 80</span><span class="level">level</span></p>
      <div class="scene">
        <div class="ground"></div>
        <div class="pillar"></div>
        <div class="rig">
          <div class="stop"></div>
          <div class="beam">
            <span class="heavy">4&times;</span>
            <span class="light"></span>
          </div>
          <span class="pin"></span>
        </div>
      </div>
    </div>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.