Skip to content
animorio
IT
Back to the library

The shattering frame

Images, PhysicsCanvas, GSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

A painting hanging behind glass in a made-up gallery. On a click the glass cracks from the point you hit, trembles and breaks: polygon shards that fall tumbling to the floor, flashing as they catch the light. Underneath, the painting changes through a circle that opens from the point of impact and the label rewrites itself; then the shards fly back up the way they fell and close into a new pane. The pointer slides a reflection across the glass.

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>The shattering frame</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="sala" aria-label="A framed painting behind glass. Click the glass to break it and rehang the next painting.">
    <header class="testata">
      <p class="testata__nome"><span class="linea"><span data-in data-gallery>Vesper Gallery</span></span></p>
      <p class="testata__mostra"><span class="linea"><span data-in>Glass &amp; Light, winter hang</span></span></p>
    </header>

    <div class="pavimento" data-floor aria-hidden="true"></div>

    <div class="parete">
      <div class="appeso" data-hang>
        <button class="cornice" type="button" data-frame data-autoclic aria-label="Break the glass and show the next painting">
          <span class="cornice__luce" data-glass>
            <span class="opere" data-art>
              <!-- four paintings drawn in SVG: the script swaps them under the glass -->
              <svg class="opera" data-work viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="lem-wall" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#e9b552" /><stop offset="1" stop-color="#b9772a" /></linearGradient>
                  <linearGradient id="lem-table" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#8f4a26" /><stop offset="1" stop-color="#5a2b17" /></linearGradient>
                  <radialGradient id="lem-fruit" cx="0.35" cy="0.3" r="0.8"><stop offset="0" stop-color="#fff4a8" /><stop offset="0.45" stop-color="#f4d33a" /><stop offset="1" stop-color="#c8951c" /></radialGradient>
                </defs>
                <g data-inner>
                  <rect width="400" height="500" fill="url(#lem-wall)" />
                  <path d="M250 0 H400 V320 L300 320Z" fill="#f6cf7a" opacity="0.45" />
                  <path d="M40 60 H170 V250 H40Z" fill="#7a3b1c" opacity="0.18" />
                  <rect y="300" width="400" height="200" fill="url(#lem-table)" />
                  <path d="M-10 318 C60 300 110 330 170 316 C230 302 300 330 410 312 V500 H-10Z" fill="#2c5a86" />
                  <path d="M60 330 C70 380 50 430 70 500 M190 322 C200 380 186 440 200 500 M300 326 C318 380 300 450 320 500" stroke="#4a7fb4" stroke-width="10" fill="none" stroke-linecap="round" opacity="0.7" />
                  <path d="M120 340 C130 400 116 450 128 500" stroke="#1b3d5e" stroke-width="8" fill="none" opacity="0.5" />
                  <path d="M150 360 L400 420 V470 L120 410Z" fill="#14263a" opacity="0.35" />
                  <path d="M96 300 Q190 380 284 300Z" fill="#f2ede1" />
                  <ellipse cx="190" cy="300" rx="94" ry="16" fill="#d9d1bf" />
                  <path d="M110 306 Q190 352 270 306" stroke="#ffffff" stroke-width="5" fill="none" opacity="0.7" />
                  <path d="M128 298 C122 270 150 250 172 258 C186 248 206 254 210 270 C214 290 196 304 168 304 C150 304 132 306 128 298Z" fill="url(#lem-fruit)" />
                  <path d="M196 292 C192 262 222 244 246 254 C262 252 272 268 266 284 C260 302 236 306 216 304 C206 304 198 300 196 292Z" fill="url(#lem-fruit)" />
                  <path d="M266 360 C254 336 278 314 304 322 C318 318 334 330 330 348 C326 368 300 378 282 374 C272 372 268 368 266 360Z" fill="url(#lem-fruit)" />
                  <path d="M160 250 Q170 236 186 238" stroke="#4d6b2a" stroke-width="5" fill="none" stroke-linecap="round" />
                  <path d="M180 240 Q206 222 222 236 Q200 250 180 240Z" fill="#5f8a34" />
                  <ellipse cx="148" cy="276" rx="7" ry="11" fill="#ffffff" opacity="0.55" />
                  <ellipse cx="286" cy="338" rx="6" ry="9" fill="#ffffff" opacity="0.5" />
                </g>
              </svg>
              <svg class="opera" data-work viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="har-sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e5715a" /><stop offset="0.6" stop-color="#f4a77c" /><stop offset="1" stop-color="#f9dcb0" /></linearGradient>
                  <linearGradient id="har-sea" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2d7287" /><stop offset="1" stop-color="#123c4c" /></linearGradient>
                </defs>
                <g data-inner>
                  <rect width="400" height="500" fill="url(#har-sky)" />
                  <circle cx="250" cy="210" r="66" fill="#fde7c4" />
                  <path d="M-10 120 Q60 100 120 118 Q170 100 220 120 H-10Z M260 70 Q310 54 360 70 Q390 64 410 74 H260Z" fill="#fbc9a0" opacity="0.7" />
                  <rect y="270" width="400" height="230" fill="url(#har-sea)" />
                  <path d="M196 286 H304 M210 302 H290 M222 318 H278 M232 336 H268 M240 356 H260" stroke="#fde7c4" stroke-width="6" stroke-linecap="round" opacity="0.8" />
                  <path d="M20 300 H90 M40 330 H130 M300 380 H380 M60 420 H170 M250 450 H360" stroke="#5aa0b2" stroke-width="4" stroke-linecap="round" opacity="0.6" />
                  <path d="M-10 500 L-10 360 L120 330 L200 500Z" fill="#3a2b38" />
                  <path d="M-10 360 L120 330 L126 344 L-10 378Z" fill="#5c4458" />
                  <rect x="62" y="236" width="26" height="110" fill="#f4ede2" />
                  <rect x="62" y="262" width="26" height="18" fill="#c8402e" />
                  <rect x="62" y="302" width="26" height="18" fill="#c8402e" />
                  <path d="M56 236 H94 L88 220 H62Z" fill="#3a2b38" />
                  <rect x="66" y="206" width="18" height="14" fill="#ffe9b0" />
                  <path d="M60 206 H90 L75 190Z" fill="#c8402e" />
                  <path d="M300 380 L300 300 L340 378Z" fill="#f8f1e4" />
                  <path d="M296 380 L296 316 L270 378Z" fill="#f2c9a2" />
                  <path d="M262 382 H350 L338 396 H274Z" fill="#2a1f2b" />
                  <path d="M170 330 L170 290 L190 328Z" fill="#f8f1e4" />
                  <path d="M156 332 H198 L192 340 H162Z" fill="#2a1f2b" />
                </g>
              </svg>
              <svg class="opera" data-work viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="orc-sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bcdcd0" /><stop offset="1" stop-color="#eef3d8" /></linearGradient>
                </defs>
                <g data-inner>
                  <rect width="400" height="500" fill="url(#orc-sky)" />
                  <path d="M40 90 Q70 66 104 80 Q130 64 160 84 Q176 96 160 104 H44 Q26 100 40 90Z" fill="#ffffff" opacity="0.8" />
                  <path d="M-10 230 Q90 170 200 210 Q300 160 410 200 V500 H-10Z" fill="#9cc27a" />
                  <path d="M-10 290 Q120 230 240 280 Q330 250 410 270 V500 H-10Z" fill="#6b9e58" />
                  <path d="M-10 370 Q140 320 410 360 V500 H-10Z" fill="#3f7348" />
                  <path d="M150 500 C170 430 230 400 236 340 C240 300 226 280 214 262" stroke="#e8d6a2" stroke-width="34" fill="none" stroke-linecap="round" />
                  <path d="M150 500 C170 430 230 400 236 340" stroke="#d2bc82" stroke-width="10" fill="none" opacity="0.6" />
                  <g fill="#2f5c3c">
                    <rect x="58" y="238" width="8" height="40" fill="#5a3b24" />
                    <circle cx="62" cy="230" r="30" />
                    <rect x="318" y="226" width="8" height="40" fill="#5a3b24" />
                    <circle cx="322" cy="218" r="32" />
                    <rect x="112" y="280" width="9" height="48" fill="#5a3b24" />
                    <circle cx="116" cy="268" r="36" fill="#264e33" />
                    <rect x="292" y="300" width="10" height="56" fill="#5a3b24" />
                    <circle cx="297" cy="286" r="42" fill="#264e33" />
                  </g>
                  <g fill="#d8453a">
                    <circle cx="50" cy="222" r="4" /><circle cx="72" cy="236" r="4" /><circle cx="64" cy="214" r="3.5" />
                    <circle cx="312" cy="206" r="4" /><circle cx="334" cy="224" r="4" /><circle cx="318" cy="232" r="3.5" />
                    <circle cx="100" cy="262" r="4.5" /><circle cx="128" cy="252" r="4.5" /><circle cx="120" cy="282" r="4" />
                    <circle cx="280" cy="276" r="5" /><circle cx="312" cy="268" r="5" /><circle cx="300" cy="300" r="4.5" />
                  </g>
                  <g stroke="#f7f4ea" stroke-width="7" stroke-linecap="round" fill="none">
                    <path d="M150 380 V470 M262 380 V470" />
                    <path d="M162 392 H250 M162 422 H250 M162 452 H250" />
                    <path d="M162 392 L250 452" />
                  </g>
                  <path d="M0 384 H140 M272 384 H400" stroke="#c9b48a" stroke-width="5" opacity="0.8" />
                </g>
              </svg>
              <svg class="opera" data-work viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
                <defs>
                  <linearGradient id="pool-sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0f1a33" /><stop offset="1" stop-color="#2a3f6b" /></linearGradient>
                  <linearGradient id="pool-water" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1f9fb0" /><stop offset="1" stop-color="#0d5d72" /></linearGradient>
                </defs>
                <g data-inner>
                  <rect width="400" height="500" fill="url(#pool-sky)" />
                  <path d="M310 70 A34 34 0 1 0 330 132 A28 28 0 1 1 310 70Z" fill="#f2e7c4" />
                  <path d="M-10 250 H410 V500 H-10Z" fill="#c9b9a0" />
                  <path d="M60 276 H340 L392 470 H8Z" fill="url(#pool-water)" />
                  <path d="M60 276 H340 L344 288 H56Z" fill="#7fd6dc" opacity="0.55" />
                  <path d="M120 320 Q150 312 180 320 T240 320 M90 370 Q130 360 170 370 T250 370 T320 372 M70 424 Q120 412 170 424 T270 424 T350 426" stroke="#9feaee" stroke-width="4" fill="none" stroke-linecap="round" opacity="0.7" />
                  <path d="M290 300 Q300 296 310 300 M282 336 Q298 330 314 336 M288 380 Q304 374 320 382" stroke="#f2e7c4" stroke-width="5" fill="none" stroke-linecap="round" opacity="0.8" />
                  <path d="M150 352 Q170 344 186 350 L200 344" stroke="#f0c6a8" stroke-width="7" fill="none" stroke-linecap="round" />
                  <circle cx="144" cy="352" r="8" fill="#3b2a2a" />
                  <path d="M24 250 L30 120 L36 250Z M30 120 Q0 110 -10 130 M30 120 Q60 100 80 116 M30 120 Q10 92 -6 96 M30 120 Q50 86 70 84 M30 120 Q36 96 30 80" stroke="#08101f" stroke-width="10" fill="#08101f" stroke-linecap="round" />
                  <path d="M372 252 L378 150 L384 252Z M378 150 Q348 140 338 160 M378 150 Q408 130 420 146 M378 150 Q356 124 342 128 M378 150 Q392 118 410 116" stroke="#08101f" stroke-width="9" fill="#08101f" stroke-linecap="round" />
                  <rect x="250" y="246" width="70" height="10" fill="#e9dfcc" />
                  <rect x="312" y="246" width="8" height="30" fill="#8a7a64" />
                </g>
              </svg>
            </span>
          </span>
        </button>

        <aside class="cartellino" aria-live="polite">
          <p class="cartellino__titolo"><span class="linea"><span data-in data-title>Lemons, Late Afternoon</span></span></p>
          <p class="cartellino__autore"><span class="linea"><span data-in data-artist>Maren Ostrova, 2019</span></span></p>
          <p class="cartellino__tecnica"><span class="linea"><span data-in data-medium>Oil on linen</span></span></p>
          <p class="cartellino__indizio"><span class="linea"><span data-in><span class="su-mouse">Click the glass</span><span class="su-dito">Tap the glass</span></span></span></p>
        </aside>
      </div>
    </div>

    <!-- the glass and its shards: drawn over everything, so the pieces can fall to the floor -->
    <canvas class="vetro" data-shards aria-hidden="true"></canvas>
  </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.

29 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 shattering frame” from Animorio and fit it into my site

What it does

A dark green wall with an oak floor, the name of a made-up gallery at the top, Vesper Gallery, and its show, Glass & Light. In the middle a painting in a dark frame with a gilded lip and a pale mat, and over it the glass: a faint green veil with two slanted reflections that slide when you move the pointer (the opposite way, as a real reflection does) and brighten a little when you hover. Beside it, the label with the title, the name and year, the medium and the prompt Click the glass. On a click a ring of light leaves the point you hit, the cracks run out to the edges like a web, rays and rings, and the frame jolts. The glass trembles, then lets go: the shards nearest the impact go first, hop a little towards you and fall spinning, and as they turn edge-on they flash. They reach the floor, bounce, slide and lie there. Meanwhile a circle opens underneath from the point of impact and the next painting appears with a small settle, and the lines of the label leave upwards and come back from below with the new title. After a pause the shards fly back up, retracing their flight, fit into the frame, the cracks draw back into the point of impact and the glass is whole again: its rim lights up, a sweep of light crosses it and the frame gives a small bounce. There are four paintings, drawn in SVG: lemons on a blue cloth, a harbour at sunset, an orchard with a gate, a pool at night.

How it works

A pane drawn over everything

The glass is not an element of the page but a canvas as big as the window, on top of everything and unclickable (pointer-events: none), so the shards can fall out of the frame down to the floor. On every frame a GSAP ticker reads the rectangle of the mat with getBoundingClientRect and draws the glass there: the veil, a light from above and two slanted reflections made with a tilted linear gradient. Their position is a number that gsap.quickTo chases as you move the pointer. The glass follows the painting even when it jolts or lifts on hover.

Voronoi shards

On a click a handful of points is picked round the impact, on slightly crooked rays and rings, close together near the impact and further apart away from it. Every point takes its Voronoi cell: start from the rectangle of the glass and cut it with the half-plane of every other point (halfway between the two), with the Sutherland-Hodgman algorithm. So the walls between the cells come out as rays and rings, like glass that has really been hit. The walls on the edge of the glass are not cracks and are skipped.

Cracks that run

Every inner wall is a crack and is drawn from its end nearer the impact: a front growing from zero to the distance of the furthest corner decides how much of it has appeared. Two lines, a dark one moved by a pixel and a light one on top, so the crack shows on the pale mat and on dark colours. While the glass is whole it is drawn in one piece, so no line is left between the cells.

A fall that spins and flashes

Every shard has x, y, a rotation, a turn round its own axis (phi) and a squash. The turn is drawn by scaling the height by cos(phi): when it passes through zero the shard is edge-on. The flash is a high power of a sine of phi and the rotation, so it comes short and bright. The flight is a timeline: a small hop away from the impact, the fall with power2.in down to the floor (the time grows with the square root of the height), the bounce with bounce.out and the squash to 0.3, which makes it look like it lies on the floor.

Forwards and back

Cracks, trembling, falls and the jolts of the frame sit in one paused timeline. A directing timeline plays it to the end with tweenFromTo, meanwhile opens the new painting with a circle clip-path from the point of impact and rewrites the label, waits for the pause and then takes it back to zero with tweenTo and power2.inOut: the shards make the same flight backwards and the cracks draw back. At the end the rim of the glass lights up and a flash crosses it.

Values to change

NameFileDefaultWhat it changes
GALLERYscript.js'Vesper Gallery'The name of the gallery, top left.
SHARDSscript.js46How many pieces the glass breaks into (roughly).
CRACKscript.js0.7How many seconds the cracks take to run across the glass.
FALLscript.js1.05How many seconds a shard takes to fall from the top of the glass to the floor.
SPINscript.js1How much the shards tumble as they fall (0 = they drop flat).
GLINTscript.js1How brightly the shards catch the light.
HOLDscript.js0.7How many seconds the shards lie on the floor before they fly back.
REBUILDscript.js1.5How many seconds the shards take to fly back and close into a new pane.

Try them in the Playground

When to use it

For the site of a gallery, a photographer or an illustrator: a memorable way to go from one work to the next, the opening of a show, a 404 page that breaks and mends itself. Real pictures can take the place of the drawings: the glass does not know what lies underneath.

On phones

The same animation: a narrower painting in the middle, the label underneath and a lower floor; a tap breaks the glass where you touch it, and the shards stay inside the screen.

With reduced motion

Nothing falls and nothing moves: the cracks fade in, the new painting fades in under the cracked glass, the label changes with a fade and the cracks fade away. The reflections stay still.

Pitfalls

  • Drawing the cells one by one while the glass is whole, the soft edges of two neighbouring polygons add up and thin lines show on the glass, like cracks before the click: until a shard has moved the glass is drawn as a single rectangle, with the cracks on top.
  • A click given by the program (the keyboard, the autopilot) has clientX and clientY at zero, outside the glass: with event.detail at 0 or a point outside the glass, a point is taken from a list, inside the glass.
  • A big shard starting near the edge of the screen and sliding sideways ends up half outside: its landing point is kept inside the window allowing for the shard's radius, not only its centre.
  • A timeline driven with tweenTo must be paused and not inside the directing timeline: only the tweens that scrub it go in there (tweenFromTo forwards, tweenTo back), otherwise the director and the timeline fight over the playhead.
  • The glass is on a canvas over the page, so it does not vanish with the painting: during the entrance its opacity is read from the painting's with gsap.getProperty, or the glass would float on its own before the painting arrives.

Libraries

GSAP 3.15.0, Newsreader (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

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

Testing

Tested on 11 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