从零开始实现一个玩具版浏览器渲染引擎(四)

简介: 从零开始实现一个玩具版浏览器渲染引擎(四)

绘制

绘制阶段主要是根据布局树中各个节点的位置、尺寸信息将它们绘制到页面。目前大多数计算机使用光栅(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 代码经过渲染引擎程序解析后生成的图片如下:

总结

至此,这个玩具版的渲染引擎就完成了。虽然这个玩具并没有什么用,但如果能通过实现它来了解真实的渲染引擎是如何运作的,从这个角度来看,它还是“有用”的。

参考资料

目录
相关文章
|
9月前
|
JavaScript 前端开发 容器
从零开始实现一个玩具版浏览器渲染引擎(三)
从零开始实现一个玩具版浏览器渲染引擎(三)
32 0
|
9月前
|
前端开发 JavaScript
从零开始实现一个玩具版浏览器渲染引擎(二)
从零开始实现一个玩具版浏览器渲染引擎(二)
46 0
|
9月前
|
Rust 自然语言处理 前端开发
从零开始实现一个玩具版浏览器渲染引擎(一)
从零开始实现一个玩具版浏览器渲染引擎
45 0
|
Web App开发 JavaScript 前端开发
浏览器渲染引擎工作原理|学习笔记
快速学习浏览器渲染引擎工作原理
216 0
浏览器渲染引擎工作原理|学习笔记
|
Web App开发 存储 移动开发
浏览器内核(渲染引擎)介绍|学习笔记
快速学习浏览器内核(渲染引擎)介绍
262 0
|
Web App开发 存储 JavaScript
浏览器渲染引擎与阻塞
浏览器渲染引擎与阻塞
|
Web App开发 JavaScript 前端开发
画了20张图,详解浏览器渲染引擎工作原理(下)
今天我们来学习一下浏览器渲染引擎的工作原理,文章内容较多,建议先收藏再学习! 先来看看Chrome浏览器的架构图:
269 0
|
4天前
|
JavaScript
浏览器插件crx文件--JS混淆与解密
浏览器插件crx文件--JS混淆与解密
25 0
|
4天前
|
JavaScript 前端开发 UED
JS:如何获取浏览器窗口尺寸?
JS:如何获取浏览器窗口尺寸?
57 1
|
4天前
|
JavaScript 前端开发 算法
Node.js中的process.nextTick与浏览器环境中的nextTick有何不同?
Node.js中的process.nextTick与浏览器环境中的nextTick有何不同?