Skip to content

Latest commit

 

History

History
267 lines (202 loc) · 10.4 KB

6.zh.md

File metadata and controls

267 lines (202 loc) · 10.4 KB

2014年11月5日

让我们构建一个浏览器引擎!

第7部分: 绘画101

我终于回到了关于构建一个简单的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; }

......产生这个:

好极了!

练习

如果你在家里玩,这里有一些你可能想尝试的事情:

  1. 编写一个替代 paint 函数,它采用 显示列表并生成矢量输出 (例如,SVG文件) 而不是光栅图像.

  2. 添加对 不透明度和Alpha混合 的支持.

  3. 编写一个函数,通过剔除 完全在画布边界之外的事物 来优化显示列表.

  4. 如果您熟悉 OpenGL,请编写一个硬件加速绘制函数,该函数使用 GL着色器 绘制矩形.

未完待续ⅆ

现在我们已经为渲染管道中的每个阶段提供了基本功能,现在是时候回过头 来填写一些缺失的功能 - 特别是 内联布局和文本渲染.

未来的文章还可能添加其他阶段,如 网络和脚本.

在这个月,我将简短地说一下"让我们构建一个浏览器引擎!"在Bay Area Rust Meetup的谈话. 聚会将于明天 (11月6日星期四) 晚上7点在 Mozilla的旧金山办公室 举行,并且还将由 我的Servo开发人员 参加 Servo会谈. 会谈视频将直播在Air Mozilla,录音将在稍后发布.