
The title that rises from a line
Transitions, Text, Images
NewFree
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.
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">°</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>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
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 siteEverything 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.
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.
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 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.
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.
| Name | File | Default | What it changes |
|---|---|---|---|
PHOTOS | script.js | 3 foto | The photos drawn by the script, one after the other: the title and how many degrees each one is off. |
IMAGE | script.js | '' | A photo of your own (an address or a file loaded in the Playground) in place of the drawn ones. |
HORIZON | script.js | 0.56 | Where the horizon is in your own photo, from the top (0) to the bottom (1): the red line settles there. |
TURN | script.js | 1.5 | The seconds the photo takes to level. |
OVERSHOOT | script.js | 0.035 | How far past zero it goes, as a part of its tilt (0 = it stops dead). |
HOLD | script.js | 1.8 | The seconds a level photo stays before the next one. |
PER_DEGREE | script.js | 14 | The pixels of ruler for every degree. |
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.
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.
Nothing turns: the photos are already level, at zero degrees, and one gives way to the next with a fade.
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.
Tested on 19 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.