Sierpinski Chaos Game
Roll a die, jump halfway, and a Sierpinski triangle appears. Take the three corners of a triangle and any point inside it. Roll: 1 or 2 means corner A, 3 or 4 means B, 5 or 6 means C. Move the point halfway to that corner, mark where it lands, and roll again. The rolls are random, yet after a few jumps every mark falls on the same lacy triangle with a hole in the middle, holes in its corners, and holes in theirs. The plate plays the first six jumps slowly so you can follow them, then speeds up to hundreds of thousands.
About this piece
This is the chaos game, the shortest route there is to a fractal. The plate starts with one point at a random spot inside the triangle, drawn as a coral ring. A die is rolled; the readout in the top-left corner shows the face and the corner it picks. A dashed line runs from the point to that corner, and the point slides halfway along it. That is one jump. The first six jumps are played at 0.6 seconds each, so you can check the rule with your own eyes. Then the game speeds up, reaches 4,000 jumps three seconds later, and plays the rest in seven more: about 650,000 jumps on a laptop-sized stage at normal pixel density, four per device pixel of the triangle’s rectangle, capped at 2 million on high-density screens. The grown triangle is held for eight seconds, then a fresh game starts from new dice.
Each mark is coloured by the corner its last jump went to: A in cobalt, B in the text ink and C in coral. That is why the figure splits into three clean thirds. Every point that just jumped towards A lies in the half-size triangle at A, and so on. The first 20 jumps are drawn as a grey path of small rings and are never marked; why 20 is explained below. Under reduced motion you get the finished triangle as one still frame, and nothing moves.
The rule, step by step
With corners A = (0, 0), B = (1, 0) and C = (½, √3/2), an equilateral triangle of side 1, one jump is:
roll a fair die 1 or 2 → v = A
3 or 4 → v = B
5 or 6 → v = C
jump halfway p′ = v + ½(p − v) = (p + v) / 2
mark p′ (after the first 20 jumps), and roll again
A worked example. Start at the centre of the triangle, (0.5, 0.289), which sits in the big middle hole. Roll a 4, corner B: the point moves to (0.75, 0.144). That is the centre of the half-size triangle at B, so it is again in a hole, one level smaller. Roll a 1, corner A: (0.375, 0.072), the centre of a quarter-size triangle, in a hole smaller again. A point that starts in a hole keeps landing in holes, but each one is half as wide as the last, so within a few jumps they are smaller than a pixel.
This is the version given in Wikipedia’s “Sierpiński triangle” article: start anywhere inside the triangle, pick a corner at random, “move half the distance from your current position to the selected vertex”, plot, repeat. Its “Chaos game” article describes the general form, in which each new point is a fixed fraction of the way to a randomly chosen corner, and notes that the first few points are thrown away. Both were read on 13 October 2026. The figure is named after the Polish mathematician Wacław Sierpiński, who described it in 1915.
Why the holes stay empty
Look at what “jump halfway to A” does to the whole triangle. It shrinks every point towards A by half, so it maps the big triangle onto the half-size triangle in the corner at A. The same goes for B and C. The three half-size corner triangles leave a quarter of the area uncovered: the upside-down triangle in the middle, whose corners are the midpoints of the three sides. Once the point is anywhere in the big triangle, the next jump puts it in one of the three corner triangles, never in the middle.
Apply the same reasoning one level down. After two jumps the point is in one of nine quarter-size triangles, and after k jumps in one of 3k triangles of side 1/2k. Which one is spelled out by the last k rolls, newest first: “C, then A” means the A-corner of the C-corner. Every level keeps three quarters of the area that was left, so after k levels only (3/4)k remains: 32% after 4 levels and 6% after 10. In the limit the area is zero, yet the figure is not just a set of points either. Its fractal dimension is log 3 / log 2 ≈ 1.585, between a line and a surface.
Why 20 jumps are thrown away. The start point is random, so it can sit in a hole, as the centre does. Each jump halves its distance to the figure, so after 20 jumps that distance is at most 1/1,048,576 of a side, far below a pixel. From then on every mark is on the figure for all practical purposes. The plate’s test suite checks this on the plate’s own code: it plays a million jumps after the 20-jump burn-in and requires that not one lands inside any of the 40 holes of levels 1 to 4 (1 + 3 + 9 + 27), with that hole geometry built in the test from the corners alone.
What happens if the jump is not halfway
One-half is not a style choice. Call the share of the distance to the corner that the point keeps the ratio; halfway keeps 0.5. Keep 0.6 (jump only 40% of the way) and the three corner copies are 0.6 of full size. They overlap, and marks pour into the middle hole. The test suite keeps a check for exactly this: at 0.6, about 64% of the marks land in a hole of levels 1 to 4. Keep 0.4 (jump 60% of the way) and the copies shrink apart. The big middle hole stays empty, but the smaller holes are no longer where Sierpiński’s are, so you get a different, dustier fractal. Only one-half makes the three copies touch exactly at their corners.
A note on naming: Wikipedia’s “Chaos game” article measures the fraction jumped, not the fraction kept. The two agree at one-half and swap everywhere else.
Reading the plate: what to watch for
- The first marks look like noise. For the first few hundred marks, the dots seem scattered at random. The holes are only visible once there are enough marks to show where nothing ever lands.
- Each third is a whole triangle. The cobalt third is a complete Sierpinski triangle at half size, and so are the other two. Inside each, the three ninths are smaller copies again. On a laptop stage the triangle is about 610 pixels wide, so holes stay wider than a pixel down to about the ninth level.
- The die is fair, and it shows. The readout gives the share of jumps that went to each corner. Within a second of the speed-up it sits near 33.3% each. The test suite rolls a million times and requires each corner to be within 0.5 percentage points of a third.
- The colours are a memory of one roll. A pixel’s colour is the corner of its latest mark only. Points in the cobalt third have just jumped to A; what they did before is not shown.
How it is computed
The plate keeps one 32-bit hit counter, and the corner of the latest hit, for every device pixel of the triangle’s rectangle. The dice come from a small seeded generator (mulberry32), so the gallery tile and the embed play the same game from the same seed. The number of jumps made is a fixed function of the game’s age on the wall clock, with frame gaps clamped at 0.1 s, so a 30 Hz screen and a 120 Hz one reach the same jump at the same moment. A hit pixel is mixed from the background towards its corner’s ink:
strength = 0.45 + 0.55 × min(1, log(1 + hits) / log(1 + 2 × average hits per lit pixel))
so a single hit is already clearly visible and a busy pixel is full strength. Only the triangle’s rectangle is written to the canvas, in one putImageData call; the outline, corner labels, die and burn-in path are ordinary strokes on top. A theme change repaints the same counts in the other theme’s inks.
Honest limits
Pixels, not infinity. The real figure has holes at every scale. On screen they stop at the pixel: a little under a quarter of the triangle’s pixels end up lit, far more than the zero area of the true figure, because every pixel that touches the figure at all is counted as hit.
“Never” is checked, not proved, for these exact numbers. The argument above is exact for exact arithmetic. The computer uses floating point, so a mark that should sit exactly on an edge can land a hair either side of it. The test therefore only counts a mark as being in a hole if it is inside by more than a billionth of a side, and it checks levels 1 to 4 with a million marks, not every level forever.
The dice are not truly random. They come from a seeded generator, so a given seed always plays the same game. That makes the tile and the embed agree and the tests repeatable. The figure does not depend on it: any sequence of corners that keeps using all three will do.
Only the triangle. The same game with four corners and a half jump just fills the square evenly. The square needs an extra rule, such as never picking the same corner twice in a row, before a fractal appears. This plate does not show that variant.
A 30-line version
The snippet below plays 200,000 jumps in 30 lines of HTML and plain JavaScript, with no library, and colours each mark by its corner in the site’s inks. It starts at the centre on purpose: that point is in the big hole, and the burn-in throws it away. The test suite runs the snippet against a stub canvas and checks that none of its marks lands inside the middle hole. It leaves out the slow first jumps, the readout, sharp drawing on high-density screens (it draws in CSS pixels) and the reaction to theme changes. Because nothing moves, there is no reduced-motion case to handle.
Things worth changing: the comment at the bottom of the snippet shows how to change the jump. Change i <= 20 to i <= 0 to switch off the burn-in, then look closely: a few stray dots appear in the middle of the holes, one per hole size, which are the start point’s first jumps from the worked example above.
<canvas id="tri" style="position:fixed; inset:0"></canvas>
<script>
const dark = matchMedia('(prefers-color-scheme: dark)').matches;
const cv = document.getElementById('tri'), ctx = cv.getContext('2d');
const W = cv.width = innerWidth, H = cv.height = innerHeight;
ctx.fillStyle = dark ? '#0B0D12' : '#E7E2D5'; // the site's paper
ctx.fillRect(0, 0, W, H);
// The three corners: A bottom left, B bottom right, C on top.
const h = Math.sqrt(3) / 2; // height of a triangle of side 1
const side = Math.min(W - 40, (H - 40) / h);
const left = (W - side) / 2, base = (H + side * h) / 2;
const corners = [[left, base], [left + side, base], [left + side / 2, base - side * h]];
// One ink per corner: the site's cobalt, text ink and coral.
const inks = dark ? ['#96B4FF', '#ECE7D9', '#FF7854'] : ['#2848CD', '#191B22', '#BE441C'];
// Start at the centre, which is in the big hole: the first jump leaves it.
let x = left + side / 2, y = base - side * h / 3;
for (let i = 1; i <= 200000; i++) {
const roll = 1 + Math.floor(Math.random() * 6); // a fair die
const k = Math.floor((roll - 1) / 2); // 1-2 A, 3-4 B, 5-6 C
const [cx, cy] = corners[k];
x = (x + cx) / 2; // jump halfway
y = (y + cy) / 2; // to that corner
if (i <= 20) continue; // burn-in: never mark the first 20
ctx.fillStyle = inks[k]; // colour by the corner jumped to
ctx.fillRect(Math.floor(x), Math.floor(y), 1, 1);
}
// Try: (x + cx) / 2 is a jump of one half. Make it x + 0.6 * (cx - x)
// and the copies separate; x + 0.4 * (cx - x) and they overlap,
// filling the middle. Four corners and a half jump fill a square evenly.
</script>
Curious how the loop and canvas fit together? Read how it works →
More from the gallery
- Plate 40 Maze Generation A maze carved by a backtracking walk, then solved by a breadth-first flood, drawn as it happens.
- Plate 41 Metaballs Seven charges summing one field, its outline traced by marching squares as the blobs melt together.
- Plate 42 Spirograph A wheel rolling inside a ring draws loops that close after a set number of turns: four hypotrochoids, one epitrochoid.
- Plate 43 Ulam Spiral Count 1, 2, 3 in a square spiral outward, light every prime, and diagonal streaks appear.
- Plate 44 Pendulum Wave Fifteen pendulums tuned to 51 to 65 swings a minute drift into snakes, split into two rows at 30 s and line up again at 60 s.
- Plate 01 Quantum Fibonacci Golden-angle phyllotaxis blooming from the seed outward.