Skip to content
animorio
IT
Back to the library

The gravitational lens background

WebGL, Backgrounds, HoverGSAP, WebGL, CanvasAdvancedCompressed weight: Demo 6.9 KBGSAP 28 KBFreeDownload the zip

A minimal-style WebGL animated background to put behind a headline: a grid of hairlines and a big word bend around an invisible mass that follows the pointer, like light around a galaxy. The letters stretch into arcs and, when the mass passes over a letter, a vermilion Einstein ring closes. A click or a tap gives the mass a kick.

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>Umbra</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>
  <!-- a word behind a gravitational lens: a WebGL 2 shader bends a hairline grid and a big word around an invisible
       mass that follows the pointer; where the light is stretched most, the arcs turn vermilion -->
  <main class="pagina" data-pagina>
    <canvas class="tela" aria-hidden="true" data-tela></canvas>
    <h1 class="parola" data-parola>Umbra</h1>

    <header class="testa">
      <p class="marchio" data-entra>Umbra</p>
      <p class="testa__nota" data-entra>Deep sky observatory</p>
    </header>

    <footer class="piede">
      <p class="nota" data-entra data-nota>Move to bend the light</p>
      <p class="scala" data-entra aria-hidden="true"><i data-scala></i>1&Prime;</p>
      <p class="misura" data-entra>Einstein radius <span data-raggio>0.00</span>&Prime;</p>
    </footer>
  </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.

23 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 gravitational lens background” from Animorio and fit it into my site

What it does

On a warm white sheet of fine graph paper, with a registration mark every four lines as on a photographic plate, there is a big word, Umbra, the name of an invented observatory. In front of it sits a mass you cannot see: light passing close to it bends, as around a galaxy or a black hole, and the grid and the word curve around it. At first the page is straight; then the mass arrives, settling on a spring, and the lines swell into circles. The mass follows the pointer, or a finger, with a slight lag and a one-pixel sway when it stops; when nobody carries it, it slowly circles the word. Passing near the letters it stretches them into long arcs, and on the other side of the mass their second image appears, small and upside down, just as it does in the sky. Where the light is stretched the most the ink turns vermilion, the only colour: when the mass passes right over a letter, the letter opens into an Einstein ring, a red circle inside a black one. Inside the circle the paper is a touch darker, the shadow that gives the observatory its name. A click or a tap gives the mass a kick, so it swells and springs back; at the bottom the Einstein radius reads in arcseconds, with the plate's scale beside it.

How it works

A pixel looks where the lens sends it

Everything is a single WebGL 2 fragment shader on one triangle as big as the page. For each pixel θ is the distance from the mass and θE the Einstein radius: the point lens says that pixel sees the sheet at β = θ · (1 − θE² / |θ|²). The sheet never moves: it is the shader that, pixel by pixel, reads a different point. A small constant under |θ|² avoids dividing by zero right on top of the mass.

The grid made with derivatives

The grid is not an image: in the shader the lines come from fract() of the sheet coordinates divided by the spacing, and they are measured in pixels with fwidth(), so they stay thin and sharp even where the lens stretches or squeezes them. Every fourth line is darker and at each crossing there is a registration mark. Where the lines would be denser than a pixel the grid fades out, and on the Einstein circle, where the whole circle looks at the same point of the sheet, it vanishes completely: otherwise it would draw a ring around the pointer.

The word and the vermilion ring

The word is drawn once in a 2D canvas as big as the page and uploaded as a texture; the shader reads it at β. The point-lens magnification is 1 / |1 − (θE/θ)⁴|: where it goes above GLOW the ink switches to vermilion, in a short step, so the most stretched arcs get a crisp red edge. When a letter sits under the mass, the whole Einstein circle looks at it and the ring is born.

The mass on a spring

The mass follows the pointer with a real spring: acceleration = stiffness · (target − position) − damping · velocity, in three small steps per frame on GSAP's ticker. With 70 and 13 the spring is just under critical, so it stops with a barely visible sway. With no pointer for two and a half seconds the target is a slow Lissajous curve around the word.

The entrance and the kicks

On entrance the small labels appear staggered and the Einstein radius grows from zero with elastic.out(1, 0.55) over 2.2 seconds: the lines swell and settle. A click takes the mass to PULSE times and brings it back with elastic.out(1, 0.32); if nobody touches, every seven seconds a timeline gives it a smaller kick on its own. The radius at the bottom only updates when its second decimal changes.

Values to change

NameFileDefaultWhat it changes
WORDscript.jsUmbraThe word behind the lens.
MASSscript.js0.17The Einstein radius, as a fraction of the page's short side: higher bends more.
STIFFNESSscript.js70How stiff the spring carrying the mass towards the pointer is.
DAMPINGscript.js13How much the spring brakes: lower, the mass sways more before it stops.
GRIDscript.js34The grid spacing, in px.
LINEscript.js1The line thickness, in px.
PULSEscript.js1.55How much the mass grows on a click or a tap.
DRIFTscript.js1How fast it drifts when nobody touches.
GLOWscript.js4From which magnification the light turns vermilion: higher, thinner vermilion arcs.
WEIGHTscript.js720The weight of the word.

Try them in the Playground

When to use it

As the opening background of an observatory, a planetarium, a science museum, an astronomy magazine or a space company; behind the title of a physics conference; as the landing page of a design studio that wants a living but clean background that only moves when you touch it.

On phones

The same animation: the word is sized on both width and height, the Einstein radius on the short side. A dragging finger carries the mass; without a finger the mass circles the word on its own and now and then gets a kick. The plate scale hides below 520 px.

With reduced motion

The lens is already in place and still, below and to the right of the word, which reads in full: no entrance, no drift and no kicks. The pointer moves the lens at once, with no spring; the labels fade in.

Pitfalls

  • On the Einstein circle β is almost zero: the whole circle reads the same point of the sheet. If a grid line is there, the entire circle becomes a line, a ring following the pointer. The grid and the marks fade out where magnification is high; only the word stays, and it makes the ring when the mass passes over it.
  • Lines drawn with a fixed threshold on sheet coordinates vanish or turn into smudges where the lens squeezes them: the thickness has to be measured in screen pixels with fwidth(), and the grid faded out where a line would be thinner than a pixel.
  • A wide threshold between black and vermilion made a blurry orange halo around the arcs; with a short step (from GLOW to 1.35 times GLOW) the red edge is crisp.
  • The typeface arrives after the first draw: the word texture is redrawn as soon as document.fonts.load says it is ready, and on every resize.
  • In GLSL ES 3.0 a vec2 cannot be initialised from a float: «vec2 cell = spacing * 4.0» does not compile. It must be declared float.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

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