
Most canvas-based tools begin in a similar way.
First, one editor appears. Then another. At first, the shared parts seem to be mostly zoom, a minimap, and a few controls. Very quickly, though, it turns out that file persistence, operation history, selection, panels, viewport, overlays, and application integration are similar too.
At that point, code organization stops being a local implementation decision. It becomes a product decision.
Pixxl starts exactly there: with the assumption that canvas applications should be treated as a family of products, not as a set of similar but separate editors.
Here, I am looking at Pixxl as a canvas-product architecture: one shared substrate for several editors, with a neutral file format underneath and product-specific engines above it. The interesting question is whether that shared layer can stay useful without turning into a universal editor by another name.
That distinction sounds abstract until rich text enters the picture.
Drawing paths, rectangles, and simple objects can still fit inside a relatively simple scene model. A document editor is a different problem. It needs commands, transactions, undo history, table semantics, text selection, page layout, import and export, clipboard handling, keyboard input, IME, accessibility, and efficient rendering.
In Pixxl, @pixxl/cantext therefore becomes the stress test for the entire architecture. If the shared canvas layer can carry rich text, it can probably handle simpler products too.
Boundaries Instead of a Mega-Editor
The most interesting decision in Pixxl is not about canvas itself or rich text itself. It is about the boundary of responsibility.
Pixxl does not try to build one mega-editor that pretends to be a document editor, a drawing tool, a graph workspace, and a chart builder at the same time. Instead, it cuts the system into four layers:
- a neutral persistence format,
CanvasDocument - domain engines for specific products
- a shared canvas application shell
- a separate application integration layer
This separation matters more than the similarity of UI components.
@pixxl/canvas acts as the shared substrate. It owns the document shape, migrations, validation, command and runtime primitives, history, renderer registry, workspace math, and shared React shell.
Alongside it are the product packages: @pixxl/cantext, @pixxl/candraw, @pixxl/cangraph, and @pixxl/canchart. Each has its own domain engine. Higher up sits the website application, where panels, local persistence, file downloads, demos, and concrete workflow integration live.
This gives Pixxl a clear system map. At the bottom is a neutral document that describes the canvas, pages, assets, elements, and viewState. Above it are product engines that know what a paragraph, chart, graph node, or vector path actually means. Next to them is the shared UI shell: application layout, minimap, zoom controls, inspector sidebar, and pages panel. At the very top are the local decisions of a specific product.
In practice, this means different applications can look like part of one family without sharing one overloaded domain model.
The File as the Most Durable Contract
The most durable contract here is not a React component. It is the persistence format.
CanvasDocument version 2 is described as a portable file format for canvas products. It contains schema, version, canvas configuration, a list of pages, assets, elements, and view state. Documents are validated, normalized, serialized, and migrated through canvasDocumentCodec. Version 1 inputs can be moved to version 2 by adding pages and assigning pageId to elements.
This is not an infrastructure detail. It signals that the architecture is designed in terms of time, not just the next feature.
The most interesting part is what the shared layer does not do. It does not try to understand the full semantics of rich text, graph layouts, or chart normalization. It validates only the shape shared by all products: a valid document header, reasonable dimensions, page references, JSON-serializable fields, and elements with the required canvas properties.
It does not deeply validate product payloads.
That is a deliberate trade-off. It allows a file to contain unknown element types and still pass through the shared layer. The cost is equally clear: domain errors are detected later, at the boundary of a specific product, by its engine adapters.
The first practical conclusion is simple: if you are building more than one product on canvas, start with the document boundary, not with shared toolbars.
In Pixxl, the document boundary is the right abstraction. Components are only the consequence of an earlier contract: what you save, what you migrate, what you may preserve without understanding, and what must be interpreted only inside the domain engine.
cantext as an Architecture Test
cantext shows what such an engine looks like when the problem becomes genuinely hard.
The canonical editor state is not the DOM, Markdown, or HTML. It is structural JSON called DocumentStructure. It covers document settings, styles, sections, blocks, inline runs, annotations, tables, anchored objects, review suggestions, and other elements of the document model.
HTML and Markdown appear only at the input and output boundaries.
That decision matters. When HTML becomes the source of truth, rendering and editing logic starts to orbit around the DOM. When structure is the source of truth, canvas can remain a rendering surface instead of becoming a prosthetic browser.
That is why cantext is not simply contenteditable on canvas.
The architecture has a clear pipeline. CantextHost wraps an HTML canvas and builds around it an EditorKernel, rendering controller, accessibility adapter, input adapter, transfer service, shortcut manager, and asset store.
EditorKernel becomes the center of the runtime. It owns the document model, selection, command bus, history, layout engine, plugins, search, review, writing checks, and asynchronously derived state.
Editing happens through commands and transactions, not by directly mutating nodes. The document layout then generates snapshots and a RenderScene, and the rendering layer paints them onto the canvas, optionally using an offscreen path when available.
This separation matters. The document model is not the rendering surface. Canvas is not the document model. The DOM is not the source of truth. Each layer has its own responsibility.
RenderScene as an Explicit Contract
One of the strongest technical decisions is treating RenderScene as an explicit contract.
The document layout does not render directly from the model. Instead, it emits a simple description of what should be drawn: pages, text runs, table cells, images, selection rectangles, overlays, and the caret.
This kind of decision is often invisible in demos, but it decides the future of the system.
An explicit rendering contract makes testing easier, allows work to move between the main thread and a worker, and forces the team to describe every visual feature precisely. It also has a cost: nothing appears on its own. Every feature must be expressed in the rendering scene.
That is a good cost if the system is expected to grow.
Without such a boundary, the renderer can easily start reading the domain model directly. Then layout, rendering, selection, export, and optimizations begin to depend on the same structures. At first, this is faster. Later, every change requires negotiation with the entire system.
RenderScene enforces the opposite dynamic. First, the system has to say what actually needs to be drawn. Only then can it decide where and how to draw it.
Optimization Must Not Decide Correctness
Around the rendering contract, Pixxl builds a second important theme: asynchronously derived state should be an optimization, not a condition for correctness.
DerivedRuntimeCoordinator can schedule layout, export, and part of the rendering work in a worker, but it can also fall back to the main thread. The coordinator cancels stale requests, ignores late responses, and degrades to main-thread work if the worker fails to start or fails during rendering or export.
That sounds ordinary, but it is a sign of maturity.
A performance system should not become the system that correctness depends on. Workers, offscreen rendering, and asynchronous pipelines are useful only when an optimization failure does not become a product failure.
There is also an important limitation: the current implementation still renders scenes containing images on the main thread. That is a useful reminder that the rendering architecture is evolutionary, not complete. The project leaves room to move more work off the main thread, but it does not pretend that everything has already been solved.
Shared UI Without Taking Over the Product
The third key decision concerns UI and viewport.
The shared chrome in @pixxl/canvas/react is intentionally boring: stable slots, controlled state, accessibility labels, and shared CSS. The most important word is controlled.
CanvasWorkspace does not hide pan and zoom inside a global singleton. It receives transform, emits onTransformChange, handles bounds, fit targets, viewport callbacks, and overlay slots.
That is exactly why it can be truly shared.
A reusable component becomes useful only when it does not try to take control of the product it hosts. A shared shell should provide structure, not impose semantics.
This architecture does not confuse visual similarity with domain similarity. A pages panel can be shared between a document editor and a diagramming tool. That does not mean both systems should use the same document model. A minimap can be shared. That does not mean the canvas layer should understand table semantics or graph edge routing.
Pixxl consistently refuses to build a shared layer that is too clever. That refusal is correct.
What This Architecture Enables
As a result, something important becomes possible at the file level.
A product-neutral CanvasDocument can preserve unknown or inactive elements, and a rich text document can coexist with charts, images, connectors, and other canvas elements.
The solution is elegant, but not magical.
Preserving foreign elements works at the boundary of a full CanvasDocument. If someone converts only from DocumentStructure back into a new canvas document, they get a fresh document containing only the rich text layer.
In other words: portability and preservation of foreign entities do not come from the adapter itself. They come from choosing the right persistence boundary.
The biggest strength of this proposal is therefore not cantext itself, but the fact that rich text was not treated as an exception.
In many codebases, text documents end up as their own world: with their own persistence, their own shell, and their own way of measuring and rendering. Here, rich text is included in the family of canvas applications without pretending that domain differences do not exist.
The file remains shared. The shell remains shared. The shared runtime ends where product semantics begin.
That is an architectural thesis, not an implementation detail.
The Boundaries of the Current Description
This does not mean the materials describe a complete system in every sense.
They are strong where module boundaries, the document model, the rendering pipeline, and host integration are concerned. They are weaker where real operational problems usually begin: multi-user collaboration, backend synchronization, access control, edit conflicts, asset lifecycle, and production telemetry.
Even within rich text itself, one important correction appears. writing issues may look like part of the canonical state, but the safer model is different: review suggestions belong to the document structure, while writing issues function as runtime state.
That is not a small detail. It is the difference between what must be saved and migrated, and what can be recomputed.
A smaller issue concerns generated application recipes: if those snippets drift from the current CanvasWorkspace prop contracts, they should be treated as examples, not as the foundation for the article's main thesis.
These gaps do not undermine the main thesis. They simply narrow it to the client layer, local persistence, and runtime integration.
The Lesson for Builders
The best lesson from Pixxl is simple and unglamorous.
If a product has more than one canvas mode, do not start with shared components. Start with four questions:
- what is the neutral file format
- what belongs to the domain engine
- what can be a shared shell without taking over product semantics
- where the platform ends and the workflow of a specific application begins
Only after those boundaries are set is it worth building recipe generators, preview grids, tool rails, and the rest of the visible reusable elements.
This approach is less spectacular than the vision of a universal editor for everything. It is also more durable.
The shared layer should maintain the contracts that truly have to be shared: file, migrations, validation, viewport, history, registries, runtime, and shell. The product engine should retain responsibility for the semantics of its domain. And the application should have room for its own workflow without pushing it down the stack.
Conclusion
Pixxl is interesting not because it promises a universal editor for everything.
On the contrary: it is interesting because it rejects that idea and proposes a more sober architecture. Shared substrate. Separate engines. Controlled chrome. Local workflows.
cantext makes this decision concrete. Rich text forces transactions, history, layout, input, accessibility, export, rendering, and asynchronously derived state. If the architecture passes that test, it is not because it is flashy. It passes because it guards its boundaries.
In a software world full of “platforms” that become monoliths under new names after two quarters, this kind of restraint does not look like a lack of ambition.
It looks like a more mature form of ambition.