This is the beginning of a new project called "Taffy Canvas". It is to be written in Rust, using Skia: https://github.com/rust-skia/rust-skia and Taffy as a layout engine: https://github.com/dioxusLabs/taffy
The goal is simple: allow rendering powerful, complex images server-side for message-based games like those on Discord, or open graph images. Having powerful layout capabilities allows focusing more on design than fighting layout, so we want this to be declarative. For simplicity, this should be a simple XML in, image out library.
It will consist of two parts: a rust API/crate, and a wrapper which exposes it to JS/Node.js using napi-rs
It must run with or without GPU (macOS laptops, linux VPSes)
XML will be fairly simple: view, text, image nodes. and support for all CSS layouts that taffy supports (and also: absolute and fixed positions) + styles that are trivial to implement (borders, backgrounds, corner radiuses, etc). Text measurement should be integrated using Skia itself.
Look into https://github.com/chenglou/pretext this library for inspiration on how to implement flawless image in-line with text layouts.
You must follow test-driven development. Make sure everything is throughly tested including edge cases. What this library renders should be exactly what the layout/XML describes.
For what exactly we're trying to render, look at /Users/dj/Developer/PokeOne/src/engine/hud/ It will have many different kind of images to render.
Essentially, this library should be able to provide an API surface that can:
What's most important is you must benchmark this. Implement all sorts of performance tricks to make rendering as fast as possible, reuse stuff, etc.
I’m treating this as a greenfield scaffold with hard constraints: Rust core, rust-skia, taffy, napi-rs, CPU/GPU-capable rendering, XML-in/image-out, and TDD with benchmarks from the start. I’m going to inspect the repo state, then I’ll lock down an initial architecture and implement the first slice with tests before expanding.
The workspace is effectively empty and not initialized as a git repo yet, so I’m starting from first principles rather than adapting existing code. Next I’m checking the directory contents and then I’ll scaffold a core workspace with tests and a benchmark harness before wiring rendering.
I have a clean slate and the toolchain is current. I’m verifying the current upstream APIs for rust-skia, taffy, and napi-rs before I commit to crate boundaries and feature flags, because those surfaces do change and I don’t want to scaffold against stale assumptions.
I have enough upstream context to pin the first design: CPU raster path first with a render abstraction that can later host GPU surfaces, Taffy for block layout, and a separate prepared-template hot path modeled after Pretext’s “prepare once, layout cheaply” split. I’m now inspecting the PokeOne HUD code to shape the XML schema and the first render cases around real usage rather than toy examples.
I’ve got the usage shape: lots of image-heavy HUDs, precise text sizing, truncation, badges, layered panels, and sprite reuse. I’m now pinning crate versions and then I’ll write the first failing tests around XML parsing, layout, templating, and deterministic raster output so the implementation grows under coverage rather than by scaffolding alone.
The dependency surface looks workable: skia-safe 0.93.1 with binary-cache and textlayout, taffy 0.10.0, and napi 3.8.x. I’m moving into scaffold mode now: workspace layout, benchmark target, and the first red tests for parser/layout/render/template caching.
Plan for the first implementation slice:
taffy-canvas-core and taffy-canvas-node.core, define the document model, XML parser, style system, template compiler, asset/text measurement traits, and a CPU raster renderer using Skia.node, expose sync and async render/template APIs via napi-rs, with a thread-pool-backed renderer handle.I’m going to scaffold the workspace and write the first failing tests now.
I’m creating the workspace skeleton now, then I’ll immediately replace the defaults with test targets so the implementation starts red.
I’ve got the minimal viable slice defined tightly enough to code against: XML view/text/image, CSS-like layout props, template placeholders, CPU raster via Skia, and an async pool surface. I’m editing the workspace manifests and adding the first failing tests now.
Tip for XML parsing: specialize it for our use-case to squeeze performance out of it.
I’m taking that as a requirement, not just an optimization note. I’ll keep the XML parser purpose-built for this schema instead of building a generic DOM parser, so we can avoid excess allocation and parse directly into our node arena/template segments.
And also initialize git repo as needed. Keep committing incrementally as you work
I’m incorporating both new constraints: the XML parser will stay specialized and I’ll keep the repo history incremental rather than dumping one large commit at the end. I’m initializing git now, then I’ll land the red-test scaffold as the first commit before moving green.
I’m laying down the API and test targets before filling in behavior. The first test set is focused on the hot path you care about: compile once, substitute params, compute layout with absolute positioning, render deterministic pixels, and fan out work through a pool.
The red suite is in place. I’m running it now to surface the real compile and behavior gaps, then I’ll close them in the core crate before the first commit.
The dependency build is still chewing through rust-skia, but I can already clean a few obvious compile issues in parallel so the next test run is signal rather than noise.
The core implementation is now in place for the first slice: specialized template parsing, param substitution, style parsing, Taffy layout, Skia raster drawing, and pooled renders. I’m running the suite again with a longer timeout because rust-skia’s first build is the slow part.
The first post-implementation run is useful now: this is down to normal Rust API mismatches, not design gaps. I’m fixing the import/export issues and rerunning immediately.
The first green slice is stable now: core tests pass end to end with compile, instantiate, layout, render, and pooled rendering. I’m going to commit this checkpoint, then I’ll extend the Node wrapper and add a usable benchmark run on top of it.
cargo new created nested git repos inside each crate, which blocks staging from the workspace root. I’m removing those nested .git directories because they’re just scaffold artifacts I created, then I’ll retry the commit from the actual project root.
The core checkpoint is committed. I’m extending the Node wrapper now with actual render APIs instead of the placeholder version() export, and I’m also cleaning the small core warnings while I’m in there so the workspace is quieter to build.
let's add github CI for building & testing too early on, and don't forget to add cache so runs can be shorter
I’m adding CI now, but I want it to exercise the actual workspace instead of just looking good in YAML. I’m first validating the current workspace build locally, then I’ll wire a cross-platform GitHub Actions workflow with Rust cache plus the Skia/Cargo artifacts cached so cold starts don’t stay cold.
The CI request exposed a real gap in the Node layer, which is good to catch now instead of in Actions: the wrapper was still missing its tokio runtime dep and JSON parameter conversion feature flags. I’m fixing that locally first so the workflow I add will actually go green.
The workspace is building and testing locally now, including the Node crate. I’m doing one formatting pass before I pin CI to those same commands, otherwise the first workflow run is likely to fail on something avoidable.
I’m wiring CI to the exact commands that are already green locally: format check, all-targets compile to catch benches/NAPI drift, and full workspace tests. I’m keeping the matrix to Linux and macOS from the start because Skia/font behavior is the kind of thing that drifts by platform if you only test one side.