Bulletproof 20150914 - SVG-Canvas 融合测试不那么尽如人意

Bulletproof 的 SVG-Canvas 融合测试不那么尽如人意,测试分支在 https://github.com/Hozuki/Bulletproof/tree/deprecated-svg-canvas

嗯,是的,deprecated-svg-canvas 分支。如名称所示的,应该不会再找个分支上继续工作了。


昨天的预告说发现了什么。是 SVG 支持的 <image> 标签

就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<svg width="150" height="150">
<defs>
<filter id="feglow0">
<feColorMatrix type="matrix" values="0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0">
</feColorMatrix>
<feGaussianBlur stdDeviation="40 40" result="coloredBlur">
</feGaussianBlur>
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<g filter="url(#feglow0)">
<image id="svgimg" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://pic19.nipic.com/20120308/7491614_141057681000_2.png" width="150" height="150">
</image>
</g>
</svg>

效果……GitHub 这里好像不能展示,或者 Hexo 的问题?(所以后面和 SVG 有关的所有展示都请大家自备服务器,放网页运行。)

可以看到,在渲染的时候,会计算透明通道,并得到正确的滤镜处理结果。能想到:如果将 canvas 的高效率绘图和 SVG 的原生滤镜结合起来会怎么样呢?

这不是不可能的。我们可以见到,在 <image> 标签中有一个 href 属性(实际上可以不用带 xlink 命名空间),指向引用的图片源。所以我们如果要动态设置 <image> 的内容,就要动态设置其 href。问题是,href 的内容是 URL,而不是像素内容。因此我们需要一个动态更新的 URL,其永远指向最新的 canvas 内容。

从另一个方面考虑,canvas 有没有导出内容的方法呢?查阅 MDN(顺带吐槽 MDN 上中文翻译有很多小错误,不过还是要尊重志愿者的服务)我们可以知道,HTMLCanvasElement 有一个方法 toDataURL(),能将当前的内容编码为 data URL。(data URL 请见 RFC 2397。)而 <image>href 是能正确解析 data URL 的,这就为我们搭建了一条从 canvas 到 SVG 的路。

题外话1:反向的路径有没有?有,可以看看 canvg 项目

题外话2:我们其实还可以见到,在 MDN 中,HTMLCanvasElement 还有一个方法 toBlob(),能将 canvas 的内容存入一个 Blob 对象以供使用。不过至少在 Chrome 45 里,以及 nw.js 0.12 里都没有相应的实现。

题外话3:data URL 在 Chrome 中,在本地是不可使用的。如果你写了一段测试脚本给某个 <img>src 或者 <image>href 赋值为 data URL,并用浏览器直接从本地打开这个页面,则会引发一个 security exception。我还没在其他浏览器中试过。考虑到实际的页面并不是直接在本地打开的,无论是 nw.js 还是网站,总之有服务器撑腰,所以不会引发这个异常。

经过以上的预备,我们大概能想到一个流程:

1
2
3
4
5
graphics.redrawObjects();
var canvas = graphics.canvas;
var url = canvas.toDataURL();
// SVG 元素属性的赋值方法,与 HTML 元素不太一样
imageElement.href.baseVal = url;

于是我花了一个晚上将底层改造为 SVG+Canvas 的形式。但是接下来发生的事情出乎我的意料。

首先来看三维球示例的表现:

3-D Ball Demo - SVG-Canvas

3-D Ball Timeline - SVG-Canvas

从结果中可以看出,每帧大多数的时间是花在绘制(painting)上的。想到在这个示例中我们启用了发光滤镜,每次更新 <image>href 的时候,都要解码数据并更新缓存→计算颜色变换(color transform)→进行高斯模糊→合成与渲染,所需的时间不可小觑。

再看小圆脸示例的表现:

Madoka Demo - SVG-Canvas

Madoka Timeline - SVG-Canvas

这次执行时间显著提升了。这是绘制的机制导致的。(与之前实现的一样,背景的椭圆只在开始的时候计算了一次效果,其余时间不会触发更新事件。所以更新来自于其他的几个 <image>。)小圆脸示例中有三层的内容是动态更新的:杏子+さやか+学姐、黑长直+小圆脸、黑长直和小圆脸的头发碎片。在高级弹幕代码中,有一个 Motion 随时间线性地改变着以上三个元素的 X 轴位置。canvas 只会画出存在于其范围内的东西,这意味着如下两段代码,虽然都是画一个圆,但是由于元素不同所以显示的完整性不同。

SVG 会显示完整的圆:

1
2
3
<svg width="120" height="120" viewBox="0 0 120 120">
<circle cx="0" cy="0" r="40" fill="none" stroke="black" transform="translate(40 40)"></circle>
</svg>

SVG+canvas 只能显示四分之一圆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<svg width="120" height="120" viewBox="0 0 120 120">
<image id="image" width="120" height="120" transform="tranlate(40 40)"></image>
<svg>

<canvas width="120" height="120" id="canvas" style="display: none;">
</canvas>

<script type="text/javascript">
var d = document.getElementById("canvas");
var c = d.getContext("2d");
c.moveTo(40, 0);
c.arc(0, 0, 40, 0, Math.PI * 2);
c.stroke();
var img = document.getElementById("image");
img.href.baseVal = d.getDataURL();
</script>

所以,必须要在 canvas 绘图之前,计算各种变换,应用到 canvas 上,绘图,再传到 <image> 上,对齐零点。这就导致图片更新频繁:每帧都要重新绘制到 canvas 上,然后通过 toDataURL() 进行图像的 Base64 编码,传递给 SVG,触发 SVG 更新,浏览器渲染。原来是绘制到 canvas 上后直接浏览器渲染。而将大数组编解码,还要附加多次渲染(因为有多个 <image>,每更新一次都要进行与脏区(dirty rectangle)有交集的所有元素的渲染),性能肯定要掉下去。这抵消了 <image> 的原生滤镜处理的优势,甚至比 canvas 的方案更糟糕。

让我们看看纯 canvas 的表现。

3-D Ball Demo - Canvas

3-D Ball Timeline - Canvas

Madoka Demo - Canvas

Madoka Timeline - Canvas

(解释一下,这次的三维球帧率比上次的有所降低。这是因为此次的绘制区域更大,所需的像素计算与拷贝时间增加了。)

这个对比简直是 canvas 虐 SVG+canvas,而且 Klingemann 的算法的执行效率几乎能赶上浏览器的原生模糊算法的执行效率了。

正因如此,使用 SVG 的滤镜辅助 canvas 绘制的方法被否决了。留下了一个存档放在新分支里。


搜索“JavaScript image processing”的时候,最先找到的是 caman.js,然后在其 GitHub 页面上见到了一段使用示例:

1
2
3
4
5
6
7
8
Caman = require('caman').Caman;

Caman("./path/to/file.jpg", function () {
this.brightness(40);
this.render(function () {
this.save("./output.png");
});
});

注意里面有一个 save() 方法,意味着 canvas 的内容是可以导出的。

下一个页面是 Best JavaScript Image Manipulation Libraries,里面第14个是 svg.js。分析 svg.js 的滤镜示例可以看到以下的 SVG 源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<svg id="SvgjsSvg1000" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="990" height="1650">
<defs id="SvgjsDefs1001">
<rect id="SvgjsRect1007" width="300" height="300"></rect>
<filter id="SvgjsFilter1013">
<feOffset id="SvgjsFeOffset1014" in="SourceAlpha" result="SvgjsFeOffset1014Out" dx="0" dy="1">
</feOffset>
</filter>
</defs>
<image id="SvgjsImage1008" xlink:href="http://distilleryimage11.ak.instagram.com/89ac2e90d9b111e297bf22000a1f9263_7.jpg" width="300" height="300" x="15" y="15" opacity="0">
</image>
<image id="SvgjsImage1010" xlink:href="http://distilleryimage11.ak.instagram.com/89ac2e90d9b111e297bf22000a1f9263_7.jpg" width="300" height="300" x="15" y="15">
</image>
<text id="SvgjsText1011" font-family="Helvetica Neue, Helvetica, sans-serif" x="165" y="259.203125" font-size="24" text-anchor="middle" font-weight="100" fill="#ffffff">
<tspan id="SvgjsTspan1012" dy="31.200000000000003" x="165">
original
</tspan>
</text>
</svg>

从中可以看到 <image> 元素。至于 SVG 标准,是看到之后才查的。

然后问题来到 canvas 内容导出。StackOverflow 上有一篇回答中有一行代码:

1
var data = canvas.toDataURL('image/jpeg');

所以才看到了 toDataURL() 方法。(之前在手册里见过,没用到所以印象不深……)

在进行 data URL 的赋值时在控制台中发现抛出了异常。有人给出答案说是防止本地文件被非法访问。(答案引文答案引文的引文)说到最后还是同源的问题啦。(Netscape 的标准对后代产生了持续的影响ww)

Information leakage can occur if scripts from one origin can access information (e.g. read pixels) from images from another origin (one that isn’t the same).

To mitigate this, bitmaps used with canvas elements are defined to have a flag indicating whether they are origin-clean. All bitmaps start with their origin-clean set to true. The flag is set to false when cross-origin images or fonts are used.

The toDataURL(), toBlob(), and getImageData() methods check the flag and will throw a SecurityError exception rather than leak cross-origin data.

附:另一种 SVG 绘制到静态图像上的方法

分享到 评论