Skip to content
animorio
IT
Back to the library

The photo brought into focus

Images, HoverGSAP, CanvasAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

In the minimal style: a photo out of focus and a thin crosshair that follows the pointer on a spring. Round the crosshair the photo comes into focus in rings, the sharpest in the middle; when the crosshair stops, the focus goes a hair too far and comes back, the crosshair closes in by one pixel and turns orange, and the lens stops down click by click, from f/2.8 to f/8, while the number under the photo loses weight.

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 photo brought into focus</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <header class="top">
      <p class="mark">Vellum</p>
      <p class="title" data-title>Still life, north light</p>
    </header>

    <!-- the plate: the photo on a canvas, sharp only round the crosshair -->
    <figure class="plate" data-plate role="img" aria-label="Still life, north light">
      <canvas class="photo" data-photo aria-hidden="true"></canvas>
      <i class="tick tick--x" data-tick-x aria-hidden="true"></i>
      <i class="tick tick--y" data-tick-y aria-hidden="true"></i>
      <div class="cross" data-cross aria-hidden="true">
        <i class="arm arm--n" data-arm></i>
        <i class="arm arm--e" data-arm></i>
        <i class="arm arm--s" data-arm></i>
        <i class="arm arm--w" data-arm></i>
      </div>

      <figcaption class="read" aria-hidden="true">
        <span class="stop"><span class="stop__f" data-f>f/</span><span class="stop__n" data-stop>2.8</span></span>
        <span class="scale" data-scale>
          <i class="scale__mark" data-scale-mark></i>
        </span>
      </figcaption>
    </figure>
  </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.

24 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 photo brought into focus” from Animorio and fit it into my site

What it does

A black page, with the name of a made-up photography studio, Vellum, at the top left and the title of the photo, Still life, north light, small and grey on the right. In the middle a photo made by the script on a canvas: a still life in front of a linen wall, a ribbed sage-green bottle, a terracotta bowl with pale painted lines, a white cup with a blue band and a printed card leaning on the wall. The photo comes in fully out of focus, and from the spot the crosshair looks at four thin lines come out, one on each side, with a small empty space in the middle. There the photo comes into focus: the sharpness spreads in rings, the sharpest in the middle and then softer and softer rings, and before it stops it goes a hair too far and comes back, like a lens hunting for focus. At that moment the crosshair closes in by one pixel with a small elastic and turns from white to orange, and the lens stops down one click at a time: f/4, f/5.6, f/8. With every click the sharp rings grow on a spring, the number under the photo changes, widening for a moment and coming back, and gets thinner: the weight of the number says how much light goes through. On the right a scale with the six stops, from 2.8 to 16, and an orange tick that slides onto the one in use. Two small ticks outside the photo, on the top edge and on the left one, say where the focus is. Moving the mouse over the photo, the crosshair follows it on a spring: while it moves the lens opens all the way again, f/2.8, and the focus shrinks to a small circle; as soon as the mouse stops the focus settles and the lens stops down again. On a phone the crosshair follows the finger. When nobody points, the crosshair goes round by itself: the bowl, the cup, the bottle and the card.

How it works

The blur made only once

At the start the script draws the photo on the canvas at the size of the screen and makes five copies of it, from sharp to the most blurred (BLUR pixels), with ctx.filter = 'blur()'. Round every copy there is a frame of the photo itself, stretched, so the blur does not pull in the black of the page. No frame blurs anything: it only mixes the five copies already made.

Focus in rings

On every frame the most blurred photo covers the whole canvas; on top, from the least sharp to the sharpest, every copy is pasted only in a circle round the crosshair, cut out by a radial gradient in destination-in that softens the edge. The circles are multiples of the sharp circle (RINGS): the blur falls away in rings towards the middle. It redraws only when the crosshair or the circle have changed.

The crosshair on a spring

The crosshair does not jump to the pointer: it gets there on a real spring (SPRING, stiffness and damping), worked out in small steps inside GSAP's ticker. With the damping a little under critical it arrives, goes a pixel or two past and settles. The four lines are one-pixel divs; GSAP does not touch them, the ticker writes their position.

Focus that settles

The sharp circle depends on two numbers: q, how much it is in focus, and s, the aperture. When the crosshair stops (or reaches a spot on its round) a timeline takes q to 1.08, then to 0.97 and finally to 1: the focus goes past and comes back. Meanwhile pinch brings the lines one pixel closer with elastic.out and the colour turns to the accent; then s goes up one at a time with back.out, a click every 0.27 s, up to REST_STOP. The circle grows by 2 to the s/4: a quarter of a doubling with every click.

The number that loses weight

The number under the photo is in Roboto Flex, a variable typeface. The ticker writes font-variation-settings on every frame: wght goes from 780 at f/2.8 to 230 at f/16, following s. When the aperture changes the text changes and wdth starts at 126 and comes back to 100 with an elastic. While the crosshair runs, q drops to WIDE and s goes back to zero: the number counts back down to 2.8 and turns heavy again.

Values to change

NameFileDefaultWhat it changes
IMAGEscript.js''The photo: an address, or a file loaded in the Playground. Empty, the script draws the photo. The frame takes the photo's proportions.
BLURscript.js14How blurred the photo is far from the crosshair, in pixels.
FOCUSscript.js0.13The sharp circle with the lens wide open (f/2.8), as a part of the photo's short side. It grows as the lens stops down.
REST_STOPscript.js8The aperture the lens stops down to when the crosshair rests: 2.8, 4, 5.6, 8, 11 or 16. Higher means more clicks and more of the photo in focus.
SPRINGscript.js[170, 22]The crosshair's spring: how stiff it is and how much it is held back (less damping, more wobble).
POINTSscript.js[[0.56, 0.64], [0.75, 0.58], [0.34, 0.38], [0.2, 0.55]]The spots the crosshair visits by itself, as parts of the photo's width and height. With your own photo, move them onto its subjects.
HOLDscript.js1.2How many seconds the crosshair stays on each spot, in focus.
TOURscript.jstrueWhether the crosshair goes round by itself when nobody points. With false it stays on the first spot until the mouse or a finger comes.

Try them in the Playground

When to use it

For a photo to be looked at slowly: the opening of a photographer's portfolio, a product page whose details you discover, the cover of a photo story, the photo of a dish or a hotel room, the site of a camera shop or a photography course. It works with any photo, including one loaded in the Playground.

On phones

The same animation with the sizes adapted: the photo is almost as wide as the screen and cropped a little at the sides, the crosshair is smaller and follows the finger; four seconds after the finger lifts, the crosshair goes back to its round.

With reduced motion

Nothing moves: the photo fades in already in focus on the first spot, at f/8, with the orange crosshair. A click or a tap moves the focus at once, with no spring and no round.

Pitfalls

  • On the terracotta bowl the orange crosshair almost disappeared: the lines have a one-pixel veil of shade (filter: drop-shadow on the crosshair's box) and the first spot of the round sits on the dark inside of the rim, not on the painted lines.
  • When the mouse left a few pixels from a spot of the round, the crosshair set off towards that same spot: it let go and focused again there, without moving. Now if it is already near a spot (an eighth of the photo) it sets off towards the next one.
  • The number's stretch used a tween with overwrite: true on the whole state: it killed the focus and the aperture too. The number's elastic has an object of its own.
  • On a phone a 3:2 photo turned into a strip: there the frame stops at 1.25 and the photo is cropped at the sides (cover). The spots of the round and the crosshair are parts of the whole photo, not of the frame, so they stay on the same things.
  • The round repeats identically (fromTo between fixed spots and repeat: -1), never with values read at the moment: so the preview video can tell where one round ends.

Libraries

GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.

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

Testing

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