OffscreenCanvas-离屏canvas使用说明

简介: OffscreenCanvas-离屏canvas使用说明

OffscreenCanvas 是一个实验中的新特性,主要用于提升 Canvas 2D/3D 绘图的渲染性能和使用体验。OffscreenCanvas 的 API 很简单,但是要真正掌握好如何使用。


OffscreenCanvas和canvas都是渲染图形的对象。不同的是canvas只能在window环境下使用,而OffscreenCanvas即可以在window环境下使用,也可以在web worker中使用,这让不影响浏览器主线程的离屏渲染成为可能。

与之关联的还有ImageBitmap对象和ImageBitmapRenderingContext。


ImageBitmap


ImageBitmap对象表示能够被绘制到 canvas上的位图图像,具有低延迟的特性。

ImageBitmap提供了一种异步且高资源利用率的方式来为WebGL的渲染准备基础结构。ImageBitmap可以通过createImageBitmap函数来创建,它可以从多种图像源生成。还可以通过OffscreenCanvas.transferToImageBitmap函数生成。


属性


ImageBitmap.height 只读 无符号长整型数值,表示ImageData的CSS像素单位的高度。ImageBitmap.width 只读 无符号长整型数值, 表示ImageData的CSS像素单位的宽度。


函数


ImageBitmap.close() 释放ImageBitmap所相关联的所有图形资源。


createImageBitmap


createImageBitmap 用于创建ImageBitmap对象。该函数存在 windows 和 workers 中。它接受各种不同的图像来源, 并返回一个Promise, resolve为ImageBitmap。


createImageBitmap(image[, options]).then(function(response) { ... });
createImageBitmap(image, sx, sy, sw, sh[, options]).then(function(response) { ... });


更多相关的内容,可以参考: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap


创建OffscreenCanvas


有两种方式可以创建OffscreenCanvas,一种是通过OffscreenCanvas的构造函数直接创建。比如下面的示例代码:


var offscreen = new OffscreenCanvas(width, height); // width 、height表示宽高。


另外一种方式,是使用canvas的transferControlToOffscreen函数获取一个OffscreenCanvas对象,绘制该OffscreenCanvas对象,同时会绘制canvas对象。比如如下代码:


var canvas = document.getElementById('canvas');
//var ctx = canvas.getContext('2d');
var offscreen = canvas.transferControlToOffscreen();
// canvas.getContext('2d'); // 会报错


上面的代码代码首先获取网页元素canvas对象,然后调用canvas对象的transferControlToOffscreen函数创建一个OffscreenCanvas对象offscreen,并把控制权交给offscreen。


需要注意的是,canvas对象调用了函数transferControlToOffscreen移交控制权之后,不能再获取绘制上下文,调用canvas.getContext('2d')会报错;同样的原理,如果canvas已经获取的绘制上下文,调用transferControlToOffscreen会报错。


OffscreenCanvas.transferToImageBitmap函数


通过transferToImageBitmap函数可以从OffscreenCanvas对象的绘制内容创建一个ImageBitmap对象。该对象可以用于到其他canvas的绘制。

比如一个常见的使用是,把一个比较耗费时间的绘制放到web worker下的OffscreenCanvas对象上进行,绘制完成后,创建一个ImageBitmap对象,并把该对象传递给页面端,在页面端绘制ImageBitmap对象。

下面是示例代码,主线程中:


var worker2 = null,canvasBitmap, ctxBitmap;
function init() {
    canvasBitmap = document.getElementById('canvas-bitmap');
    ctxBitmap = canvasBitmap.getContext('2d');
    worker2 = new Worker('./bitmap_worker.js');
    worker2.postMessage({msg:'init'});
    worker2.onmessage = function (e) {
      ctxBitmap.drawImage(e.data.imageBitmap,0,0);
    }
}
function redraw() {
  ctxBitmap.clearRect(0, 0, canvasBitmap.width, canvasBitmap.height)
  worker2.postMessage({msg:'draw'});
}


worker线程中:


var offscreen,ctx;
onmessage = function (e) {
  if(e.data.msg == 'init'){
    init();
    draw();
  }else if(e.data.msg == 'draw'){
    draw();
  }
}
function init() {
  offscreen = new OffscreenCanvas(512, 512);
  ctx = offscreen.getContext("2d");
}
function draw() {
   ctx.clearRect(0,0,offscreen.width,offscreen.height);
   for(var i = 0;i < 10000;i ++){
    for(var j = 0;j < 1000;j ++){
      ctx.fillRect(i*3,j*3,2,2);
    }
  }
  var imageBitmap = offscreen.transferToImageBitmap();
  postMessage({imageBitmap:imageBitmap},[imageBitmap]);
}


  • 在主线程中,获取canvas对象,然后生成worker对象,并把绘制命令传递给worker。
  • 在worker线程中,创建一个OffscreenCanvas,然后执行绘制命令,绘制完成后,通过transferToImageBitmap函数创建imageBitmap对象,并通过postMessage把imageBitmap对象传递给主线中。
  • 主线程接收到imageBitmap对象之后,把imageBitmap绘制到canvas对象上。


最终的绘制效果如下:


微信图片_20220424104744.jpg


把绘制放到web worker中的好处是,绘制的过程不阻塞主线程的运行。读者可以自行运行代码查看,在绘制过程过程中,界面可以交互, 比如可以选择下拉框。


ImageBitmapRenderingContext


ImageBitmapRenderingContext接口是 canvas 的渲染上下文,它只提供使用给定 ImageBitmap 替换 canvas 的功能。它的上下文 ID (HTMLCanvasElement.getContext() 或 OffscreenCanvas.getContext() 的第一个参数) 是 "bitmaprenderer"。这个接口可用于 window context 和 worker context.


方法


ImageBitmapRenderingContext.transferFromImageBitmap函数用于 在与此“渲染上下文”对应的 canvas 中显示给定的 ImageBitmap对象。ImageBitmap 的所有权被转移到画布上。


在前面的例子中,可以做如下修改:


function init() {
   ...
  ctxBitmap = canvasBitmap.getContext('bitmaprenderer');
   ...
  worker2.onmessage = function (e) {
    ctxBitmap.transferFromImageBitmap(e.data.imageBitmap);
  }
}


首先,把获取渲染上下文的id改成“bitmaprenderer”,返回额ctxBitmap是一个ImageBitmapRenderingContext对象。然后,在渲染ImageBitmap对象的时候,把drawImage函数改为transferFromImageBitmap函数。

最终渲染效果和上图显示一样。


transferControlToOffscreen函数


transferControlToOffscreen函数可以通过页面的canvas对象来创建一个OffscreenCanvas。既然可以通过构造函数创建OffscreenCanvas对象,为啥还需要这样操作。原因是这样的:我们看前面一个示例,我们在worker线程中创建OffscreenCanvas对象并绘制然后获取ImageBitmap对象,通过web worker通信把ImageBitmap传递给页面。


而如果通过canvas.transferControlToOffscreen生成的OffscreenCanvas对象,不需要再通过web worker通信来传递绘制的效果,生成了OffscreenCanvas对象之后,OffscreenCanvas对象的绘制会自动在canvas元素上面显示出来。这相对于web worker通信有着不言而喻的优势。


通过transferControlToOffscreen函数创建的OffscreenCanvas对象有两大功能:

  • 避免绘制中大量的计算阻塞主线程
  • 避免主线程的重任务阻塞绘制


下面我们将会通过示例来说明以上结论。

首先,我们写一个Circle类,这个类的作用主要是用于绘制一个圆,并且可以启动动画,不断的改变圆的半径大小:


class Circle {
   constructor(ctx){
     this.ctx = ctx;
     this.r = 0;
     this.rMax = 50;
     this.color = 'black';
     this.bindAnimate = this.animate.bind(this);
   }
   draw(){
     this.ctx.fillStyle = this.color;
     this.ctx.beginPath();
     this.ctx.arc(this.ctx.canvas.width/2,this.ctx.canvas.height/2,this.r,0,Math.PI*2);
     this.ctx.fill();
   }
   animate(){
      this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
      this.r =  this.r + 1;
      if(this.r > this.rMax){
        this.r = 0;
      }
      this.draw();
      requestAnimationFrame(this.bindAnimate);
   }
   changeColor(){
     fibonacci(41);
 if(this.color == 'black'){
       this.color = 'blue';
     }else{
       this.color = 'black';
     }
     this.r = 0;
   }
}


  • draw 函数用于绘制一个填充的圆形
  • animate 用于动画,其不断改变圆形的半径


另外还有一个函数changeColor,表示改变绘制的颜色,其会在黑色和蓝色之间不断变化,本示例中,为了模拟比较耗时的操作,在changeColor函数中,调用了下fibonacci函数,fibonacci函数用于计算斐波那契数列,当传入值是41的时候,计算量较大,主线程会把阻塞一段时间。下面是fibonacci的定义:


function fibonacci(num) {
  return (num <= 1) ? 1 : fibonacci(num - 1) + fibonacci(num - 2);
}


然后,我们定义两个canvas,一个用于普通的canvas应用,一个用于呈现离屏绘制的内容:


<canvas id="canvas-window" width="300" height="400" style="background: white;left: 10px;top: 20px;position: relative;"></canvas>
  <canvas id="canvas-worker" width="300" height="400" style="background: white;left: 10px;top: 20px;position: relative;"></canvas>


对于第一个canvas,我们直接在其上不断绘制半径变化的圆形:


var canvasInWindow = document.getElementById('canvas-window');
    var ctx = canvasInWindow.getContext('2d');
    var circle = new Circle(ctx);
    circle.animate();
    canvasInWindow.addEventListener('click', function () {
      circle.changeColor();
    });


并在该canvas上添加‘click’事件,当点击时,调用Circle类的changeColor函数。

对于第二个canvas,我们使用webworker,首先使用transferControlToOffscreen函数创建OffscreenCanvas对象offscreen,然后创建worker对象,并把offscreen发送给worker线程:


var canvasInWorker = document.getElementById('canvas-worker');
    // var ctxInWorkder = canvasInWorker.getContext('2d');
    var offscreen = canvasInWorker.transferControlToOffscreen();
    var worker = new Worker('./worker.js');
    worker.postMessage({ msg: 'start', canvas: offscreen }, [offscreen]);
    canvasInWorker.addEventListener('click', function () {
      worker.postMessage({msg:'changeColor'});
    });
    // canvasInWorker.getContext('2d'); // 会报错


该canvas上同样添加‘click’事件,当点击时,发送changeColor的命令给worker线程。


然后,我们看下worker.js线程的内容:


var offscreen = null,ctx,circle;
onmessage = function (e) {
    var data = e.data;
    if(data.msg == 'start'){
      offscreen = data.canvas;
      ctx = offscreen.getContext('2d');
      circle = new Circle(ctx);
      circle.animate();
    } else if (data.msg == 'changeColor' && circle) {
      circle.changeColor();
    }
}
function fibonacci(num) {
  return (num <= 1) ? 1 : fibonacci(num - 1) + fibonacci(num - 2);
}
class Circle {
  constructor(ctx) {
    this.ctx = ctx;
    this.r = 0;
    this.rMax = 50;
    this.color = 'black';
    this.bindAnimate = this.animate.bind(this);
  }
  draw() {
    this.ctx.fillStyle = this.color;
    this.ctx.beginPath();
    this.ctx.arc(this.ctx.canvas.width / 2, this.ctx.canvas.height / 2, this.r, 0, Math.PI * 2);
    this.ctx.fill();
  }
  animate() {
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
    this.r = this.r + 1;
    if (this.r > this.rMax) {
      this.r = 0;
    }
    this.draw();
    requestAnimationFrame(this.bindAnimate);
  }
  changeColor() {
    fibonacci(41);
    if (this.color == 'black') {
      this.color = 'blue';
    } else {
      this.color = 'black';
    }
    this.r = 0;
  }
}


在worker.js中,定义了一个同样的Circle类和fibonacci函数。在onmessage函数中,接受页面端传递来的信息,当接受到start命令时,在接收到的OffscreenCanvas对象offscreen上绘制圆形的动画。当接受到changeColor命令时,调用Circle类的changeColor函数。


读者可以看出,在worker线程中绘制了图形之后,并没有传递给页面端,其内容会自动显示给页面的断的canvas。最终显示的效果如下图:


微信图片_20220424105000.gif


可以看到两个canvas都在绘制动画。区别在于,单击的时候,都会调用比较重的changeColor函数,页面端的canvas会阻塞主线程,而离屏的canvas不会阻塞主线程,演示如下。


微信图片_20220424105029.gif


除了不阻塞主线程之外,离屏的OffscreenCanvas对象也不会被主线程的重任务阻塞,比如我们在页面添加一个button,调用一个耗时的任务:


<button id='heavyTask' style="position: absolute;display:inline;left: 100px;"  onclick="heavyTask()">heavyTask</button>


其实耗时的任务还是用了fibonacci函数来模拟:


function heavyTask() {
   fibonacci(41);
}


当点击按钮的时候,页面的canvas会停止动画,而离屏的canvas不会停止动画:


微信图片_20220424105119.gif


如果读者不清楚canvas相关知识点,建议学习相关知识,也推荐有兴趣读者,订阅专栏(本文内容就摘取自专栏):Canvas高级进阶 https://xiaozhuanlan.com/canvas,相关知识会在专栏中介绍。

相关文章
|
前端开发 JavaScript 定位技术
GIS前端编程-Leaflet插件扩展
GIS前端编程-Leaflet插件扩展
266 0
|
前端开发 应用服务中间件 API
VUE+websocket编写实现PC web端控制摄像头
WebSocket是一种全双工通信的数据通信协议。WebSocket的主要功能用处是允许服务器主动地向客户端推送数据信息,使得客户端和服务端之间的数据交换变得更加的简单。
VUE+websocket编写实现PC web端控制摄像头
|
移动开发 前端开发 JavaScript
使用html-to-image代替html2canvas,结合jspdf实现下载pdf(下载截图下载前端dom元素)
本文介绍了在前端项目中,当使用`html2canvas`遇到问题时,如何使用`html-to-image`库作为替代方案,结合`jspdf`实现将DOM元素生成为PDF文件并提供下载。文章首先讨论了`html2canvas`可能遇到的问题,并提供了该库的使用示例代码。随后,详细介绍了`html-to-image`库的安装和使用方法,展示了如何将DOM元素转换为Canvas,再利用`jspdf`生成PDF文件。最后,文章通过示例代码说明了整个转换和下载的过程,并展示了效果截图。
972 0
|
5月前
|
人工智能 开发框架 前端开发
斩获3K+ star,再见传统开发!这款开源AI后台开发框架让效率提升300%
ruoyi-ai 是基于 ruoyi-plus 框架开发的开源 AI 平台,集成 ChatGPT4、DALL·E-3 和 MidJourney 等前沿模型,提供聊天、绘画、语音克隆等全栈式 AI 能力。其核心价值在于多模态交互与企业级部署支持,开发者可快速搭建智能应用,个人用户亦能轻松体验 AI 创作魅力。项目支持自定义知识库训练、AI 绘画生成、语音克隆、弹幕互动等功能,采用 Java17+SpringBoot3.X 技术栈,前后端分离设计,具备高效性能与扩展性。相比同类项目,ruoyi-ai 提供更丰富的功能组合和企业级管理能力,适用于多种场景需求。
601 3
axios的get请求传入数组参数
axios的get请求传入数组参数
|
11月前
|
缓存 前端开发 JavaScript
Webpack 4 和 Webpack 5 区别?
【10月更文挑战第23天】随着时间的推移,Webpack 可能会继续发展和演进,未来的版本可能会带来更多的新特性和改进。保持对技术发展的关注和学习,将有助于我们更好地应对不断变化的前端开发环境。
|
Linux 数据处理
Linux中的numfmt命令:数字格式化的强大工具
**numfmt命令在Linux中用于数字格式化,如转换进制、添加千位分隔符、处理字节单位。它支持从文件读取数字并能自定义分隔符、小数位数。例如:`numfmt 12345` 输出12,345(十进制),`numfmt -b 255` 输出11111111(二进制),`numfmt --to=iec 1000000` 输出976.6K(字节单位)。使用时注意选项组合及单位标准。**
|
存储 安全 数据管理
新一代数据库技术:融合区块链的分布式数据存储系统
传统数据库系统面临着数据安全性、可信度和去中心化等挑战,而区块链技术的兴起为解决这些问题提供了新的思路。本文介绍了一种新一代数据库技术,将区块链技术与传统的分布式数据存储系统相融合,实现了更高水平的数据安全性和可信度,以及去中心化的优势。通过结合区块链的不可篡改性和分布式存储系统的高性能,这一新型数据库技术将在未来的数据管理领域发挥重要作用。
|
存储 SQL 算法
动态规划Dynamic programming详解-背包问题【python】
动态规划Dynamic programming详解-背包问题【python】
|
JavaScript
【Js】检查Date对象是否为Invalid Date
【Js】检查Date对象是否为Invalid Date
402 0