OffscreenCanvas
OffscreenCanvas,也即离屏Cavnas,可无须事先创建任何一个Canvas就可在后台进行渲染。渲染完成后,可将渲染结果快速地绘制至任意一个前台的Canvas之上。
初识OffscreenCanvas
在下面的例子中,网页上有2个Canvas对象。
它们的大小分别为:
#canvas1 {
width: 200px;
height: 200px;
}
#canvas2 {
width: 150px;
height: 150px;
}
我们要使用OffscreenCanvas进行2次离屏渲染,然后,将这些渲染结果分别绘制到上述两个Canvas上面。
为使代码更加灵活,我们使用了回调机制。首先,在OffscreenCanvas的原型上添加一个renderInto方法:
OffscreenCanvas.prototype.renderInto = function (targetCanvas, callback) {
this.width = targetCanvas.clientWidth;
this.height = targetCanvas.clientHeight;
callback(this.getContext('2d'));
let imageBitmap = this.transferToImageBitmap();
let targetCtx = targetCanvas.getContext('bitmaprenderer');
targetCtx.transferFromImageBitmap(imageBitmap);
};
该方法先使离屏Canvas的宽度与高度分别与参数targetCanvas保持一致,然后,将一个CanvasRenderingContext2D对象注入到参数callback回调函数中,这样,调用者可利用该CanvasRenderingContext2D对象进行渲染。
之后,调用OffscreenCanvas的transferToImageBitmap方法将渲染结果存储为一个ImageBitmap的实例变量imageBitmap。
最后,调用targetCanvas的getContext('bitmaprenderer')方法,返回一个ImageBitmapRenderingContext实例,该实例只有一个方法transferFromImageBitmap,用以接收所渲染完毕的结果。
可以多次调用transferToImageBitmap,并向多个canvas传递。
客户端代码如下:
const offscreenCanvas = new OffscreenCanvas(300, 300);
let canvas1 = document.getElementById('canvas1');
offscreenCanvas.renderInto(canvas1, ctx => {
ctx.fillStyle = '#336699';
ctx.fillRect(0, 0, 100, 100);
});
let canvas2 = document.getElementById('canvas2');
offscreenCanvas.renderInto(canvas2, ctx => {
ctx.fillStyle = '#990099';
ctx.fillRect(0, 0, 100, 100);
});
运行应用。
从代码运行效果来看,即先离屏渲染两个图像,然后再分别显示到两个canvas上面。
transferControlToOffscreen
OffscreenCanvas还可以通过Canvas的transferControlToOffscreen方法来获取。
let canvas = document.getElementById('canvas');
canvas.width = 500;
canvas.height = 500;
const offscreenCanvas = canvas.transferControlToOffscreen();
let ctx = offscreenCanvas.getContext('2d');
ctx.fillStyle = '#336699';
ctx.fillRect(0, 0, 100, 100);
渲染完成后,offscreenCanvas自动将渲染结果立即绘制到canvas上。
运行应用。
这种方式,与普通的Canvas渲染看似相似。但我们还可以更进一步,将渲染环节交由Web worker来完成。
Offscreen Canvas Web worker
先编写下面代码:
let canvas = document.getElementById('canvas');
const worker = new Worker('canvas-worker.js');
worker.postMessage(canvas);
当我们运行上面的代码,就会出现错误:
DataCloneError: The object can not be cloned.
这是因为Worker的postMessage方法,只接受能处理structured clone算法的JavaScript对象。Transferable objects支持这种算法。Canvas不属于Transferable objects,而OffscreenCanvas属于Transferable objects。因此,我们需要将代码改为:
let canvas = document.getElementById('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
const worker = new Worker('canvas-worker.js');
worker.postMessage(offscreenCanvas, [offscreenCanvas]);
postMessage方法的第2个可选参数是一个数组,里面列出所有需要转移支配权(ownership)的Transferable objects。只有显式地列出这些可转移支配权的对象,才能在不同的线程中安全地交换数据。
在不同线程中转移支配权,意味着在任一时刻,只有在拥有支配权的线程中,我们才能安全地访问该对象。看下面的代码:
let canvas = document.getElementById('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
console.log(offscreenCanvas.width); // 300
const worker = new Worker('canvas-worker.js');
worker.postMessage(offscreenCanvas, [offscreenCanvas]);
console.log(canvas.width); // 300
console.log(offscreenCanvas.width); // 0
在offscreenCanvas转移支配权之前,我们可以访问其width属性值。
而在将offscreenCanvas的主线程的支配权转移至Worker线程后,我们在主线程中若访问其width属性值,将得到错误的数值。除非分支线程重新转移回其支配权。
而由于canvas并非转移其支配权,因此在主线程中我们仍可安全地访问其各个属性值。
下面是canavs-worker.js的内容:
onmessage = (e) => {
const offscreenCanvas = e.data;
const ctx = offscreenCanvas.getContext('2d');
ctx.fillStyle = '#336699';
ctx.fillRect(0, 0, 100, 100);
};
由于我们不需要在主线程中对offscreenCanvas进行进一步的操作,因此分支线程无需返回offscreenCanvas及其支配权。
运行应用。
在分支线程中实现动画
主线程代码:
let canvas = document.getElementById('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
const worker = new Worker('canvas-animation.js');
worker.postMessage(offscreenCanvas, [offscreenCanvas]);
分支线程代码:
onmessage = (e) => {
const offscreenCanvas = e.data;
const ctx = offscreenCanvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.font = 'bold 64px Verdana';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
let centerX = offscreenCanvas.width / 2;
let centerY = offscreenCanvas.height / 2;
let count = 1;
let prevTimeStamp = 0;
requestAnimationFrame(initFirstFrame);
function initFirstFrame(timeStamp) {
ctx.fillText(count, centerX, centerY);
count++;
prevTimeStamp = timeStamp;
requestAnimationFrame(render);
}
function render(timeStamp) {
let elapsedTime = timeStamp - prevTimeStamp;
if (elapsedTime / 1000 >= 1.0) {
ctx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
ctx.fillText(count, centerX, centerY);
count++;
prevTimeStamp = timeStamp;
}
requestAnimationFrame(render);
}
};
运行应用。