Skip to main content
konva·February 28, 2026·6 min read

Building the export canvas from scratch with Konva

Outsourcing a canvas sounded like a shortcut. The vendor pricing turned it into a tax. Building it with Konva was cheaper and better.

J
Jean P.Founder

The first version of the export canvas was not built. It was rented.

When the campaign export feature first shipped, I had outsourced the canvas to a hosted editor library. It got me to a working multi-shape editor faster than building one myself. That was the right call for the moment, because the alternative was spending weeks on canvas plumbing for a feature I was not yet sure people would use.

The cost of that decision did not show up as engineering pain. It showed up on the invoice. The library's licensing model meant that every step toward shipping the export canvas as a real product was a step toward a meaningful recurring expense, and the math stopped working long before the feature was finished. I had not made the canvas. I had leased it. And the lease was not getting cheaper.

So I gave up on outsourcing it.

What I needed was a 2D canvas

The export canvas's job is straightforward to describe and not to build.

The user composes a document, mostly text and stat blocks and assets, on a 2D surface. They drag elements around. They resize them. They snap them to columns. They group them into pages. They export the result as a PDF or an image. There is no perspective, no animation, no shaders, no physics. The whole thing lives in two dimensions and rarely needs to redraw the entire scene at sixty frames per second.

That description is also a description of what the web's 2D Canvas API does. Pixels in, pixels out, immediate-mode rendering, no GPU magic required.

The reason this matters is that a lot of the canvas libraries on the market solve a much harder problem than the one I had. WebGL-backed renderers like Pixi.js are excellent for scenes with thousands of moving objects, real-time game loops, or shader-based effects. The platform's VTT engine uses Pixi for exactly that reason: a virtual tabletop with dynamic lighting and many tokens needs a GPU. The export canvas does not. Reaching for WebGL when the actual problem is "lay out twenty rectangles on a page" is choosing a more complex tool than the work requires.

I evaluated most of the obvious alternatives. Fabric.js, Paper.js, Excalidraw, react-flow, the native Canvas API, the same hosted library I had been using but at the next pricing tier. Each one solved some part of the problem. None of them fit the shape of the problem cleanly enough to be obviously right.

Konva was the one that fit.

What Konva is, and why it works for this

Konva is a 2D scene graph for the web. It is not an editor. It is not a whiteboard. It does not come with opinions about how shapes should behave or how users should interact with them. What it gives you is a tree of nodes, a set of geometric primitives, a Transformer component for resize and rotate handles, and good React bindings. Everything else, you build.

That sounded like the wrong tradeoff at first. I had been outsourcing precisely so I would not have to build the editor's behavior. But the more I worked through what the export canvas actually needed, the more I realized the editor's behavior was the part I most wanted control over. The shape system is bespoke (a stat block is not a rectangle, it is a structured document fragment). The selection logic is bespoke (selecting a stat block should select the whole composite, not just the body text). The snapping is bespoke (column-based, not grid-based, with margin guides). The export pipeline is bespoke (Konva to PDF goes through a different path than Konva to PNG).

A library that handed me an editor would have handed me an editor's worth of behavior to fight against. A library that handed me a scene graph let me write the editor I actually needed.

The hard part is the canvas, not the rendering

What the migration taught me, and what nothing about the previous version had made clear, is that building a 2D canvas application is mostly not about rendering.

Rendering is solved. Konva renders. The browser renders. There is no novel performance work to do for a document with a hundred elements on it. The hard parts are everywhere else.

Hit-testing is hard. When the user clicks at coordinates (x, y), which element should select? The top one in the z-order, unless the click landed in the transparent margin of a shape, in which case it should fall through. Unless the user is in a specific tool that wants the click anyway. Unless the element is a composite, in which case the click selects the parent.

Selection is hard. A single selection is fine. A multi-selection that drags as a unit, transforms as a unit, and respects the constraints of the elements inside it, is a different category of work. The Transformer component handles a lot of this, but the rules for what a multi-selection means are application-level.

Snapping is hard. Column snap means the element wants to align to a column edge while it is being dragged, but only when the drag has not committed yet, and the snap should preview before it commits, and the preview should not flicker, and the column edges depend on the page the element is currently over.

Coordinate spaces are hard. The canvas has a viewport that pans and zooms. The page has its own coordinate system. The element has its own local coordinates. Doing any operation that crosses these spaces (mouse position to page position, page position to element position) is a transform multiplication that has to be right every time.

None of this is interesting individually. All of it has to work for the canvas to feel like a real tool.

What the migration shipped

A 2D canvas built on Konva with bespoke shape components, custom hit-testing, multi-selection that respects composite elements, column snapping with preview, and an export pipeline that produces PDFs and images from the same scene graph. A licensing line item that went to zero. A codebase that I now own end to end, including the parts that are hard.

The bigger lesson is one I keep relearning. Outsourcing infrastructure is sometimes the right call, especially early. But there is a moment in a product's life where the thing you outsourced becomes the thing you need most control over, and the cost of holding the lease becomes higher than the cost of building it. Knowing when that moment arrives is most of the skill.

For the export canvas, that moment arrived a few weeks before I noticed it. The migration was the catch-up.

SHARE
~ 6 min read · 1,112 words
Discord

Discuss this post

Join the D3 Designs Discord to share thoughts and follow along.

Join Discord