大家好,我是微微笑的蝸牛,??。
上一節我們講了如何確定節點布局信息,輸出了布局樹。今天,將介紹最后一個環節,繪制。內容不多,相對也比較好理解。
附上前面幾節的鏈接,想了解的童靴可以先看看。
整體流程
整個過程如下:
- 根據布局樹生成繪制命令列表
- 光柵化,生成像素點信息
- 將像素轉換為圖片
- 顯示到屏幕
繪制命令是個啥?它描述了你該如何繪制一個圖形,比如位置大小、顏色、形狀等等。
這里,我們只處理背景色和邊框的繪制,文字暫不涉及。所以只需支持矩形繪制,知道矩形的位置大小、顏色就好。
數據結構
繪制命令,定義為枚舉類型。暫且只支持矩形,關聯顏色和區域。
// 繪制命令,目前只支持顏色繪制
enum DisplayCommand {
case SolidColor(Color, Rect)
}
畫布,用來保存像素信息。
// 畫布
struct Canvas {
var width: Int
var height: Int
// 像素點,argb
var pixels: [Color]
}
生成繪制命令
背景
背景的繪制命令比較好生成。從節點樣式表中取出背景色 background
,再計算出背景區域就好。
背景區域包括「內容區+內邊距+邊框」。
代碼如下:
// 繪制背景
func renderBackground(list: inout [DisplayCommand], layoutBox: LayoutBox) {
// 獲取背景色
if let color = getColor(layoutBox: layoutBox, name: "background") {
// 背景包括 padding + border + content
let displayCommand = DisplayCommand.SolidColor(color, layoutBox.dimensions.borderBox())
list.append(displayCommand)
}
}
邊框
同樣,首先從樣式表中取出邊框顏色 border-color
。
另外,邊框分為上下左右四個矩形區域,需分別計算出來。如下所示:
繪制命令的生成跟背景色差不多,只是注意下矩形區域的計算。
總繪制列表
遞歸遍歷布局樹,將每個節點的繪制命令加入到數組,即可得到總繪制命令列表。
// 生成總體繪制列表
func buildDisplayList(layoutRoot: LayoutBox) -> [DisplayCommand] {
var list: [DisplayCommand] = []
renderLayoutBox(list: &list, layoutBox: layoutRoot)
return list
}
func renderLayoutBox(list: inout [DisplayCommand], layoutBox: LayoutBox) {
// 繪制背景
renderBackground(list: &list, layoutBox: layoutBox)
// 繪制邊框
renderBorder(list: &list, layoutBox: layoutBox)
// 遍歷子節點,遞歸生成命令
for child in layoutBox.children {
renderLayoutBox(list: &list, layoutBox: child)
}
}
光柵化
將繪制命令轉換為像素信息。
繪制命令中包含顏色和區域,將該區域中每個點的色值寫入像素數組就好。
// 生成像素點
mutating func genPixel(color: Color, rect: Rect) {
// 將 rect 范圍內的點填充為 color,不能超過畫布大小
// clamp 主要是用于限制范圍
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)))
// 遍歷所有點,橫向一行行填充
for y in y0...y1 {
for x in x0...x1 {
let index = y * width + x
pixels[index] = color
}
}
}
生成圖片
得到像素信息數組后,使用 CoreGraphics 的 api 生成圖片。注意 color 的順序是 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)
}
顯示
這步只需創建一個 ImageView 將 image 顯示出來。
測試數據
測試數據在工程目錄下的 Example 下面,test.html 和 test.css。
效果如下圖所示:
完整代碼可點此查看:https://github.com/silan-liu/tiny-web-render-engine-swift。
總結
這一節主要介紹了繪制的相關操作,重點在于將布局信息生成繪制列表,然后進行光柵化,轉換為像素點的過程。
至此,「聽說你想寫個渲染引擎」系列已全部完結,感謝您的閱讀~