Skip to content
animorio
IT
All components

The dividing photo

Loading, Images, TransitionsCanvas, GSAPAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

A big RAW file loading in a photo app: the picture starts as a single tile of its average colour, then the tile with the most detail splits into four, again and again, each with a small elastic snap, until the landscape is sharp. Once every byte is in, the grid closes, the focus corners lock onto the picture and the real photo comes into focus under the last tiles.

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, viewport-fit=cover">
  <title>The dividing photo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- a photo app opening a big RAW file: the picture starts as a single tile of its average colour, then the tile
         with the most detail left in it splits into four, again and again, each split with a small elastic snap, until
         the photo is sharp. The landscape is painted by script.js on a hidden canvas: no image is loaded -->
    <section class="raw" aria-labelledby="raw-title" data-raw>
      <div class="raw__head">
        <div class="raw__words">
          <h2 class="raw__title" id="raw-title"><span class="raw__swap" data-title>DSC04182.ARW</span></h2>
          <p class="raw__meta"><span class="raw__swap" data-meta>Sony RAW · 14-bit · 62.4 MB</span></p>
        </div>
        <p class="raw__figure" aria-hidden="true">
          <span class="raw__pct" data-pct>61 MP</span>
          <svg class="raw__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="raw__frame" role="progressbar" aria-labelledby="raw-title" aria-valuemin="0" aria-valuemax="100"
           aria-valuenow="0" aria-valuetext="Not opened" data-bar>
        <canvas class="raw__paint" aria-hidden="true" data-paint></canvas>
        <!-- the four corners of the focus frame: they lock onto the picture when it is sharp -->
        <div class="raw__af" aria-hidden="true" data-af>
          <svg viewBox="0 0 16 16" class="raw__corner raw__corner--tl"><path d="M1 15V5a4 4 0 0 1 4-4h10"/></svg>
          <svg viewBox="0 0 16 16" class="raw__corner raw__corner--tr"><path d="M1 15V5a4 4 0 0 1 4-4h10"/></svg>
          <svg viewBox="0 0 16 16" class="raw__corner raw__corner--bl"><path d="M1 15V5a4 4 0 0 1 4-4h10"/></svg>
          <svg viewBox="0 0 16 16" class="raw__corner raw__corner--br"><path d="M1 15V5a4 4 0 0 1 4-4h10"/></svg>
        </div>
      </div>
      <div class="raw__foot">
        <p class="raw__info"><span class="raw__swap" data-info>Shot 14 Sep · 35 mm · f/8</span></p>
        <button class="raw__btn" type="button" data-action data-autoclic><span class="raw__label" data-label>Open</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>

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.

26 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 “The dividing photo” from Animorio and fit it into my site

What it does

On a black background, alone in the middle, a graphite panel with round corners: at the top the file name, DSC04182.ARW, with Sony RAW · 14-bit · 62.4 MB below, and 61 MP on the right; in the middle a 3:2 frame filled with a single mauve rectangle, the photo’s average colour; below, Shot 14 Sep · 35 mm · f/8 and the pale Open button. Pressed, the rectangle gives a little jolt, the focus corners appear and the line says Reading header…; then the bytes arrive and the rectangle splits into four, and each of the four again, always where the picture has the most detail: the children are born in the parent’s colour, bunched at its centre, and snap out to their places with a bounce as they take their own colours, with a thread of light round the edge. The sky stays in big tiles, while the horizon, the sun, the cypresses and the road of light on the water fill with smaller and smaller tiles, parted by a dark grid that thins out on the smallest. At the top the percentage climbs, the line says Loading RAW and how many megabytes have arrived, the one below Refining and how many tiles there are; the button becomes Cancel, clear and outlined. At the end the grid closes, the corners turn amber and close in on the picture with a snap, like autofocus locking on, the real photo, grain and all, comes up over the tiles with a small breath, an amber tick is drawn in place of the percentage and the lines say 9,568 × 6,384 · ready to edit and f/8 · 1/250 s · ISO 100 · 35 mm. Open again takes the photo away, opens the grid and folds the tiles back down to a single one, then starts over. Cancel does the same and goes back to rest. One time in two, in the demo, the memory card is pulled out halfway: the frame shakes, the line says Card removed at 38.7 MB in red, the button becomes Retry and carries on from where it stopped.

How it works

A landscape with no photo

The lake at dusk is painted by the script with the 2D canvas: a gradient for the sky, a radial glow and the sun’s disc, thin clouds as softened ellipses, two ridges of hills made from a 1D noise in four octaves, the near hill with its cypresses drawn as Bézier curves, the water with the sky upside down and the hills mirrored, the road of light as strokes that get wider and sparser towards the shore, the reeds and a vignette. Everything is in fractions of the width and the height and the chance comes from a seeded generator, so the same scene paints at any size: a sharp copy at the screen’s density, with grain, and a small clean copy of 384 × 256 for the analysis.

Mean and variance in four lookups

From the small copy the script builds four summed area tables: red, green, blue and the sum of their squares. The sum over any rectangle costs four lookups, so a tile’s mean colour is S ÷ n and its error, the sum of the squared distances from the mean colour, is Σq − (R² + G² + B²) ÷ n, without ever going over the pixels again. 384 × 256 halves cleanly down to tiles of 3 × 2.

The quadtree grows from a priority queue

The quadtree is built in full ahead of time, once: a binary heap keeps the leaves in order of error, and every step takes out the one with the most detail and splits it into four, which go into the heap. The order of the splits is the load: the bytes that have arrived say how many splits to show (on a curve a little slower at first, so that the big tiles can be seen). A node is a leaf at step k if it was born before k and splits at k or later: going forward and back costs only a counter.

Every split has its snap

Every node remembers when it appeared. For 0.62 seconds the four children start from the parent’s centre, at 64% of their size and in its colour, and go to their places on a hand written spring, 1 − 2^(−9t) cos(2.7πt), which overshoots by about a tenth and settles; the colour goes from the parent’s to their own in 0.3 seconds. Underneath, for a moment, the parent stays drawn in its colour, so the background does not show between the children. Leaves at rest are drawn aligned to the screen’s pixels, so without the grid they meet exactly; the grid thins out on the small tiles and the corners are rounded only on the big ones.

Coming into focus

When every byte has arrived a GSAP timeline takes the grid to zero, closes the focus corners in with an elastic.out as they turn amber, and raises the sharp copy over the tiles; the canvas takes a small breath of scale. From then on, while nothing moves, the canvas is not redrawn. Open again and Cancel do the opposite: the photo goes down, the grid comes back and a tween takes the split counter back, so the tiles fold up in reverse order, each with a little swell.

For those who cannot see it and those who want no motion

The frame is a role=progressbar with aria-valuenow and an aria-valuetext in megabytes, the panel has aria-busy while it loads and an aria-live line says when the file opens, stops on an error, is cancelled and is ready. The button changes its word (Open, Cancel, Retry, Open again) and the pill takes the new width with a bounce; a click from a script, the library preview’s, never cancels.

Values to change

NameFileDefaultWhat it changes
FILE_NAMEscript.js'DSC04182.ARW'The file being opened.
MEGAPIXELSscript.js61How big the photo is, in megapixels (the size in pixels is worked out from it).
SIZE_MBscript.js62.4The size of the file, in megabytes.
SECONDSscript.js6.5How long a load takes in the demo, in seconds (a real one follows its bytes).
TILESscript.js5200How many tiles the photo ends up in before it comes into focus.
POPscript.js1The elastic snap of every split: 0 none, 1 lively.
GRIDscript.js0.8The dark line between the tiles while loading, in pixels.
FAIL_EVERYscript.js2In the demo, one load in how many loses the memory card (0 never).
FAIL_ATscript.js0.62How far the load that goes wrong gets, from 0 to 1.
SUNscript.js0.68Where the sun sets, from 0 (left) to 1 (right).
SEEDscript.js7The shape of the hills, the trees and the ripples.

Try them in the Playground

When to use it

For opening a big picture that arrives in pieces: a RAW from a card, a photo from a network archive, a map, a scan, a generated preview. The quadtree says «the detail is on its way» better than a bar, and after a few bytes you can already tell what the photo is; for a file that is not a picture another indicator is better.

On phones

The panel takes the screen’s width less 32 pixels (358 on a 390 screen) and the frame stays 3:2; the canvas paints at the screen’s real density, up to twice, and the sharp photo is repainted at the new size when the screen turns. The button is a pill 34 tall: under a finger it squashes and springs back, and its words widen on the width axis only under the mouse.

With reduced motion

No snaps or movement: the children appear in their places at once and fade from the parent’s colour to their own, the frame does not shake, the focus corners light up and go out without closing in, the photo fades up without the breath, and the tiles go back to one at once. The photo still divides, because it is what says how far along things are.

Pitfalls

  • The sun never showed: it sat halfway up, behind the second ridge. Raised to 38% of the height it sets right on the ridges, and the quadtree goes for it at once.
  • With one grid line for every tile, the smallest tiles (under three pixels) were a third black and the photo went dull towards the end: the line now thins down to a quarter on small tiles.
  • While the children snapped out of the centre, the black background showed between them, a dark flash at every split: under the young children the parent is now drawn for a moment in its colour.
  • Without the grid the leaves left lighter threads between them, because their edges fell on half pixels and were softened twice: leaves at rest are now drawn with their edges rounded to the screen’s pixels.
  • The near hill ended in an upright cut halfway across the photo, and so did its reflection: it now runs down to the horizon and meets it.
  • Analysing the copy with grain, the noise gave the same error everywhere and the sky split as much as the horizon: the analysis now looks at a copy without grain.
  • Starting over with Open again the lines still said ready to edit and the percentage went back to 99% while the tiles folded up: the words now change at once, before the rewind.

Libraries

GSAP 3.15.0, Instrument Sans (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 13 KB; the libraries: GSAP 28 KB.

Testing

Tested on 8 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 components