2014年11月5日
我终于回到了关于构建一个简单的HTML呈现引擎的系列文章:
在本文中,我将添加非常基本的绘画代码. 此代码从 布局模块 中获取 框的树,并将它们转换为像素数组. 此过程也称为"光栅化-rasterization".
浏览器通常借助 图形API 和 Skia,Cairo,Direct2D等库 来实现光栅化. 这些API提供绘制 多边形,线条,曲线,渐变和文本的功能. 现在,我要编写自己的光栅化器,它只能绘制一个东西: 矩形.
最终我想实现文本呈现. 那时,我可能会扔掉这个玩具绘画代码,并切换到"真正的"2D图形库. 但就目前而言,矩形足以将我的块布局算法的输出 转换为图片.
自从我上一篇文章发表以来,我对之前文章中的代码进行了一些小改动. 这些包括一些小的重构,以及一些更新,以保持代码与最新的 Rust nightly版本 兼容.
这些变化对于理解代码都不是至关重要,但是如果你很好奇,请检查提交历史.
在绘画之前,我们将遍历 布局树 并构建一个显示列表-display list. 这是一个图形操作列表,如"绘制圆圈"或"绘制一串文本". 或者在我们的例子中,只是"画一个矩形". 为什么要将命令放入显示列表,而不是立即执行?
由于几个原因,显示列表很有用. 您可以在其中搜索,要在以后的操作中完全覆盖的项目,和删除以消除浪费的绘画. 如果您只知道某些项目已更改,则可以修改和重复使用显示列表. 您可以使用相同的显示列表 生成不同类型的输出: 例如,用于在屏幕上显示的像素,或用于发送到打印机的矢量图形.
Robinson 的显示列表是DisplayCommands 的向量. 目前只有一种 DisplayCommand,一种纯色矩形:
type DisplayList = Vec<DisplayCommand>;
enum DisplayCommand {
SolidColor(Color, Rect),
// 在这里插入更多命令
}
为了构建显示列表,我们遍历布局树 并为每个框生成一系列命令. 首先我们绘制框的背景,然后我们在背景上绘制边框和内容.
fn build_display_list(layout_root: &LayoutBox) -> DisplayList {
let mut list = Vec::new();
render_layout_box(&mut list, layout_root);
return list;
}
fn render_layout_box(list: &mut DisplayList, layout_box: &LayoutBox) {
render_background(list, layout_box);
render_borders(list, layout_box);
// TODO:渲染文字
for child in &layout_box.children {
render_layout_box(list, child);
}
}
默认情况下,HTML元素按它们出现的顺序堆叠: 如果两个元素重叠,则后一个元素将在前一个元素之上 绘制. 这反映在我们的显示列表中,它将按照它们在DOM树中出现的顺序绘制元素. 如果此代码支持 z-index) 的属性,然后 单个元素 将能够覆盖此堆叠顺序,我们需要相应地对显示列表进行排序.
背景很简单. 它只是一个实心矩形. 如果未指定背景颜色,则背景是透明的,我们不需要生成显示命令.
fn render_background(list: &mut DisplayList, layout_box: &LayoutBox) {
get_color(layout_box, "background").map(|color|
list.push(DisplayCommand::SolidColor(color, layout_box.dimensions.border_box())));
}
// 返回CSS属性`name`的指定颜色,或者 None 如果没有指定.
fn get_color(layout_box: &LayoutBox, name: &str) -> Option<Color> {
match layout_box.box_type {
BlockNode(style) | InlineNode(style) => match style.value(name) {
Some(Value::ColorValue(color)) => Some(color),
_ => None
},
AnonymousBlock => None
}
}
边框类似,但我们不是一个矩形,而是为每个边框绘制四个边框.
fn render_borders(list: &mut DisplayList, layout_box: &LayoutBox) {
let color = match get_color(layout_box, "border-color") {
Some(color) => color,
_ => return // 如果没有边框颜色,过滤掉
};
let d = &layout_box.dimensions;
let border_box = d.border_box();
// 左边界
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y,
width: d.border.left,
height: border_box.height,
}));
// 右边界
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x + border_box.width - d.border.right,
y: border_box.y,
width: d.border.right,
height: border_box.height,
}));
// 顶部边界
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y,
width: border_box.width,
height: d.border.top,
}));
// 底部边界
list.push(DisplayCommand::SolidColor(color, Rect {
x: border_box.x,
y: border_box.y + border_box.height - d.border.bottom,
width: border_box.width,
height: d.border.bottom,
}));
}
接下来,渲染功能将绘制每个框的子项,直到整个布局树 已转换为显示命令.
现在我们已经构建了显示列表,我们需要通过执行每个 DisplayCommand 将其转换为像素.
我们将像素存储在Canvas中:
struct Canvas {
pixels: Vec<Color>,
width: usize,
height: usize,
}
impl Canvas {
// 创建一个空白画布
fn new(width: usize, height: usize) -> Canvas {
let white = Color { r: 255, g: 255, b: 255, a: 255 };
return Canvas {
pixels: repeat(white).take(width * height).collect(),
width: width,
height: height,
}
}
// ...
}
要在画布上绘制一个矩形,我们只需使用一个循环遍历 其行和列的 辅助方法确保我们不要超出画布的范围.
fn paint_item(&mut self, item: &DisplayCommand) {
match item {
&DisplayCommand::SolidColor(color, rect) => {
// 将矩形剪切到画布boundaries.
let x0 = rect.x.clamp(0.0, self.width as f32) as usize;
let y0 = rect.y.clamp(0.0, self.height as f32) as usize;
let x1 = (rect.x + rect.width).clamp(0.0, self.width as f32) as usize;
let y1 = (rect.y + rect.height).clamp(0.0, self.height as f32) as usize;
for y in (y0 .. y1) {
for x in (x0 .. x1) {
// TODO:alpha合成存在的像素
self.pixels[x + y * self.width] = color;
}
}
}
}
}
请注意,此代码仅适用于不透明颜色. 如果我们增加透明度 (通过阅读opacity
属性,或在 css 解析器中 添加支持rgba()
) 它需要的是混合每个新像素值,无论它在上面绘制的是什么.
现在我们可以把所有东西放在一起了paint
函数,它构建一个显示列表,然后将其栅格化 为画布:
// 将 LayoutBoxes树 绘制为像素数组。
fn paint(layout_root: &LayoutBox, bounds: Rect) -> Canvas {
let display_list = build_display_list(layout_root);
let mut canvas = Canvas::new(bounds.width as usize, bounds.height as usize);
for item in display_list {
canvas.paint_item(&item);
}
return canvas;
}
最后,我们可以写几行代码使用rust image库将像素数组保存为PNG文件.
最后,我们已经到了渲染管道的末尾. 在不到1000行代码中,robinson 现在可以解析这个HTML文件:
<div class="a">
<div class="b">
<div class="c">
<div class="d">
<div class="e">
<div class="f">
<div class="g">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
...和这个CSS文件:
* { display: block; padding: 12px; }
.a { background: #ff0000; }
.b { background: #ffa500; }
.c { background: #ffff00; }
.d { background: #008000; }
.e { background: #0000ff; }
.f { background: #4b0082; }
.g { background: #800080; }
......产生这个:
好极了!
如果你在家里玩,这里有一些你可能想尝试的事情:
-
编写一个替代 paint 函数,它采用 显示列表并生成矢量输出 (例如,SVG文件) 而不是光栅图像.
-
添加对 不透明度和Alpha混合 的支持.
-
编写一个函数,通过剔除 完全在画布边界之外的事物 来优化显示列表.
-
如果您熟悉 OpenGL,请编写一个硬件加速绘制函数,该函数使用 GL着色器 绘制矩形.
现在我们已经为渲染管道中的每个阶段提供了基本功能,现在是时候回过头 来填写一些缺失的功能 - 特别是 内联布局和文本渲染.
未来的文章还可能添加其他阶段,如 网络和脚本.
在这个月,我将简短地说一下"让我们构建一个浏览器引擎!"在Bay Area Rust Meetup的谈话. 聚会将于明天 (11月6日星期四) 晚上7点在 Mozilla的旧金山办公室 举行,并且还将由 我的Servo开发人员 参加 Servo会谈. 会谈视频将直播在Air Mozilla,录音将在稍后发布.