首先介绍下canvas, 前端的同学可能很熟悉,举个很简单的例子, 平常用的网页截图、H5游戏、前端动效、可视化图表...,都有canvas 的应用场景,我们看下官方的定义:
canvas是HTML5提供的一种新标签, ie9才开始支持的,canvas是一个矩形区域的画布,可以用JS控制每一个像素在上面绘画。canvas 标签使用 JavaScript 在网页上绘制图像,本身不具备绘图功能。canvas 拥有多种绘制路径、矩形、圆形、字符以及添加图像的方法。
看着很简单,其实canvas这个标签的加入,赋予了我们更多创建惊艳的前端效果的能力。但是你知道他也有性能问题??本篇文章就简单谈一谈canvas的性能优化。
好了现在进入今天的主题:「canvas 性能优化」, 读完本篇文章你可以学到下面:
我们都知道浏览器上渲染动画 每一秒高达60帧,也就是1秒钟内我们完成60次图像绘制, 也就是每一帧图像的绘制时间其实就是(1000/ 60)。如果在每一帧动画的时间小于 16.7 ms 辣么就会出现卡顿、丢帧。而canvas 其实是一个「指令式绘图系统」, 他通过绘图指令来完成绘图操作。那么很容易想到两个很关键的因素:
很容易理解,假设绘制 绘制一个图形 几毫秒 那么如果绘制10000个图形呢??肯定时间就长了, 如果后面其他操作、ui交互、渲染其他图形... 这就会导致渲染的时间比较长了。canvas 绘制的图像都是一个个小像素点构成的、 你绘制一个半径为5 的圆 和半径为1000的圆所需要的像素点 肯定是不一样的。这里给大家看一张图清晰的明白一个绘制的构成。
图片
你图形数量越大需要的像素点就越多,那么 片元着色器就要不断的去执行。
我写了「两个小demo」 来验证我们的猜想,纸上得来终觉浅,绝知此事要躬行哇!
主要是绘制100个 和绘制10000个小球作对比
我们先看下代码:
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const WIDTH = canvas.width
const HEIGHT = canvas.height
function randomColor() {
return (
'rgb( ' +
((Math.random() * 255) >> 0) +
',' +
((Math.random() * 255) >> 0) +
',' +
((Math.random() * 255) >> 0) +
' )'
)
}
function drawCirle(radius = 10) {
const x = Math.random() * WIDTH
const y = Math.random() * HEIGHT
ctx.fillStyle = randomColor()
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.fill()
}
function draw(count = 1) {
for (let i = 0; i < count; i++) {
drawCirle()
}
}
function update() {
ctx.clearRect(0, 0, WIDTH, HEIGHT)
draw()
requestAnimationFrame(update)
}
requestAnimationFrame(update)
绘制100 个小球的fps 还是能够稳定在30fps 的如图:
100个
100个
绘制10000个小球的fps 掉帧的厉害 是真的卡。所以也就验证了我们的猜想 ,canvas 的性能是和绘制图形个数有关系的
这个fps 帧率显示器是 chrome 自带的 输入 command + shift + p 搜索 render fps 自然就找到了。
我们再来验证绘制图形的大小会不会影响fps 。我将小球的数量改成1000, 半径改成10 。
目前的「小球的数量 是1000」 , 「半径大小是10」。
我们看下gif 图:
我们看到帧率大概是稳定在30fps 的, 这时候 数量不变的情况下,我将小球的半径改成200, 我们在看下帧率变化,然后呢 你会看到帧率有较为明显的下跌。如图:
Oct-24-2021 20-24-16
所以总结以上来看,影响canvas的因素就是有以上两点:
其实我来深度分析, 「第一个渲染的图形数量多,就是调用绘图指令的次数比较多,」
「第二个渲染的图形大,就是一次绘图渲染的时间比较长」
「然后下面我就开始优化canvas」
这句话怎么理解呢 , 假设你要在场景中画正n变形,这是一个 很常见的需求可能你稍不注意写下了下面这几行代码:
function drawAnyShape(points) {
for(let i=0; i<points.length; i++) {
const p1 = points[i]
const p2 = i=== points.length - 1 ? points[0] : points[i+1]
ctx.fillStyle = 'black'
ctx.beginPath();
ctx.moveTo(...p1)
ctx.lineTo(...p2)
ctx.closePath();
ctx.stroke()
}
}
points 对应的生成多边形的点,代码如下:
function generatePolygon(x,y,r, edges = 3) {
const points = []
const detla = 2* Math.PI / edges;
for(let i= 0;i<edges;i++) {
const theta = i * detla;
points.push([x+ r * Math.sin(theta), y + r * Math.cos(theta)])
}
return points
}
主要是根据一个圆心,根据边数生成对应的点。
乍一看这代码没什么问题,生成一个正多边形, 这时候我在页面上画了1000个正多边形:如下图:
优化前
一看这fps低成这个样子,很多人这时候说,你画的图形多,那我只要悄悄的改下代码,就能让fps 回归正常
我又重写了正多边形的方法:
function drawAnyShape2(points) {
ctx.beginPath();
ctx.moveTo(...points[0]);
ctx.fillStyle = 'black'
for(let i=1; i<points.length; i++) {
ctx.lineTo(...points[i])
}
ctx.closePath();
ctx.stroke()
}
我们看下这时候的fps 帧率:
看了下fps 已经成功升到了30fps, 这是为什么呢, 第一段我们在循环中去做绘图操作, 循环一次, stoke() 一次,这显然是不合理的,第二个直接把stoke() ,放到循环外,其实就调用了一次,所以我们可以得出减少绘图指令是可以提高「canvas的性能的」
为什么需要分层渲染, 在游戏中,假设人物的不停地在移动,但是呢背景可能加了很多花里胡里呼哨的元素,但是我在每一次更新的时候,场景本身是不变的,变的只有人物不停的移动,如果每一帧再去重绘不就造成了性能浪费, 这时候分层canvas就出现了 我们先看下一张图你可能就明白了。
分层渲染
我通过3个canvas叠在一起,通过设置每个canvas的 z-index 达到了3个画布还是在同一层的错觉,这样我在requestAnimation中,只需要对 动的图形去做重新绘制就好了,其余的依旧是保持不动 。
我写了下面这些伪代码
<canvas id="backgroundCanvas" />
<canvas id="peopleActionCanvas" />
const peopleActionCanvas = document.getElementById('peopleActionCanvas');
const backgroundCanvas = document.getElementById('backgroundCanvas');
function draw(){
drawPeopleAction(peopleActionCanvas);
if (needDrawBackground) {
drawBackground(backgroundCanvas);
}
requestAnimationFrame(draw);
}
一个背景层一个运动层, 在抽象一点,我们什么时候应该去做分层 ,如果画布纯是静态的就没有必要去做分层了, 如果当前有静态有东动态的,你可以逻辑层放在最上面,然后展示层 放在最底下就可以实现所谓的 分层渲染了,但是最好保持在3-5个。
局部渲染的话其实就是调用canvas 的 clip方法, 这个方法很多同学不知道是什么。我们先看下官方文档MDN 对这个方法的使用
**CanvasRenderingContext2D**
「.clip()
」 是 Canvas 2D API 将当前创建的路径设置为当前剪切路径的方法
你可以试着思考一下 如何用canvas 画一个1/4圆。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red'
ctx.arc(100, 100, 75, 0, Math.PI*2, false);
//ctx.clip();
ctx.fillRect(0, 0, 100,100);
这里填充的时候 没有用clip 画面上应该是一个矩形。如图:
矩形
这时候我把clip注释解开来, 你会发现矩形 变成了一个半圆。所以clip 这个 api 结合 fillRect 填充 就是实现填充任意图形路径。我们看下图:
半圆
这时候就会有同学问这东西和我们局部渲染有什么关系,大家都知道canvas 实现动画,每一帧都会把当前画面的上的东西全部清楚然后再重新更新一遍,看下下面这个场景:
for(let i=0;i< 20;i++) {
ctx.beginPath();
ctx.fillStyle = `rgba(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255},1)`
ctx.arc(Math.random() * 800,Math.random() * 800, Math.random() * 100, 0, Math.PI*2, false);
ctx.fill()
}
随机生成了20个圆形, 如图所示:
random
这时候如果我说我只想改中间那个绿色的圆颜色我只想把它改成蓝色,但是其他的圆其实没有属性上的改变,按照正常的操作应该就是下面几步
这对于画布中图形比较少的情况下是比较OK的,但是如果当画布出现下面这样子是不是就会出现所谓的渲染浪费嘛
1000个图形
这是canvas 中画了1000 个圆形, 如果你只改一个颜色,那其他999都是不变的 这种浪费是肯定存在性能问题, 如果在做动画效果可想而知,丢帧非常厉害。这里就可以使用我们上面的api
红色区域
正确的做法其实就是我们要做局部刷新:
「clip()」 确定绘制的的裁剪区域,区域之外的图形不能绘制,详情查看 CanvasRenderingContext2D.clip()「clearRect(x, y, width, height)」 擦除指定矩形内的颜色,查看 CanvasRenderingContext2D.clearRect()
我们刚才说的用一个框去把图形包围住, 其实在几何中我们叫「包围盒」 或者是「boundingBox」。可以用来「快速检测」两个图形是否相交, 但是还是不够准确。最好还是用图形算法去解决。或者游戏中的碰撞检测,都有这个概念。因为我这里讨论的是2d的boudingbox, 还是比较简单的。我给你看几张图, 或许你就瞬间明白了。
image-20210822113735608
任意多边形
虚线框其实就是boundingBox, 其实就是根据图形的大小,算出一个矩形边框。理论我们知道了,映射到代码层次, 我们怎么去表达呢? 我这里带大家原生实现一下bound2d 类, 其实我们每个2d图形,都可以去实现。因为2d图形都是由点组成的,所以只要获得每一个图形的离散点集合, 然后对这些点,去获得一个2d空间的boundBox。
想看具体的实现可以参考「three.js box2d」 的源码, 写的很清楚,这里就不过多介绍了。
我们先说下 什么是离屏canvas???
「OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。它在窗口环境和web worker环境均有效。
」
脱离屏幕渲染的canvas对象,这对我们实际写动画的时候真的有用吗???
答案是肯定的
想象以下这个场景:如果发现自己在每个动画帧上重复了一些相同的绘制操作,请考虑将其分流到屏幕外的画布上。然后,您可以根据需要频繁地将屏幕外图像渲染到主画布上,而不必首先重复生成该图像的步骤。由于浏览器是单线程,canvas的计算和渲染其实是在同一个线程的。这就会导致在动画中(有时候很耗时)的计算操作将会导致App卡顿,降低用户体验。
幸运的是, OffscreenCanvas 离屏Canvas可以非常棒的解决这个麻烦!
到目前为止,canvas的绘制功能都与<canvas>
标签绑定在一起,这意味着canvas API和DOM是耦合的。而OffscreenCanvas,正如它的名字一样,通过将Canvas移出屏幕来解耦了DOM和canvas API。
由于这种解耦,OffscreenCanvas的渲染与DOM完全分离了开来,并且比普通canvas速度提升了一些,而这只是因为两者(Canvas和DOM)之间没有同步。但更重要的是,将两者分离后,canvas将可以在Web Worker中使用,即使在Web Worker中没有DOM。这给canvas提供了更多的可能性。
这就离屏canvas 为啥和webworker 这么配的缘故了。
创建离屏canvas有两种方式:
「一种」是通过OffscreenCanvas的构造函数直接创建。比如下面的示例代码:
// 离屏canvas
const offscreen = new OffscreenCanvas(200, 200);
「第二种是使用canvas的transferControlToOffscreen」函数获取一个OffscreenCanvas对象,绘制该OffscreenCanvas对象,同时会绘制canvas对象。比如如下代码:
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
我写了下面这个小demo 验证下到底是不是可靠的
const canvas = document.getElementById('canvas');
// 离屏canvas
const offscreen1 = new OffscreenCanvas(200, 200);
const offscreen2 = canvas.transferControlToOffscreen();
console.error(offscreen1,offscreen2, '222')
打开浏览器的截图如下:
离屏canvas
没什么问题,我们的猜想是对的
那么问题又来了,离屏canvas怎么与主线程的canvas通信呢
这时候引用另外一个api 「transferToImageBitmap」
通过transferToImageBitmap函数可以从OffscreenCanvas对象的绘制内容创建一个ImageBitmap对象。该对象可以用于到其他canvas的绘制。
比如一个常见的使用是,把一个比较耗费时间的绘制放到「web worker」下的OffscreenCanvas对象上进行,绘制完成后,创建一个ImageBitmap对象,并把该对象传递给页面端,在页面端绘制ImageBitmap对象。
我们写个小demo测试下:
我们画 10000 * 10000 个矩形看看页面的响应和时间,代码如下:
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function draw() {
for(let i = 0;i < 10000;i ++){
for(let j = 0;j < 1000;j ++){
ctx.fillRect(i*3,j*3,2,2);
}
}
}
draw()
ctx.arc(100,75,50,0,2*Math.PI);
ctx.stroke()
我们直接看效果:
before
可以很明显的感受到,在渲染出图形前,浏览器是失去响应的,我们无法做认可操作。这样的用户体验肯定是非常差的。
我们使用离屏canvas + webworker 进行优化,代码如下:
我们先看下worker 的代码:
let offscreen,ctx;
// 监听主线程发的信息
onmessage = function (e) {
if(e.data.msg == 'init'){
init();
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);
}
}
const imageBitmap = offscreen.transferToImageBitmap();
// 传送给主线程
postMessage({imageBitmap:imageBitmap},[imageBitmap]);
}
看下主线程的代码:
const worker = new Worker('./worker.js')
worker.postMessage({msg:'init'});
worker.onmessage = function (e) {
// 这里就接受到work 传来的离屏canvas位图
ctx.drawImage(e.data.imageBitmap,0,0);
}
ctx.arc(100,75,50,0,2*Math.PI);
ctx.stroke()
直接看效果:
after
对比两个很明显的变化, 画多个矩形是个非常耗时的操作会影响其他图形渲染,可以采用离屏canvas + webworker 来解决这种失去响应。
本期的canvas优化就到这里了,如果有什么问题,欢迎评论区和我交流,或者有什么写的不对的地方欢迎指正。我简单做一个总结
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/qEr5Tm49bAlJjgyDUp_3_w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。