Skip to content
animorio
IT
Back to the library

Pages lined up in space

WebGL, 3D, ImagesWebGL, GSAPAdvancedCompressed weight: Demo 9.9 KBGSAP 28 KBThree.js 174 KBFreeDownload the zip

Projects as thin pages lined up in space, seen from the side: the wheel, the arrows or a finger move the row, the pages bend with the speed and the one in the middle steps out of the row and turns towards you.

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">
  <title>Pages lined up in space</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
  <script type="importmap">
    { "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.178.0/build/three.module.min.js"
    } }
  </script>
</head>
<body>
  <!-- the work page of a made-up designer. The projects are thin pages standing in a row in space, like magazines on a
       shelf seen from the side; the wheel, the arrows or a finger move the row, the pages bend with the speed, and the
       one in the middle steps out of the row and turns to face you -->
  <main class="sala" data-sala>
    <canvas class="tela" data-tela aria-hidden="true"></canvas>

    <header class="testata">
      <p class="firma">KAI VERO<sup>&reg;</sup></p>
      <p class="sopra">Selected work</p>
    </header>

    <section class="scheda" aria-live="polite">
      <p class="scheda__tipo" data-tipo>Brand &middot; editorial</p>
      <h1 class="scheda__nome" data-nome>Paper Moons</h1>
      <p class="scheda__frase" data-frase>A lunar calendar printed on tracing paper.</p>
    </section>

    <nav class="comandi" aria-label="Projects">
      <button type="button" data-passo="-1" aria-label="Previous project">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
      </button>
      <button type="button" data-passo="1" aria-label="Next project">
        <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
      </button>
    </nav>
    <p class="aiuto">Scroll, use the arrows or drag</p>
  </main>

  <!-- the projects: the script draws a cover for each one -->
  <ul class="progetti" data-progetti hidden>
    <li data-colore="#ff4b3e" data-carta="#f6efe6" data-forma="luna">Paper Moons|Brand · editorial|A lunar calendar printed on tracing paper.</li>
    <li data-colore="#181818" data-carta="#e9e4da" data-forma="solchi">Low Tide Records|Label · website|A record label that only releases at high tide.</li>
    <li data-colore="#3b5bdb" data-carta="#eef0f6" data-forma="griglia">Field Notes Co.|Product · launch|Notebooks with a ruler printed on the spine.</li>
    <li data-colore="#f2b705" data-carta="#1d1b18" data-forma="onde">Sunday Ferry|Travel · app|Timetables for a ferry that runs once a week.</li>
    <li data-colore="#2f9e44" data-carta="#f3f1ea" data-forma="foglie">Greenhouse 9|Studio · website|A plant nursery with a room for every season.</li>
    <li data-colore="#e64980" data-carta="#191919" data-forma="cerchi">Neon Choir|Music · campaign|Posters for a choir that sings in car parks.</li>
    <li data-colore="#1971c2" data-carta="#f1ede4" data-forma="righe">Blue Hour Hotel|Hospitality · brand|Rooms named after the minutes after sunset.</li>
    <li data-colore="#e8590c" data-carta="#efe9df" data-forma="scala">Orange Steps|Archive · collection|Every staircase of a hill town, one a day.</li>
  </ul>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
  <script type="module" 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.

42 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 “Pages lined up in space” from Animorio and fit it into my site

What it does

The work page of Kai Vero, a made-up designer. On a light grey background the projects are thin pages standing in a long row that crosses the screen diagonally, like magazines on a shelf seen from the side; each page has a drawn cover. On arrival the row slides into place, braking, and after a second the middle page steps out: the others move away, leaving it room on both sides, and it turns towards the viewer. With the wheel, the arrows, the buttons or by dragging, the row moves one page at a time and never ends; while it runs, the pages bend in a soft curve, stronger the faster they go, and straighten when they stop. Moving the mouse over another page lifts it a little; clicking it brings it to the middle. At the bottom left the name of the project blurs and comes back in focus at every change, and the whole row turns slightly towards the mouse.

How it works

One mesh, many copies

The pages are a single Three.js box, PAGE_W wide, PAGE_H tall and a hundredth thick, split into 32 by 16 little squares so it can bend, repeated COPIES times with InstancedBufferGeometry. Each copy only knows its number and where its cover is in the atlas, a canvas with all the covers drawn by the script.

The row that never ends

In the vertex shader the place of each page is its number times SPACING minus the scroll, brought back with mod inside the length of the row: what leaves on one side comes back on the other. Pages near the middle are pushed away by GAP spacings with a smoothstep, so the middle one has room around it.

The bend and the turn

The bend is a wave along the height, sin(2y) times 0.4, multiplied by the speed and by CURVE: when the row stops, the speed dies away by 0.829 every frame and the pages straighten. The middle page turns by FOCUS_TURN on the three axes, in proportion to how close it is to the centre.

The camera

The camera is orthographic, with no perspective: it sees VIEW units from top to bottom, sits high on the right and looks down at the row, which is turned by TILT. The script then slides it sideways so the middle page falls where the layout wants it; on a phone it sees more from top to bottom.

The wheel, the mouse and the name

The wheel moves a target and the row covers FOLLOW of the way every frame; at the end of the gesture it settles on a page, and a small gesture still counts as one step. To know which page is under the mouse the script does the shader’s sums again, without the bend, and projects the four corners of each page onto the screen; that page rises in LIFT_TIME with power4.out. The name blurs by 8 pixels in 0.2 seconds and comes back in focus in 0.85.

Values to change

NameFileDefaultWhat it changes
GAPscript.js5.3How much room the middle page has on each side, in spacings (5.3 on the site).
CURVEscript.js2.25How much the pages bend with the speed (2.25 on the site).
FOCUS_TURNscript.js[0.06681, -0.36331, -0.51318]How the middle page turns, in radians on the three axes (the site’s numbers).
FOCUS_TIMEscript.js1.1How long the middle page takes to step out and turn on arrival (1.1 on the site, after 1.06).
LIFT_ZOOMscript.js0.156How much the middle cover zooms in under the mouse (0.156 on the site).
VIEWscript.js10.8How much of the scene the camera sees from top to bottom (10.8 on the site).
AUTOscript.js3.2The seconds of stillness before the row moves on by itself; 0 for never.

Try them in the Playground

When to use it

For the work page of a designer, a studio or a magazine: a few projects with a good cover each, to leaf through one at a time.

On phones

The same: the camera sees more space from top to bottom, the middle page sits in the centre of the screen and the row moves with a finger.

With reduced motion

The row is already in place with the middle page turned; the pages change at once, with no bend, and the name changes without blurring.

Pitfalls

  • In a shader half is a reserved word: a variable with that name does not compile, and Three.js shows only an empty screen.
  • With settling at the end of a gesture, one notch of the wheel moves the row less than half a page and the settling brings it back: a small gesture must still count as one step in its direction.
  • The mouse check must use the mesh matrix (matrixWorld) before projecting: the shader sums are in the mesh’s space, which is moved and turned.
  • The part of the cover that ends up on the thin sides of the pages must be covered with the paper colour (you tell it by the normal), or stretched stripes show on the edges.

Libraries

Three.js 0.178.0, GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, Geist (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 9.9 KB; the libraries: GSAP 28 KB, Three.js 174 KB.

Testing

Tested on 14 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 animations