# Prototypes that become products · Canvases and tools for thought

<!-- https://learn-ui.com/chapters/canvases/prototypes-to-products -->

### Making stuff with the wrong tools

Steve Ruiz’s [about page](https://steveruiz.me/about) calls him “a developer, designer, and now startup founder in London” with “a background in visual art,” and states this section’s thesis: “I build lots of prototypes. Sometimes those prototypes turn into products.” His open source list (tldraw, “a tiny little drawing app”; globs; perfect-freehand; perfect-arrows; state-designer) reads like a lab notebook.

In 2022 he formed a company around the **infinite canvas** (Steve Ruiz: 'The first is the canvas. You can think of this as a fixed plane of infinite dimensions.' tldraw is his infinite canvas SDK for React.) SDK of the same name; the demo at tldraw.com became an app with close to 500,000 monthly active users.

A 2021 post on [Figma’s Interactive Components](https://steveruiz.me/posts/it-wasnt-made-to-do-that) shows the temperament: “Personally, I’m a big fan of making stuff with the wrong tools.” A Figma prototype once tracked one piece of state, the current Frame, and the limits “do make for some excellent creative constraints.”

### Canvas, camera, screen, viewport

[Creating a Zoom UI](https://steveruiz.me/posts/zoom-ui) (2021) names four things: the canvas, “a fixed plane of infinite dimensions”; the camera, “suspended in front of this plane”; the screen, “where we see what the camera sees”; and the viewport, “the part of the canvas that is visible on the screen,” extending down and right from the camera rather than centring on it. The camera has a point and a zoom, where 1 is 100%, and the two coordinate systems convert in four lines.

> Figure. TypeScriptSteve Ruiz, Creating a Zoom UI

A pan moves the camera point by the delta divided by the zoom, so it feels the same at every magnification. A zoom is toward a canvas point: convert the pointer to canvas space under the old camera and the new one, and shift the camera by the difference so the point under the cursor stays put.

Both arrive as wheel events, the control key marking a zoom (a MacBook trackpad pinch fires a WheelEvent with `ctrlKey: true`). The result is a CSS transform where “the order is: first scale, then translate.”

> Figure. TypeScriptSteve Ruiz, Creating a Zoom UI

### Drag from the origin, not the last event

[Perfect Dragging](https://steveruiz.me/posts/perfect-dragging) says there is a wrong way: adding the pointer’s movement since the last event to the shape’s current position. Instead keep the original position and the drag’s origin, and set position to original plus the delta from that origin. That buys a dead zone, updates while scrolling, restore on cancel, and a delta you can bend for snapping, precision mode, or elastic bounds. “The only way to implement these features is to never rely on the shape’s ‘current’ position.”

> Figure. TypeScriptIllustration

[Dead Zone Dragging](https://steveruiz.me/posts/dead-zone) builds the first item. A **dead zone** (Steve Ruiz: a minimum distance needed before a shape will begin to drag, which prevents accidental drags during clicks.) is “a minimum distance needed before a shape will begin to drag,” a three-state machine: idle, pointing, dragging. In pointing, movement does nothing; once the pointer leaves the zone the shape jumps to origin plus delta, and the pointer should sit where it did relative to the shape at the press. Two or three pixels is enough: a frame or two, still catching slips on touch and stylus.

Source

The incremental approach fails here: the zone would equal a single move, so a slow pointer could stay inside it forever. Steve Ruiz, Dead Zone Dragging (2021).

[Fixing the Drift in Shape Rotations](https://steveruiz.me/posts/rotating-shapes) finds the same kind of invariant. Figma, Excalidraw, and tldraw all pivot around the selection’s average centre, which moves after the first rotation, so rotating back lands elsewhere. The fix holds the centre until a new selection, as Canva appears to do.

The two Reordering posts ([arrays](https://steveruiz.me/posts/reordering), [tables and fractional indexing](https://steveruiz.me/posts/reordering-fractional-indices)) treat Send to Back as array moves, then as a table problem where renumbering one item can rewrite every index: database writes, multiplayer packets, renders. **Fractional indexing** (Steve Ruiz: an index only needs to ensure that, when our items are sorted by their index, the items end up in the right order; the values do not have to be integers or evenly distributed.), credited to Figma’s article, only needs sorting by index to give the right order.

Indices start at 1 because “we can’t cut zero into fractions,” and since a JavaScript number splits 52 times before losing precision, the durable version sorts strings like `a0`, `a1`, `a2a`, after David Greenspan.

> Figure. TypeScriptSteve Ruiz, Reordering Part 2

### The same craft at company scale

For [An exhaustive review of design tool hover areas](https://tldraw.dev/blog/a-review-of-design-tool-hover-areas) (2026), nothing shows the **hover areas** (Steve Ruiz: the regions around a selection where a drag inside the box translates, a drag on the edges resizes along that axis, a drag from the corner resizes along both, and a drag further out rotates.) around a selection, so a Hammerspoon script moved the mouse in a 50x50 grid, logged the cursor, and rendered a PNG.

tldraw’s resize corner is a square slightly offset from the geometry corner; Figma’s edges are bigger and its rotate area an arc around the corner; Excalidraw is “a beacon of rationality” with one size for both; Miro’s corner is smaller than its overlapping edges; Spline’s edges taper; Rive’s arc looks rotated by degrees where radians were meant. If the SDK diffuses to thousands of apps, “perhaps we’ll define the next conventions.”

[Erasing shapes](https://tldraw.dev/blog/erasing) fixes a bug in FigJam and many others: the eraser is a point, fast pointer samples land far apart, so a stroke can cross a shape with no sample near it. Game developers call it tunneling; “an eraser is a bullet you drag around with your hand.”

tldraw tests line segments against each outline via the geometry system (Rectangle2d, Circle2d, Polyline2d, CubicBezier2d), with the margin divided by zoom to stay constant on screen, and nothing deleted until release: “Erasing is a proposal until the moment you let go.”

[Back to Content](https://tldraw.dev/blog/back-to-content) is the opposite: two near-identical lists, not visible shapes and culled shapes (never selected or editing ones), the button on the wrong one, a bug alive more than 20 months. “The bug came from using the wrong concept for the job.”

Two posts are about time. In [Debugging a physical race condition with modifier keys](https://tldraw.dev/blog/adding-delays-to-modifier-keys) (with Ani Krishnan), Shift constrains, Alt duplicates, Ctrl snaps, all changeable mid-drag; releasing key and mouse “at the same time” sometimes sent keyup first and cancelled the effect, “a physical race condition based on the imprecision of our bodies.” A modifier now reads as released only after 150ms.

[Redesigning the laser tool](https://tldraw.dev/blog/redesigning-lasers) replaces a self-consuming polyline with marks that stay until you stop, then fade together, on the scribble system (ephemeral lines above shapes, below cursors). Consumption is modulated so every session takes the same time and eased to start slowly, since linear progress looks like it slows at the end, with a manual cancel because “it’s never nice to get locked into waiting.”

> Figure. TypeScriptSteve Ruiz, Redesigning the laser tool

Each post isolates one interaction, names the quantity that must not change (the pointer’s offset, the pivot, the sort order, the eraser’s size on screen), and builds the code around preserving it. When you cannot see what a competitor did, build the instrument that makes it visible.
