A small note on an animated length of silk

Making movement feel held.

The Maison Aubry site is an editorial room with one restless object in it: a lit, physical ribbon that keeps remembering the last motion of your hand.

01 / Concept

The ribbon is the subject, not decoration.

Aubry makes silk in the old loom district of Lyon, so the interface needed the same mix of restraint and liveliness: spare editorial type, long pauses of pearl paper, then a piece of cloth that refuses to behave like a rectangle. The hero deliberately puts the WebGL canvas between layers of type: the main title sits in front; the atelier note falls behind it. Further down, a light table lets a visitor turn each cloth toward an imagined window. The material crosses the reading plane without taking the page over.

01Editorial scale, not a product grid.
02Light belongs to the cloth.
03Motion carries a little inertia.

02 / Signature technique

A real tube curve, rebuilt in time.

The hero uses Three.js TubeGeometry on a Catmull–Rom curve. Fifteen curve points are sampled into a 72-segment tube; two overlapping frequencies prevent the movement from reading like a regular sine wave. The pointer position eases toward its target, while velocity opens the iridescent core, pearl selvedge and wine-dark under-weave. Geometry is refreshed on each animation frame with deliberately lean tube densities, keeping the motion continuous without making the ribbon read like a regular band.

index.html / actual ribbon coreThree.js 0.180.0
function ribbonPoints(time, offset, response) {
  const points = [];
  const w = 10.6;
  for (let i = 0; i < 15; i++) {
    const u = i / 14;
    const x = -w / 2 + w * u;
    const wave = Math.sin(u * 7.9 + time * .78 + offset) * .98
      + Math.sin(u * 17.2 - time * .41 + offset) * .16;
    const sway = Math.cos(u * 5.2 + time * .52 + offset) * .23;
    points.push(new THREE.Vector3(
      x + pointerX * (u - .42) * response,
      wave + sway + pointerY * (0.25 + Math.sin(u * Math.PI) * .68) * response,
      Math.sin(u * 9.8 + time * 1.08 + offset) * .46
        + Math.hypot(velocityX, velocityY) * .2 * Math.sin(u * Math.PI)
    ));
  }
  return points;
}

const tubeSegments = 72;
const core = makeTube(ribbonPoints(time, 0, response),
  .34 + speed * .035, tubeSegments, 8);
const weave = makeTube(ribbonPoints(time * .91, 3.2, response * .72)
  .map((point, index) => point.add(new THREE.Vector3(
    0, -.92 + Math.cos(index * .72 + time * .8) * .16, .22
  ))), .16 + speed * .012, tubeSegments - 12, 6);
setGeometry(ribbon, core);
setGeometry(weaveRibbon, weave);
material.iridescence = .78 + Math.sin(time * .7) * .13;

In the final pass, the core and wine-dark under-weave also receive a tiny generated grain canvas as both roughness and normal detail. Its offset creeps with the ribbon, so the reflection breaks along the weave instead of sliding across a perfectly smooth tube. The renderer caps pixel density at 2 and pauses when its canvas is out of view or the document is hidden. Reduced-motion users receive one composed frame rather than the continuous loop.

03 / Palette + type

Warm paper, a wine-dark anchor.

Champagne and pearl keep the pages tangible rather than sterile. Bordeaux does the structural work: text, rules and the dark atelier section. Blush is saved for reflected colour and the ribbon’s unexpected face.

Champagne#E7D3B7
Blush#D9A0A0
Bordeaux#4A1220
Pearl#F7F3EE

Silk remembers.

Bodoni Moda · display didone

Quiet, well-spaced body copy gives the larger gestures a place to settle.

DM Sans · body / DM Mono · notation

Iteration notes.

Pass 2 — design depth

  1. 1. The cloth section was the weakest repeated moment. Three equal cards made the collection read like a safe catalogue. It is now an off-beat editorial spread: the first length enters low and large, the second anchors the headline, and the third retreats high into a trailing ribbon that cuts through the field.
  2. 2. The type hierarchy was polished but too even. Display type now uses Bodoni Moda, a true high-contrast didone, with tighter display tracking, a more decisive specimen headline, balanced wrapping, and a deliberate scale jump between the paper-like body copy and collection title.
  3. 3. The hero’s signature ribbon was doing too little layering. The existing iridescent core and pearl edge now gain a wine-dark under-weave with its own phase and cursor response. It creates a braided foreground over the atelier note while the headline remains in front; the resilient CSS fallback now echoes the same three-pass gesture.

Pass 3 — complexify

  1. 1. The page had one excellent interactive object, but no reason to linger after the hero. The new light table turns the autumn lengths into a second, deliberate encounter. Choosing Rivière, La Lettre or Minuit redraws a procedural cloth; moving or dragging across it moves a reflected window through hand-drawn folds and fine weft lines. It is keyboard-operable through the cloth buttons, and the selected state is announced rather than left to colour alone.
  2. 2. The WebGL ribbon still read a little too pristine at close range. A 128px generated grain, made from irregular curved weft marks, now feeds the core and under-weave roughness and normal maps. The grain drifts at a fraction of the cloth motion, breaking the highlight into something closer to silk than polished plastic.
  3. 3. Loading needed to feel composed, not merely safe. Both animated surfaces begin as designed CSS cloth. The hero canvas fades in only after a rendered Three.js scene is ready; the light table keeps its folded fallback if a 2D context is unavailable. There is always a selected cloth, so the interaction never lands on an empty state.