-
2021-08-10 16:53:25
canvas 2d 绘图(一)
约定原则:手绘画图原则
- 画布 一张无线大的能作画的布,默认以左上角顶点位置为原始坐标
- 画笔 一只能够变换任何颜色,任意粗细程度的画笔
- 填充 在绘制的范围内填充任何颜色,包含渐变和透明
- 图形 圆形/矩形/路径
- 路径记忆 将上次创建过的路径保存起来,下次继续使用
- 变形 添加一层自定义的透明画布进行绘画
- 画布快照 对画布进行编号,拿起上一次遍过号的画布来绘画
准备工作
获取dom
var canvas = document.getElementById("myCanvas")
生成作画空间
var ctx = canvas.getContext("2d")
设置画笔
// 设定画笔颜色 (单位为前端常用的正常色值) ctx.strokeStyle = "orange" // 设定线条宽度 (单位为像素) ctx.lineWidth = 10 // 设置线条末端样式 var lineCap = ['butt','round','square'] ctx.lineCap = lineCap[i]; // 设定线条与线条间接合处的样式 var lineJoin = ['round', 'bevel', 'miter']; ctx.lineJoin = lineJoin[i] // 限制当两条线相交时交接处最大长度 ctx.miterLimit = 8.8; // 设置当前虚线样式 setLineDash数组指定线段与间隙的交替, lineDashOffset属性设置起始偏移量. ctx.setLineDash([4, 2]); ctx.lineDashOffset = 0; // 返回一个包含当前虚线样式,长度为非负偶数的数组 ctx.getLineDash() // [4, 2]
填充
// 设置填充颜色 (单位为前端常用的正常色值) ctx.fillStyle = "rgba(255,165,0,1)"; // 设定渐变样式 (线性渐变 => 表示渐变的起点 (x1,y1) 与终点 (x2,y2) var lineargradient = ctx.createLinearGradient(0,0,150,150); // 上色 (第一个参数表示0.0 与 1.0 之间的数值,第二个参数表示对应位置的颜色) lineargradient.addColorStop(0,'white'); lineargradient.addColorStop(1,'black'); // 渐变样式能够添加到fillStyle,也能添加到strokeStyle ctx.fillStyle = lineargradient; ctx.strokeStyle = lineargradient; // 设定渐变样式 (径向渐变 => 前三个定义一个以 (x1,y1) 为原点,半径为 r1 的圆,后三个参数则定义另一个以 (x2,y2) 为原点,半径为 r2 的圆。 var radgrad = ctx.createRadialGradient(45,45,10,52,50,30); radgrad.addColorStop(0, '#A7D30C'); radgrad.addColorStop(0.9, '#019F62'); radgrad.addColorStop(1, 'rgba(1,159,98,0)'); // 图案填充 (需要保证img已经加载完毕) var ptrn = ctx.createPattern(img, 'repeat'); ctx.fillStyle = ptrn; ctx.fillRect(0, 0, 150, 150); // 阴影 (x方向/y方向/阴影模糊程度/阴影颜色) ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; ctx.shadowBlur = 2; ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; // 填充规则 (该填充规则根据某处在路径的外面或者里面来决定该处是否被填充) ctx.fill("evenodd"); // nonzero为默认值
图形
// 矩形 (参数为起点,距离) fillRect(x,y,width,height) strokeRect(x,y,width,height) clearRect(x,y,width,height) // 路径 beginPath() // 新建一条路径 closePath() // 闭合路径 moveTo(x,y) // 移动画笔到某个位置 lineTo(x,y) // 从上一个点开始划线到目标点 // 圆形 (画一个以x,y为圆心的以radius为半径的圆弧/圆,从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成) ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise) // 绘制 stroke() // 通过线条来绘制图形轮廓 fill() // 通过填充路径生成实心的图形 // 绘制二次贝塞尔曲线,cp1x,cp1y为一个控制点,x,y为结束点 (注意绘制贝塞尔曲线之前确定有一个起始点) quadraticCurveTo(cp1x, cp1y, x, y) // 绘制三次贝塞尔曲线,cp1x,cp1y为控制点一,cp2x,cp2y为控制点二,x,y为结束点 (注意绘制贝塞尔曲线之前确定有一个起始点) bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
路径记忆
// 将对应的线路路径保存起来 new Path2D(); // 空的Path对象 new Path2D(path); // 克隆Path对象 new Path2D(d); // 从SVG建立Path对象
变形
// 温馨提示:在执行变形操作之前,使用ctx.save保存格式快照 translate(x,y) // 移动canvas的原点位置 rotate(angle) // 旋转canvas的角度, 参数为顺时针以弧度为单位的值 scale(x,y) // 缩放画布的水平和垂直单位,默认值为 1 // 最后一个方法允许对变形矩阵直接修改。 // 这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform 方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成。 transform(a, b, c, d, e, f)
画布快照
// 使用场景,当需要临时大量改变绘制格式的时候,而之后还要恢复绘制格式的情况下使用 // 保存绘制格式快照 ctx.save() // 返回上一次绘制格式快照 ctx.restore()
综合场景
// 获取画布地图 var c=document.getElementById("myCanvas"); // 获取2d生成能力 var cxt=c.getContext("2d"); // 设置填充颜色 cxt.fillStyle="#FF0000"; // 设置矩形坐标 cxt.fillRect(0,0,150,75); // 设置画笔起点 cxt.moveTo(10,10); // 连接线条到150,50坐标 cxt.lineTo(150,50); // 从150,50连接线条到10,50坐标 cxt.lineTo(10,50); // 结束 cxt.stroke(); // 设置开始路径 cxt.beginPath(); // 设置圆的大小 cxt.arc(70,18,15,0,Math.PI*2,true); // 结束路径 cxt.closePath(); // 填充 cxt.fill(); // 设置渐变背景坐标 var grd=cxt.createLinearGradient(0,0,175,50); // 设定起点颜色 grd.addColorStop(0,"#FF0000"); // 设定第2起点颜色 grd.addColorStop(1,"#00FF00"); // 填充颜色 cxt.fillStyle=grd; // 填充到对应的坐标 cxt.fillRect(0,0,175,50); // 画布设置出图片 var img=new Image() // new出的图片设置src img.src="flower.png" // 绘画从0开始 cxt.drawImage(img,0,0);
canvas 2d 绘图(二)
约定原则:手绘画图原则
- 裁剪
- 合成
- 像素操作
- 动画
- 事件 (点击区域)
- 性能优化
裁剪
// 将当前正在构建的路径转换为当前的裁剪路径 (默认情况下,canvas 有一个与它自身一样大的裁切路径(也就是没有裁切效果)) // 设定矩形 -> 裁剪 -> 填充 => 只显示矩形这一部分的图形 ctx.rect(50, 50, 20, 10); ctx.clip(); ctx.fillRect(0, 0, 100, 100);
合成
// 这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识12种遮盖方式的字符串 // 通常用于新图形和旧图形相交部分,做自定义处理 ctx.globalCompositeOperation = "difference"; // 下面两个矩形相交的地方,会随着globalCompositeOperation的设置而变更 ctx.fillStyle = "blue"; ctx.fillRect(10, 10, 100, 100); ctx.fillStyle = "red"; ctx.fillRect(50, 50, 100, 100);
像素操作
// 创建一个ImageData对象 const myImageData = ctx.createImageData(width, height); // 得到场景像素数据 (参数数据算法和矩形算法一致) const myImageData = ctx.getImageData(left, top, width, height); // 获取某一点的rgba像素值 const pixel = ctx.getImageData(0, 0, 1, 1); const data = pixel.data; const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`; // rgba(67,73,35,1) // 写入像素数据 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // 获取整个画布的像素数据 const data = imageData.data; for (var i = 0; i < data.length; i += 4) { // 改变像素数据 data[i] = 255 - data[i]; // red data[i + 1] = 255 - data[i + 1]; // green data[i + 2] = 255 - data[i + 2]; // blue } ctx.putImageData(imageData, 0, 0); // 写入像素数据到原本的画布 (这里是将像素反相) // 缩放 (canvas为原画布), Math.abs(x - 5), Math.abs(y - 5)为获取原画布的x,y起点,10/10表示起点之后的距离,0 0 表示新画布的起点,200 200表示画布的宽高 zoomctx.drawImage(canvas, Math.abs(x - 5), Math.abs(y - 5), 10, 10, 0, 0, 200, 200); // 反锯齿属性 (默认为true) zoomctx.imageSmoothingEnabled = true; // 保存图片 canvas.toDataURL('image/png') // 默认设定。创建一个PNG图片。 canvas.toDataURL('image/jpeg', quality) // 创建一个JPG图片。你可以有选择地提供从0到1的品质量,1表示最好品质,0基本不被辨析但有比较小的文件大小。 // 你也可以从画布中创建一个Blob对像。 canvas.toBlob(callback, type, encoderOptions) // 这个创建了一个在画布中的代表图片的Blob对像。 // callback 回调函数,可获得一个单独的Blob对象参数。 // type DOMString类型,指定图片格式,默认格式为image/png。 // encoderOptions 可选 // Number类型,值在0与1之间,当请求图片格式为image/jpeg或者image/webp时用来指定图片展示质量。如果这个参数的值不在指定类型与范围之内,则使用默认值,其余参数将被忽略。
动画
// 每隔一段时间执行 window.setInterval() / window.clearInterval() // 过一段时间执行一次 window.setTimeout() / window.clearTimeout() // 浏览器重绘之前调用指定的回调函数更新动画 window.requestAnimationFrame() / window.cancelAnimationFrame()
事件
1. 通过设定指定区域,然后判断鼠标点击的位置是否在制定区域从而是否触发动画 (适用于矩形) 2. 先将需要执行点击事件的图形通过离屏绘制出来,然后添加到画布,并且记录距离画布的left/top 点击事件的时候,获取到对应的x,y点,然后使用x-left,y-top , 得到离屏绘制的图形范围内相对应的位置,然后根据这个位置在离屏绘制的图形中判断data的alpha值 window.onload = function () { // 主画布 const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); canvas.width = dw canvas.height = dh // 创建一个离屏画布 const { canvas: newCanvas, ctx: newCtx } = created() // 再创建一个 const { canvas: oldCanvas, ctx: oldCtx } = created() // 保存对应的ctx到数组 const list = [newCtx, oldCtx] // 主画布追加离屏画布 ctx.drawImage(newCanvas, -10, -10) ctx.drawImage(oldCanvas, 100, 100) // 监听click事件 canvas.addEventListener('click', () => { // 获取对应的xy const x = event.layerX const y = event.layerY // 循环离屏画布 for (let i = 0; i < list.length; i++) { // 获取追加离屏画布时候的位置值 const addx = i === 0 ? -10 : 100 const addy = i === 0 ? -10 : 100 // 得到离屏画布的像素值(位置经过计算得到相对应的坐标点) const pixel = list[1].getImageData(x - addx, y - addy, 1, 1); const data = pixel.data; // 判断当前像素点的alpha值是否为 0 if (data[3] !== 0) { console.log('你点击的是离屏画布 ' + i); } } }) }
更多相关内容 -
SVG/Canvas会触发重绘(repaint)和重排/回流(reflow)吗?
2022-04-07 14:43:17SVG/Canvas会触发重绘(repaint)和重排/回流(reflow)吗?
我有一个同门认为SVG里元素的改变不会触发重排,但是我觉得会,所以这篇文章需要被扩展一下,想要深入讨论一下这个问题。一、什么是重绘和重排?
1.1 浏览器渲染的过程
- 根据html文件构建DOM树和CSSOM树。构建DOM树期间,如果遇到JS,阻塞DOM树及CSSOM树的构建,优先加载JS文件,加载完毕,再继续构建DOM树及CSSOM树。
- 构建渲染树(render Tree)。
- 页面的重绘(repaint)与重排/回流(webkit引擎—Safari称作reflow,Blink引擎—Chrome称作layout)。页面渲染完成后,若JS操作了DOM节点,根据JS对DOM操作动作的大小,浏览器对页面进行重绘或是重排。
重绘:某些元素的外观被改变,例如:元素的填充颜色。
重排:重新生成布局,重新排列元素。当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。1.2 减少重排的传统方法
- 样式集中改变
- 批量修改DOM元素
- 离线DOM元素,display:none
- 使用 absolute 或 fixed 脱离文档流
虽然修改元素的几何属性会导致浏览器触发重排或重绘时。但
现代浏览器
优化了这个过程,它会把这些操作放进渲染队列,等到队列中的操作到了一定的数量或者到了一定的时间间隔时,浏览器就会批量执行(flush
)这些操作。但有些操作会引起浏览器提前flush队列,比如,当我们向浏览器请求以下style信息时,就会提前让浏览器flush队列:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop/Left/Width/Height
- clientTop/Left/Width/Height
- width,height
- 请求了getComputedStyle()或者IE的currentStyle
原因:
请求以上这些值时,浏览器需要清空队列,计算出最新的元素尺寸和位置样式信息(重绘回流),因为浏览器认为队列中的某些操作会造成我们获取的值并不是最精确的!1.3 减少重排的现代方法
1.3.1 Document.Fragment
在使用JavaScript来操作DOM元素时,比如使用appendChild()方法。每次调用该方法时,浏览器都会重新渲染页面。如果大量的更新DOM节点,则会非常消耗性能,影响用户体验。
JavaScript提供了一个文档片段DocumentFragment的机制。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点。把所有要构造的节点都放在文档片段中执行,这样可以不影响文档树,也就不会造成页面渲染。当节点都构造完成后,再将文档片段对象添加到页面中,这时所有的节点都会一次性渲染出来,这样就能减少浏览器负担,提高页面渲染速度。(实际上还是一种批量修改DOM的方法)1.3.2 requestAnimationFrame
网页的动画效果主要有两种实现方式:
CSS3动画
– CSS是关键帧动画,补间动画部分由浏览器完成,便于浏览器进行优化,可以更好控制动画执行过程
– CSS的动画执行在合成线程,专事专干,不阻塞主线程,合成线程的动画也不会触发回流和重绘
– CSS动画允许在GPU,专注渲染,更快(复合图层的概念,后面会提到)JS动画
– JS是逐帧动画,每一帧都是由代码控制,操作不当,极易引发回流
– JS的动画执行在主线程,主线程还有其他任务要执行,容易引发阻塞和等待,降低动画执行效率
– JS动画运行在CPU,但CPU还有其他任务,易受影响
既然JS动画看上去这么拉,为什么要使用JS动画?因为可以通过编程实现复杂的动画效果。通常会使用setTimeout或者setInterval,以及requestAnimationFrame。
然而相比于setTimeout和setInterval而言,requestAnimationFrame有两个优点:
- requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
- 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的CPU、GPU和内存使用量。
二、Canvas会触发重绘和重排吗?
除非Canvas本身的位置或者大小发生变化,影响了
render tree
,才会发生重排和回流。理由是:重绘和回流都是相对于
render tree
上的元素而言的,而对canvas本身进行绘制并未对页面任何其他元素做更改,故只会引起Canvas画布本身的重绘。credit to.
html canvas 改变,html5 –
每当有任何变化时,Canvas会重绘自己吗?
Canvas vs HTML DOM?
操作canvas 会不会导致页面reflow?三、SVG会触发重绘和重排吗?
首先说结论:我现在觉得SVG其实并不会触发重排,因为SVG内存在一个坐标系,所有的SVG内的元素在SVG画布上是绝对定位的(除
<g>
内的子元素是相对于<g>
定位的,aka相对定位)。仅仅更新SVG内部的元素,只会相对于SVG(root),对其所有子元素进行布局的计算,不能称作浏览器的重排。但SVG内的DOM元素还是还会在DOM树里,也会在Render树里,只不过针对文档流而言,不会有位置的改变,所以不会引起重排?
至于为什么当SVG内的元素多了之后,效率相比于Canvas会低很多:- Immediate Mode(即时模式):Canvas没有DOM或文档对象模型。在使用Canvas绘制像素时,绘图指令执行了就不管了,减少了维护图形内部模型所需的额外内存。
- Retained Mode(保留模式):在使用SVG绘制图像的时候,绘制的每个对象都会添加到浏览器的内部模型中,使得性能降低。除此之外,SVG依旧依赖于Render树进行渲染,只不过不存在“重排”。
3.2 浏览器渲染时,产生图层
浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。在渲染DOM的时候,浏览器所做的工作实际上是:
- 获取DOM后分割为多个图层
- 对每个图层的节点计算样式结果 (
recalculate style
–样式重计算) - 为每个节点生成图形和位置 (
layout
–重排,回流) - 将每个节点绘制填充到图层位图中 (
paint
–重绘) - 图层作为纹理上传至GPU
- 组合多个图层到页面上生成最终屏幕图像 (
Composite Layers
–图层重组)
产生图层的条件:
- 拥有具有3D变换的CSS属性:
transform:rotate(7deg)
;rotateX()
、rotateY()
、rotateZ()
- 使用加速视频解码的节点
<canvas>
节点- CSS3动画的节点
- 拥有CSS加速属性的元素(will-change)
https://www.cnblogs.com/lichuntian/p/8616107.html
credit to
浅谈浏览器的图层与重绘重排(详细),以及如何用于性能优化
层叠上下文 渲染图层 复合图层(硬件加速)区别与联系四、如何减少SVG所带来的重绘和重排?
https://www.w3.org/TR/SVG11/struct.html#Head
-
原生JS使用Canvas实现拖拽式绘图功能
2020-12-11 06:51:284、拖拽式绘制(鼠标移动过程中不断进行canvas重绘) 5、图片绘制(作为背景图片时重绘会发生闪烁现象,暂时有点问题,后面继续完善) 5、清空绘制功能 6、新版本优化绘制性能(使用共享坐标变量数组,减少了大量的... -
canvas 性能优化
2021-04-08 14:31:04渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,留给我渲染一帧的时间,只有短短的 16ms。在这 16ms 中,我不仅需要处理一些游戏逻辑,计算每个对象的位置、状态,还需要把它们都画出来。如果消耗的...Canvas 最常见的用途是渲染动画。渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,留给我渲染一帧的时间,只有短短的 16ms。在这 16ms 中,我不仅需要处理一些游戏逻辑,计算每个对象的位置、状态,还需要把它们都画出来。如果消耗的时间稍稍多了一些,用户就会感受到「卡顿」。所以,在编写动画(和游戏)的时候,我无时无刻不担忧着动画的性能,唯恐对某个 API 的调用过于频繁,导致渲染的耗时延长。
计算与渲染
把动画的一帧渲染出来,需要经过以下步骤:
- 计算:处理游戏逻辑,计算每个对象的状态,不涉及 DOM 操作(当然也包含对 Canvas 上下文的操作)。
- 渲染:真正把对象绘制出来。
2.1. JavaScript 调用 DOM API(包括 Canvas API)以进行渲染。
2.2. 浏览器(通常是另一个渲染线程)把渲染后的结果呈现在屏幕上的过程。
之前曾说过,留给我们渲染每一帧的时间只有 16ms。然而,其实我们所做的只是上述的步骤中的 1 和 2.1,而步骤 2.2 则是浏览器在另一个线程(至少几乎所有现代浏览器是这样的)里完成的。动画流畅的真实前提是,以上所有工作都在 16ms 中完成,所以 JavaScript 层面消耗的时间最好控制在 10ms 以内。
虽然我们知道,通常情况下,渲染比计算的开销大很多(3~4 个量级)。除非我们用到了一些时间复杂度很高的算法(这一点在本文最后一节讨论),计算环节的优化没有必要深究。
我们需要深入研究的,是如何优化渲染的性能。而优化渲染性能的总体思路很简单,归纳为以下几点:
- 在每一帧中,尽可能减少调用渲染相关 API 的次数(通常是以计算的复杂化为代价的)。
- 在每一帧中,尽可能调用那些渲染开销较低的 API。
- 在每一帧中,尽可能以「导致渲染开销较低」的方式调用渲染相关 API。
Canvas 上下文是状态机
Canvas API 都在其上下文对象
context
上调用。var context = canvasElement.getContext('2d');
我们需要知道的第一件事就是,
context
是一个状态机。你可以改变context
的若干状态,而几乎所有的渲染操作,最终的效果与context
本身的状态有关系。比如,调用strokeRect
绘制的矩形边框,边框宽度取决于context
的状态lineWidth
,而后者是之前设置的。context.lineWidth = 5; context.strokeColor = 'rgba(1, 0.5, 0.5, 1)'; context.strokeRect(100, 100, 80, 80);
说到这里,和性能貌似还扯不上什么关系。那我现在就要告诉你,对
context.lineWidth
赋值的开销远远大于对一个普通对象赋值的开销,你会作如何感想。当然,这很容易理解。Canvas 上下文不是一个普通的对象,当你调用了
context.lineWidth = 5
时,浏览器会需要立刻地做一些事情,这样你下次调用诸如stroke
或strokeRect
等 API 时,画出来的线就正好是 5 个像素宽了(不难想象,这也是一种优化,否则,这些事情就要等到下次stroke
之前做,更加会影响性能)。我尝试执行以下赋值操作 106 次,得到的结果是:对一个普通对象的属性赋值只消耗了 3ms,而对
context
的属性赋值则消耗了 40ms。值得注意的是,如果你赋的值是非法的,浏览器还需要一些额外时间来处理非法输入,正如第三/四种情形所示,消耗了 140ms 甚至更多。somePlainObject.lineWidth = 5; // 3ms (10^6 times) context.lineWidth = 5; // 40ms context.lineWidth = 'Hello World!'; // 140ms context.lineWidth = {}; // 600ms
对
context
而言,对不同属性的赋值开销也是不同的。lineWidth
只是开销较小的一类。下面整理了为context
的一些其他的属性赋值的开销,如下所示。属性 开销 开销(非法赋值) line[Width/Join/Cap]
40+ 100+ [fill/stroke]Style
100+ 200+ font
1000+ 1000+ text[Align/Baseline]
60+ 100+ shadow[Blur/OffsetX]
40+ 100+ shadowColor
280+ 400+ 与真正的绘制操作相比,改变
context
状态的开销已经算比较小了,毕竟我们还没有真正开始绘制操作。我们需要了解,改变context
的属性并非是完全无代价的。我们可以通过适当地安排调用绘图 API 的顺序,降低context
状态改变的频率。分层 Canvas
分层 Canvas 在几乎任何动画区域较大,动画较复杂的情形下都是非常有必要的。分层 Canvas 能够大大降低完全不必要的渲染性能开销。分层渲染的思想被广泛用于图形相关的领域:从古老的皮影戏、套色印刷术,到现代电影/游戏工业,虚拟现实领域,等等。而分层 Canvas 只是分层渲染思想在 Canvas 动画上最最基本的应用而已。
分层 Canvas 的出发点是,动画中的每种元素(层),对渲染和动画的要求是不一样的。对很多游戏而言,主要角色变化的频率和幅度是很大的(他们通常都是走来走去,打打杀杀的),而背景变化的频率或幅度则相对较小(基本不变,或者缓慢变化,或者仅在某些时机变化)。很明显,我们需要很频繁地更新和重绘人物,但是对于背景,我们也许只需要绘制一次,也许只需要每隔 200ms 才重绘一次,绝对没有必要每 16ms 就重绘一次。
对于 Canvas 而言,能够在每层 Canvas 上保持不同的重绘频率已经是最大的好处了。然而,分层思想所解决的问题远不止如此。
使用上,分层 Canvas 也很简单。我们需要做的,仅仅是生成多个 Canvas 实例,把它们重叠放置,每个 Canvas 使用不同的 z-index 来定义堆叠的次序。然后仅在需要绘制该层的时候(也许是「永不」)进行重绘。
var contextBackground = canvasBackground.getContext('2d'); var contextForeground = canvasForeground.getContext('2d'); function render(){ drawForeground(contextForeground); if(needUpdateBackground){ drawBackground(contextBackground); } requestAnimationFrame(render); }
记住,堆叠在上方的 Canvas 中的内容会覆盖住下方 Canvas 中的内容。
绘制图像
目前,Canvas 中使用到最多的 API,非
drawImage
莫属了。(当然也有例外,你如果要用 Canvas 写图表,自然是半句也不会用到了)。drawImage
方法的格式如下所示:context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
数据源与绘制的性能
由于我们具备「把图片中的某一部分绘制到 Canvas 上」的能力,所以很多时候,我们会把多个游戏对象放在一张图片里面,以减少请求数量。这通常被称为「精灵图」。然而,这实际上存在着一些潜在的性能问题。我发现,使用
drawImage
绘制同样大小的区域,数据源是一张和绘制区域尺寸相仿的图片的情形,比起数据源是一张较大图片(我们只是把数据扣下来了而已)的情形,前者的开销要小一些。可以认为,两者相差的开销正是「裁剪」这一个操作的开销。我尝试绘制 104 次一块 320x180 的矩形区域,如果数据源是一张 320x180 的图片,花费了 40ms,而如果数据源是一张 800x800 图片中裁剪出来的 320x180 的区域,需要花费 70ms。
虽然看上去开销相差并不多,但是
drawImage
是最常用的 API 之一,我认为还是有必要进行优化的。优化的思路是,将「裁剪」这一步骤事先做好,保存起来,每一帧中仅绘制不裁剪。具体的,在「离屏绘制」一节中再详述。视野之外的绘制
有时候,Canvas 只是游戏世界的一个「窗口」,如果我们在每一帧中,都把整个世界全部画出来,势必就会有很多东西画到 Canvas 外面去了,同样调用了绘制 API,但是并没有任何效果。我们知道,判断对象是否在 Canvas 中会有额外的计算开销(比如需要对游戏角色的全局模型矩阵求逆,以分解出对象的世界坐标,这并不是一笔特别廉价的开销),而且也会增加代码的复杂程度,所以关键是,是否值得。
我做了一个实验,绘制一张 320x180 的图片 104 次,当我每次都绘制在 Canvas 内部时,消耗了 40ms,而每次都绘制在 Canvas 外时,仅消耗了 8ms。大家可以掂量一下,考虑到计算的开销与绘制的开销相差 2~3 个数量级,我认为通过计算来过滤掉哪些画布外的对象,仍然是很有必要的。
离屏绘制
上一节提到,绘制同样的一块区域,如果数据源是尺寸相仿的一张图片,那么性能会比较好,而如果数据源是一张大图上的一部分,性能就会比较差,因为每一次绘制还包含了裁剪工作。也许,我们可以先把待绘制的区域裁剪好,保存起来,这样每次绘制时就能轻松很多。
drawImage
方法的第一个参数不仅可以接收Image
对象,也可以接收另一个Canvas
对象。而且,使用Canvas
对象绘制的开销与使用Image
对象的开销几乎完全一致。我们只需要实现将对象绘制在一个未插入页面的Canvas
中,然后每一帧使用这个Canvas
来绘制。// 在离屏 canvas 上绘制 var canvasOffscreen = document.createElement('canvas'); canvasOffscreen.width = dw; canvasOffscreen.height = dh; canvasOffscreen.getContext('2d').drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); // 在绘制每一帧的时候,绘制这个图形 context.drawImage(canvasOffscreen, x, y);
离屏绘制的好处远不止上述。有时候,游戏对象是多次调用
drawImage
绘制而成,或者根本不是图片,而是使用路径绘制出的矢量形状,那么离屏绘制还能帮你把这些操作简化为一次drawImage
调用。第一次看到
getImageData
和putImageData
这一对 API,我有一种错觉,它们简直就是为了上面这个场景而设计的。前者可以将某个 Canvas 上的某一块区域保存为ImageData
对象,后者可以将ImageData
对象重新绘制到 Canvas 上面去。但实际上,putImageData
是一项开销极为巨大的操作,它根本就不适合在每一帧里面去调用。避免「阻塞」
所谓「阻塞」,可以理解为不间断运行时间超过 16ms 的 JavaScript 代码,以及「导致浏览器花费超过 16ms 时间进行处理」的 JavaScript 代码。即使在没有什么动画的页面里,阻塞也会被用户立刻察觉到:阻塞会使页面上的对象失去响应——按钮按不下去,链接点不开,甚至标签页都无法关闭了。而在包含较多 JavaScript 动画的页面里,阻塞会使动画停止一段时间,直到阻塞恢复后才继续执行。如果经常出现「小型」的阻塞(比如上述提及的这些优化没有做好,渲染一帧的时间超过 16ms),那么就会出现「丢帧」的情况,
CSS3 动画(
transition
与animate
)不会受 JavaScript 阻塞的影响,但不是本文讨论的重点。偶尔的且较小的阻塞是可以接收的,频繁或较大的阻塞是不可以接受的。也就是说,我们需要解决两种阻塞:
- 频繁(通常较小)的阻塞。其原因主要是过高的渲染性能开销,在每一帧中做的事情太多。
- 较大(虽然偶尔发生)的阻塞。其原因主要是运行复杂算法、大规模的 DOM 操作等等。
对前者,我们应当仔细地优化代码,有时不得不降低动画的复杂(炫酷)程度,本文前几节中的优化方案,解决的就是这个问题。
而对于后者,主要有以下两种优化的策略。
- 使用 Web Worker,在另一个线程里进行计算。
- 将任务拆分为多个较小的任务,插在多帧中进行。
Web Worker 是好东西,性能很好,兼容性也不错。浏览器用另一个线程来运行 Worker 中的 JavaScript 代码,完全不会阻碍主线程的运行。动画(尤其是游戏)中难免会有一些时间复杂度比较高的算法,用 Web Worker 来运行再合适不过了。
然而,Web Worker 无法对 DOM 进行操作。所以,有些时候,我们也使用另一种策略来优化性能,那就是将任务拆分成多个较小的任务,依次插入每一帧中去完成。虽然这样做几乎肯定会使执行任务的总时间变长,但至少动画不会卡住了。
看下面这个 Demo,我们的动画是使一个红色的
div
向右移动。Demo 中是通过每一帧改变其transform
属性完成的(Canvas 绘制操作也一样)。然后,我创建了一个会阻塞浏览器的任务:获取 4x106 次
Math.random()
的平均值。点击按钮,这个任务就会被执行,其结果也会打印在屏幕上。如你所见,如果直接执行这个任务,动画会明显地「卡」一下。而使用 Web Worker 或将任务拆分,则不会卡。
以上两种优化策略,有一个相同的前提,即任务是异步的。也就是说,当你决定开始执行一项任务的时候,你并不需要立刻(在下一帧)知道结果。比如,即使战略游戏中用户的某个操作触发了寻路算法,你完全可以等待几帧(用户完全感知不到)再开始移动游戏角色。
另外,将任务拆分以优化性能,会带来显著的代码复杂度的增加,以及额外的开销。有时候,我觉得也许可以考虑优先砍一砍需求。小结
正文就到这里,最后我们来稍微总结一下,在大部分情况下,需要遵循的「最佳实践」。
-
- 将渲染阶段的开销转嫁到计算阶段之上。
- 使用多个分层的 Canvas 绘制复杂场景。
- 不要频繁设置绘图上下文的 font 属性。
- 不在动画中使用 putImageData 方法。
- 通过计算和判断,避免无谓的绘制操作。
- 将固定的内容预先绘制在离屏 Canvas 上以提高性能。
- 使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行。
-
浏览器重排(回流)重绘以及优化方案
2021-03-20 22:57:55一、什么是重排和重绘 要说清重排(reflow)和重绘(repaint),首先要理解排列和绘制,浏览器渲染页面时,在获取完html、css资源之后,会大致经过以下步骤。 (1) html生成html树 (2) css形成css规则 (3) 两者形成一个...一、什么是重排和重绘
要说清重排(reflow)和重绘(repaint),首先要理解排列和绘制,浏览器渲染页面时,在获取完html、css资源之后,会大致经过以下步骤。
(1) html生成html树
(2) css形成css规则
(3) 两者形成一个渲染树
(4) 去文档当中找寻各自的布局位置----- 排列
(5) 将内容填充到文档上 ----- 绘制
【排列】就是计算位置调整布局的过程,而【绘制】就是把它画上去的过程。【重排】也就是除了最开始排列的布局,后续经过一些操作而使得dom元素重新找寻位置的过程,【重绘】就是重新绘制内容的过程。
二、什么情况会引发重排和重绘
1、先来说重排,重排和位置的移动布局的变化有关,主要有以下几种情况会引发重排
(1) 浏览器的窗口发生变化,每放大、缩小一次浏览器的窗口,该页面的所有元素都要进行重排重绘
(2) 增加、删除、移动dom元素,更改dom元素的宽高内外边距、内容,修改dom元素的样式
(3) 进行dom元素宽高等属性的查询,因为每查询一次,浏览器都会对所有的元素进行重新计算,以确保计算的值是正确的2、再来说重绘,重绘主要是元素的外观发生变化,不会重新布局,有以下情况会引发重绘
元素的背景(background)、文字颜色(color)、边框样式(outline)发生变化3、重排和重绘的关系
重绘不一定需要重排,因为可能只是元素修改文字颜色,不需要重新布局,重排大多数情况都需要重绘,因为重新排列元素之后要绘制到屏幕上。以下显示重排和重绘的耗时,方块两秒之后右移了100px
三、为什么要优化因为重排和重绘不只是对单个的dom元素进行操作,而是对整个【图层】进行操作,需要花费时间,如果频率高,非常的影响性能。
那什么是图层?如下图所示
因为每一次的重排重绘都是操作整个图层,那么我们可以将需要频繁操作的dom元素另外建立一个图层,这样可以尽可能少的触发重排重绘。那么什么情况可以开启图层?
针对频繁操作的行为,chrome浏览器自动开启了图层,主要有以下几种情况
1、css 3D变化的图形 ---- transform: translateX(0)
2、html5中的<video>标签
3、canvas绘图中的节点
4、css 动画的节点 --- keyframes animation
5、拥有css加速属性 --- will-change: transform除此之外,还可以使用以下方式进行优化
1、对元素进行移动时,使用transform替代对元素top、left、right的操作,因为css3的整个操作是对图层的组合来实现的,所以不会引发重绘重排。#node{ // position: relative; transform: translateX(0); width: 100px; height: 100px; background: pink; } var node = document.querySelector("#node"); setTimeout(function(){ // node.style.left="100px"; node.style.transform="translateX(100px)"; },2000);
同样是对一个元素两秒后移动100px,可以看到对比下图对比
2、将多次对样式的操作合并成一次
不要一次一次的修改样式,而是预先定义好class,直接修改DOM的className,这样只会引发一次重排重绘
3、将dom离线后修改
如果要对dom元素进行多次操作,首先将dom设置为不可见,然后再对dom操作,操作完成后再将dom元素设置为可见,这样只会有两次重排重绘
4、利用文档碎片 documentFragment
documentFragment 不是真实 dom树的一部分,它的变化不会触发dom树的重新渲染,且不会导致性能等问题,将创建的新元素全部添加到documentFragment上,最后让documentFragment一起插入到dom元素中const list = ['哈尔的移动城堡', '千与千寻的神隐', '起风了'] const ul = document.getElementsByTagName("ul")[0] let fragment = document.createDocumentFragment() for(let i in list){ fragment.appendChild(list[i]) } ul.appendChild(fragment)
-
Canvas性能优化
2021-06-12 13:55:39渲染动画的基本原理无非是反复的擦除和重绘。为了动画的流畅,留了一帧的时间,我们需要在16ms完成游戏逻辑的处理,对象位置、状态的计算以及把他们画出来。一旦消耗时间多了,用户就会感觉卡顿。所以提高动画的性能... -
前端性能优化:Canvas
2022-05-14 15:23:27渲染动画的基本原理,无非是反复地擦除和重绘。为了动画的流畅,60Hz 刷新率设备的帧预算为 16.67ms,在这个时间之内,计算每个对象的位置、状态,还需要把它们都画出来。高刷设备的帧预算更低。所以需要时刻关注... -
canvas绘图:绘图基础知识
2020-11-19 12:03:01位图:支持更多像素但放大会模糊 canvas 矢量图: 放大依旧很清晰 svg canvas默认大小300*150 标签上设置height和width来设置大小 1.获取canvas标签,并设置绘制环境 let c1=document.getElementById(‘c1’) let ... -
Flutter组件重绘
2021-10-09 09:58:59前言:在flutter页面刷新中,我们会经常用到setState(() { })函数,当程式执行该函数时,该页面下的所有组件都会被重绘,不需要被刷新的组件也会被刷新,造成了不必要的性能开销。 (1)刷新单个组件:重绘所有... -
UGUI性能优化之Canvas
2020-06-23 11:24:24UGUI性能优化之Canvas 自从Unity问世以来,UI一直都存在比较大的问题,自带的OnGUI不能所见即所得,制作过程比较麻烦。于是出现了很多第三方的优秀的UI插件,比如很多项目里面用到的NGUI,或者后来出的FairyGUI。... -
性能优化: 避免重绘与回流的实现方式
2021-06-06 12:02:18之前有整理过一部分知识点, 一直没有发布, 因为都是有关 CSS 方面的...1. 避免重绘与回流: 实现方式 1. 使用 translate 代替 top 改变 。 2. 使用 opacity 代替 visibility 。 3. 不要一条一条的修改 DOM 的样式, -
浏览器网页生成过程,重绘重排,与优化
2022-01-13 10:18:42网页生成过程 ...CSS则被CSS解析成CSSOM树 DOM树CSSOM树结合,生成一颗渲染树(Render Tree) 生成布局(flow),即将所有渲染树的所有节点进行平面合成 将布局绘制再屏幕上 第四步和第五步是最耗时的...重排比重绘大(谁 -
Unity UI优化总结
2021-06-03 23:48:01虽然说可以通过一些简单的技巧单方面地减少批次或者减少重绘,但进行过一波优化之后,最终还是要面临批次和重绘的平衡问题的。 常见的四大UI优化问题: 1、片段着色器利用率过高(或者说GPU fill-rate填充率过高),... -
前端性能优化 - 减少重绘与回流
2020-04-20 23:26:54重绘 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,不会影响布局,比如bg-color 6.3 避免重绘和回流的两种方式 1.盒模型相关的属性会触发重新布局,会触发页面回流 Wi... -
浏览器回流与重绘
2022-02-12 13:36:19回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。根据改变... -
消除canvas闪烁
2021-08-26 14:50:01使用clearRect清空局部区域后重绘能会造成闪烁。PC上问题不大,移动端比较明显。(注clearRect(0,0,canvas.width,canvas.height)也会闪烁,除非是clearRect(0,0,canvas.width+1,canvas.height+1))。 原因: 可能... -
性能优化之回流重绘
2021-01-17 14:14:35引发多次回流重绘,而是将这些合并为一次,因为浏览器中有缓冲机制,通过这个缓冲机制,能够减少回流重绘的次数,比如控制在100ms周期内,多次回流重绘合并为一次,这属于浏览器自身的性能优化。</strong><br> ,type... -
浏览器重绘重排与优化-前端进阶
2018-12-24 10:43:23很多人都知道要减少浏览器的重排和重绘,但对其中的具体原理以及如何具体操作并不是很了解,当突然提起这个话题的时候,还是会一脸懵逼。希望大家可以耐着性子阅读本文,仔细琢磨,彻底掌握这个知识点! 博客、前端... -
前端知识点整理——v-if 和 v-show / 事件冒泡和捕获 / Git的操作方式 / .../ 重排与重绘 / async和defer
2021-09-02 16:10:11触发重绘的事件: color visibility background background-image background-repeat 对重排的优化 减少重排的范围 尽可能在低层级的dom上修改样式。避免通过父元素去影响子元素 少使用table布局。每次改动会造成... -
unity 官方ui优化,记录
2021-06-15 18:06:55(那就是父亲重绘时候不会强制子canvas重绘)。除非父canvas的变化导致子Canvas的大小调整时候 2 Canvas是可以嵌套的 3 Graphic是ui中最基础的类 4 layout组件,只依赖于RectTransform,并且只会 影响相关的... -
提高HTML5 canvas性能的几种方法(转)
2021-06-12 13:56:26简介HTML5 canvas 最初起源于苹果(Apple)的一项实验,现在已经成为了web中受到广泛支持的2D快速模式绘图(2Dimmediate mode graphic)的...已经存在众多优化canvas性能的方法了,但是还没有一篇文章将这些方法系统的... -
canvas动画实战与性能优化
2020-12-13 13:56:45插播一篇关于 canvas 动画及性能优化的文章,为我们可以更好的进入到 webgl 的世界奠定基础。 本篇文章的内容可能会稍难理解,还希望大家有问题及时提出。闲话我们就不多说了,开始今天的正题吧。 1. 动画实战 首先... -
CSS重排与重绘总结
2019-08-09 14:38:25昨天面试被问到了什么是重排和重绘,回答的并不是很好,下面来总结一下:关于CSS重排和重绘的概念,在制作中考虑浏览器的性能,减少重排能够节省浏览器对其子元素及父类元素的重新渲染;避免过分的重绘也能节省... -
小程序Canvas性能优化实战
2019-12-12 17:13:02以下内容转载自totoro的文章《小程序Canvas性能优化实战!》 作者:totoro 链接:https://blog.totoroxiao.com/canvas-perf-mini/ 来源:https://blog.totoroxiao.com/ 著作权归作者所有。商业转载请联系作者获得... -
css的重绘与重排实现实例
2021-08-05 04:55:57本文主要和大家介绍了css重绘与重排的方法的相关资料,小编觉得挺不错的,现在分享给大家,希望能帮助到大家。浏览器加载页面原理通常在文档初次加载时,浏览器引擎会解析HTML文档来构建DOM树,之后根据DOM元素的几何... -
HTML5 Canvas的性能提高技巧经验分享
2021-06-18 00:57:20使用缓存技术实现预绘制,减少重复绘制Canvs内容很多时候我们在Canvas上绘制与更新,总是会保留一些不变的内容,对于这些内容应该预先绘制缓存,而不是每次刷新。直接绘制代码如下:复制代码代码如下:context.font=... -
node-canvas实现百度地图个性化底图绘制
2020-12-21 10:45:31将node运行中,对cpu的依赖降到最低,尽量不要把复杂的计算放在js中计算,代码一定优化优化再优化。 node-开发辅助 使⽤用supervisor提⾼高 nodejs调试效率(监听js ⽂文件变化 ,⾃自动重启 node) ,参考文章 使用... -
第二篇《重排(reflow)和重绘(repaint)》
2021-12-03 09:29:01将 DOM 离线 “离线”意味着不在当前的 DOM 树中做修改,我们可以这样做: 使用 display:none一旦我们给元素设置 display:none 时(只有一次重排重绘),元素便不会再存在在渲染树中,相当于将其从页面上“拿掉”,... -
HTML5 CANVAS游戏开发实战(PDF和源代码)
2018-05-07 10:49:572.2.2 利用clip在指定区域绘 图 / 30 2.2.3 绘制自定义图形 / 31 2.3 绘制文本 / 32 2.3.1 绘制文字 / 32 2.3.2 文字设置 / 33 2.3.3 文字的对齐方式 / 38 2.4 图片操作 / 41 2.4.1 利用drawimage绘制图片 ...