The infinite-zoom engine
packages/engine is the heart of the product. It is a zero-dependency
TypeScript module, framework-agnostic, with unit tests covering the
precision-critical math.
The problem
A naive “world coordinates + camera scale” approach collapses when the zoom range exceeds float64 precision: after ~2⁵⁰ of zoom, adjacent points become indistinguishable and strokes jitter or corrupt.
The solution: a cell tree (“deep zoom” portals)
The world is a quadtree of coordinate frames:
- The root cell covers the whole world; its local space is
[-1, 1]². - Each cell may have 4 children. A child’s local space is also
[-1, 1]²and maps into its parent asparent = offset(quadrant) + 0.5 · local. - Cell ids are paths of quadrant digits (
"013"= NW → NE → SE).
Consequences:
- Every coordinate is bounded. A stroke always lives inside roughly
[-1.5, 1.5]of its cell — at any depth — so float64 precision is constant regardless of how deep you zoom. - Composition is exact. Each level is a power-of-two scale plus a small offset; composing N levels multiplies by exact powers of two, which float64 represents without error. 512 levels ≈ 2⁵¹² zoom range.
- The camera keeps a focus cell plus a local view (pan, scale kept in
[0.5, 2), rotation). Zooming in descends into the child containing the viewport center; zooming out ascends. All per-frame math stays bounded.
Rendering
Strokes are vector, but rendering is raster-cached per (cell, layer):
- Each (cell, layer) with content has a small offscreen canvas (512 px across the cell’s extent) that replays its elements.
- The visible frame composites only the tiles intersecting the viewport — O(screen), not O(document) — so canvases with millions of strokes pan at 60+ fps and zoom-in never upscales a cache (the camera descends cells before upscaling would be visible).
- A bounded LRU evicts stale tiles; evicted tiles simply re-render on demand.
- The in-flight stroke is drawn vector-straight to the screen each frame (live preview), then committed into its tile.
- Eraser strokes are elements too: they replay with
destination-outinside the tile, so vector erasing, undo, and per-layer erasing all work.
Element model
| Element | Description |
|---|---|
stroke | polyline with per-point pressure; rendered as a variable-width ribbon outline (left/right offset curves with round caps) |
shape | rect / ellipse with stroke style |
fill | solid polygon from the fill (paint-bucket) tool; drawn under strokes/shapes |
text | vector text with font size (in cell units) |
image | embedded raster (≤ 3 MB), positioned in cell space |
bookmark | a saved (cell, point) for navigation |
Layers are world-level; each element belongs to a layer, and tiles are keyed by (cell, layer), which is what makes per-layer blend modes cheap.
Exports share one geometry path
The same ribbon outline builder feeds the canvas tiles, the SVG exporter
(strokes become filled paths), and PNG. SVG export maps the cell tree onto
nested <g transform="translate(…) scale(0.5)"> groups — a literal deep-zoom
SVG — with eraser strokes expressed as masks, so exports match the screen
exactly.
Tests
packages/engine/src/__tests__/ covers cell-tree transforms (including
60-level round-trips with < 1e-12 error), camera zoom/pan invariants
(anchor stays fixed across depth transitions), serialization, history, and
stroke geometry.