Skip to content
animorio
IT
All components

The sync braid

Loading, Physics, WebGLWebGL, GSAPAdvancedCompressed weight: Demo 16 KBGSAP 28 KBFreeDownload the zip

The sync panel of a photo app, with a coloured cord for every device: as the bytes arrive the three taut cords are plaited from the left into a single braid, and the braid is the progress bar. If a device goes offline its cord comes out of the clamp and lies slack and grey until Retry plugs it back in; at the end the clamp shuts on the braid, a tightening runs back along it and a glint runs forward.

The code

A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <title>The sync braid</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/radio-canada@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the sync panel of a photo app: one cord for every device, plaited into a single braid as the bytes arrive.
         The braid is the progress bar; the loose cords still running to the clamp on the right are what is left.
         The cords are painted by a WebGL shader on the canvas; the words, the clamps and the button are HTML -->
    <section class="sync" aria-labelledby="sync-title" data-sync>
      <div class="sync__head">
        <div class="sync__words">
          <h2 class="sync__title" id="sync-title"><span class="sync__swap" data-title>Photos</span></h2>
          <p class="sync__meta"><span class="sync__swap" data-meta>Last synced yesterday</span></p>
        </div>
        <p class="sync__figure" aria-hidden="true">
          <span class="sync__pct" data-pct>2.4 GB</span>
          <svg class="sync__tick" viewBox="0 0 24 24" data-tick><path d="M5.5 12.6l4.1 4.1L18.6 7.4" pathLength="1"/></svg>
        </p>
      </div>
      <div class="sync__bar" role="progressbar" aria-labelledby="sync-title" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" data-bar>
        <canvas class="sync__paint" aria-hidden="true" data-paint></canvas>
        <span class="sync__cap sync__cap--start" aria-hidden="true"></span>
        <span class="sync__cap sync__cap--end" aria-hidden="true" data-clamp></span>
      </div>
      <div class="sync__foot">
        <p class="sync__devices" data-devices></p>
        <button class="sync__btn" type="button" data-action data-autoclic><span class="sync__label" data-label>Sync now</span></button>
      </div>
      <p class="sr" role="status" aria-live="polite" data-live></p>
    </section>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script src="script.js"></script>
</body>
</html>

Playground

Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.

32 values to change

Or ask Claude Code for it

With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:

Take “The sync braid” from Animorio and fit it into my site

What it does

On a white background, alone in the middle, a pale grey panel with round corners: at the top Photos and Last synced yesterday, on the right 2.4 GB; in the middle three cords, blue, green and coral, that come out of a steel cap on the left and run slack and slightly curved to an open steel clamp on the right; below, Laptop · Tablet · Phone, each in the colour of its cord, and the black Sync now button. Pressed, the cords are pulled one after another and go taut with a twang, ringing; while the devices are compared (Comparing 3 devices…) pulses run along the cords from the clamp to the left. Then the bytes arrive and the cords are plaited from the left: the three strand braid grows to the right, from its tip the cords fan out back into their lanes, and every crossing sends a small ripple along the stretch still loose. At the top the percentage climbs and the line says how many gigabytes have arrived and how long is left; the button becomes Pause, pale and outlined. Paused, the cords go slack and curve again (Sync paused, Resume); resuming, they are pulled taut again. When the braid reaches the end the clamp shuts on its tip with an elastic squash, a tightening runs along the braid from right to left, a glint crosses it from left to right, a green tick is drawn in place of the percentage and the title becomes Photos up to date; Sync again opens the clamp, the braid comes undone back to the left and the sync starts over. One time in two, in the demo, the phone goes offline halfway: the coral cord comes out of the clamp, springs back with a bounce and lies slack and grey along its loose stretch (what it has already plaited keeps its colour), the other two give a shudder, the title says Phone went offline in red, Phone below is struck through and the button becomes Retry. Retry puts the cord back into the clamp, gives it its colour back, pulls it taut with the twang and the plaiting goes on from where it stopped.

How it works

One column per pixel

The cords are not drawn as strokes: for every column of pixels of the canvas the script works out how high each of the three cords passes and how deep it is, and writes the numbers into a small texture three rows tall (the height in two bytes, to a 256th of a pixel, the depth in one, plus a byte that says whether the cord is there at all). Every frame the texture is updated with texSubImage2D: a few hundred columns and no geometry.

Tubes as in a ray tracer

For every pixel the shader looks for the nearest point of each cord among the columns round it (the segments from one column to the next) and takes its distance d. Within the radius r the cord is a tube: its height there is the strand’s depth plus the round of the tube, √(r² − d²); the highest tube wins, and the three are laid down from the lowest to the highest with an edge softened over half a pixel. So the crossings of the braid are exact to the pixel, even where two cords touch at a slant. The tube’s normal gives the diffuse light and the highlight, slanted plies twist round every cord, and the shadows fall down and to the right on the panel and on the cord underneath, the further the higher the cord is.

The three strand braid

Strand k follows y = A sin θ and z = sin 2θ, with θ = 2π (x ÷ P + k ÷ 3): seen from the front it is the classic plait, and the double sine of the depth takes every strand once over and once under the others. Up to the tip (the part that has arrived) the strands follow the braid; past it, over a stretch a little longer than one turn, they open out towards their lanes with a smootherstep and the depth fades with them. The tip follows the bytes on a critically damped spring that never goes back.

The loose cords are real strings

The loose stretch of every cord is a row of 48 points that moves sideways by the wave equation, with a pull towards the curve it should lie in and some damping, worked out in steps of 1/480 of a second. The tension changes everything: taut, the waves run fast and the cord is pulled straight; slack, the waves are lazy and the cord goes back to the curve it remembers (the two outer ones bow outwards, the middle one into an S). Pulled taut at once, it rings: that is the twang. While the devices are compared one pulse at a time sets off, with the position and the speed of a wave going left, w = g(x + ct), so that it runs one way only; while plaiting, every crossing gives the cords a little push.

Offline, retry and the end

When a device goes offline its cord comes away: its end springs back with back.out, it becomes free (the string has a mirrored ghost point instead of a held end) and the shader greys it only past the tip of the braid. Retry takes it back into the clamp, gives it its colour back and pulls it taut. At the end the clamp shuts with an elastic squash while the lanes close in on the braid; then a tightening, the braid’s width dipping under a bell, runs from right to left, and a glint from left to right.

For those who cannot see it and those who want no motion

The bar is a role=progressbar with aria-valuenow and an aria-valuetext in gigabytes, the panel has aria-busy while it works and an aria-live line says when the sync starts, stops, loses a device and ends. The button changes its word (Sync now, Pause, Resume, Retry, Sync again) and the pill takes the new width with a little bounce; a click from a script, the library preview’s, never pauses. Without WebGL the cords become three stripes drawn by the CSS, and everything else works.

Values to change

NameFileDefaultWhat it changes
SECONDSscript.js5.5How long a sync takes in the demo, in seconds (a real one follows its bytes).
SIZEscript.js2.4The gigabytes to sync.
DEVICESscript.js['Laptop', 'Tablet', 'Phone']The three devices, one per cord, from the top.
FAIL_EVERYscript.js2In the demo, one sync in how many goes wrong (0 never).
FAIL_ATscript.js0.58How far the sync that goes wrong gets, from 0 to 1.
OFFLINEscript.js2Which device goes offline then (0 the top one, 2 the bottom one).
CORDscript.js9How thick a cord is, in pixels.
PITCHscript.js4.2How long one turn of the braid is, in cord thicknesses.
SPREADscript.js1.65How far apart the loose cords run, in cord thicknesses.
TWANGscript.js1How long a cord rings when it is pulled taut: 0 not at all, 1 a lot.
FOLLOWscript.js0.35How long the braid takes to catch up with the bytes, in seconds.
SHEENscript.js0.8The glint that runs along the finished braid: 0 none, 1 bright.

Try them in the Playground

When to use it

For a wait of a few seconds that has an outcome: syncing devices, merging libraries, a backup, an import from several sources. The three cords say «three things becoming one» well, and the device that drops out shows at once; for a single file to upload a plainer bar is better.

On phones

The panel takes the screen’s width less 32 pixels (358 on a 390 screen) and stays 183 tall; the braid gets shorter with it and the shader paints at the screen’s real density, up to twice. The button is a pill 34 tall: under a finger it squashes and springs back, and its words widen on the width axis only under the mouse.

With reduced motion

No twangs, pulses or shudders: the cords go straight from the slack curve to the taut line, the cord that goes offline is out of the clamp at once, the clamp shuts without a squash, a tightening or a glint, and the tick and the new words fade in. The braid still grows, because it is what says how far along things are.

Pitfalls

  • When the braid reached the clamp a thin grey line showed up, as tall as the canvas: the shadow measured the distance from the straight line through the segment, and a nearly upright segment (the cords opening out within a few pixels) has its line close to the whole column. The shadow now measures the distance from the real segments.
  • With a device offline its whole cord went grey, the plaited part too, as if that data were lost: the shader now gets the tip of the braid and greys the cord only past it.
  • Once the sync was done the cords’ ends stuck out above and below the shut clamp, still in their lanes: the lanes now close in on the braid together with the clamp, and the cords end under the steel.
  • The twang did not show: the taut cord rang more than six times a second, damped quickly, starting from a curve of a few pixels. With a lower stiffness, less damping and the slack cords a little more curved, the ringing shows for three or four swings.
  • A kick of speed at one point of a string splits into two half waves going either way, small and muddled: the pulse now starts with the position and the speed of a wave going left, and runs one way only.
  • At the start the line said «about 76 s left»: the speed was measured during the comparing of the devices too, when nothing arrives. It is now the average since the bytes started, and the time is written only after 8% has arrived.
  • The braid’s spring comes up on the last stretch very slowly, and the clamp stayed open for half a second with the braid already at the end: the clamp now shuts as soon as the tip is under the steel.
  • With reduced motion the clamp shut with a fade, and halfway through it uncovered the cords’ ends: it now just shuts.
  • The offline device’s name stayed coral even struck through: the colour written inline beat the class. The names’ colours now come from the CSS, a little deeper than the cords so that they read well.

Libraries

GSAP 3.15.0, Radio Canada (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.

Testing

Tested on 5 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: To do

License

Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.

All components