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.
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.
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.
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