Skip to content
animorio
IT
Back to the library

Prints under the hair dryer

Hover, Physics, WebGLWebGL, Canvas, GSAPAdvancedCompressed weight: Demo 22 KBGSAP 28 KBThree.js 174 KBFreeDownload the zip

A wedding photographer’s proofs, on glossy paper, laid out on a linen table: the pointer is a hair dryer. Where it points the air slips under the photos: the near edge curls up and flaps, the print tips up like a lid and slides away, the heat bends it, the window of the room slides across the glossy paper and the rose petals fly off. Take the dryer away and the prints drop back, slap the table and settle.

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>Prints under the hair dryer</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full-italic.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.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>
  <main class="stage" data-stage>
    <!-- the linen table and the glossy prints, in WebGL -->
    <canvas class="stage__table" data-table aria-hidden="true"></canvas>

    <!-- the label of the proofs, lying on the corner of the table -->
    <header class="label" data-label>
      <p class="label__kicker"><span data-line>Wren Hollis Photography</span></p>
      <h1 class="label__title"><span data-line>Clara</span> <span data-line class="label__amp">&amp;</span> <span data-line>Sam</span></h1>
      <p class="label__line"><span data-line>Six glossy proofs, still damp from the lab. Give them a blow before they go in the album.</span></p>
      <dl class="label__facts">
        <div><dt><span data-line>Paper</span></dt><dd><span data-line>Glossy, 255 g</span></dd></div>
        <div><dt><span data-line>Album</span></dt><dd><span data-line>Ready by Friday</span></dd></div>
      </dl>
    </header>

    <!-- the cord of the dryer, then the dryer: the tip of the nozzle is the pointer -->
    <svg class="cord" data-cord aria-hidden="true"><path data-cord-path d="M0 0"></path></svg>
    <div class="dryer" data-dryer aria-hidden="true">
      <svg class="dryer__svg" viewBox="-110 -40 340 260" overflow="visible">
        <defs>
          <linearGradient id="dryer-body" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="var(--dryer-light)"></stop>
            <stop offset="0.45" stop-color="var(--dryer)"></stop>
            <stop offset="1" stop-color="var(--dryer-dark)"></stop>
          </linearGradient>
          <linearGradient id="dryer-trim" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0" stop-color="var(--trim-light)"></stop>
            <stop offset="1" stop-color="var(--trim)"></stop>
          </linearGradient>
          <linearGradient id="air-shade" x1="1" y1="0" x2="0" y2="0">
            <stop offset="0" stop-color="#5a3a2c" stop-opacity="0.3"></stop>
            <stop offset="1" stop-color="#5a3a2c" stop-opacity="0"></stop>
          </linearGradient>
          <linearGradient id="air-fade" x1="1" y1="0" x2="0" y2="0">
            <stop offset="0" stop-color="#fff" stop-opacity="0.95"></stop>
            <stop offset="1" stop-color="#fff" stop-opacity="0"></stop>
          </linearGradient>
        </defs>
        <g data-dryer-body>
          <!-- the air coming out of the nozzle -->
          <g class="dryer__air dryer__air--shade" data-air>
            <path d="M-6 54 C-40 49 -74 38 -124 22"></path>
            <path d="M-6 60 C-46 60 -86 60 -144 61"></path>
            <path d="M-6 66 C-40 71 -76 82 -128 98"></path>
            <path d="M-6 57 C-34 55 -60 49 -100 41"></path>
            <path d="M-6 63 C-34 66 -62 71 -102 80"></path>
          </g>
          <g class="dryer__air" data-air>
            <path d="M-6 54 C-40 49 -74 38 -124 22"></path>
            <path d="M-6 60 C-46 60 -86 60 -144 61"></path>
            <path d="M-6 66 C-40 71 -76 82 -128 98"></path>
            <path d="M-6 57 C-34 55 -60 49 -100 41"></path>
            <path d="M-6 63 C-34 66 -62 71 -102 80"></path>
          </g>
          <g data-dryer-shake>
            <!-- the handle, with its switch -->
            <path class="dryer__handle" d="M112 92 L152 92 L184 196 Q186 210 172 212 L160 212 Q150 212 146 202 Z"></path>
            <rect class="dryer__switch" x="150" y="120" width="12" height="30" rx="6" transform="rotate(-17 156 135)"></rect>
            <rect class="dryer__switch-knob" data-switch x="152" y="123" width="8" height="12" rx="4" transform="rotate(-17 156 135)"></rect>
            <!-- the barrel -->
            <rect class="dryer__barrel" x="26" y="16" width="160" height="88" rx="44"></rect>
            <path class="dryer__shine" d="M54 30 Q110 22 168 30"></path>
            <rect class="dryer__band" x="62" y="17" width="10" height="86" rx="3"></rect>
            <!-- the rear cap with the vents -->
            <ellipse class="dryer__cap" cx="184" cy="60" rx="16" ry="42"></ellipse>
            <g class="dryer__vents">
              <path d="M186 32 L186 88"></path>
              <path d="M193 38 L193 82"></path>
              <path d="M179 34 L179 86"></path>
            </g>
            <!-- the concentrator nozzle -->
            <path class="dryer__nozzle" d="M0 44 L36 30 L36 90 L0 76 Q-3 60 0 44 Z"></path>
            <path class="dryer__slot" d="M1 47 L1 73"></path>
          </g>
        </g>
      </svg>
    </div>

    <p class="hint" data-hint><span>Move the dryer over the prints. Hold to blow harder.</span></p>
  </main>

  <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.

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 “Prints under the hair dryer” from Animorio and fit it into my site

What it does

The linen table of Wren Hollis, a made-up wedding photographer, with six proofs of Clara and Sam’s wedding on glossy paper: the kiss against the light in a meadow, the rings on a dark cloth, the bouquet from above, the exit from the chapel under the confetti, the bride from behind by the sea in black and white, the three-tier cake. The photos are painted by the script, with film grain, and on the white back the lab’s line is printed over and over on a slant. On arrival the prints are tossed onto the table one after the other, glide on a cushion of air and land with their edge flapping, while a handful of rose petals falls turning. The pointer becomes a pink hair dryer with its nozzle, its cord and the stream of air coming out: where it points, the air runs along the table and slips under the photos nearby. The side facing the dryer curls up, until it almost stands and shows its back, and flaps with a wave running along the edge; the photo tips up like a lid, resting on the opposite edge, slides a little away from the stream and turns if the stream hits it from the side. With the dryer still over a photo the edges all around lift and tremble, and the heat bends it like a gutter, then it slowly cools and goes flat. The window of the room is reflected on the glossy paper, sliding and warping as the paper bends. The petals fly off the stream, lift up turning, fall back and settle on one face or the other. Holding the button down runs the dryer at full blast: the photos curl more and float. Move the dryer away and every photo drops back, bounces a little on the table and goes back to its place.

How it works

The painted photos

Every photo is a 640 by 800 canvas painted by the script: skies and meadows in gradients, figures made of Bézier curves, rings made of many small pieces of ellipse each with its own light, flowers of overlapping petals. The parts out of focus are only the blurred shadow of a shape moved off the sheet, which also works where the canvas blur filter is missing. Then the film grain, the vignette and, for one, black and white. They become the textures of the front; the back is another canvas with the lab’s line.

A sheet that bends

Every print is a Three.js plane of 21 by 26 points, bent at every frame on the processor. First the heat, a cylinder that keeps the length of the paper; then the side facing the dryer, which past a line wraps on a fixed radius (ROLL) and, past the steepest point, goes on straight, so it never rolls over itself; then the trembling edges and the tilt. At the end the sheet is raised until its lowest point touches the table: that is how it rests on the opposite edge.

The air of the dryer

The pointer is carried onto the table with a ray. For every photo the distance between the stream and its rectangle is measured: the force drops off like a bell (REACH). The side that curls turns smoothly towards the stream; the curl, the tilt, the edges and the floating are springs with little damping, which drop and bounce when the stream goes away. The push moves the photo away from the stream, a hit from the side turns it, and a spring (SETTLE) takes it back to its place.

The glossy paper

The front is a physical material with a clear varnish on top (clearcoat, GLOSS): underneath it is almost matte, the varnish reflects sharply. The room to reflect is a small separate scene, a dark room with a four-pane window and a pale board, baked into an environment map. When the paper bends the reflected window slides and warps. A light from above casts the shadows: a raised edge shades the table and the photo itself.

The dryer and the petals

The dryer is an SVG with its nozzle on the pointer, leaning against the way it moves; the stream is made of dashes that run, the cord is a Bézier curve with one point on a spring, so it dangles. The petals are a single instanced mesh: each one has a position, a height, a speed and two turns. The stream pushes them away and, when it is strong, lifts them; in the air they fall slowly and sway, on the table they stop and settle on one face.

Values to change

NameFileDefaultWhat it changes
BLOWscript.js1How strong the air of the dryer is.
REACHscript.js1How far the air reaches across the table: more, and the photos nearby move too.
CURLscript.js1How much the side of the photo facing the dryer curls up.
FLUTTERscript.js1How much the lifted edges flap and tremble.
HEATscript.js1How much the heat bends the photos like a gutter (0 = never).
GLOSSscript.js1How glossy the paper is: 0 is a matte paper, with no reflections.
SETTLEscript.js1How fast the photos drop back and return to their place.
--dryerstyle.css#e7a99cThe colour of the dryer (with --dryer-light and --dryer-dark for its light and shade; the table is --table).

Try them in the Playground

When to use it

For the site of a wedding or portrait photographer, a print lab, a studio that delivers albums: the proofs page, the gallery of work or the home page. The photos can be touched like real ones fresh from the printer, and playing with the dryer makes you want to look at them all.

On phones

The label sits at the top and the photos in two columns of three. The finger is the dryer: it appears where you touch and blows while you drag, with the same physics, the same reflections and the same petals, only smaller and fewer petals. With no button to hold, the full blast is not there.

With reduced motion

Nothing moves: the photos are already on the table, flat, and the petals in their place; there is no dryer. The pointer is a lamp: where it passes, the glossy paper lights up with a reflection, which fades in and out.

Pitfalls

  • A flat photo sits two thousandths above the table, too close for the depth buffer of many graphics cards (and of the browser with no graphics card): the table won and holed the paper right where it was lowest, with holes that looked like tears. The table must be drawn first and without writing depth (renderOrder -1, depthWrite false): it never hides anything, because nothing is under it.
  • With the paper glossy all through, that is a low roughness under the varnish too, the reflection of the window turns into a grey haze that dulls the dark photos. Only the varnish (clearcoat) gives the sharp reflection; underneath the paper is almost matte and reflects little (low specularIntensity). And the window must be aimed at the edge of the top row, not the middle, or a still photo is all covered.
  • The script is a module (it imports Three.js): opening index.html with a double click, the browser blocks it. To try it locally you need a small server, for instance npx serve in the folder.

Libraries

Three.js 0.178.0, GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.

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

Testing

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