Words made of still lifes
Every character here comes from a 5x7 bitmap font. Each lit pixel of a glyph becomes a 2x2 block, the smallest still life in Conway's Game of Life, so the finished word sits on the board unchanged instead of decaying the moment the simulation runs. Nothing paints those blocks directly. All of them are built by gliders launched from outside the text, colliding in the right place at the right time.
One block takes two gliders. Fire them on the right offset and four generations later they have eaten each other down to a single 2x2 block. That collision is the only building material in the app, and the real problem is scheduling a few hundred of them so that no glider ever clips a block that is already finished, or one that is still on its way in.
How the search works
Blocks are placed one at a time, sorted by distance from the centre of the text. The middle of a word is the hardest place to reach once the outside is standing, so it gets built first, while later gliders still have empty board to fly across.
Each block takes its launch direction from where it sits relative to that centre, so a block up and to the right is built by gliders arriving from up and to the right. The dominant axis is tried first and the other axis is kept as a fallback, which gives every block two orientations to work with.
Then there is the delay. A glider repeats its own shape every four generations, so a synthesis can be launched any whole number of periods early and still land on exactly the same square, just later. The planner walks these delay slots upward from zero, trying both orientations at each one, and keeps the first combination that fits. That is the whole search: the smallest delay that works, the first orientation that works, no backtracking. Text dense enough to exhaust the slot budget is rejected rather than approximated.
What keeps it quick is the test for whether a candidate fits. Re-simulating the board for every candidate would be far too slow, so the planner never simulates the combination at all. Instead it carries a running summary of the work already placed: every cell any construction touches at every generation, and how many live neighbours each dead cell is being handed at each generation. A candidate is accepted when two conditions hold.
- At every generation, each live cell of the candidate is at least two steps away from every live cell already placed, measured on the Moore neighbourhood. No cell ever sees a neighbour it would not have seen with its own block running alone.
- At every dead cell being fed live neighbours by both the candidate and the placed work, the combined count has to produce the same next state that each side produces on its own. This is the only way two harmless patterns can turn into something new together: between them they flip a cell that neither of them flips alone.
Hold both and the two patterns laid on top of each other evolve exactly the way they evolve apart, which is what lets a slot be accepted after one comparison against the summary rather than a fresh run of the board. A cheap filter runs before either check: if the candidate never touches a cell that any placed block touches at any generation, the checks are vacuous and the slot is taken immediately.
Once every block has a slot the combined seed is simulated once, in full, and compared against the target. If a single cell is out of place the construction is thrown away instead of shown, so anything that reaches the canvas is known to settle exactly.
Exporting patterns
The export .rle button writes the current board as Run Length Encoded text, the usual interchange format for Life patterns, so a word built here opens unchanged in Golly or LifeViewer. Boards can also be seeded at random or left blank and drawn on by hand, with wrapping edges on or off.
How it runs
The canvas and the simulation loop are TypeScript. The planner is the same Python package that backs the desktop build, running under Pyodide in a worker thread, which is why the first visit spends a moment fetching a WebAssembly runtime and NumPy before the generate button lights up. After that it is cached.
The source, including the PySide6 desktop version, is on GitHub under the MIT licence.