Main thread → Web Worker
Input
At once: your finger, the moment it moves.
Game Tailors
Source: Fast on a cheap tablet
The drawing app where the paper is real. No ads. No accounts. No personal data: only anonymous counts, which you can switch off.
Game Tailors · Schieweg 15Y-40, 2627 AN Delft (NL)
KvK 92437702 · VAT NL866044474B01
info@gametailors.com · +31 6 4227 5919
© 2026 Game Tailors
Fast on a cheap tablet
Every spot of the page holds its own water, colour, goop and heat, and a big page has almost two hundred thousand spots. A budget tablet can't work them all sixty times a second. So the game rests the quiet parts of the page, does its heaviest sums in a faster language, and serves the place under the finger first. The rest catches up a moment later.

Professor Oops runs as two halves that don't wait for each other. One half listens to your child's finger and paints the screen. The other works out what the ketchup, the water and the fire do next. That second half runs in a Web Worker, a separate line of work that the browser gives a web page. The two pass notes to each other many times a second.
So a page swimming in paint can't freeze the hand. While the simulation is busy, the screen still draws the brush or the spoon in your child's hand from the newest news it has, every time the screen refreshes.
The drawing half has no copy of the world. It gets a replica: the things on the page (a ball, a candle, a sprinkle of glitter), sent as only what changed since the last note, and pictures of the squares of paper that changed. A test fails if any drawing code reaches for the simulation directly, because that kind of mistake works when everything runs together and silently does nothing in the worker. The test's own header says it has happened four times.
If the worker can't start, the game moves over by itself and runs the simulation alongside the drawing instead. Whatever your child already did goes with it.
Main thread → Web Worker
At once: your finger, the moment it moves.
Main thread → Web Worker
The time that has passed.
Web Worker → main thread
The changed tiles, the replica, the sounds, and the milliseconds it took.
Main thread → Web Worker
Handed back for the next frame, never copied.
From the game's worker design. A stroke shows one frame later than it would if everything ran on one thread; in return the finger is never blocked.
Seven of the game's busiest loops have a second, faster version in WebAssembly, a compact, low-level form of code that browsers can run quickly. They cover the wind's pressure, runny goop flowing, water and dye creeping through paper, oil floating on water, fire, soap racing over milk, and fingers pushing paint about. Four of them also work on several numbers at once (SIMD, "single instruction, multiple data").
The readable JavaScript versions stay in the game as the reference. The tests run both on the same messy page and compare them. For five of the seven, every number on the page must match to the last binary digit. The other two, paper and the floating layer, trade a little rounding for speed and must agree within a hundred-thousandth and a ten-thousandth.
If someone changes the slow version and forgets the fast one, the tests stop them. If a phone can't run the fast versions at all, the game uses the slow ones and plays nearly the same game, with less to spare. The one known difference: on the slow path at the two lowest detail levels, the wind blows a little weaker.
The game's own rule This runs the real thing: the 4,199-byte WebAssembly module the game ships for the wind's pressure solve, and a copy of its JavaScript twin. A swirl stirs the same wind on both sides, and after every solve each of the numbers is compared. What to look for: zero numbers differ, solve after solve. Then press Nudge the twin by one bit: it changes one weight in the JavaScript by the smallest step a 32-bit float can take, and the two drift apart within a solve. The times are measured on your device, right now.
| Kernel | What it works out | Numbers | Tested against its twin |
|---|---|---|---|
| Air | The wind's pressure solve, full multigrid | f32 × 4 lanes | to the bit, 12 grid sizes, every array after 150 ticks |
| Goop | Runny goop flowing between cells | f64, scalar | to the bit, 10 pages, every array |
| Paper | Water, dye and the wick in paper | f32 × 4 lanes | within 10−5, its own parity test |
| Floating layer | Oil and films on water | f32 × 4 lanes | within 10−4, every material |
| Fire | Heat spreading and burning | f64 × 2; f32 × 4 to find the hottest cell | to the bit, 9 pages, every saved field |
| Soap on milk | Marangoni flow carrying colour | f64, scalar | to the bit, 8 pages |
| Pushing | Fingers and things shoving goop | f64, scalar | to the bit, 14 pages and the redraw tiles |
Each kernel was timed against its own JavaScript loop on the same page. Paper went 1.6 to 2.9 times faster, the floating layer 2.2 to 2.4. Fire gained least, 1.0 to 1.8, because its exact doubles allow only two lanes and many cells take slow paths. The pushing kernel has no SIMD at all and still gained 2.6 to 2.9 times.
The rule was to keep a kernel only at 1.2 times or better on its own sim. A WebAssembly copy of goop's slow pass (soaking, staining, melting) reached 1.1 to 1.2 and was taken out again. Every figure here comes from a desktop; there are no tablet measurements of the kernels on record.
Six of the kernels share one block of memory with the JavaScript. The goop height, the water and the temperature live there once, and every kernel and every JavaScript loop reads the same numbers in place. When the fire kernel warms a cell, the goop kernel sees it at once, with nothing copied and no message sent.
That memory is worked out in advance from what each kernel says it needs, and it never grows. If a phone hasn't room for all of it, the game builds the page again with fewer kernels, and if need be with none.
The paper is a grid of up to 512 × 384 cells, each about half a millimetre across. The game cuts it into small squares, 768 of them on a big page. Each kind of simulation decides for every square whether something is going on there. A square of dried paint or bare paper rests, and gets only a quick look now and then, more often for goop that has just settled than for an empty square. Working only the parts of the page that changed is a known trick of falling-sand games Purho 2019; here it's a little bookkeeping per square that the tiled simulations share.
When more squares want work than a pass can afford, each simulation's clock picks this pass's squares. The ones under the hand go first. Next comes any square that has waited too long, whatever else is going on. The rest go in turn. A square that waited isn't forgotten: it's told how many passes it missed and takes them in one longer step, up to a cap that keeps the step stable.
Each process catches up in its own way. A stain creeping towards its goop's colour goes by the steps the missed visits would have taken, so it never overshoots. Egg white sets by chance on each visit, so the game draws one random number, works out which missed visit it would have set on, and plays the visits before as raw and the rest as cooked.
Fire goes further. A square whose heat, neighbours, goop and material all come in exactly as they did last step, and which is cool and quiet, keeps last step's answer. On a warm dough page 680 of 768 fire squares sleep, for 4.6 times less fire work, and the page comes out identical to the bit.
Every square here wants work, as on a page covered in moving paint. This is the game's own tile clock, ported for squares that want work (the looks at settled squares are left out): hand first, then the deadline, then the round. Each square shows a ripple of colour at the time it has caught up to, so a square that waited shows an older ripple. Squares outlined in yellow are worked this pass; a red corner means it waited longer than the cap and lost the rest. It runs slower than the game, so you can follow it. Try: switch the order to rows and watch a band sweep the page; press and drag to put a hand down (or switch on A hand in the middle) and see its squares never wait; pick the big page at detail 4 and watch time get lost, then the phone page, where it isn't.
At the lowest detail a big page of 768 squares takes 38.4 passes a round, past the catch-up cap of 16; a phone page of 252 takes 12.6. At the richest detail every page takes under 5.
| squares on the page | richest detail | middle detail | lowest detail | catch-up cap |
|---|---|---|---|---|
| 252 | 2 | 4 | 13 | 16 |
| 300 | 2 | 5 | 15 | |
| 432 | 3 | 7 | 22 | |
| 768 | 5 | 13 | 38 | 16 |
Source: Arithmetic from the game's detail levels: the squares on the page divided by the squares a pass may take, against the catch-up cap.


On a device that keeps up, the game picks a level of detail and holds it while it fits. When steps start running long, it moves a level lighter, quickly. When there's plenty of room, it moves a level richer, slowly. Down fast, back slowly.
On a device the game has measured as slow, or one whose frames keep running long, it shares out each frame's time instead. It aims for 30 frames a second, or 60 when that fits with room. The work that has to happen comes first: the simulations that aren't cut into squares, the squares under the hand, and sending pictures to the screen. The background always keeps a guaranteed share of the frame, so it never stops catching up, and the hand's own pushing keeps a share too.
While a hand is on the page, the background gets its guaranteed share and no more. The frame finishes early, and what the hand does shows sooner. What is "under the hand" is wide on purpose: every finger, a thing skidding across the page, a thing landing, a burst. They all mark one shared map, which all six clocks read.
A heavy frame is cut into slices of a few milliseconds, and the worker takes in new touches between them. The slice ends at the next natural break in a step, so a long part can run over, but a touch no longer waits for a whole frame.
The bar is one frame, shared out by the game's own rule. The dashed line is the target (1/60 or 1/30 s), picked by a simplified form of the game's rule. The tiles under the hand are taken as nine for the picture. The time each part takes is listed under the bar. Try: lift the hand and watch the background take what's free; make the device slower and watch the target drop to 30 frames a second; push harder and watch the hand's pushes stop at their share, the rest waiting for the next frames.
A test smears ketchup across a 320 × 240 page at the lowest detail and counts the frames in which the stroke changed at least a quarter as many cells near the hand as it would with no limit at all. Before the hand came first, 19 frames of a 44-frame stroke were live. Now about 42 are. With a hand in oil on water, live frames rose from 28 to at least 35, and under the hand the goop, the oil and both paper clocks missed none of the passes they wanted.
Giving the hand everything was tried too. With all of a pass for the hand, the film of oil over the rest of the page waited so long that it left steps round the stroke, 1.71 against 1.63 with a share, and the deadline had to go over budget. So the hand gets a share, and the rest of the page keeps the remainder.
On its first launches, after an update and now and then after that, the game quietly times itself. In a moment when nothing is happening, a throwaway worker runs the real simulations on a small busy scratch page: six puddles of paint, a soaked disk of dye, two fires, confetti, glitter and a hot hair dryer. A fast device gets a 512 × 384 page, a middling one 384 × 288, a slow one 320 × 240. A phone's small screen sets its own limit, down to 288 × 216.
A smaller page doesn't shrink the brushes or the creatures. Everything is measured in cells, so a smaller page shows it all a little bigger and coarser. A page keeps the size it was drawn at; a new size only ever applies to a new page.
The rule is generous going up and careful going down. One good measurement raises the size; it takes several bad ones in a row to lower it. If a tablet plays several long sessions in a row mostly at the lowest detail levels, the next new page comes one size smaller.
The shape of the game's rule: each border has a margin both ways, so a page drops a size a little past it and rises a little before it. Down takes several measurements in a row; up takes one. Try: slide the device slower past a border and back again: the page keeps its smaller size until the device is clearly faster. On the slider each position stands for enough measurements, and the scale has no units: it shows the rule's shape, not its numbers.
The time-skip buttons let a child jump ahead while paint dries or dough rises. A skip ignores all the rationing above. It works the same number of squares a pass whatever the device, clears the hand's focus, and takes longer on a slow tablet. A test waits two hours on three pages (paint, oil, ketchup) at detail 0, 2 and 4 and gets the same page, on 18 fields. Before this, at a slow tablet's budget, a three-hour skip left paint 7 % and oil 16 % less spread.
A skip doesn't play hours out step by step. Runny things get a fixed number of flow passes for each hour, up to a limit, and the rest is worked out with formulas per cell, such as a closed form for drying. The skip runs in short slices so the screen stays alive, and a stroke drawn meanwhile lands after it.
Undo used to keep whole copies of the page. Now each older step keeps only the squares that differ from the step after it, and an empty square costs almost nothing. After four strokes and a flood, undo on a desktop page went from 144 MB to 17 MB, and on a phone page from 46 MB to 6.6 MB.
So undo goes back as far as a fixed memory budget allows, with a floor and a ceiling on the number of steps: about 65 strokes on a dry 384 × 288 page and 11 on a wet one.
Speed work is full of ideas that sound faster. Each one here was built and measured against what was already there. These are the ones that didn't make it, with what the measurement said.
| The idea | What the measurement said | Outcome |
|---|---|---|
| Run the paper and goop on the graphics chip | Against WebAssembly on the main thread it took 25 to 70 % off a paint or water frame, and nothing in the busiest. The worker already matched or beat it in every scene, without 3 to 4 ms of extra GPU time a frame (desktop Chrome). Not worth a third copy of the logic. | Left out |
| Repay clamped time with extra sub-passes | Built and tested. At the lowest detail on oil, film, ketchup and smear pages, the mean frame and the slowest one in twenty were within a few per cent either way. | Switched off |
| Give all of a pass to the hand | The oil over the rest of the page waited so long it left steps round the stroke, 1.71 against 1.63 with a share, and the deadline went over budget. | A share instead |
| Go round the page backwards every other round | Squares at the ends waited two rounds and then none; with a short cap they got back only 53 to 97 % of their time. | Blocks instead |
| Look at settled goop less and less often | It fell behind its neighbours and the step between them stood straight along the square's edge, 3.5 against 2.8. | A fixed rhythm |
| Work the wind on the paper in patches by turns | Seams, patches or grain. Bands along the wind replaced them, and a test keeps the seams in bounds. | Bands instead |
| WebAssembly for goop's slow local pass | 1.1 to 1.2 times faster, under the 1.2 the rule asks for. | Reverted |
| A first kernel for standing water | 1.0 to 1.15 times, worse on sparse tiles. Water kernels came later in another form. | Dropped then |
What hasn't been measured matters as much. There are no kernel timings from a real tablet on record, no measured memory total of a running page, and no recorded touch-to-screen latency. Much of this was tested on a desktop told to behave like a slow device: a development switch makes the worker busy-wait several times as long and scales the costs it measures, so the scheduler sees the device it's meant for. That's why so much could be tested without a tablet, and also why it still needs one.
None of the machinery here knows about ketchup or candles. It reads the same shared marks every simulation writes, so a new tool needs no scheduler code.
Shared: one shared map of the hand, read by six clocks
Under the hand, goop, oil and both paper passes miss no pass they want, and a test checks it.
Shared: fire remembers what came in last step
680 of 768 fire squares sleep and the page comes out the same to the bit, pinned by a test.
Shared: a shared mark of what changed
Only the pigment and water pictures are sent again, not the goop or the height, and a test checks it.
Shared: the floating layer, then the layers settling
The oil floats up on it, with the floating-layer kernel and without; the kernel gate checks both.
Shared: the air's share of the frame
The wind works in bands along its own direction, and a test keeps the seams within bounds.
Shared: a skip at one fixed rate, the hand's focus cleared
The same page at three detail levels, pinned by a test.