
The cube stories
Stories, 3D, Transitions
NewFree
A website’s image optimizer as a 3x3 puzzle cube in CSS 3D: at rest it is scrambled, and while the 248 images are made lighter it solves itself one move after another, every move a layer turning a quarter with a little bounce. The last move closes every face, the stickers flash and the cube hops and does a pleased half turn; Run again scrambles it with a flurry of quick moves.
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 solving cube</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/standard.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/geist-mono@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the image optimizer of a website, as a 3x3 puzzle cube in CSS 3D. At rest the cube is scrambled; pressed, it
solves itself one move at a time as the images are done, every move a layer turning a quarter with a little
bounce, and the last one closes every face. The 27 small cubes are built by script.js -->
<section class="opt" aria-labelledby="opt-title" data-opt>
<div class="opt__scene" role="progressbar" aria-labelledby="opt-title" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-valuetext="Not started" data-scene>
<div class="opt__shadow" aria-hidden="true" data-shadow></div>
<div class="opt__lift" aria-hidden="true" data-lift>
<div class="cube" data-cube></div>
</div>
</div>
<div class="opt__head">
<div class="opt__words">
<h2 class="opt__title" id="opt-title"><span class="opt__swap" data-title>Optimize images</span></h2>
<p class="opt__meta"><span class="opt__swap" data-meta>248 images · 64.2 MB</span></p>
</div>
<p class="opt__figure" aria-hidden="true">
<span class="opt__pct is-quiet" data-pct>0%</span>
<svg class="opt__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="opt__foot">
<p class="opt__file" aria-hidden="true"><span class="opt__swap" data-file>To AVIF and WebP</span></p>
<button class="opt__btn" type="button" data-action data-autoclic><span class="opt__label" data-label>Optimize</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>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.
30 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The solving cube” from Animorio and fit it into my siteThe cube is made of 27 small cubes, each one a div with six sides in CSS 3D (preserve-3d and backface-visibility hidden). Every small cube knows where it is on the 3x3x3 grid (three whole numbers from −1 to 1) and how it is turned (a 3x3 rotation matrix of whole numbers). The sides on the outside of the big cube carry the sticker of that face’s colour; the inner ones are black plastic, and show in the gap when a layer turns. The small cube in the middle is there too: without it, halfway through a move you would see a hole.
A move picks an axis, a layer (−1 or 1) and a direction. While it plays, the nine small cubes of that layer get one more rotation about the axis, with the angle animated by GSAP with back.out (the little bounce). At the end the exact quarter turn, rounded to whole numbers, multiplies each one’s matrix and place: no errors pile up, and after a hundred moves the cube is still perfect. The transforms are written by hand as matrix3d, so what you see is exactly what the script works out.
The scramble is a list of random moves, never two on the same axis in a row, so that none undoes or repeats the one before. The solution is the same list read from the end, every move the other way: the last one has to bring every small cube home. The moves are tied to the images: after k images out of N, k ÷ N of them have been made, but the last one only once the last image is done, so the cube closes exactly at 100%. If the work runs ahead of the moves, the queued moves speed up.
In every frame in which something moves, the normal of every side goes through its small cube’s rotation, the turning layer’s and the view’s (and the lean towards the pointer). Its product with the lamp’s direction gives the shade, a black veil over the side; its product with the vector halfway between the lamp and you, raised to the eighteenth, gives the glint on the sticker. The script writes only the values that change, and when nothing moves it does not redraw.
When a layer turns, the rest of the cube turns a couple of degrees the other way about the same axis and comes back on an elastic spring, as it would in your hand. At the start the cube hops and turns a quarter with back.inOut while the images are read; at the end it flashes (a CSS variable that brightens all the stickers), hops and does a half turn with back.out, and drops with bounce.out while its shadow shrinks and comes back.
The cube is a role=progressbar with aria-valuenow and an aria-valuetext (how many images out of how many), the panel has aria-busy while it works and an aria-live line says when the work starts, stops and ends. The button changes its word (Optimize, Pause, Resume, Run again) and the pill takes the new width with a little bounce; a click from a script, the library preview’s, never pauses.
| Name | File | Default | What it changes |
|---|---|---|---|
IMAGES | script.js | 248 | How many images the optimizer goes through. |
SECONDS | script.js | 7 | How long a run takes in the demo, in seconds (a real one follows its images). |
MOVES | script.js | 12 | How many moves the scramble has, so how many moves the solving takes. |
CUBIE | script.js | 34 | The size of one small cube, in pixels. |
TURN | script.js | 0.34 | How long a move takes, in seconds. |
BOUNCE | script.js | 1.8 | How far a layer overshoots before it settles: 0 not at all. |
TILT | script.js | 1 | How much the cube leans towards the pointer: 0 not at all. |
WEIGHT | script.js | 64.2 | The images’ weight before, in megabytes. |
SAVED | script.js | 0.6 | How much of it the optimizer saves, from 0 to 1. |
For a job of a few seconds made of many alike pieces that in the end «fall into place»: optimizing or converting images, tidying a library, deduplicating files, rebuilding an index. The cube says «mess becoming order» well; for a load of unknown length an indicator without a percentage is better.
The panel takes the screen’s width less 32 pixels (358 on a 390 screen) and stays 330 tall; the cube keeps its size, since it has room in its place. Without a mouse the cube does not lean, everything else is the same: the same moves, the same bounces and the same light. The button is a pill 36 tall: under a finger it squashes and springs back, and its words widen on the width axis only under the mouse.
The layers do not turn: every move snaps in already done, one at a time, so you still see the cube putting itself in order. No hops, no half turns, no lean or kick; the new scramble is instant, at the end the stickers only flash with a fade and the tick and the new words fade in.
GSAP 3.15.0, Instrument Sans (font variabile) 5.3.0, Geist Mono (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 11 KB; the libraries: GSAP 28 KB.
Tested on 8 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.