Skip to content
animorio
IT
All components

Noughts and crosses in pen

Games, SVG, PhysicsGSAPAdvancedCompressed weight: Demo 11 KBGSAP 28 KBFreeDownload the zip

In the minimal style: noughts and crosses against the barista, to pass the time for a café’s customers while they wait. The grid is four thin hairlines that draw themselves on arrival and are strings: when an X or an O lands next to them they give a pixel or two and settle. The X and O are written stroke by stroke, the pen going a hair too far and coming back; the barista thinks, glances at a square it does not take and then writes. The winning row is drawn in vermilion over its three squares and plucks the strings it crosses, the others fade; Play again rewinds the game from the last move.

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>Noughts and crosses in pen</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">
    <!-- a game of noughts and crosses against the barista, for a café's customers while the coffee brews. The four
         grid lines are thin strings drawn by the script: they draw themselves on arrival and give a little when a
         mark lands next to them. X and O are written stroke by stroke, the barista thinks for a moment before its
         move, the winning row is drawn in vermilion over its three squares and the rest fade. Play again rewinds
         the game, the last move first. The nine squares are real buttons: arrows move between them, Enter plays -->
    <section class="game" aria-labelledby="ttt-title" data-game data-autoclic>
      <h2 class="game__title" id="ttt-title" data-title>Beat the barista</h2>

      <div class="board" data-board>
        <svg class="board__ink" viewBox="0 0 300 300" aria-hidden="true" data-ink>
          <g class="board__grid" data-grid></g>
          <g class="board__marks" data-marks></g>
          <path class="board__ghost" data-ghost-x />
          <path class="board__ghost" data-ghost-o />
          <path class="board__win" data-win />
        </svg>
        <div class="cells" role="group" aria-label="Board, three squares by three" data-cells>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Top left, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Top middle, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Top right, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Middle left, empty"></button>
          <button class="cell" type="button" tabindex="0" data-cell aria-label="Centre, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Middle right, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Bottom left, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Bottom middle, empty"></button>
          <button class="cell" type="button" tabindex="-1" data-cell aria-label="Bottom right, empty"></button>
        </div>
      </div>

      <p class="status" aria-live="polite" data-status>Pick a square. You’re X.</p>

      <div class="foot">
        <p class="score" data-score>You <b data-n="X">0</b><span class="score__sep">·</span>Barista <b data-n="O">0</b><span class="score__sep">·</span>Draws <b data-n="D">0</b></p>
        <button class="again" type="button" data-again><span class="again__bg" aria-hidden="true"></span><span class="again__label">Play again</span></button>
      </div>
    </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.

25 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 “Noughts and crosses in pen” from Animorio and fit it into my site

What it does

On a white background, at the top the title Beat the barista in a variable grotesque, which on arrival opens out from its narrow, light shape to its full one. Below, four thin black hairlines draw themselves one after the other, each from a different side, go a hair past their end, come back and quiver slightly: it is the grid of the game. Further down a line says what is happening (Pick a square. You’re X.) and the score, You 0 · Barista 0 · Draws 0. With the mouse over a free square a pale X appears, the move about to be made; with a finger it appears while pressing, with the keyboard on the square chosen with the arrows, framed by a thin outline. With a click, a tap or Enter the X is written for real, first one stroke and then the other, each going a little too far and coming back, and the hairlines around the square give outwards and settle. The line says The barista is thinking: after a short pause a pale O begins in one square and goes back into the pen, then the barista writes its O in another, going round anticlockwise, and the line says where it went. When the move is forced (winning or blocking a row of two) the barista does not look around and is quicker. When someone gets three in a row a vermilion line is drawn over the three squares, starting from the last move if it is at the end of the row, and plucks the hairlines it crosses; the other moves fade, the number in the score takes weight and settles, and Play again appears, a thin pill that swells under the mouse while its word widens. In a draw every move goes grey and the four hairlines sound one after the other. Play again rewinds the game: the vermilion line goes back into the pen, then the moves are erased from the last to the first, each stroke backwards, quicker and quicker, and at every square that empties the hairlines feel a little knock.

How it works

The strings of the grid

The four hairlines are SVG paths in a 300 by 300 viewBox (a square is 100), with vector-effect non-scaling-stroke so they stay one and a half pixels at any size. Each one has how much of it is drawn (t) and a damped spring (STRING: stiffness and damping) that bends it at the point where it was plucked: every frame it becomes a quadratic curve with its control point there, twice as far as the bend.

One loop

The springs are integrated on GSAP’s ticker in four sub-steps, with the step capped at 1/30 of a second; the tweens only move numbers (how much of a stroke is written, the opacity) and mark the frame as dirty. When the strings are still and no tween has moved anything, the loop takes itself off the ticker.

The pen

An X is two strokes, a number from 0 to 1 each, the second starting before the first has settled; an O is an arc rebuilt every frame as a polyline, 364 degrees anticlockwise. Each number goes up to 1 + OVERSHOOT and comes back to 1 with a sine.out: the pen goes a hair past the end and stops. When the move is written, the strings around the square get a push outwards (PLUCK), full for those beside it and smaller for the others.

The barista

It plays full minimax (winning sooner is worth more), with a chance SLIP of picking a worse move, or nobody would ever win. It waits a random time within THINK and meanwhile draws and takes back half a pale O in a square it will not take; when the move is forced it does not look around and is quicker. The whole turn is a GSAP timeline, so it stops with the workbench like everything else.

The win and the rewind

The vermilion line goes from the middle of a square at one end of the row to the middle of the other, stretched by 38 units each way; before drawing it, where it crosses each string is worked out, and as the stroke moves on each string is plucked when the pen goes over it. Play again brings the faded moves back to full, takes back the line and erases the moves in reverse order, each stroke backwards, with a gap that shortens by 14% at every move.

Keyboard, screen and preview

The nine squares are real buttons in a group, with a single tabindex 0 that moves with the arrows, Home and End; each square says its name and what is in it (Top left, X) and aria-disabled when it cannot be played, the status line is aria-live. A click from a script on the component (the library’s preview, detail 0) plays a whole game written in advance, with a fork, the win, the pause and the rewind; a real hand interrupts it and the game goes on from there.

Values to change

NameFileDefaultWhat it changes
SLIPscript.js0.2How often the barista misses its best move: 0 never loses, 1 plays at random.
THINKscript.js[0.55, 0.95]The barista’s pause before a move, at random between these two, in seconds.
DRAWscript.js0.3The seconds one stroke takes to write (an O takes one and a half times as long).
OVERSHOOTscript.js0.04How far the pen goes past the end of the stroke before coming back (0.04 = 4%).
PLUCKscript.js1.8How far the grid lines give, in pixels, when a move lands next to them.
STRINGscript.js[1700, 0.11]The grid lines’ spring: the stiffness (higher quivers faster) and the damping (lower quivers for longer).
FADEscript.js0.16The opacity of the moves outside the winning row.

Try them in the Playground

When to use it

For the site or digital menu of a café, a bar or a pastry shop, the waiting page of an order or a table, a venue’s screen, a 404 or maintenance page: anywhere someone waits a few minutes and a small, careful game keeps them company.

On phones

The grid is at most 300 pixels wide and fits the screen with 32 pixels on each side; the squares are almost a third of the grid, comfortable under a finger. Pressing a free square shows the pale X, like hovering with the mouse, and letting go writes it. Under 360 pixels the status line and the score condense on the font’s width axis to stay on one line.

With reduced motion

No running strokes and no quivering strings: the grid and the title appear with a fade, the X and O light up already whole, the vermilion line appears over the three squares and the others fade, and Play again fades everything out together. The game, the barista and the keyboard stay the same.

Pitfalls

  • A barista with perfect minimax never loses and is boring after three games: it needs a chance of a mistake, but not when the move is obviously forced, or it looks absent-minded rather than human. That is why the pause and the glance depend on how urgent the move is.
  • With the viewBox scaled the hairlines get thinner on a phone: vector-effect non-scaling-stroke keeps them the same size in pixels, and so nothing has to be worked out again on resize.
  • The library’s preview clicks from a script every three seconds or so: one step per click left the video halfway. The first click plays the whole game in a single GSAP direction and the clicks that arrive during the tour are ignored.
  • A mouse resting on a square while the barista thinks gets no new pointerenter: the pale X has to be put back by itself when the squares are free again (at the end of the entrance, after the barista’s move, after the rewind).
  • An O closed with its radius drawing in left a little tooth where it closes: the radius stays the same and the arc runs over its own start, so the join disappears.
  • A second Play again during the rewind would have erased the moves twice: a flag allows one rewind at a time.

Libraries

GSAP 3.15.0, Archivo (font variabile) 5.3.0.

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

Testing

Tested on 16 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 components