05 Nov 2014
I'm returning at last to my series on building a simple HTML rendering engine:
In this article, I will add very basic painting code. This code takes the tree of boxes from the layout module and turns them into an array of pixels. This process is also known as "rasterization."
Browsers usually implement rasterization with the help of graphics APIs and libraries like Skia, Cairo, Direct2D, and so on. These APIs provide functions for painting polygons, lines, curves, gradients, and text. For now, I'm going to write my own rasterizer that can only paint one thing: rectangles.
Eventually I want to implement text rendering. At that point, I may throw away this toy painting code and switch to a "real" 2D graphics library. But for now, rectangles are sufficient to turn the output of my block layout algorithm into pictures.
Since my last post, I've made some small changes to the code from previous articles. These includes some minor refactoring, and some updates to keep the code compatible with the latest Rust nightly builds. None of these changes are vital to understanding the code, but if you're curious, check the commit history.
Before painting, we will walk through the layout tree and build a display list. This is a list of graphics operations like "draw a circle" or "draw a string of text." Or in our case, just "draw a rectangle."
Why put commands into a display list, rather than execute them immediately? The display list is useful for a several reasons. You can search it for items that will be completely covered up by later operations, and remove them to eliminate wasted painting. You can modify and re-use the display list in cases where you know only certain items have changed. And you can use the same display list to generate different types of output: for example, pixels for displaying on a screen, or vector graphics for sending to a printer.
Robinson's display list is a vector of DisplayCommands. For now there is only one type of DisplayCommand, a solid-color rectangle:
To build the display list, we walk through the layout tree and generate a series of commands for each box. First we draw the box's background, then we draw its borders and content on top of the background.
By default, HTML elements are stacked in the order they appear: If two elements overlap, the later one is drawn on top of the earlier one. This is reflected in our display list, which will draw the elements in the same order they appear in the DOM tree. If this code supported the z-index property, then individual elements would be able to override this stacking order, and we'd need to sort the display list accordingly.
The background is easy. It's just a solid rectangle. If no background color is specified, then the background is transparent and we don't need to generate a display command.
The borders are similar, but instead of a single rectangle we draw four—one for each edge of the box.
Next the rendering function will draw each of the box's children, until the entire layout tree has been translated into display commands.
Now that we've built the display list, we need to turn it into pixels by executing each DisplayCommand. We'll store the pixels in a Canvas:
To paint a rectangle on the canvas, we just loop through its rows and columns, using a helper method to make sure we don't go outside the bounds of our canvas.
Note that this code only works with opaque colors. If we added transparency
(by reading the
opacity property, or adding support for
in the CSS parser) then it would need to blend each new pixel with
whatever it's drawn on top of.
Now we can put everything together in the
paint function, which builds a
display list and then rasterizes it to a canvas:
At last, we've reached the end of our rendering pipeline. In under 1000 lines of code, robinson can now parse this HTML file:
…and this CSS file:
…to produce this:
If you're playing along at home, here are some things you might want to try:
Write an alternate painting function that takes a display list and produces vector output (for example, an SVG file) instead of a raster image.
Add support for opacity and alpha blending.
Write a function to optimize the display list by culling items that are completely outside of the canvas bounds.
If you're familiar with OpenGL, write a hardware-accelerated painting function that uses GL shaders to draw the rectangles.
Now that we've got basic functionality for each stage in our rendering pipeline, it's time to go back and fill in some of the missing features—in particular, inline layout and text rendering. Future articles may also add additional stages, like networking and scripting.
I'm going to give a short "Let's build a browser engine!" talk at this month's Bay Area Rust Meetup. The meetup is at 7pm tomorrow (Thursday, November 6) at Mozilla's San Francisco office, and it will also feature talks on Servo by my fellow Servo developers. Video of the talks will be streamed live on Air Mozilla, and recordings will be published there later.