Skip to content
animorio
IT
Back to the library

The funhouse mirror

Hover, ImagesCanvas, GSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

The mirror of a made-up funfair, with a gilded frame and lit bulbs: inside is the reflection of a big top. The pointer swells, drags and ripples the picture, which springs back with an elastic; a click changes the curve of the glass: concave, convex, waves.

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 funhouse mirror</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <h1 class="sign" data-sign>Hall of Mirrors</h1>

    <!-- the button is exactly the glass: the frame, the crest and the plaque hang outside it -->
    <button class="mirror" type="button" data-mirror aria-label="Funhouse mirror: click to change the curve of the glass">
      <span class="mirror__drop" data-drop>
        <span class="mirror__tilt" data-tilt>
          <span class="glass" data-glass>
            <canvas class="glass__canvas" data-canvas></canvas>
            <span class="glass__sheen" data-sheen></span>
            <span class="glass__shade"></span>
          </span>
          <svg class="frame" data-frame aria-hidden="true"></svg>
        </span>
      </span>
    </button>

    <p class="hint" data-hint>Move over the glass, click to bend it</p>
    <p class="sr" aria-live="polite" data-said></p>
  </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.

21 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 funhouse mirror” from Animorio and fit it into my site

What it does

On a night background, under the words “Hall of Mirrors” dropping in letter by letter with a bounce, an arched mirror with a gilded frame comes down, squashes a little and settles; the bulbs along the frame pop up from the middle outwards and light up, then stay lit and steady. A glint runs across the gold. In the glass there is an evening at the Marigold Pier fair: the big top in red and cream stripes with its flag, the arched title, a Ferris wheel, balloons, strings of pennants, the moon and the stars, the wooden boardwalk. The glass, flat at first, bends with an elastic into its first curve, concave, and a ring of ripples runs from the middle to the edges. When the pointer moves over the glass the picture swells under it like a bubble, is dragged in the direction it moves and sends out little ripples; the mirror leans slightly towards the pointer and the glint follows it. When the pointer leaves, the bubble springs back with an elastic: it goes through a squash and then stops. A click changes the curve: concave makes everything tall and thin, convex short and wide, waves keeps the picture swaying. The glass overshoots the new shape and comes back into it, a ring starts from the point of the click, the frame takes the knock and the plaque at the bottom flips to the new name. Until someone clicks, the curve changes by itself every so often.

How it works

A hidden picture

The fair scene is painted once on a canvas nobody sees, with the 2D API: a gradient sky, stars, the moon, the title letter by letter on an arc, the wheel, the balloons, the boardwalk, the big top with its stripes clipped inside its outline. Every size is in hundredths of the width, so the picture is the same at any size. Then getImageData takes its pixels: they are the source the mirror reads.

The bending glass

On every frame, for every pixel of the visible canvas, the script works out which point of the picture it should read. The curve of the glass is a simple function of coordinates between minus one and one: u plus k times u times (1 minus u squared). With k positive across and negative down the middle gets narrow and tall (concave), the other way round short and wide (convex); at the edges the function is always one, so the picture never comes away from the frame. The waves are two sines, damped towards the edges, with a phase that moves on with time.

The bubble under the pointer

Around the pointer a Gaussian bell moves the reading towards the middle (the picture swells) or away (it squashes): one number does it, swell, which GSAP raises with elastic.out when the pointer comes in and brings to zero when it leaves, and the elastic, dipping below zero, makes the squash. The pointer's speed moves a small spring that drags the picture along, and sets off circular ripples. The sums are done only in the square around the pointer, and the reading is bilinear, so stretched parts stay smooth.

The click

The three curves are three weights in one object: a click takes the new weight to one and the others to zero with elastic.out, so the glass overshoots the shape and comes back into it. At the same moment a ring of ripples starts from the point of the click and spreads to the edges, the frame squashes and bounces, a glint runs across the gold (a soft rectangle inside a clipPath of the frame) and the plaque closes with scaleY and opens again with the new name. A delayedCall changes the curve by itself until someone really clicks (isTrusted).

The frame and the bulbs

The frame is an SVG the script builds to the size of the mirror: the outer and inner arch in one path with fill-rule evenodd, a gold gradient, the bevels, the shell on top and the plaque. The bulbs sit on the middle line of the frame, lined up with getPointAtLength, skipping the ones that would fall under the shell or the plaque. They light up once in the entrance and then stay steady. The button is as big as the glass and everything inside it has pointer-events none, so the pointer always meets a single element.

Values to change

NameFileDefaultWhat it changes
TITLEscript.js'Marigold Pier'The title painted on an arc in the sky of the reflection: if it is long, the letters shrink to fit.
CURVEscript.js1How curved the glass is: 0 is a flat mirror, 1.5 bends it a lot.
RADIUSscript.js0.3The size of the bubble under the pointer, in mirror widths.
SWELLscript.js0.42How much the bubble magnifies; on the way back the elastic turns it into a squash.
WOBBLEscript.js1How much the picture is dragged and ripples when the pointer moves (0 = the bubble only).
SETTLEscript.js1.6The seconds of the elastic when the curve changes or the pointer leaves.
AUTOscript.js2.4How many seconds between automatic curve changes, until someone clicks (0 = only on click).

Try them in the Playground

When to use it

For the page of a funfair, a circus, a festival or a summer event, for a game or a welcome page that should make people smile, or to show an illustration in a way that asks to be touched. Any picture can be painted in place of the scene: the mirror reads the pixels of a canvas.

On phones

The same animation with the mirror almost as wide as the screen. A finger touching and sliding on the glass makes the bubble, drags it and lets it spring back when it lifts; a tap without sliding changes the curve.

With reduced motion

Nothing moves: the sign, the mirror and the line fade in with the glass already concave. The pointer does not bend the picture; when the curve changes the glass dims for a moment and comes back with the new curve already in place.

Pitfalls

  • With a curve that took the picture past the edges, the glass showed empty strips: the function u plus k times u times (1 minus u squared) is one at the edges, so the reading never goes out, and the coefficient stays below 0.4 so that the elastic, overshooting the weight, does not fold it over itself.
  • Inside the button the canvas, the glint and the shade took the pointer in turn, and every move from one to another counted as leaving: all the content has pointer-events none and the button is as big as the glass.
  • Working out every pixel on every frame is costly: the canvas has at most 260 thousand pixels (CSS stretches it), the parts of the curve that depend on one coordinate are worked out once per row and per column, the bubble only in the square around the pointer, and nothing is drawn when nothing moves.
  • On a phone a finger sliding on the glass also ended with a click and changed the curve: the click counts only if the finger moved less than ten pixels.
  • The title in the reflection uses the page's typeface: the scene is painted after document.fonts.load (with a time limit), otherwise it keeps the fallback font.
  • The automatic curve change stops at the first real click: without the isTrusted check it would also have stopped with the autopilot's fake click.

Libraries

GSAP 3.15.0, Mona 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