My short illustrated narrative “Life Story” embeds interactive simulations of Conway’s Game of Life into a text about the same. The simulations are powered by a TypeScript library written specifically
for the story. To readers, it (hopefully) seems as though text and patterns from the Game of Life are interleaved on the page and moving the page causes the patterns to evolve according to the Life update rule.
However, this is an illusion. Or, more properly, two illusions:
- Text and patterns are interleaved on the page
- Patterns evolve according to the Life update rule
Neither of these is strictly true. But the code goes to great lengths to sustain the impression that they are. Here’s why that’s necessary, and how it’s done.
Interleaving
The Game of Life is rendered using an HTML element. The most natural way to layout this
element would be to have the element start at the top of the page and stretch all the way to the bottom, overlaying the entire text. However, large
elements slow down the page. In order to make the page perform well, the
must instead be sized to the viewport (i.e. the screen) rather than the page.
This means that the patterns aren’t actually interleaved with the text. Instead, they sit in a separate layer on top of it. Whenever the page scrolls, the code rerenders each pattern in a slightly updated position on the screen. If the rendering is fast enough, it seems as though patterns and text are interleaved and so that the patterns move in tandem with the text. It seems like the patterns are part of the text.
That’s a big if, though – because in order to make scrolling feel completely smooth and seamless, the page would ideally rerender the pattern at 60 frames per second, which means that all the Life evolution and rendering code must run in 16ms.
When it doesn’t run this fast (and in practice, it never runs quite this fast), the Life patterns seem to slide around on top of the text.
The library’s biggest performance optimization is that it runs the Life update calculations on the GPU. This makes a big difference in performance. However, it’s not perfect. Transferring data from the GPU back to the CPU is expensive – the page spends 1/3rd of its total script execution time just transferring the evolved Life state back from the GPU.
If the Life update rule was the only logic the page needed to run, it would be a relatively simple matter to keep everything on the GPU and avoid ever