Skip to content
animorio
IT
Back to the library

The horizon that levels itself

Images, TransitionsGSAP, CanvasMediumCompressed weight: Demo 7.9 KBGSAP 28 KBFreeDownload the zip

In the minimal style, the straighten tool of a photo app: behind a still crop frame there is a photo a few degrees off. A red line settles on the horizon, the photo turns until it is level, goes a fraction of a degree past and comes back, while a ruler of degrees slides underneath and the number takes on weight. It can be dragged too: let go, it levels itself.

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 horizon that levels itself</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/archivo@5.3.0/wdth.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <header class="top">
      <p class="mark">Plumb</p>
      <p class="tool">Straighten</p>
    </header>

    <!-- the crop: a level frame with the photo behind it, tilted. A red hairline finds the horizon, the photo turns
         until it is level and the dial under it counts the degrees. Drag the photo or the dial to tilt it -->
    <section class="desk" data-desk>
      <p class="name" data-name aria-live="polite"></p>
      <figure class="crop" data-crop role="img">
        <div class="photo" data-photo>
          <div class="photo__img" data-img></div>
          <i class="photo__level" data-level aria-hidden="true"></i>
        </div>
        <div class="thirds" data-thirds aria-hidden="true">
          <i class="thirds__v" style="left: 33.333%"></i>
          <i class="thirds__v" style="left: 66.666%"></i>
          <i class="thirds__h" style="top: 33.333%"></i>
          <i class="thirds__h" style="top: 66.666%"></i>
        </div>
      </figure>
      <i class="corner corner--tl" data-corner aria-hidden="true"></i>
      <i class="corner corner--tr" data-corner aria-hidden="true"></i>
      <i class="corner corner--bl" data-corner aria-hidden="true"></i>
      <i class="corner corner--br" data-corner aria-hidden="true"></i>

      <div class="dial" data-dial aria-hidden="true">
        <div class="dial__ruler" data-ruler></div>
        <i class="dial__needle"></i>
      </div>
      <p class="angle" data-angle aria-hidden="true"><span data-angle-n>0.0</span><span class="angle__deg">&deg;</span></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.

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 horizon that levels itself” from Animorio and fit it into my site

What it does

A paper-coloured page with the name of a made-up photo app, Plumb, at the top left and the name of the tool, Straighten, on the right. In the middle a crop frame with four black corners and inside it a photo drawn by the script: the open sea in the morning, with a sail on the horizon. Above the frame the title of the photo, below it a ruler of degrees with a red needle fixed in the middle and, lower down, the number of degrees. The photo arrives crooked, the horizon leaning four and a half degrees, and the ruler slides with it. A thin red line settles on the horizon, from left to right, and the rule-of-thirds grid appears over the photo. Then the photo turns until the horizon is level: as it turns it grows just enough to always fill the frame, the ruler slides under the needle and the number counts towards zero, blacker and narrower all the time. It goes a little past, a fraction of a degree that at the edges is one pixel, and comes back. When it settles the corners of the frame close in by a pixel and come back, the number takes a beat of weight, the red line leaves towards the right and the grid fades. A moment later the next photo arrives, crooked the other way: a salt flat at noon with a pole and its shadow, then a lake shore with a row of posts in the water. The photo or the ruler can be dragged to tilt it by hand; let go, it levels itself.

How it works

One number, the angle

Everything follows one number, the angle in degrees. On every frame GSAP's ticker turns the photo by that angle, moves the ruler by angle × PER_DEGREE pixels under the needle and writes the number with one decimal and the real minus sign (String.fromCharCode(0x2212)). The animations only move the angle.

The photo that always fills the frame

Turned, a photo the size of the frame uncovers the corners. To cover it it must grow by max(cos θ + (H/W) sin θ, cos θ + (W/H) sin θ), with W and H the width and height of the frame: the same sum photo apps do. So at zero degrees the photo sits exactly in the frame.

Level, one pixel past

Levelling is a timeline: first the red line settles on the horizon (scaleX from the left) and the thirds appear, then the angle goes to −tilt × OVERSHOOT with power3.inOut and back to zero with sine.inOut. With 4.6 degrees and 0.035 that is 0.16 degrees, about one pixel at the edges of the photo.

The number that takes on weight

The number is in Archivo, a variable typeface with the wght and wdth axes. The script works out how close the angle is to zero and takes the weight from 300 to 680 and the width from 112 to 94: the straighter the photo, the blacker and narrower the number. When it settles, an extra beat of weight with elastic.out, together with the corners of the frame moving in by a pixel.

Drag and let go

Pressing on the photo or on the ruler pauses the timeline and the angle follows the pointer (backwards on the ruler, because you drag the scale). Letting go starts a new levelling from the angle where it was left, then the next photo.

Values to change

NameFileDefaultWhat it changes
PHOTOSscript.js3 fotoThe photos drawn by the script, one after the other: the title and how many degrees each one is off.
IMAGEscript.js''A photo of your own (an address or a file loaded in the Playground) in place of the drawn ones.
HORIZONscript.js0.56Where the horizon is in your own photo, from the top (0) to the bottom (1): the red line settles there.
TURNscript.js1.5The seconds the photo takes to level.
OVERSHOOTscript.js0.035How far past zero it goes, as a part of its tilt (0 = it stops dead).
HOLDscript.js1.8The seconds a level photo stays before the next one.
PER_DEGREEscript.js14The pixels of ruler for every degree.

Try them in the Playground

When to use it

For an app or a site that works on photos (an editor, a print service, a photography course), to explain a tool on the product page, or as a curious opening for a landscape photographer's portfolio.

On phones

The same animation with the sizes adapted: the frame turns upright and crops the photo at the sides, the ruler is as wide as the screen. The photo and the ruler can be dragged with a finger.

With reduced motion

Nothing turns: the photos are already level, at zero degrees, and one gives way to the next with a fade.

Pitfalls

  • Turned, a photo the size of the frame uncovers the paper in the corners: on every frame it grows by what the angle of that moment needs, not by a fixed amount (too little when crooked, too big when level).
  • The numbers of the ruler came out in italics: the ticks are <i> elements, and the browser's italics passed on to the numbers inside. It takes font-style: normal.
  • Rounded to one decimal, an angle of −0.04 would read '−0.0': under five hundredths it writes 0.0, and the minus is the typographic one, as wide as the figures.
  • The corners of the frame sat inside the box that crops the photo and disappeared: they are outside it, next to it, placed by the script.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

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