What it is
A concept pill bottle shown as a scroll story: it turns, opens up into cap, liner, foil, label, glass and tablets, then the cap unscrews and sets itself down, the foil seal pops off and the bottle tips over a dish so the tablets pour out; scrolling on stands it up and screws the cap back on. In the last picker you choose one of four cap colours and drive it all by hand: Open / close, Pour, Rattle, turn buttons, an unscrew slider and drag. It is a study of the technique, not a product: the label on the bottle says so.
Engineering decisions
- Everything is generated in code: lathe profiles for the glass and the cap, instanced boxes for the grip ribs, a short cylinder for the liner, an open cylinder for the label and one instanced mesh of capsules for the tablets. There is no model file and no image; the label is drawn on a canvas at load time.
- Lighting is a few emissive panels rendered once through
PMREMGenerator into an environment map, so there is no HDRI to download. A radial gradient on a plane stands in for the contact shadow and spreads as the bottle opens. - Camera poses, the opening amount and the story text are data in
data.ts: one pose per step, spaced evenly along the scroll. samplePose interpolates between frames and moves for the first 80 % of each segment before resting, so the text arrives after the camera has settled. A unit test checks that every pose keeps the camera away from the model. - GSAP scrubs one plain state object (
progress, scrub: 0.5, low because Lenis already smooths the wheel); the render loop reads it every frame. Scrolling never re-renders React. The canvas runs with frameloop="demand" and is invalidated whenever the progress, a drag, a rotation or a colour change moves something. - Two independent openings per pose:
explode (exploded view: cap, liner and foil lift straight up, the label swells) and uncap (0..1: the cap turns 1.5 times up the thread, then arcs aside and lies on its side; the foil seal peels off at the end, derived from uncap by sealAmount). tilt pours; it only applies once the cap is off, and closing again sets the bottle upright before the cap goes back on. Yaw keeps growing between frames, so the bottle visibly turns as you scroll; 0 (mod 2π) faces the label to the camera. - The glass is one closed lathe loop (outside up, inside down), so it has real wall thickness and an open mouth; the thread is a tube along a helix. The cap, liner and tablets in the dish live outside the tilt group so they stay put on the table while the bottle moves.
- Poured tablets are a tiny simulation: each falls under gravity from the mouth with a velocity aimed at its own slot in the dish, bounces once and rests there. Reduced motion places them at once.
- Hand controls write
manualOpen and manualTilt (null while the scroll drives); the scene drops them as soon as the progress moves more than 1 % and tells the page, which kills its tween. The status chip and the slider are written straight into the DOM from the render loop. - A drag only lasts until the next scroll: once the story moves, the bottle eases back (the shortest way round) to the angle the next screen is framed for. The turntable is a small pointer handler with momentum instead of orbit controls; the canvas has
touch-action: pan-y, so a vertical swipe scrolls the page and a horizontal drag turns the bottle. The Rotate buttons and the arrow keys on the cap picker are the keyboard route. - The cap colour eases to the chosen finish, and a soft glow behind the stage follows it. The picker is a radio group with a roving tabindex.
- Quality tiers:
low (phones, 4 cores or fewer, or ?quality=low) uses lower DPR, fewer geometry segments and ribs, no clear coat and no antialiasing. - The canvas is
aria-hidden; the steps are real headings and paragraphs. If the graphics context is lost the poster is shown, and the canvas is rebuilt when the context comes back.
Effects
- Amber dust drifting through the light, a spark burst when the seal pops, a moving key light and a slow glow pulse run only on the high tier with motion allowed (the render loop is continuous then, on screen only); the low tier and reduced motion stay on demand.
- Choosing a cap colour sends a ring across the table and tints the ground glow; the mouse tilts the bottle a little; a tap rattles the tablets.
Performance notes
Three.js is the bulk of this page's JavaScript and loads only on this page, after the poster is in view and the browser is idle. Geometries, materials and textures are disposed when the page unmounts.
Reduced motion
No intro, no camera travel and no momentum. Each step cuts to its pose behind a 0.15 s fade, the cap colour switches at once and the Rotate buttons turn the bottle in one step.