Hello, I am the smiling snail, 🐌.

In the previous section we looked at how to determine the node layout information and output the layout tree. Today, I will introduce the last part, drawing. Not much content, relatively easy to understand.

Attach the previous several sections of the link, want to know about the children’s boots can have a look.

  • I heard you wanted to write a rendering engine. – Preface
  • I heard you wanted to write a rendering engine – HTML parsing
  • I heard you wanted to write a rendering engine – CSS parsing
  • I heard you wanted to write a rendering engine – style tree
  • I heard you wanted to write a rendering engine. – Layout tree

The whole process

The whole process is as follows:

  • Generate a list of draw commands from the layout tree
  • Raster, generate pixel information
  • Convert pixels into pictures
  • Show to screen

What is a draw command? It describes how you should draw a graph, such as position size, color, shape and so on.

Here, we only deal with the background color and border drawing, not the text. So only need to support rectangle drawing, know rectangle position size, color is good.

The data structure

Draw command, defined as an enumeration type. For the time being, only rectangles, associated colors and regions are supported.

// Draw command, currently only supports color draw
enum DisplayCommand {
    case SolidColor(Color, Rect)
}
Copy the code

Canvas, used to hold pixel information.

/ / the canvas
struct Canvas {
    var width: Int
    var height: Int
    
    // Pixel point, arGB
    var pixels: [Color]
}
Copy the code

Generate draw command

background

Background draw commands are easier to generate. Take the background color from the node stylesheet and calculate the background area.

The background area includes “content area + inside margin + border”.

The code is as follows:

// Draw the background
func renderBackground(list: inout [DisplayCommand], layoutBox: LayoutBox) {
    // Get the background color
    if let color = getColor(layoutBox: layoutBox, name: "background") {
        
        // Background includes padding + border + content
        let displayCommand = DisplayCommand.SolidColor(color, layoutBox.dimensions.borderBox())
        
        list.append(displayCommand)
    }
}
Copy the code

A border

Again, first take the border color border-color from the stylesheet.

In addition, the frame is divided into four rectangular areas, which need to be calculated separately. As follows:

The draw command is generated in much the same way as the background color, but note the calculation of the rectangle area.

Master drawing list

Recursively traverses the layout tree, adds the drawing command of each node to the array, then obtains the total drawing command list.

// Generate an overall drawing list
func buildDisplayList(layoutRoot: LayoutBox) -> [DisplayCommand] {
    var list: [DisplayCommand] = []
    
    renderLayoutBox(list: &list, layoutBox: layoutRoot)
    
    return list
}

func renderLayoutBox(list: inout [DisplayCommand], layoutBox: LayoutBox) {
    // Draw the background
    renderBackground(list: &list, layoutBox: layoutBox)
    
    // Draw the border
    renderBorder(list: &list, layoutBox: layoutBox)

    // Iterate over child nodes to generate commands recursively
    for child in layoutBox.children {
        renderLayoutBox(list: &list, layoutBox: child)
    }
}
Copy the code

rasterizer

Convert draw commands to pixel information.

The draw command contains colors and regions, and writes the color value of each point in the region to an array of pixels.

// Generate pixels
mutating func genPixel(color: Color, rect: Rect) {
    // Fill the points in the recT range with color and cannot exceed the canvas size
		// clamp is mainly used to restrict scope
    let x0 = Int(rect.x.clamp(min: 0.max: Float(self.width)))
    let y0 = Int(rect.y.clamp(min: 0.max: Float(self.height)))

    let x1 = Int((rect.x + rect.width).clamp(min: 0.max: Float(self.width)))
    let y1 = Int((rect.y + rect.height).clamp(min: 0.max: Float(self.height)))
    
    // Walk through all the points, horizontal row by row
    for y iny0... y1 {for x inx0... x1 {let index = y * width + x
            pixels[index] = color
        }
    }
}
Copy the code

Generate images

Once you have the pixel information array, you use the CoreGraphics API to generate the image. Note that the order of color is arGB.

func imageFromARGB32Bitmap(pixels: [Color], width: Int, height: Int) -> UIImage? {
        guard width > 0 && height > 0 else { return nil }
        guard pixels.count == width * height else { return nil }

        let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
        let bitsPerComponent = 8
        let bitsPerPixel = 32

        var data = pixels // Copy to mutable []
        guard let providerRef = CGDataProvider(data: NSData(bytes: &data,
                                length: data.count * MemoryLayout<PixelData>.size)
            )
            else { return nil }

        guard let cgim = CGImage(
            width: width,
            height: height,
            bitsPerComponent: bitsPerComponent,
            bitsPerPixel: bitsPerPixel,
            bytesPerRow: width * MemoryLayout<PixelData>.size,
            space: rgbColorSpace,
            bitmapInfo: bitmapInfo,
            provider: providerRef,
            decode: nil,
            shouldInterpolate: true.intent: .defaultIntent
            )
            else { return nil }

        return UIImage(cgImage: cgim)
    }
Copy the code

According to

This step just creates an ImageView to display the image.

The test data

The test data is in the project directory under Example, test.html and test.css.

The effect is shown below:

The full code can be viewed here: github.com/silan-liu/t… .

conclusion

This section mainly introduces the operations related to drawing, focusing on the process of generating a drawing list of layout information, rasterizing it, and converting it into pixels.

At this point, “I heard you want to write a rendering engine” series has been completed, thank you for reading ~