
Words that melt into each other
Text, Scroll, SVG
NewFree
A protractor drawn in hairlines: the free arm turns round the scale, clicks into a notch every fifteen degrees with a hair of bounce, and the number of degrees widens with the angle, narrow at zero and very wide at 180. At ninety degrees the little arc in the corner folds into the small square of a right angle, in the one accent colour. Minimal style.
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 opening angle</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/anybody@5.3.0/wdth.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- a protractor drawn with hairlines: one arm is fixed on the base line, the other is dragged round the scale and
clicks into a notch every fifteen degrees. The number underneath opens with the angle (Anybody's width axis),
and at ninety degrees the little arc in the corner folds into the square of a right angle -->
<main class="stage" data-stage>
<header class="top">
<span class="top__name">Bevel</span>
<span class="top__note">An angle, measured</span>
</header>
<div class="dial" role="slider" tabindex="0" aria-label="Angle" aria-valuemin="0" aria-valuemax="180" aria-valuenow="0" aria-valuetext="0 degrees" data-dial>
<svg class="dial__svg" viewBox="-320 -306 640 322" aria-hidden="true">
<path class="base" pathLength="1" data-base></path>
<path class="scale" pathLength="1" data-scale></path>
<path class="ticks" data-ticks></path>
<path class="ticks ticks--in" data-ticks-in></path>
<path class="mark" data-mark></path>
<path class="arm arm--fixed" data-fixed></path>
<path class="arm" data-arm></path>
<path class="grip" data-grip></path>
<path class="pivot" data-pivot></path>
</svg>
<div class="labels" aria-hidden="true" data-labels></div>
</div>
<div class="read" aria-hidden="true">
<div class="num" data-num><span class="num__in" data-num-in><span data-digits>0</span><span class="num__deg">°</span></span></div>
<div class="name" data-name></div>
</div>
<p class="hint">Drag the arm round the scale</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>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.
30 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 opening angle” from Animorio and fit it into my siteThe SVG has a viewBox of 640 by 322 with the vertex at the origin and the scale at radius 260. Angles are counted as in geometry, anticlockwise from zero on the right, so a point at d degrees is (r·cos d, −r·sin d): y goes down. The ticks are segments from the scale towards the middle, 4, 9, 16 or 24 long depending on the degree, in two paths redrawn every frame: the ones inside the angle darker, the others lighter. Below 520 pixels wide the one-degree ticks are left out, so they do not make a grey band.
The hand asks for an angle (atan2 from the vertex to the pointer, between 0 and 180; below the base line it is 0 or 180). If it is less than MAGNET degrees from a multiple of STEP, the request becomes the notch: that is the magnet that pulls the arm in. The arm goes towards the request on SPRING, integrated by hand on GSAP's ticker in four steps a frame with the time capped at 1/30 of a second; held back only a little, it goes a degree past and comes back. The base line is a stop: if the angle passes 0 or 180 it is put back on the edge and the speed is reversed, multiplied by BOUNCE.
Every time the request enters a notch (and every time the arm hits the stop) a knock is recorded: where, and how long ago. Each tick stretches by LEAN × e^(−(distance/6.5)²) × e^(−7t) × cos(21t), a bell round the notch that shivers and dies down in under a second. At the same moment the number's weight goes up by WEIGHT[1] and comes back down with a power3.out tween.
The number's width is font-stretch from WIDTH[0] to WIDTH[1] in proportion to the arm's angle, spring included. The digits show the notch when the arm is less than 0.9 degrees from it, so the settling does not make them dance between 89 and 91. The mark in the corner is a line of 28 points taken twice, on the arc of radius 58 and on the right angle's corner line (A, B, C, which works for angles other than 90 too, as a rhombus); a value m from 0 to 1 blends them point by point, and the colour goes from ink to accent with gsap.utils.interpolate. m rises with back.out only when the request is 90 and the arm has settled.
A GSAP timeline opens the base line from the middle, grows the ticks with a back.out when the sweep reaches them (and draws the scale with stroke-dashoffset), stretches out the arms and raises the number. Then the walk starts: a timeline with repeat −1 that takes the hand's request through the TOUR angles with power2.inOut, holding each one HOLD seconds, and drops back onto zero with power3.in. When the hand presses, the walk stops; let go, after IDLE seconds a gsap.delayedCall takes the arm back to zero and the walk starts again. The protractor is a role=slider with aria-valuenow.
| Name | File | Default | What it changes |
|---|---|---|---|
STEP | script.js | 15 | Every how many degrees there is a notch the arm clicks into. |
MAGNET | script.js | 2.5 | From how many degrees away a notch pulls the arm in (0, no magnet). |
SPRING | script.js | [190, 15] | The arm's spring: how stiff, and how much it is held back (less hold, more overshoot). |
BOUNCE | script.js | 0.45 | How much of its speed the arm keeps when it hits the base line. |
LEAN | script.js | 14 | How far the ticks round a notch reach in when the arm clicks into it. |
WIDTH | script.js | [56, 148] | The width of the number with the arms closed and lying flat (50 to 150). |
WEIGHT | script.js | [560, 190] | The weight of the number (100 to 900), and how much heavier it gets at each click. |
TOUR | script.js | [30, 90, 150, 180, 45, 0] | The angles the arm walks through by itself, from 0 to 180. |
HOLD | script.js | 1.1 | The seconds the arm rests on each angle of the walk (at 90 and 180 one and a half times as long). |
IDLE | script.js | 2.4 | After how many seconds without hands the arm goes back to zero and starts off again. |
For a value that is an angle or an opening and should be felt: the tilt of a solar panel or a seat back on a product page, the angle of a gradient or a rotation in a design app, the beam of a spotlight, an architect's or a carpenter's studio introducing itself with its tools. It works where there is room for a single scene; in a form full of fields a normal slider is enough.
The same animations with the sizes adapted: the protractor is as wide as the screen, the scale numbers smaller, the one-degree ticks left out below 520 pixels, the number of degrees scales with the screen. The arm follows a finger dragging anywhere on the page (the page does not scroll there, it is a single scene) and stays where the finger leaves it; then it goes back to zero and starts off by itself.
No spring, no bounces, no shivering ticks and no rising letters: the arm goes straight to the angle, the square appears already folded, the angle's name swaps with a short fade. In the walk on its own the arm does not move between one angle and the next: it fades back in already in place. When the page opens everything fades in.
GSAP 3.15.0, Anybody (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 7.9 KB; the libraries: GSAP 28 KB.
Tested on 16 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.