Explain · Canvases and tools for thought
Prototypes that become products

Prototypes that become products

Steve Ruiz describes himself on his about page as “a developer, designer, and now startup founder in London” with “a background in visual art” who works “primarily in creative tools for the web.” The next two sentences are the thesis of this section: “I build lots of prototypes. Sometimes those prototypes turn into products.” Before tldraw he worked with Play on a mobile-first design application for iOS, and before that he made educational content for Framer. His open source list reads like a lab notebook: tldraw (“a tiny little drawing app”), globs, perfect-freehand, perfect-arrows (which, he notes, “made for some fun tweet threads”), and state-designer, a state chart library with its own visual environment. In 2022 he formed a company around the SDK of the same name. Developers use it, in his words, “to accelerate development of whiteboards and other canvas-like applications,” and the project’s demo at tldraw.com became an end-user app in its own right with close to 500,000 monthly active users.

The temperament behind that path shows in a 2021 post about Figma’s Interactive Components: “Personally, I’m a big fan of making stuff with the wrong tools.” He explains why Figma prototypes struggled with anything complex (before Interactive Components the prototype kept track of one piece of state, the user’s current Frame, so every state combination needed its own Frame) and then says the limitations “do make for some excellent creative constraints.” A prototype is a question asked in the cheapest available material. What follows is what happens when the questions get answered carefully enough, over enough years, that the answers become a product. Each of the posts below is one such answer.

Start with the camera. In Creating a Zoom UI (2021), Steve 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.” The camera has a point and a zoom, and the viewport extends down and right from it rather than centring on it. The two coordinate systems convert with four lines: screen to canvas is x / z - cx, canvas to screen is (x + cx) * z, and a zoom of 1 is 100%. A pan adjusts the camera point by the delta divided by the zoom, so it feels the same at every magnification. A zoom must be toward a canvas point: compute the new zoom, convert the pointer’s screen point to canvas space under the old camera and again under the new one, and shift the camera by the difference so the point under the cursor stays put. In the browser both gestures arrive as wheel events, with the control key marking a zoom (a MacBook trackpad pinch fires a WheelEvent with ctrlKey: true), and the result is applied as a CSS transform where “the order is: first scale, then translate.”

Then dragging. Perfect Dragging argues that there is a wrong way: many apps add the pointer’s movement since the last event to the shape’s current position. The better way is to keep the shape’s original position and the point where the drag began, and on every move set the position to original plus the delta from that origin. The payoff is a list: a dead zone against accidental drags, updating position while scrolling mid-drag, restoring position if the user cancels, and freely adjusting the delta for snapping, precision mode, or elastic bounds. “The only way to implement these features is to never rely on the shape’s ‘current’ position.” The next day’s post, Dead Zone Dragging, builds the first item: a is “a minimum distance needed before a shape will begin to drag,” implemented as a three-state machine (idle, pointing, dragging). In pointing, pointer movement does nothing; once the pointer leaves the dead zone, the machine moves to dragging and the shape jumps to origin plus delta. The correctness test is that the pointer ends up in the same place relative to the shape as when the press began. In a real app, he says, two or three pixels is enough: it lasts a frame or two and still catches the accidental moves common on touch and stylus.

Two smaller mechanisms show the same habit of finding the invariant. In Fixing the Drift in Shape Rotations, he notices that in Figma, Excalidraw, and his own tldraw, rotating a few shapes and rotating them back leaves them somewhere else. Every app pivots around the average centre of the selection, and after the first rotation that average has moved, so the second rotation pivots around a different point. The fix: hold the centre once a rotation starts, reuse it for later rotations, and give it up only when the user makes a new selection (he notes Canva appears to do the same). In the two Reordering posts (arrays, tables and fractional indexing) he works through Send to Back and friends first as array moves, then as a problem for tables of objects where renumbering one item might mean rewriting every index, which costs database writes, larger multiplayer packets, and renders. , which he credits to Figma’s article, only requires that sorting by index gives the right order, so you can put an item between two others: getIndicesBetween(2, 3, 3) yields 2.25, 2.5, 2.75. Indices start at 1 because “we can’t cut zero into fractions,” and since a JavaScript number can be split 52 times before it loses precision, the durable version sorts strings like a0, a1, a2a, after David Greenspan’s method.

The tldraw.dev posts are the same craft at company scale. For An exhaustive review of design tool hover areas (2026), Steve wanted to compare the invisible regions around a selection box where a drag translates, resizes along one axis, resizes along two, or rotates. Since nothing shows those , he had a Hammerspoon script move the mouse in a 50x50 grid, record the cursor at every position, and render a PNG. The survey is merciless and specific: tldraw’s resize corner is a square slightly offset from the geometry corner, with a rotate square whose inside corner sits at that same corner; Figma’s edges are bigger and its rotate area is an arc that wraps the corner; Excalidraw is “a beacon of rationality” using one size for edges and corners; Miro’s corner is smaller than its edges, which overlap; Spline’s edges taper; and Rive’s rotation arc looks rotated by degrees where radians were meant. His closing thought is about conventions: if the SDK diffuses its designs to thousands of apps, “perhaps we’ll define the next conventions.”

Erasing shapes fixes a bug he finds in FigJam and many other apps: the eraser is a point, and because pointer input arrives as discrete samples that land far apart when you move fast, a quick stroke can cross a shape without any sample landing near it. Game developers call it tunneling; in tldraw “an eraser is a bullet you drag around with your hand.” The fix is to treat input as line segments and test each segment against every shape’s outline through the geometry system (Rectangle2d, Circle2d, Polyline2d, CubicBezier2d) that also powers selection, snapping, and arrow binding. The hit margin is divided by the zoom level so it stays the same size on screen, a rotated rectangle is tested inside its own transform, and a filled shape returns a negative distance that always passes. Hits accumulate and render semi-transparent; nothing is deleted until release. “Erasing is a proposal until the moment you let go.” Back to Content is a post-mortem of the opposite kind. The button is supposed to appear when you pan to an empty part of the canvas, and tldraw keeps two nearly identical lists: not visible shapes, whose bounding boxes miss the viewport, and culled shapes, which it does not bother to render, except that selected and editing shapes are never culled. The button leaned on the wrong list, and the bug survived more than 20 months. “Rendering optimization and selection UX are different concerns. 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 (with Ani Krishnan), Shift constrains a drag, Alt duplicates, Ctrl snaps, and any of them can be added or removed mid-interaction. Users releasing key and mouse “at the same time” sometimes sent the keyup first, cancelling the effect before the mouseup ended the drag: “a physical race condition based on the imprecision of our bodies.” The fix marks a modifier released only after a delay, long enough to cover the gap and short enough not to feel sticky; they chose 150ms, joining the timeouts tldraw already uses to tell single clicks from doubles and two-finger pinches from multi-selects. “One more little detail in tldraw that cheats reality to make the application feel better than it should.” In Redesigning the laser tool, the original laser was a self-consuming polyline modelled on a real pointer; the redesign follows his earlier telestrator app and an early Slack annotation tool, where marks stay until you stop, then fade together. It runs on the scribble system (ephemeral lines drawn above shapes, below cursors) with an idle timeout of 1200ms and a grouped, ease-in fade, and the consumption rate is modulated so every session completes in the same time and eased so it starts slowly, because linear progress looks like it slows at the end. There is also a manual cancel, since “it’s never nice to get locked into waiting.”

What to take from this is less any single formula than the shape of the work. Each post isolates one interaction, names the quantity that should not change (the pointer’s offset from the shape, the rotation pivot, the sort order, the size of the eraser on screen), and builds the code around preserving it. Write your own dragging from the origin, not from the last event. Keep a pivot until the selection changes. When a bug hides for months, ask whether two concepts were conflated. And when you cannot see what a competitor did, build the instrument that makes it visible.