Skip to content
animorio
IT
Back to the library

The pinball table

Physics, SVGGSAPAdvancedCompressed weight: Demo 15 KBGSAP 28 KBFreeDownload the zip

A small pinball table seen from above and tilted like a real one, all in SVG: the steel ball follows physics (gravity on the slope, bounces, pop bumpers that kick it with a full flash, drop targets that fall, a ramp to climb) and the two flippers snap up with a click or the keys. On its own an autopilot plays; the score rolls like a mechanical counter.

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>Night Comet pinball</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/unbounded@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <!-- the backbox: the name of the table, the rolling score and a strip for messages -->
    <header class="backbox" data-backbox>
      <p class="backbox__title">Night Comet</p>
      <div class="score" data-score role="img" aria-label="Score"></div>
      <p class="message" data-message aria-live="polite"></p>
    </header>

    <!-- the table, tilted away from you: the playfield is one SVG, 440 by 800 units -->
    <div class="tilt" data-tilt>
      <div class="tilt__plane">
        <svg class="table" data-table viewBox="0 0 440 824" role="img" aria-label="A small pinball table with three pop bumpers, five drop targets, a ramp and two flippers">
          <defs>
            <radialGradient id="pf" cx="50%" cy="38%" r="75%">
              <stop offset="0" class="stop-pf-a"/>
              <stop offset="1" class="stop-pf-b"/>
            </radialGradient>
            <radialGradient id="steel" cx="38%" cy="32%" r="72%">
              <stop offset="0" stop-color="#ffffff"/>
              <stop offset="0.35" stop-color="#d9dde6"/>
              <stop offset="0.75" stop-color="#7d8496"/>
              <stop offset="1" stop-color="#3b4152"/>
            </radialGradient>
            <linearGradient id="tail" x1="0" y1="1" x2="1" y2="0">
              <stop offset="0" class="stop-tail-a"/>
              <stop offset="1" class="stop-tail-b"/>
            </linearGradient>
            <filter id="soft" x="-30%" y="-30%" width="160%" height="160%">
              <feGaussianBlur stdDeviation="3"/>
            </filter>
            <clipPath id="field">
              <path d="M20 800 L20 220 A200 200 0 0 1 420 220 L420 800 Z"/>
            </clipPath>
          </defs>

          <g data-art>
            <path class="playfield" d="M20 800 L20 220 A200 200 0 0 1 420 220 L420 800 Z"/>
            <g clip-path="url(#field)">
              <!-- the comet: a head and three streaks of tail -->
              <path class="comet__tail" d="M188 506 C240 470 300 420 384 300 C320 390 262 440 206 490 Z"/>
              <path class="comet__tail comet__tail--thin" d="M176 494 C210 440 250 380 300 300 C270 370 236 420 196 486 Z"/>
              <path class="comet__tail comet__tail--thin" d="M200 520 C260 500 320 470 388 420 C330 480 270 512 214 530 Z"/>
              <path class="comet__head" d="M190 478 C194 500 202 508 224 512 C202 516 194 524 190 546 C186 524 178 516 156 512 C178 508 186 500 190 478 Z"/>
              <path class="comet__core" d="M190 496 C192 506 196 510 206 512 C196 514 192 518 190 528 C188 518 184 514 174 512 C184 510 188 506 190 496 Z"/>
              <!-- the orbit rings drawn round the bumpers -->
              <ellipse class="orbit" cx="204" cy="282" rx="118" ry="78"/>
              <ellipse class="orbit orbit--dash" cx="204" cy="282" rx="150" ry="104"/>
              <!-- a few stars, drawn as four-point sparkles -->
              <g class="stars">
                <path d="M300 520 l3 -9 l3 9 l9 3 l-9 3 l-3 9 l-3 -9 l-9 -3 Z"/>
                <path d="M120 560 l2 -6 l2 6 l6 2 l-6 2 l-2 6 l-2 -6 l-6 -2 Z"/>
                <path d="M330 130 l2 -6 l2 6 l6 2 l-6 2 l-2 6 l-2 -6 l-6 -2 Z"/>
                <path d="M150 150 l2 -7 l2 7 l7 2 l-7 2 l-2 7 l-2 -7 l-7 -2 Z"/>
                <path d="M250 610 l2 -6 l2 6 l6 2 l-6 2 l-2 6 l-2 -6 l-6 -2 Z"/>
                <path d="M78 600 l1.6 -5 l1.6 5 l5 1.6 l-5 1.6 l-1.6 5 l-1.6 -5 l-5 -1.6 Z"/>
              </g>
              <text class="label" x="93" y="458" text-anchor="middle">ORBIT</text>
              <text class="label" x="204" y="408" text-anchor="middle">POP</text>
            </g>
            <!-- the apron at the bottom, with the drain between the flippers -->
            <path class="apron" d="M20 560 L121 672 L140 740 L158 800 L20 800 Z"/>
            <path class="apron" d="M388 560 L287 672 L268 740 L250 800 L388 800 Z"/>
            <text class="apron__name" x="72" y="770" text-anchor="middle">NIGHT</text>
            <text class="apron__name" x="334" y="770" text-anchor="middle">COMET</text>
          </g>

          <g data-walls>
            <path class="rail" d="M20 800 L20 220 A200 200 0 0 1 420 220 L420 792"/>
            <path class="rail rail--thin" d="M388 262 L388 800"/>
            <path class="rail rail--thin" d="M20 560 L121 672"/>
            <path class="rail rail--thin" d="M388 560 L287 672"/>
            <rect class="lane" x="390" y="262" width="28" height="550"/>
            <circle class="post" cx="68" cy="426" r="5"/>
            <circle class="post" cx="118" cy="426" r="5"/>
            <path class="lip" d="M72 408 L114 408"/>
            <path class="rail rail--thin" d="M388 800 L388 812 M420 792 L420 812"/>
            <rect class="lockbar" x="12" y="812" width="416" height="11" rx="4"/>
          </g>
          <g data-parts></g>
          <g data-ball-low></g>
          <g data-ramp></g>
          <g data-ball-high></g>
          <g data-pops></g>
        </svg>
      </div>
    </div>

    <p class="hint" data-hint>Click the left or right half to flip. Keys: Z and M, or the arrows.</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>

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.

39 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 pinball table” from Animorio and fit it into my site

What it does

On a midnight-blue background stands a made-up pinball table, Night Comet, tilted away in perspective: the blue field with a pink comet, orbit rings, three round pop bumpers in pink, teal and amber, five yellow drop targets along the left wall with the letters C O M E T next to them, a clear plastic ramp that climbs on the left, runs across the top and comes down on the right, two slingshots above the flippers and the plunger lane on the right. Above the table is the backbox with the name, a seven-digit score and a strip for messages. On opening the table settles, the bumpers pop up with a bounce, the targets rise and the flippers make a test flick. Then the steel ball appears in the lane, the plunger winds up (the spring squashes and trembles) and shoots it up: the ball runs round the arch, comes down and from there on it is real physics. It rolls down the slope, bounces off the rails, the bumpers kick it away with a flash that fills them with light and an elastic jolt, the slingshots throw it back with their rubber band stretching; when it hits a target, the target drops into its slot and the letter beside it lights up. If it enters the ramp fast enough it climbs it, getting a little bigger (it is closer to us) with its shadow coming away, and comes down the other side; if it is too slow it stops and rolls back. Every point rolls the digits of the score and floats up small from the spot that was hit. With all five letters lit comes the bonus and the targets rise again. On its own an autopilot plays: when the ball rolls onto a flipper it waits for a spot picked at random and hits it back; after eighteen seconds it lets the ball go, the ball drops into the drain, the score flashes and rolls back to zero, and the round starts again. A click on the left or right half of the page (or the keys Z and M, or the arrows) raises the flipper on that side for as long as it is held, and the autopilot steps aside.

How it works

A table in a tilted SVG

The table is an SVG of 440 by 800 units, and the physics uses the same units: no conversions. The tilt is plain CSS: the container has perspective, the plane inside has rotateX with its origin on the bottom edge, so the top recedes. The clicks do not need to know where the ball falls on the screen: all that matters is whether they are left or right of the middle of the page.

Physics in fixed steps

The ball moves in steps of 1/480 of a second, as many as fit into the frame time (requestAnimationFrame, with an accumulator). On every step gravity adds downward speed, then the ball moves and the collisions are checked: segments (rails, slingshots, targets, the arch made of forty pieces), circles (bumpers and posts) and the flippers. For each one the closest point is found: if it is closer than the radius, the ball is put back outside and the part of its speed going into the surface is reversed, damped. With such small steps the ball never passes through a rail.

Flippers that hit

A flipper is a segment with round ends, thicker at the pivot. It swings up and down at a fixed speed towards its angle, and its angular speed gives the speed of the point touching the ball (perpendicular to the radius, proportional to the distance from the pivot): the bounce is worked out against that moving point. That is why a ball hit near the tip leaves harder and in another direction than one hit near the pivot. The bumpers instead guarantee a minimum speed going out, and the slingshots add a push along their edge.

The ramp and the targets

The ramp is a curve of four Béziers, sampled and measured once. When the ball enters the mouth going up, it leaves the playfield and moves along the curve: its speed drops going up and grows coming down according to the height of the ramp at that point, like a real ball. If it stops before the top it rolls back and comes out of the mouth. While it is on the ramp it is drawn in another layer, above the plastic, bigger and with its shadow moved away. A target hit hard enough stops counting in the collisions; when all five are down they rise again after a moment, if the ball is not in front of them.

The autopilot, the score and the round that comes back the same

The autopilot watches where the ball is along the flipper and flips when it reaches the spot picked at random (or gets near the tip), holds it up for a fifth of a second and then waits. The chance comes from a seeded generator: the same seed gives the same game, so the first and second rounds are identical and the preview video closes by itself. The score is seven columns of digits from 0 to 9 plus one more 0: GSAP takes each column to its whole number (the hundreds of 12,300 are 123), and the shift shown is that number modulo 10, so the digits roll through every one in between and the carry turns over by itself.

Values to change

NameFileDefaultWhat it changes
GRAVITYscript.js1How steep the table is: 0.6 is slow and floaty, 1.6 fast and heavy.
LAUNCHscript.js1How hard the plunger shoots the ball up.
BUMPERscript.js1How hard the pop bumpers and the slingshots kick: 0.2 barely, 2 twice as hard.
FLIPPERscript.js1How fast, and so how hard, the flippers snap up.
ROUNDscript.js18How many seconds the autopilot plays before it lets the ball drop and starts again.
TILTscript.js26How far the table leans away from you, in degrees (0 = seen straight from above).

Try them in the Playground

When to use it

For a gaming page or an arcade bar with pinball machines, an arcade-themed event, a 404 page you can play, an app's waiting screen, the site of a studio that wants to show physics in the browser. The collision model (segments, circles, moving flippers) also works for other SVG table games.

On phones

The same table and the same physics, with the sizes adapted: the table takes all the free height under the backbox and the type gets smaller. A tap on the left or right half of the screen raises the flipper on that side, and two fingers raise both.

With reduced motion

The ball does not travel: the table fades in and every two and a half seconds the ball fades out and back in at a new spot (a bumper lighting up, the targets down with their letters lit, the ramp), with the score already updated. The flippers go up on a click with no motion.

Pitfalls

  • The table tilted with rotateX and the entrance with GSAP (which moves and fades) were on the same element: GSAP's inline transform wiped out the CSS rotation. The entrance moves the container, the rotation lives on the child.
  • The bottom of the side slope ended right on the flipper's pivot: the ball stopped in the hollow between the two and the round never ended. The slope now ends a little above the flipper and the ball slides onto it.
  • The first bank of targets, slanted in the middle of the field, with a rail joining it to the wall made a V where a ball coming down along the wall stayed put. The targets now sit on the wall itself, which acts as the backstop when they drop.
  • A ball that stays still for more than a second and a half outside the lane gets a little nudge: without it, a rare jam froze the demo for good.
  • With true chance every game is different and the preview video never finds a round that comes back the same: the first two rounds use the same seed and the physics moves in fixed steps, so they repeat exactly; from the third on the seed is new.
  • The preview video stops when no GSAP animation is scheduled, but the physics runs on requestAnimationFrame: every ball starts a long empty tween that keeps the direction open until the next round.

Libraries

GSAP 3.15.0, Unbounded (font variabile) 5.3.0.

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

Testing

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