The in-house renderer for BubblyDoo.
🚧
This page is still a work in progress. Please check back later for updates.
The BubblyDoo Renderer is the rendering engine that powers BubblyDoo.
It's quite unique in a few ways, and it has gone through a lot of iterations over the years, and it's one of the most interesting projects I've worked on for BubblyDoo.
It a React-based renderer, built around SVGs and
.
One of the strengths of BubblyDoo's product development is that
It is integrated with a custom Photoshop plugin that allows you to use layer selection and coloring inside of Photoshop.
It supports async rendering of HTML layers, and as such it does double buffering.
There were a lot of challenges, both from technical and a process perspective.
From a technical perspective, here's a small summary:
- async rendering
- done tracking
- live updates without re-rendering
- coloring using LUTs (with WASM and WebGL)
- integration with the in-browser editors
- partial deferring
- compression of images
- compatibility between Photoshop features and SVGs
History
First, this renderer was purely built in vanilla Javascript, without any frameworks.
This worked, but it was primarily based on full rerendering on every update, and partial updating was added as an afterthought.
Simple updates were possible, but big changes to the DOM tree couldn't be handled, which made the library inflexible.
I therefore decided to switch to React as the main technology behind the renderer. React makes it easy to do updates to the tree,
avoiding unnecessary DOM updates.
For SVG images, we parse the SVG into a React tree, and then modify the tree. That means that every image element in a personalized image on our website is represented by a React component, probably wrapped into multiple other React components. Although this causes thousands of React components to render,
React takes care of it without a problem, and the reduced complexity of the code is worth the React overhead.
React enabled us to do multiple things: deferring image loading, partial rendering to canvas, WebGL transformations, updating rendering between sRGB and P3 screens, etc.
A big challenge was keeping track of when layers were done rendering. For example, we need to make sure all images are loaded before we show
the preview to the customer, or before we're able to "capture" the image as a PDF. We preferably need to do this on a partial tree, because
we want to render some parts of the tree to a canvas. And we also need to be able to reset parts of the tree on update, e.g. when there are new
props and we need to rerender one layer of the tree.
This is why I wrote
, allowing us to wait for React components to be "done".
For async operations on the image, double buffering was essential, as it allowed us to do async operations on the images
without creating a flash on the screen when the content was updating, and we avoid tearing between multiple components.
To get around the feColorMatrix
bug (described below), and to get around the low fidelity of feColorMatrix
(after all, the whole color transformation is defined by 20 numbers), we tried using 3D LUTs.
We wrote a script in WebAssembly (through AssemblyScript) to do color transformations in real time.
But even in web workers, the latency was too high.
An additional problem of LUTs was that their file size is quite high. For this we built and on-the-fly LUT compressor in Cloudflare Workers.
But in the end, the latency was to high, so we only enabled LUTs on macOS Chromium and to render the final PDFs we send for printing.
We didn't realize that there was a better solution: using WebGL.
Is this the best approach?
The big question will be: was this a better solution than writing a renderer that just renders everything to a canvas?
The enormous advantage of the DOM approach is that we can add custom components as easily as writing a React component.
It is quite ergonomical to work with, and even async operations (like downloading parts of the rendered image on demand), just work.
We can also easily wrap each React component, and interact with it (this is how our editor works).
The disadvantage is that cross-browser support is sometimes difficult, and that the DOM can be slow and difficult to work with.
The problem of feColorMatrix
on P3 screens in Chromium
Color rendering in the BubblyDoo renderer heavily relies on feColorMatrix.
However, in Chromium browsers, feColorMatrix has
on non-sRGB screens:
the RGB values that Chrome uses to multiply with the color matrix are in the color space of the display, not in sRGB.
This created strange situations where colors were off depending on the screen you were using.
🔧 React🔧 WebGL🔧 Cloudflare Workers🔧 Vue.js🔧 AWS Lambda