HTML5 Canvas 性能和优化技巧、技巧和编码最佳实践

IT技术 javascript performance optimization canvas
2021-03-13 16:28:05

你知道一些关于画布的最佳实践吗??

请将您所知道的、学到的或在线阅读的任何和所有 Canvas 最佳实践、性能提示/技巧添加到此线程

由于 Canvas 对互联网来说仍然很新,而且我在未来看不到它会变老的迹象,因此没有太多记录在案的“最佳实践”或其他真正重要的技巧,这些技巧对于开发来说是“必须知道的”它在任何一个特定的地方。像这样的事情在鲜为人知的网站上多次散布。

人们需要了解的东西太多了,还有很多东西需要学习。


我想分享一些东西来帮助那些正在学习 Canvas 的人,也许一些已经很了解它的人,我希望从其他人那里得到一些反馈,让他们知道在 HTML5 中使用 Canvas 的一些最佳实践或其他技巧和窍门.

我想从我个人认为对开发人员来说非常有用但非常罕见的事情开始。

1. 缩进你的代码

就像您在其他任何时候一样,无论情况如何,都可以使用任何其他语言。这一直是其他一切的最佳实践,我发现在复杂的画布应用程序中,在处理多个不同的上下文和保存/恢复状态时,事情可能会变得有点混乱。更不用说代码更具可读性,整体看起来也更干净。

例如:

...
// Try to tell me this doesn't make sense to do
ctx.fillStyle = 'red';
ctx.fill();
ctx.save();
    if (thing < 3) {
        // indenting
        ctx.beginPath();
            ctx.arc(2, 6, 11, 0, Math.PI*2, true);
        ctx.closePath();
        ctx.beginPath();
            ctx.moveTo(20, 40);
            ctx.lineTo(10, 200);
            ctx.moveTo(20, 40);
            ctx.lineTo(100, 40);
        ctx.closePath();
        ctx.save();
            ctx.fillStyle = 'blue'
            ctx.fill();
        ctx.restore();
    } else { 
        // no indenting
        ctx.drawImage(img, 0, 0, 200, 200);
        ctx.save();
        ctx.shadowBlur();
        ctx.beginPath();
        ctx.arc(2, 60, 10, 0, Math.PI*2, false);
        ctx.closePath();
        ctx.fillStyle 'green';
        ctx.fill();
        ctx.restore();
    }
ctx.restore();
ctx.drawRect();
ctx.fill();
...

IF 语句不是比 ELSE 语句更易于阅读和更清晰的阅读并了解立即发生的事情吗?你能看到我在这里说什么吗?我认为这应该是开发人员应该继续练习的一种方法,就像他们在编写普通的 'ol javascript 或任何其他语言时一样。

使用 requestAnimationFrame 而不是 setInterval / setTimeout

setInterval 和 setTimeout 从未打算用作动画计时器,它们只是用于在时间延迟后调用函数的通用方法。如果您将来将间隔设置为 20 毫秒,但您的函数队列需要比执行时间更长的时间,则在这些函数完成之前,您的计时器不会触发。这可能需要一段时间,这在动画方面并不理想。RequestAnimationFrame是一种告诉浏览器正在发生动画的方法,因此它可以相应地优化重绘。它还会限制非活动选项卡的动画,因此如果您将其在后台打开,它不会耗尽移动设备的电池。

Nicholas Zakas在他的博客上写了一篇关于 requestAnimationFrame的非常详细和翔实的文章,非常值得一读。如果您想要一些严格而快速的实施说明,那么Paul Irish 编写了一个 requestAnimationFrame shim – 这是我最近在我制作的每个 Canvas 应用程序中使用的内容。

实际上

甚至比使用 requestAnimationFrame 代替 setTimeout 和 setInterval 更好,Joe Lambert 编写了一个新的和改进的垫片,称为 requestInterval 和 requestTimeout,他解释了使用 requestAnimFrame 时存在的问题。您可以查看脚本要点

实际 x2

现在所有的浏览器都已经赶上了这个规范,requestAnimFrame() polyfill已经更新了,它可能会继续用于覆盖所有供应商。

使用多个画布

@nicolahibbert她的一篇关于优化 Canvas 游戏帖子中提到了一种用于大量动画游戏的技术,其中提到最好使用多个叠加在一起的画布,而不是在一个画布中完成所有工作。Nicola 解释说,“在同一画布上同时绘制太多像素会导致帧率下降。以 Breakout 为例。尝试绘制砖块、球、桨、任何电源或武器, 然后是背景中的每个星星——这根本行不通,依次执行这些指令需要很长时间。通过将星空和游戏的其余部分拆分到单独的画布上,您可以确保体面的帧率。”

在屏幕外渲染元素

我不得不为我制作的一些应用程序执行此操作,包括三星的奥林匹克基因组项目 facebook 应用程序了解并利用它是否需要,这是一件非常有用的事情。它极大地减少了加载时间,而且它可以是一种非常有用的技术,可以将图像加载到屏幕外,因为它们有时需要一段时间。

var tmpCanvas = document.createElement('canvas'),
    tmpCtx = tmpCanvas.getContext('2d'),
    img = document.createElement('img');

img.onload = function() {
    tmpCtx.drawImage(thumbImg, 0, 0, 200, 200);
};
img.src = '/some/image/source.png';

请注意,图像的 src 是在加载后设置的。这也是要记住做的关键事情。一旦图像完成加载并绘制到这些临时画布中,您就可以使用相同的 ctx.drawImage() 将它们绘制到主画布上,但不是将图像作为第一个参数,而是使用 'tmpCtx.canvas'引用临时画布。

其他提示、技巧和资源

Canvas 有一个反向引用

2d 上下文具有对其关联 DOM 元素的反向引用:

var ctx = doc.getElementById('canvas').getContext('2d');
console.log(ctx.canvas);    // HTMLCanvasElement

我很想从其他人那里听到更多关于这方面的信息。我正在制定一份清单,列出我们应该标准化的内容,以便在我公司的前端代码标准和最佳实践中添加一个新部分我很想得到尽可能多的反馈。

4个回答

重绘区域

动画的最佳画布优化技术是限制在每帧上清除/绘制的像素数量。最容易实现的解决方案是重置整个画布元素并重新绘制所有内容,但这对于您的浏览器来说是一项昂贵的操作。

在帧之间重复使用尽可能多的像素。这意味着每帧需要处理的像素越少,程序运行得越快。例如,在使用 clearRect(x, y, w, h) 方法擦除像素时,仅清除和重绘发生变化的像素而不是整个画布是非常有益的。

程序精灵

按程序生成图形通常是可行的方法,但有时这并不是最有效的方法。如果您要绘制带有实心填充的简单形状,那么按程序绘制它们是最好的方法。但是,如果您使用笔触、渐变填充和其他对性能敏感的构成来绘制更详细的实体,则最好使用图像精灵。

两者混合使用是可能的。当您的应用程序启动时,在画布上按程序绘制图形实体一次。之后,您可以通过绘制它们的副本来重复使用相同的精灵,而不是重复生成相同的阴影、渐变和笔触。

状态堆栈和转换

画布可以通过旋转和缩放等变换进行操作,从而改变画布坐标系。这是了解状态堆栈的重要之处,有两种方法可用:context.save()(将当前状态推送到堆栈)和 context.restore()(恢复到先前的状态)。这使您可以对绘图应用变换,然后恢复到之前的状态,以确保下一个形状不受任何先前变换的影响。状态还包括诸如填充和笔触颜色之类的属性。

合成

使用画布时,手头的一个非常强大的工具是合成模式,除其他外,它允许蒙版和分层。有多种可用的复合模式,它们都是通过画布上下文的 globalCompositeOperation 属性设置的。复合模式也是状态堆栈属性的一部分,因此您可以应用复合操作,堆叠状态并应用不同的状态,然后恢复到您创建第一个状态之前的状态。这可能特别有用。

抗锯齿

为了允许子像素绘图,画布的所有浏览器实现都采用抗锯齿(尽管这似乎不是 HTML5 规范中的要求)。如果您想绘制清晰的线条并注意到结果看起来很模糊,请务必记住抗锯齿。发生这种情况是因为浏览器将插入图像,就好像它实际上是在这些像素之间一样。它会产生更流畅的动画(您可以真正以每次更新半像素的速度移动),但它会使您的图像显得模糊。

要解决此问题,您需要四舍五入为整数值或偏移半个像素,具体取决于您是绘制填充还是描边。

对 drawImage() x 和 y 位置使用整数

如果在 Canvas 元素上调用 drawImage,将 x 和 y 位置四舍五入为整数会快得多。

这是一个关于 jsperf 的测试用例,显示了使用整数比使用小数快多少。

因此,在渲染之前将 x 和 y 位置四舍五入为整数。

比 Math.round() 快

另一个 jsperf 测试表明Math.round() 不一定是舍入数字的最快方法。使用按位 hack 实际上比内置方法更快。

画布精灵优化

清除画布

要清除整个画布上的任何现有像素,通常使用 context.clearRect(x, y, w, h) - 但还有另一个选项可用。每当设置画布的宽度/高度时,即使将它们重复设置为相同的值,也会重置画布。在使用动态大小的画布时,了解这一点很有好处,因为您会注意到绘图消失了。

计算分布

Chrome 开发者工具分析器对于找出您的性能瓶颈非常有用。根据您的应用程序,您可能需要重构程序的某些部分以提高性能以及浏览器如何处理代码的特定部分。

优化技术

根据经验,我注意到 context.clearRect(x, y, w, h) 比 canvas.width=canvas.width 快
2021-05-14 16:28:05

这是我的提示

1)使用clearRect来清除画布而不是canvas.width=canvas.width,因为后面会重置画布状态

2)如果您在画布上使用鼠标事件,请使用以下功能,它是可靠的,并且在大多数情况下都有效。

/**  returns the xy point where the mouse event was occured. 
 @param ev The event object.
*/
function getXY(ev){
   return getMousePosition(ev, ev.srcElement || ev.originalTarget);
}

 /**  returns the top-left point of the element
       @param elem The element
   */
function getElementPos(elem){
   var obj = elem;
   var top = 0;
   var left = 0;
    while (obj && obj.tagName != "BODY") {
      top += obj.offsetTop-obj.scrollTop;
      left += obj.offsetLeft -obj.scrollLeft ;
      obj = obj.offsetParent;
     }
  return {
    top: top,
    left: left
    };
};

/**  returns the xy point where the mouse event was occured inside an element. 
@param ev The event object.
 @param elem The element
*/
function getMousePosition(evt, elem){
var pageX, pageY;
if(typeof(window.pageYOffset)=='number') {
    pageX=window.pageXOffset;
    pageY=window.pageYOffset;
}else{
    pageX=document.documentElement.scrollLeft;
    pageY=document.documentElement.scrollTop;
}
var mouseX = evt.clientX - getElementPos(elem).left + pageX;
var mouseY = evt.clientY - getElementPos(elem).top + pageY;
return {
    x: mouseX,
    y: mouseY
};
};

3) 如果您想支持 IE7,请使用 ExplorerCanvas

4)不是清除整个画布,而是只清除需要清洁的部分。它对性能有好处。

Excanvas 是一个很好的支持 IE 的库,另外一个很好的库是 Flashcanvas。我两个都用过,选择应该取决于你对项目的需求。
2021-04-27 16:28:05
我注意到 Flashcanvas 对 Canvas 中的更多东西有更好的支持,但我遇到的一件事是它不支持在临时画布中进行屏幕外操作,然后使用 drawImage() 将临时画布绘制到主画布中语境。除非这些画布不在屏幕外,否则它无法做到这一点。FxCanvas 声称对此提供支持,但在发布前我不得不尝试让它工作的时间段内我无法让它工作
2021-05-12 16:28:05

以下是我昨晚放入值得分享的列表中的更多提示和建议。

  • 不要包含 jQuery,除非您需要做的不仅仅是选择<canvas>.

    我在画布上制作的几乎所有东西都没有它

  • 创建抽象函数解耦您的代码将功能与外观或初始绘制状态分开。

    尽可能使常用功能可重用。理想情况下,您应该使用module模式,您可以创建一个包含常用功能的 utils 对象。

  • 在有意义时使用单字母和双字母变量名称( x, y, z )。

    Canvas 中的坐标系添加了更多通常声明为变量的单个字母。这可能导致创建多个单/双变量(dX, dY, aX, aY, vX, vY)作为元素的一部分。

    我建议你输入或缩写。这个词 ( dirX, accelX, velX ) 或者是描述性的,否则以后你可能会很困惑,相信我。

  • 创建可以根据需要调用以制作游戏元素的构造函数。您可以在构造函数中添加自定义方法和属性,并创建您可能需要的任意数量的方法和属性,它们都将拥有自己的属性和方法。

    我制作的 Ball 构造函数示例:

    // Ball constructor
    var Ball = function(x, y) {
        this.x = x;
        this.y = y;
    
        this.radius = 10;
        this.color = '#fff';
    
        // Direction and min, max x,y
        this.dX = 15;
        this.dY = -15;
    
        this.minX = this.minY = 20 + this.radius;
        this.maxX = this.radius - (canvasWidth - 20);
        this.maxY = this.radius + canvasHeight;
    
        this.draw = function(ctx) {
            ctx.beginPath();
                ctx.arc(this.x, this.y, this.radius, 0, twoPI, true);
            ctx.closePath();
            ctx.save();
                ctx.fillStyle = this.color;
                ctx.fill();
            ctx.restore();
        };
    };
    

创造球

ball = new Ball(centerX, canvasHeight - paddle.height - 30);
ball.draw(ctx);
  • 一个好的基础是创建 3 个函数: init() - 完成所有初始工作,并设置基本变量和事件处理程序等... draw() - 调用一次以开始游戏并绘制第一帧游戏,包括创建可能会改变或需要构建的元素。update() - 在 draw() 结束时调用,并通过 requestAnimFrame 在其内部调用。更新变化元素的属性,这里只做你需要做的。

  • 在循环中做最少的工作,对变化的部分或元素进行更新创建游戏元素执行动画循环之外的任何其他 UI 工作。

    动画循环通常是一个递归函数,这意味着它在动画期间快速重复地调用自己以绘制每一帧。

    如果有许多元素同时进行动画处理,您可能希望首先使用构造函数创建元素(如果还没有的话),然后在构造函数中创建一个具有 requestAnimFrame/setTimeout 的“timer”方法,就像您通常使用的那样在任何动画循环中,但仅特别影响此元素。

    您可以让每个游戏元素在构造函数中都有自己的计时器、绘制和动画方法

    这样做可以让您完全分离对每个元素的控制,并且根本不需要一个大的动画循环,因为循环被分解为每个元素并且您可以随意开始/停止。

或另一种选择:

  • 创建一个 Timer() 构造函数,您可以使用它并单独赋予每个动画元素,从而最大限度地减少动画循环内的工作量
如果您查看visitmix 动画的源代码,您会发现名为Clock 的构造函数以及它的后续原型,这些原型继承和扩展了为动画的不同对象创建的每个新时钟。
2021-04-20 16:28:05
你能给出你上面所说的一些示例代码 - 为单独的动画对象创建单独的动画方法吗?
2021-05-16 16:28:05
我一直致力于创建的另一种可能的技术涉及使用构造函数来创建您需要的元素,您可以创建一个方法来控制它自己的动画的“这个”元素的时钟计时器.
2021-05-17 16:28:05
我看到它实现的一种方法是使用动画数组来允许每个动画间隔。这解释了您将如何做到这一点:planetb.ca/2012/03/... 来自Visitmix的Adobe Illustrator 到Canvas 插件实际上对这种用于为单个对象设置动画的技术进行了更加复杂和复杂的实现。你可以在这里看到使用它的东西的演示:visitmix.com/labs/ai2canvas/common/tutorials/part09/export/...
2021-05-19 16:28:05

最近推出使用 Canvas 的 Facebook 应用程序和用户 Facebook 个人资料信息(它必须容纳的数据量对某些人来说是巨大的)进行工作之后,将您和您的朋友也使用该应用程序与奥林匹克运动员相匹配,例如 6 度分离类型的事情,我在我的广泛努力中学到了很多东西,我可以尝试提高应用程序的性能。

我真的花了几个月,几天的时间来重构我已经非常熟悉的代码,并相信这是做事情的最佳方式。

尽可能使用 DOM 元素

事实是,浏览器还没有准备好在 Canvas 中处理更密集的运行应用程序,特别是如果您需要开发支持 IE 8 的应用程序。有时在某些情况下 DOM 比当前的实现更快撰写本文时的 Canvas API。至少我发现它是在为三星处理一个非常复杂的单页动画 html5 和画布应用程序时。

我们能够在改进性能方面做得很好,同时仍然使用 Canvas 来完成一些复杂的工作,将图像裁剪成圆形,这可能与我们的做法一致。

在发布前几天,我们决定尝试一种不同的技术,而不是在屏幕外创建临时画布,这些画布一旦被裁剪成圆形等就被放置在可见画布上,我们只是在画布上附加了 Image DOM 元素,使用 x以及我们之前用于放置临时画布的 y 坐标。

为了将图像裁剪成圆形,这很简单,我们只使用了 CSS3 的边界半径属性来完成它,这比复杂的状态变化系列要少得多,虽然巧妙而有创意但过度使用了 .clip( ) 方法。

一旦它们被放置在 DOM 中,就会发生图像的动画,并且每个图像的 DOM 节点都作为 Canvas 的单独实体进行动画处理。我们可以通过 CSS 轻松完全控制样式。

这种技术类似于执行此类工作的另一种方法,该方法也很值得了解,它涉及将 Canvas 分层放置在彼此之上,而不是将它们绘制到一个上下文中。