绘制
绘制阶段主要是根据布局树中各个节点的位置、尺寸信息将它们绘制到页面。目前大多数计算机使用光栅(raster,也称为位图)显示技术。将布局树各个节点绘制到页面的这个过程也被称为“光栅化”。
浏览器通常在图形API和库(如Skia、Cairo、Direct2D等)的帮助下实现光栅化。这些API提供绘制多边形、直线、曲线、渐变和文本的功能。
实际上绘制才是最难的部分,但是这一步我们有现成的 canvas 库可以用,不用自己实现一个光栅器,所以相对来说就变得简单了。在真正开始绘制阶段之前,我们先来学习一些关于计算机如何绘制图像、文本的基础知识,有助于我们理解光栅化的具体实现过程。
计算机如何绘制图像、文本
在计算机底层进行像素绘制属于硬件操作,它依赖于屏幕和显卡接口的具体细节。为了简单起点,我们可以用一段内存区域来表示屏幕,内存的一个 bit 就代表了屏幕中的一个像素。比如在屏幕中的 (x,y)
坐标绘制一个像素,可以用 memory[x + y * rowSize] = 1
来表示。从屏幕左上角开始,列是从左至右开始计数,行是从上至下开始计数。因此屏幕最左上角的坐标是 (0,0)
。
为了简单起见,我们用 1 bit 来表示屏幕的一个像素,0 代表白色,1 代表黑色。屏幕每一行的长度用变量 rowSzie
表示,每一列的高度用 colSize
表示。
绘制线条
如果我们要在计算机上绘制一条直线,那么只要知道计算机的起点坐标 (x1,y1)
和终点坐标 (x2,y2)
就可以了。
然后根据 memory[x + y * rowSize] = 1
公式,将 (x1,y1)
至 (x2,y2)
之间对应的内存区域置为 1,这样就画出来了一条直线。
绘制文本
为了在屏幕上显示文本,首先必须将物理上基于像素点的屏幕,在逻辑上以字符为单位划分成若干区域,每个区域能输出单个完整的字符。假设有一个 256 行 512 列的屏幕,如果为每个字符分配一个 11*8 像素的网格,那么屏幕上总共能显示 23 行,每行 64 个字符(还有 3 行像素没使用)。
有了这些前提条件后,我们现在打算在屏幕上画一个 A
:
上图的 A
在内存区域中用 11*8 像素的网格表示。为了在内存区域中绘制它,我们可以用一个二维数组来表示它:
const charA = [ [0, 0, 1, 1, 0, 0, 0, 0], // 按从左至右的顺序来读取 bit,转换成十进制数字就是 12 [0, 1, 1, 1, 1, 0, 0, 0], // 30 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [1, 1, 1, 1, 1, 1, 0, 0], // 63 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [1, 1, 0, 0, 1, 1, 0, 0], // 51 [0, 0, 0, 0, 0, 0, 0, 0], // 0 [0, 0, 0, 0, 0, 0, 0, 0], // 0 ]
上面二维数组的第一项,代表了第一行内存区域每个 bit 的取值。一共 11 行,画出了一个字母 A
。
如果我们为 26 个字母都建一个映射表,按 ascii 的编码来排序,那么 charsMap[65]
就代表字符 A
,当用户在键盘上按下 A
键时,就把 charsMap[65]
对应的数据输出到内存区域上,这样屏幕上就显示了一个字符 A
。
绘制布局树
科普完关于绘制屏幕的基础知识后,我们现在正式开始绘制布局树(为了方便,我们使用 node-canvas 库)。
首先要遍历整个布局树,然后逐个节点进行绘制:
function renderLayoutBox(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D, parent?: LayoutBox) { renderBackground(layoutBox, ctx) renderBorder(layoutBox, ctx) renderText(layoutBox, ctx, parent) for (const child of layoutBox.children) { renderLayoutBox(child, ctx, layoutBox) } }
这个函数对每个节点依次绘制背景色、边框、文本,然后再递归绘制所有子节点。
默认情况下,HTML 元素按照它们出现的顺序进行绘制。如果两个元素重叠,则后一个元素将绘制在前一个元素之上。这种排序反映在我们的布局树中,它将按照元素在 DOM 树中出现的顺序绘制元素。
绘制背景色
function renderBackground(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D) { const { width, height, x, y } = layoutBox.dimensions.borderBox() ctx.fillStyle = getStyleValue(layoutBox, 'background') ctx.fillRect(x, y, width, height) }
首先拿到布局节点的位置、尺寸信息,以 x,y
作为起点,绘制矩形区域。并且以 CSS 属性 background
的值作为背景色进行填充。
绘制边框
function renderBorder(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D) { const { width, height, x, y } = layoutBox.dimensions.borderBox() const { left, top, right, bottom } = layoutBox.dimensions.border const borderColor = getStyleValue(layoutBox, 'border-color') if (!borderColor) return ctx.fillStyle = borderColor // left ctx.fillRect(x, y, left, height) // top ctx.fillRect(x, y, width, top) // right ctx.fillRect(x + width - right, y, right, height) // bottom ctx.fillRect(x, y + height - bottom, width, bottom) }
绘制边框,其实我们绘制的是四个矩形,每一个矩形就是一条边框。
绘制文本
function renderText(layoutBox: LayoutBox, ctx: CanvasRenderingContext2D, parent?: LayoutBox) { if (layoutBox.styleNode?.node.nodeType === NodeType.Text) { // get AnonymousBlock x y const { x = 0, y = 0, width } = parent?.dimensions.content || {} const styles = layoutBox.styleNode?.values || {} const fontSize = styles['font-size'] || '14px' const fontFamily = styles['font-family'] || 'serif' const fontWeight = styles['font-weight'] || 'normal' const fontStyle = styles['font-style'] || 'normal' ctx.fillStyle = styles.color ctx.font = `${fontStyle} ${fontWeight} ${fontSize} ${fontFamily}` ctx.fillText(layoutBox.styleNode?.node.nodeValue, x, y + parseInt(fontSize), width) } }
通过 canvas 的 fillText()
方法,我们可以很方便的绘制带有字体风格、大小、颜色的文本。
输出图片
绘制完成后,我们可以借助 canvas
的 API 输出图片。下面用一个简单的示例来演示一下:
<html> <body id=" body " data-index="1" style="color: red; background: yellow;"> <div> <div class="lightblue test">test1!</div> <div class="lightblue test"> <div class="foo">foo</div> </div> </div> </body> </html>
* { display: block; } div { font-size: 14px; width: 400px; background: #fff; margin-bottom: 20px; display: block; background: lightblue; } .lightblue { font-size: 16px; display: block; width: 200px; height: 200px; background: blue; border-color: green; border: 10px; } .foo { width: 100px; height: 100px; background: red; color: yellow; margin-left: 50px; } body { display: block; font-size: 88px; color: #000; }
上面这段 HTML、CSS 代码经过渲染引擎程序解析后生成的图片如下:
总结
至此,这个玩具版的渲染引擎就完成了。虽然这个玩具并没有什么用,但如果能通过实现它来了解真实的渲染引擎是如何运作的,从这个角度来看,它还是“有用”的。