OffscreenCanvas 是一个实验中的新特性(在最新版本的 Chrome 和 Firefox 上都可以通过实验室开关打开,Chrome 的开关是 chome://flags -> Experimental Web Platform features(离屏渲染chrome86已默认支持,不需要开启),本文的例程是在 Chrome 67 Canary 上进行验证),主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验。
OffscreenCanvas和canvas都是渲染图形的对象。 不同的是canvas只能在window环境下使用,而OffscreenCanvas既可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染成为可能。
跟 OffscreenCanvas 关系比较紧密的还有另外两个新的 API,ImageBitmap 和 ImageBitmapRenderingContext。
使用解析
OffscreenCanvas 目前主要用于两种不同的使用场景:
- Transfer 模式:一种是在 Worker 线程创建一个 OffscreenCanvas 做后台渲染,然后再把渲染好的缓冲区 Transfer 回主线程显示;
- Commit 模式:一种是主线程从当前 DOM 树中的 Canvas 元素产生一个 OffscreenCanvas,再把这个 OffscreenCanvas 发送给 Worker 线程进行渲染,渲染的结果直接 Commit 到浏览器的 Display Compositor 输出到当前窗口,相当于在 Worker 线程直接更新 Canvas 元素的内容;
一、Transfer模式
应用场景:Transfer 模式主要用于后台渲染,避免耗时的渲染任务会阻塞前台线程,导致应用无法及时响应用户的操作,比如一些 2D/3D 图表,图形可视化应用,地图应用等。
Transfer Demo 运行流程大致如下:
- 主线程启动 Worker 线程,并请求初始化;
- Worker 线程创建 OffscreenCanvas;
- Worker 线程获取 OffscreenCanvas 的 WebGL Context 并进行绘制;
- Worker 线程获取 OffscreenCanvas 的缓冲区(ImageBitmap),然后 Transfer 回主线程;
- 主线程将 Worker 线程回传的缓冲区分别绘制在两个不同的 Canvas 上,一个 Canvas 使用 CanvasRenderingContext2D,一个 Canvas 使用 ImageBitmapRenderingContext;
- 3 ~ 5 重复运行;
1、渲染到canvas上的两种方式:
- 方案一:ImageBitmap 可以被当做普通的 Image 绘制在一个 2D Canvas 上;
- 方案二:可以通过 ImageBitmapRenderingContext Transfer 到一个 Bitmap Canvas;
2、ImageBitmap介绍
- ImageBitmap 主要是用来封装一块 GPU 缓冲区,可以被 GPU 读写,并且实现了 Transferable 的接口(transferFromImageBitmap和transferToImageBitmap),可以在不同线程之间 Transfer。
- 跟 ImageData 不一样,ImageBitmap 并没有提供 JavaScipt API 供 CPU 进行读写,这是因为使用 CPU 读写 GPU 缓冲区的成本非常高,需要拷贝到临时缓冲区进行读写然后再写回。这也是为什么规范的制定者没有扩展 ImageData,而是提供了一个新的 ImageBitmap 的缘故。
3、渲染的具体流程:
- (1) 当我们使用 OffscreenCanvas,通过 2D/3D 进行绘制时==》我们有一块画板,上面有一些画纸,我们可以在画纸上作画;
- (2) 调用 OffscreenCanvas.transferToImageBitmap 获取 ImageBitmap 封装的缓冲区==》我们把当前绘画的画纸取下来;
function TransferBuffer() {
let image_bitmap = canvas.transferToImageBitmap();
postMessage({name:"TransferBuffer", buffer:image_bitmap},
[image_bitmap]);
}
- (3) 把 ImageBitmap 作为 Image 绘制在一个 2D Canvas 上(方案一)==》我们对已经绘制好的图画在新的画纸上进行临摹;
function init() {
g_bitmap_canvas = helper.GetCanvas("bitmap");
g_2d_canvas = helper.GetCanvas("2d");
g_render_worker = new Worker("./render_worker.js");
g_render_worker.onmessage = function(msg) {
if (msg.data.name === "TransferBuffer") {
GetTransferBuffer(msg.data.buffer);
}
}
}
function GetTransferBuffer(buffer) {
let context_2d = g_2d_canvas.getContext("2d");
context_2d.clearRect(0, 0, g_2d_canvas.width, g_2d_canvas.height);
context_2d.save();
context_2d.translate(g_bitmap_canvas.width / 2, g_bitmap_canvas.height / 2);
context_2d.rotate(g_angle * Math.PI / 180);
context_2d.scale(0.5, 0.5);
context_2d.translate(-g_bitmap_canvas.width / 2, -g_bitmap_canvas.height / 2);
context_2d.drawImage(buffer, 0, 0);
context_2d.restore();
g_angle += 15;
if (g_angle &