Html5 Canvas的充分运用:实用示例

转自:http://bbs.9ria.com/thread-244675-1-1.html

http://dev.tutsplus.com/tutorials/html5-canvas-optimization-a-practical-example--active-11893

如果你经常研究Javascript代码,并且有一定的经验,那你肯定让你的浏览器崩溃过。这些问题经常是一些Javascript的小bug比如说是一个无限的while语句循环;如果不是这个问题,那么你该考虑是不是页面转换或者是页面动画出了问题:就是那一类包含了在网页或者动态CSS风格界面中添加/删除元素的代码。这个教程的目标是教会你们充分运用Javascript和Html5中的<canvas>元素。当你看到了下面的基于Html5制作的生动的动画,那么这个教程也就开始了:

我们将会带着它走完整个教程,浏览不同的canvas使用技巧和技术,并且将它们运用到Javascript动画中的源代码中去。目标就是使用更精简更高效的Javascript代码来提升动画的执行速度,让动画更加平滑的结束,拥有更动态的动画结构。

教程中每一个步骤包含了Html和Javascript的源代码下载链接,你可以从任何一个步骤中开始你的学习。

让我开始第一个步骤。

第一步:播放电影预告

这段装饰是基于“sintel”制作的电影预告,sintel是一个用3D图像软件Blender制作的3D电影。它是用Html5最常用的两个功能构建的:<canvas>和<video>元素。

<video>元素载入和播放Sintel视频文件时,<canvas>代码通过进行快照功能对播放的视频进行快速截图,并且将这些图片与文字和其他的图画混合起来,生成了它自己对应的动态播放序列。当你点击播放视频时,<canvas>弹出栩栩如生的黑色背景,这个黑色背景是黑白版本的视频组成。缓缓的,视频的彩色截图出现在画面中,以一个电影的滚动视觉滑入。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

在左上角,我们设置了标题和一些说明的文本,采用动态的渐变风格进入和退出。这个代码的运行速度和关联的指标以图片和闪亮的字体展示,都包含在一起放在在最下方的黑色方框中。稍后我们会更仔细的观察这个特别的东西。

最后,在动画的开头有个大型循环的长条飞过场景,这个长条的图画是从外部的PNG图片文件中读取的。

第二步:查看源代码

源代码包括Html,CSS和Javascript代码的混合。Html代码很少:仅有<canvas>和<video>标签,嵌套在<div>容器中。

  1. <div id="animationWidget" >    <canvas width="368" height="208" id="mainCanvas" ></canvas>    <video width="184" height="104" id="video" autobuffer="autobuffer" controls="controls" poster="poster.jpg" >        <source src="sintel.mp4" type="video/mp4" ></source>        <source src="sintel.webm" type="video/webm" ></source>    </video></div>
复制代码

<div>容器的ID名取为(animationWidget),为它所有的内容和目录应用对应的CSS规则(如下)。

  1. #animationWidget{    border:1px #222 solid;    position:relative;    width: 570px;    height: 220px;}#animationWidget canvas{    border:1px #222 solid;    position:absolute;    top:5px;    left:5px;          }#animationWidget video{    position:absolute;    top:110px;    left:380px;    }
复制代码

。Html和CSS作为修饰和调节用,Javascript代码是整个模块中的主体部分

。在顶部,我们设置了主要对象,这个对象将通过引用canvas元素的代码和它的2D内容实现。

。init()函数在视频播放的任何时候被调用,用作设置好代码中的所有对象。

。当setBlade()函数按动态功能的要求载入一张外部图片时,sampleVideo()函数记录当前播放的视频的帧。

。canvas动态的步伐和内容都由main()函数控制,这个函数好比整个代码的核心部分。一旦视频还是播放,该函数就在有规则的间隙中运行。它为首次表现canvas的各个结构进行着色,之后调用五个绘制代码:

drawBackground()

drawFilm()

drawTitle()

darwDescription()

drawStats()

就像这些名字设定的,每个绘制函数都会在动态场景中绘制一个对象。这样构建代码能提升稳定性,并且降低代码的后续维护工作难度。

下面展示了整个代码,花些时间去评估,并且看看你能不能做出一些改动,让代码执行速度更快。

第三步:代码优化:了解规则

第一个代码性能优化规则就是“不要”。

这个规则的要点是为了减少优化而优化,因为优化的过程是要付出代价的。

高度优化的代码能让浏览者的处理和分析变得容易,但是通常给那些维护人员带来重担,他们会发现它难以遵循和维护。每当你决定某些优化是必要的,于是预先设定了一些目标,所以你不要得意忘形而做过头。

这个对象的优化目标是让main()函数的运行时间不超过33毫秒,这将匹配上播放的视频文件的帧率。这些文件以每秒30帧的速度编码,转换过来就是每帧大约0.33秒或者33毫秒。

由于Javascript每次在canvas中调用main()函数绘制一个新的动画帧时,我们的优化目标就是让这个功能每次在调用的时候运行所需要的时间少于33毫秒。这个功能通过一个setTimeout()函数中的一个Javascript计时器重复调用自身,如下图所示。

  1. var frameDuration = 33; // set the animation's target speed in millisecondsfunction main(){    if( video.paused || video.ended ){ return false; }             setTimeout( main, frameDuration );
复制代码

第二个代码优化规则就是“还没”

这个规则强调的是优化工作应该在编写结束之前完成,因为编写工作过程中你已经充实了一部分完整的工作代码。因为这个装饰的代码是一个完成的完美示例,优化校正工作程序已经准备运作。

第三个代码优化规则:“还不是现在,分析第一”

这个规则是了解你的程序在运行时性能。剖析有助于你了解而不是去猜测这个脚本最常用的功能代码,这样你就可以在优化过程中专注优化这一部分。这是至关重要的,足以让领先的浏览器附带上内置Javascript分析器或者提供这种服务的拓展功能。

我在firefox附带的firebug插件下运作了一下这个装饰对象,下面是截图的结果。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

第四步:设置一些性能指标

当你在运行这个装饰对象时。我敢肯定你会发现所有的Sintel对象都不错,而且会绝对称赞canvas右下角的那个拥有漂亮图画和闪亮文字的项目。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

因为它不仅仅是一个漂亮的表面,那个项目还提供了一些正在运行的程序的实时性能统计数据。它实际上是一个简单的不能再简单的Javascript分析器。那就对了!哟,我听说你喜欢分析,所以我在你的视频中设置了一个分析器,这样你就可以在你观看的时候进行分析了。图表跟踪着渲染的时间,计算出每次main()函数运行时所花费的了多少毫秒。由于这是绘制每一帧动画的功能,所以它对动画的帧率影响很大。每一根垂直的蓝线在图片中显示出一帧所花费的时间。红色的水平线是目标速度,我们设置在了33毫秒,为的是与视频文件的帧率相匹配。在图表的下面,main()函数最后一次运行的时间以毫秒为单位给出。这个分析器也是一个方便的浏览器渲染速度测试器。此时在火狐浏览器下渲染时间为55毫秒,在IE9浏览器下渲染时间为90毫秒,在谷歌浏览器下渲染时间为41毫秒,在Opera浏览器下渲染时间为148毫秒,在Safari浏览器下渲染时间为63毫秒。所有的浏览器都在Windows XP环境下运行,除了IE9是在Windows Vista环境下分析的。接下来指标的是Canvas FPS(Canvas 帧每秒),通过计算在每一秒钟内main()函数被调用了多少次。当视频还在播放时,分析器展示了最近的Canvas FPS数值,当视频播放结束后,分析器将展示main()函数的平均调用速度。最后一个指标是浏览器FPS,这个指标衡量的是在每一秒钟浏览器重绘了多少个当前的窗口。如果你再Firefox浏览器中观看这个装饰对象,这是唯一可用的功能,因为它依赖一个只可以在Firefox浏览器中运行并被成为window.mozPaintCount的插件,这个插件能跟踪当网页从首次载入起浏览器窗口一共被重新绘制了多少次。

重绘通常发生在会改变页面外观的某事件或行为中,就像你想下滚动页面,或者把鼠标放在一个链接上。这对浏览器的真实帧率影响很大,决定了浏览器当前的繁忙程度。

为了衡量是否优化对canvas动画在mozPaintCount插件中是否有影响,我去掉了canvas标签和Javascript代码,以便跟踪浏览器仅仅是播放一个视频时的帧率。我的测试在Firebug插件的控制台中执行,使用了下面的函数:

  1. var lastPaintCount = window.mozPaintCount;setInterval( function(){    console.log( window.mozPaintCount - lastPaintCount );    lastPaintCount = window.mozPaintCount;}, 1000);
复制代码

结果表明:当视频在播放的的过程中,浏览器的帧率在30到32FPS之间,当视频停止后,帧率掉至0到1FPS左右。这也就是说火狐浏览器调整了它的窗口重绘频率为30FPS,匹配播放以30FPS编码的视频。当测试将未优化的canvas动画和视频一起运行时,浏览器帧率下降至16FPS,浏览器正在奋力的运行全部的Javascript代码并且同时的重绘窗口,这让播放的视频和canvas动画变得很卡顿和呆滞。

我们现在开始调整我们的计划,我们将这样做,我们继续跟踪渲染时间,Canvas FPS和浏览器FPS,以衡量我们变化的影响。

第五步:使用requestAnimationFrame()函数

Javascript最后的两个片段是setTimeout()函数和setInterval()定时器功能的使用。要使用这些功能,你可以指定一个以毫秒为单位的时间间隔和在时间间隔后你想执行的返回函数。

两者的不同之处在于setTimeout()函数只能调用你的功能一次,而setInterval()函数能重复调用。

虽然这些功能一直是Javascript动画工具中不可或缺的工具,但是它们确实有一些缺陷:

第一,设定好的时间间隔并不是总是可靠。如果当时间间隔结束时程序仍然在执行别的东西时,回调函数就会比原先设置执行的更晚,一旦如此浏览器就不再繁忙了。在main()函数中,我们设定了时间间隔是33毫秒,但是在分析器中返回的数据中表明,在Opera浏览器中这个函数实际上每隔148毫秒才调用一次

第二,在浏览器重绘这部分有个问如果我们有个每秒产生20个动画帧的回调函数,但是浏览器在每秒重绘窗口12次,还有8次调用就相当于浪费了,用户将永远看不到结果。

最后,浏览器没办法知道被调用的函数是不是文档中的动画元素。这就意味着如果这些元素滚动出了屏幕,或者用户点击了另一个动画的切换,这个回调函数仍然会被反复的执行,占用了CPU的资源。

使用requestAnimationFrame()函数可以解决以上的大部分问题,并且它可以用来代替HTML5中的动画定时器功能。resqusetAnimationFrame()函数会将函数的调用次数和浏览器窗口重绘的次数进行同步,而不是我们去指定一个固定的时间间隔。这样带来的结果是动画会更加流畅,没有被丢弃的帧,当浏览器知道动画在播放时会做更进一步的优化。

为了在我们的装饰对象中替换setTimeout()和requestAnimationFrame()函数,首先我们应该在我们的代码开头添加如下代码:

  1. requestAnimationFrame = window.requestAnimationFrame ||                        window.mozRequestAnimationFrame ||                        window.webkitRequestAnimationFrame ||                        window.msRequestAnimationFrame ||                        setTimeout;
复制代码

由于这个规则还是很新的,一些浏览器的版本已经有了它们自己的实现性运行验证,这就证明了如果函数是可用的,那么函数名就会指向正确的函数方法。如果不是,那么这个代码会倒回到setTimeout()函数的实现的旧功能。我们在main()函数中,改写这一行:

  1. setTimeout( main, frameDuration );
复制代码

  1. requestAnimationFrame( main, canvas );
复制代码

第一个参数采用回调函数,是在main()函数里面的情况下。第二个参数是可选的,并指定包含了包含动画的DOM元素。它应该是用来计算额外的优化。

注意getStates()方法也使用了一个setTimeout()函数,但是我们不要它,因为这个特别的功能在场景的动化中起不到作用。requestAnimationFrame()函数是特意为动画创建的,所以如果你的回调函数对动画没有任何作用,你仍然可以使用setTimeout()函数和setInterval()函数。

第六步:使用页面能见度API

在上一步我们使用了requestAnimationFrame()函数支撑了canvas动画,现在我们又有了新的问题。如果我们开始运行这个装饰对象,然后最小化浏览器窗口或者转换一个页面,这个装饰对象的窗口重绘频率将会降低以节约能源。这也将降低动画的播放速度,因为动画的播放速度与窗口的重绘频率已经同步,如果视频没有播放到最后这将是很完美的设定。

我们需要一个方法去检测一个页面是不是正在被浏览,这样我们就可以暂停播放视频,这就是页面能见度API功能的用武之地。

API包含了一组我们可以用于检测网页是否正在被浏览的工具、函数和事件。所以我们可以添加这些代码来相应的调整我们程序的行为。当页面无效时我们将用API来暂停这个装饰对象所播放的视频。

首先我们在代码中添加一个新的时间监听器:

  1. document.addEventListener( 'visibilitychange', onVisibilityChange, false);
复制代码

接下来是时间处理函数:

  1. // Adjusts the program behavior, based on whether the webpage is active or hiddenfunction onVisibilityChange() {    if( document.hidden && !video.paused ){         video.pause();     }else  if( video.paused ){         video.play();     } }
复制代码

第七步:对于自定义形状,一次性绘制整个路径

路径是用来在<canvas>元素中创建和绘制自定义形状和轮廓的,这样在任何时候都会有一个活动的路径。

一个路径包含了所有的子路径,每一个子路径都是由canvas坐标点连起来的一条线或者一条曲线。所有的路径绘制功能都是canvas'sscontext对象所拥有的,并且可以分为两组。

这些是子路径的创建函数,用来定义一个子路径,包括lineTo()函数,quadraticCurveTo()函数,bezierCurveTo()函数和arc()函数。我们还有子路径绘制函数:stroke()函数和fill()函数。使用stroke()函数将残生一个轮廓,而fill()函数将创建一个用颜色,渐变或者图案填充的形状。当在canvas中绘制形状和轮廓时,首先创建整个路径会更加有效率,然后一次性对整个路使用stroke()函数和fill()函数。这比每次定义一个子路径再去绘制它来的更快。

以第四步所说的分析器图标为例,每个垂直的蓝线是子路径,那么所有的蓝线共同构成了当前的完整路径。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

现在stroke()函数被用来循环定义每个子路径:

  1. mainContext.beginPath();               for( var j = 0; i < imax; i++, j += 2 ){                mainContext.moveTo( x + j, y ); // define the subpaths starting point    mainContext.lineTo( x + j, y - speedLog[i] * graphScale );  // set the subpath as a line, and define its endpoint      mainContext.stroke(); // draw the subpath to the canvas}
复制代码

首先定义所有的子路径,然后一次性会支持整个路径,这样能让分析器的图标生成的更快,下面展示代码:

  1. mainContext.beginPath();               for( var j = 0; i < imax; i++, j += 2 ){                mainContext.moveTo( x + j, y ); // define the subpaths starting point    mainContext.lineTo( x + j, y - speedLog[i] * graphScale );  // set the subpath as a line, and define its endpoint}  mainContext.stroke(); // draw the whole current path to the mainCanvas.
复制代码

第八步:使用屏幕外的canvas来建立场景

这项优化技术与前面步骤中说的有关,就是他们都基于最小化网页重绘的原则。每当一些要改变文档或者内容的时间被触发,浏览器将会捡块安排重绘工作来更新界面。重绘工作很占用CPU的资源,尤其是网页上密密麻麻的排满了元素和动画。如果你正在创建一个复杂的动画场景,并且同时在canvas中加入了很多动画项目,每个新添加的项目可能就触发了整个页面的重绘。

如果你再屏幕外(内存中)的<canvas>中创建这个场景,一旦完成就一次性绘制出整个屏幕,这样会更好并且更快。

下面的代码就是引用了装饰对象中的<canvas>代码和它的内容,我们将会添加5行用于建立屏幕外的canvas DOM对象的代码,并且符合它最原始的<canvas>尺寸。

  1. var mainCanvas = document.getElementById("mainCanvas"); // points to the on-screen, original HTML canvas elementvar mainContext = mainCanvas.getContext('2d'); // the drawing context of the on-screen canvas elementvar osCanvas = document.createElement("canvas"); // creates a new off-screen canvas elementvar osContext = osCanvas.getContext('2d'); //the drawing context of the off-screen canvas elementosCanvas.width = mainCanvas.width; // match the off-screen canvas dimensions with that of #mainCanvasosCanvas.height = mainCanvas.height;
复制代码

之后我们将查找所有引用了"mainCanvas"的绘制函数,并将其替换为"osCAnvas"。引用了"mainContext"的将被替换为"osContext"。

现在所有的东西都会被绘制在屏幕外的canvas中,而不是在原来的canvas中。

最后,我们再添加一行main()函数,这样用于在我们原始的<canvas>中重绘目前屏幕外的<canvas>中的内容。

  1. // As the scripts main function, it controls the pace of the animationfunction main(){    requestAnimationFrame( main, mainCanvas );    if( video.paused || video.currentTime > 59  ){ return; }         var now = new Date().getTime();    if( frameStartTime ){        speedLog.push( now - frameStartTime );    }    frameStartTime = now;    if( video.readyState < 2 ){ return; }         frameCount++;    osCanvas.width = osCanvas.width; //clear the offscreen canvas    drawBackground();    drawFilm();    drawDescription();    drawStats();    drawBlade();    drawTitle();    mainContext.drawImage( osCanvas, 0, 0 ); // copy the off-screen canvas graphics to the on-screen canvas}
复制代码

第九步:尽可能将路径缓存为位图像

在很多类型的图表中,用drawImage()函数绘制图像会比用canvas中的路径方法绘制更快。如果你发现你的一大块代码是用来一次又一次的重复绘制相同的形状和轮廓,你应该将图表存储为位图像来降低浏览器的工作压力,当要求调用drawImage()函数时在一次性将其重新绘制在canvas上。

实现这个有两个方法:

第一个方法就是创建一个JPG,GIF,或者PNG格式的外部图片,然后用Javascript代码将其读取并拷贝到你的canvas上去。这种方法的缺点是你的程序必须从网上下载这些额外的文件,但是如果根据图片的类型或者你的应用程序,这实际上可能是一个很好的解决方案。动画装饰对象用这个方法去加载一个旋转的刀片图片,但是它不能仅仅通过canvas路径绘制功能来重建。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

第二个方法是在屏幕外的canvas即时绘制一个图形,而不是去加载一个外部的图片。

我们将用这个方法去缓存这个动画装饰对象的主题。我们先创建一个变量来引用要创建的新屏幕外的canvas元素。并将其默认值设定为False,这样我们就可以被告知图片的缓存是否被创建,并且在代码开始运行时是否被保存。

  1. var titleCache = false; // points to an off-screen canvas used to cache the animation s
复制代码

然后我们编辑drawTitle()函数功能首先检查titleCache变量的canvas图片是否被创建。如果没有创建,它将创建一个屏幕外的图片并且存储,用titleCache变量引用。

  1. // renders the canvas titlefunction drawTitle(){    if( titleCache == false ){ // create and save the title image        titleCache = document.createElement('canvas');        titleCache.width = osCanvas.width;        titleCache.height = 25;                 var context = titleCache.getContext('2d');                 context.fillStyle = 'black';        context.fillRect( 0, 0, 368, 25 );        context.fillStyle = 'white';        context.font = "bold 21px Georgia";        context.fillText( "SINTEL", 10, 20 );          }     osContext.drawImage( titleCache, 0, 0 );}
复制代码

第十步:用clearRect()函数清理Canvas

绘制一个新的动画帧的第一步就是清除当前canvas上的所有东西。这可以通过重新设定canvas元素的宽度或者使用clearRect()函数来完成。重设宽度有个副作用就是清理了canvas上所有东西后,canvas上的内容会全部恢复到默认状态,这会拖慢我们的工作进度。用clearRect()函数一般会更加便捷的完成清理canvas工作。

在main()函数中我们改变如下:

  1. osCanvas.width = osCanvas.width; //clear the off-screen canvas
复制代码

第十一步:工具层

如果你之前用过类似与Gimp或者Photoshop这一类的图片视频编辑软件,那么你应该大致了解层的概念了,整个图片就是很多图层堆叠的最顶部,而且每个堆叠层都可以独立选择和编辑。

将其引用只鹅canvas动画场景中,每个层都将是一个独立的canvas元素,放在彼此的顶部并应用CSS代码来创建出一个单一元素的视觉效果。

作为一中优化技术,他的工作原理就是最好在一个场景中前景和背景元素有明确的区分,并且大部分动作都在前台发生。背景可以绘制一些在每一个动画帧中没太大变化的canvas元素,而且前景中会有一个更加动态的canvas附在它上面。用这种方法,真哥哥场景就不用在每一个动画帧之后重绘一次了。

不幸的是,这个动画的装饰对象并不是一个当我们能充分运用这种技术的例子,因为前景后背景的元素都很动态和复杂。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

第十二步:仅仅更新动画不断变化的场景

这是另一种优化技术,很大程度上依赖于动画场景的组成。

它可用于动画场景集中在canvas一个特定的矩形的情况。这样我们就能清除和仅仅重绘那个矩形地区。

举个例子,这个Sintel标题在整个动画过程都不变化,这样在下个动画帧开始时清理相应的canvas而不管这个地方。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

为了引用这个技术,我们将这些在main()函数中绘制函数替换为如下:

  1. if( titleCache == false ){ // If titleCache is false, the animation's title hasn't been drawn yet    drawTitle(); // we draw the title. This function will now be called just once, when the program starts    osContext.rect( 0, 25, osCanvas.width, osCanvas.height ); // this creates a path covering the area outside by the title    osContext.clip(); // we use the path to create a clipping region, that ignores the title's region}
复制代码

第十三步:尽量减少子像素的渲染

子像素渲染或者是抗锯齿发生是,浏览器会自动引用图像效果以消除锯齿状边缘。它的结果是让图片和动画看上去更平滑,只要你在canvas上绘制图像的时候指定少量的坐标就会自动激活。因为现在它应该做的工作还没有一个标准,,所以像素渲染在跨浏览器的渲染输出结果会有一点不一致。这也将降低渲染速度因为浏览器必须对生成的效果进行一些计算。由于canvas的抗锯齿不能直接关闭,唯一能绕过这个功能的方法就是在你的绘图坐标中使用整数。

我们必须用Math.floor()函数来确定在我们代码中的整数能否应用。举个例子,这是在drawFilm()函数中一些代码:

  1. punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate
复制代码

第十四步:衡量结果

我们已经看了不少关于动画动画的技术,现在是时候去回顾结果了。

<ignore_js_op style="word-wrap: break-word;">Html5 Canvas的充分运用:实用示例

这张表展示了平均渲染时间和canvasFPS在前后的区别,我们可以看到在所有浏览器中都有重大提升,尽管只有谷歌浏览器真正实现了我们最初的33毫秒渲染时间的目标。这意味着要实现这个目标还需要做很多工作。我们可以继续通过应用更加普遍的Javascript优化技术,如果仍然失败,我们应该要考虑通过删除花里胡哨的效果来淡化动画。但是我们此时不会寻求任何别的技术,因为这里的重点是优化<canvas>动画。

Canvas API在现在仍然是新技术并且还在发展,所以继续研究,测试,探索和分享,感谢阅读这个教程。

相关推荐