HTML5 Полотно оптимізація: практичний приклад
() translation by (you can also view the original English article)
Якщо ви займаєтеся розробкою на JavaScript досить довго, ви, швидше за все, розбився Ваш браузер кілька разів. Проблема, як правило, виявляється деякі JavaScript помилка, як нескінченний цикл while; якщо ні, то наступний підозрюваний-сторінка перетворень або анімації, що включає додавання і видалення елементів на веб-сторінці або анімації CSS властивості. Цей підручник фокусується на оптимізацію анімації виробляється з допомогою JS і HTML5 в елемент .
Цей підручник починається і закінчується з тим, що віджет HTML5 анімації, які ви бачите нижче:
Ми візьмемо його з нами в подорож, досліджуючи різні виникають полотно поради з оптимізації та методи їх застосування у вихідному коді JavaScript віджета. Мета полягає в тому, щоб поліпшити швидкість виконання віджета і в кінцевому підсумку з гладкою, більше рідини віджет анімація, живлення від компактною, більш ефективний код JavaScript.
Вихідний файл містить HTML і JavaScript від кожного кроку в підручник, так що ви можете слідувати поряд з будь-якої точки.
Давайте зробимо перший крок.
Крок 1: грати в кіно трейлер
Віджет створений на основі фільму Трейлер Синтел, 3D анімаційне кіно Фонд блендер. Він побудований з використанням двох з найбільш цікавих доповнень в HTML5: на елементі і елементи.
В теге для завантаження і відтворення відео файлів Синтел, в той час як <полотно> генерує свою послідовність анімації зйомки ігрового відео і змішування його з тексту та іншої графіки. При натисканні на відтворення відео, полотно оживає з темним фоном, що велика чорно-біла копія ігрового відео. Менше, кольоровий екран-кадри відео копіюються на місце, і вона промайне, як частина плівки малюнку.



У лівому верхньому куті, заголовок і кілька рядків тексту, які з'являються і зникають, як відтворюється анімація. Швидкість роботи скрипта і супутні показники включені як частина анімації, невеликий чорний ящик в лівому нижньому куті графіка і яскравий текст. Ми будемо дивитися на цей конкретний пункт більш докладно пізніше.
Нарешті, є великий обертається лопаті, яка літає по сцені на початку анімації, графіки яких завантажується з зовнішнього файлу зображення у форматі PNG.
Крок 2: перегляньте вихідний код
Вихідний код містить звичайне з'єднання з HTML, CSS і JavaScript. HTML є негусто: тільки з допомогою і теги, укладений в контейнер з:
1 |
<div id="animationWidget" > |
2 |
<canvas width="368" height="208" id="mainCanvas" ></canvas> |
3 |
<video width="184" height="104" id="video" autobuffer="autobuffer" controls="controls" poster="poster.jpg" > |
4 |
<source src="sintel.mp4" type="video/mp4" ></source> |
5 |
<source src="sintel.webm" type="video/webm" ></source> |
6 |
</video>
|
7 |
</div>
|
Контейнер присвоюється ідентифікатор (animationWidget), яка виступає в якості гака для всіх правила CSS застосовуються до нього і його зміст (нижче).
1 |
|
2 |
#animationWidget{ |
3 |
border:1px #222 solid; |
4 |
position:relative; |
5 |
width: 570px; |
6 |
height: 220px; |
7 |
}
|
8 |
#animationWidget canvas{ |
9 |
border:1px #222 solid; |
10 |
position:absolute; |
11 |
top:5px; |
12 |
left:5px; |
13 |
}
|
14 |
#animationWidget video{ |
15 |
position:absolute; |
16 |
top:110px; |
17 |
left:380px; |
18 |
}
|
У той час як HTML і CSS є мариновані спеції і приправи, його в JavaScript, що це м'ясо віджета.
- У верхній частині, ми маємо основні об'єкти, які будуть часто використовуватися через скрипт, у тому числі посилання на елемент canvas, а її 2D-контекст.
- Функція init() викликається щоразу, коли відео починає грати, і встановлює всі об'єкти, що використовуються в сценарії.
- У sampleVideo (функція) відображає поточного кадру відтвореного відео, а setBlade() завантажує зовнішні зображення вимагає анімації.
- Темпи і зміст анімації контролюються функції main (), яка є як серцебиття сценарію. На регулярній основі, як тільки відео починає грати, він малює кожен кадр анімації, знявши спочатку полотно, то виклик кожного з п'яти функцій скрипта малювання:
метод drawbackground()
drawFilm()
drawTitle()
drawDescription()
drawStats()
Як видно з назв, кожна функція малювання відповідає за малювання елемента в сцені анімації. Структурувати код, таким чином, підвищує гнучкість і дозволяє в подальшому легше.
Повний сценарій показана нижче. Знайдіть хвилинку, щоб оцінити його, і подивитися, якщо ви можете визначити будь-які зміни, які ви хотіли зробити, щоб його прискорити.
1 |
(function(){ |
2 |
if( !document.createElement("canvas").getContext ){ return; } //the canvas tag isn't supported |
3 |
|
4 |
var mainCanvas = document.getElementById("mainCanvas"); // points to the HTML canvas element above |
5 |
var mainContext = mainCanvas.getContext('2d'); //the drawing context of the canvas element |
6 |
var video = document.getElementById("video"); // points to the HTML video element |
7 |
var frameDuration = 33; // the animation's speed in milliseconds |
8 |
video.addEventListener( 'play', init ); // The init() function is called whenever the user presses play & the video starts/continues playing |
9 |
video.addEventListener( 'ended', function(){ drawStats(true); } ); //drawStats() is called one last time when the video end, to sum up all the statistics |
10 |
|
11 |
var videoSamples; // this is an array of images, used to store all the snapshots of the playing video taken over time. These images are used to create the 'film reel' |
12 |
var backgrounds; // this is an array of images, used to store all the snapshots of the playing video taken over time. These images are used as the canvas background |
13 |
var blade; //An canvas element to store the image copied from blade.png |
14 |
var bladeSrc = 'blade.png'; //path to the blade's image source file |
15 |
|
16 |
var lastPaintCount = 0; // stores the last value of mozPaintCount sampled |
17 |
var paintCountLog = []; // an array containing all measured values of mozPaintCount over time |
18 |
var speedLog = []; // an array containing all the execution speeds of main(), measured in milliseconds |
19 |
var fpsLog = []; // an array containing the calculated frames per secong (fps) of the script, measured by counting the calls made to main() per second |
20 |
var frameCount = 0; // counts the number of times main() is executed per second. |
21 |
var frameStartTime = 0; // the last time main() was called |
22 |
|
23 |
// Called when the video starts playing. Sets up all the javascript objects required to generate the canvas animation and measure perfomance
|
24 |
function init(){ |
25 |
if( video.currentTime > 1 ){ return; } |
26 |
|
27 |
bladeSrc = new Image(); |
28 |
bladeSrc.src = "blade.png"; |
29 |
bladeSrc.onload = setBlade; |
30 |
|
31 |
backgrounds = []; |
32 |
videoSamples = []; |
33 |
fpsLog = []; |
34 |
paintCountLog = []; |
35 |
if( window.mozPaintCount ){ lastPaintCount = window.mozPaintCount; } |
36 |
speedLog = []; |
37 |
frameCount = 0; |
38 |
frameStartTime = 0; |
39 |
main(); |
40 |
setTimeout( getStats, 1000 ); |
41 |
}
|
42 |
|
43 |
// As the scripts main function, it controls the pace of the animation
|
44 |
function main(){ |
45 |
setTimeout( main, frameDuration ); |
46 |
if( video.paused || video.ended ){ return; } |
47 |
|
48 |
var now = new Date().getTime(); |
49 |
if( frameStartTime ){ |
50 |
speedLog.push( now - frameStartTime ); |
51 |
}
|
52 |
frameStartTime = now; |
53 |
if( video.readyState < 2 ){ return; } |
54 |
|
55 |
frameCount++; |
56 |
mainCanvas.width = mainCanvas.width; //clear the canvas |
57 |
drawBackground(); |
58 |
drawFilm(); |
59 |
drawDescription(); |
60 |
drawStats(); |
61 |
drawBlade(); |
62 |
drawTitle(); |
63 |
}
|
64 |
|
65 |
// This function is called every second, and it calculates and stores the current frame rate
|
66 |
function getStats(){ |
67 |
if( video.readyState >= 2 ){ |
68 |
if( window.mozPaintCount ){ //this property is specific to firefox, and tracks how many times the browser has rendered the window since the document was loaded |
69 |
paintCountLog.push( window.mozPaintCount - lastPaintCount ); |
70 |
lastPaintCount = window.mozPaintCount; |
71 |
}
|
72 |
|
73 |
fpsLog.push(frameCount); |
74 |
frameCount = 0; |
75 |
}
|
76 |
setTimeout( getStats, 1000 ); |
77 |
}
|
78 |
|
79 |
// create blade, the ofscreen canavs that will contain the spining animation of the image copied from blade.png
|
80 |
function setBlade(){ |
81 |
blade = document.createElement("canvas"); |
82 |
blade.width = 400; |
83 |
blade.height = 400; |
84 |
blade.angle = 0; |
85 |
blade.x = -blade.height * 0.5; |
86 |
blade.y = mainCanvas.height/2 - blade.height/2; |
87 |
}
|
88 |
|
89 |
// Creates and returns a new image that contains a snapshot of the currently playing video.
|
90 |
function sampleVideo(){ |
91 |
var newCanvas = document.createElement("canvas"); |
92 |
newCanvas.width = video.width; |
93 |
newCanvas.height = video.height; |
94 |
newCanvas.getContext("2d").drawImage( video, 0, 0, video.width, video.height ); |
95 |
return newCanvas; |
96 |
}
|
97 |
|
98 |
// renders the dark background for the whole canvas element. The background features a greyscale sample of the video and a gradient overlay
|
99 |
function drawBackground(){ |
100 |
var newCanvas = document.createElement("canvas"); |
101 |
var newContext = newCanvas.getContext("2d"); |
102 |
newCanvas.width = mainCanvas.width; |
103 |
newCanvas.height = mainCanvas.height; |
104 |
newContext.drawImage( video, 0, video.height * 0.1, video.width, video.height * 0.5, 0, 0, mainCanvas.width, mainCanvas.height ); |
105 |
|
106 |
var imageData, data; |
107 |
try{ |
108 |
imageData = newContext.getImageData( 0, 0, mainCanvas.width, mainCanvas.height ); |
109 |
data = imageData.data; |
110 |
} catch(error){ // CORS error (eg when viewed from a local file). Create a solid fill background instead |
111 |
newContext.fillStyle = "yellow"; |
112 |
newContext.fillRect( 0, 0, mainCanvas.width, mainCanvas.height ); |
113 |
imageData = mainContext.createImageData( mainCanvas.width, mainCanvas.height ); |
114 |
data = imageData.data; |
115 |
}
|
116 |
|
117 |
//loop through each pixel, turning its color into a shade of grey
|
118 |
for( var i = 0; i < data.length; i += 4 ){ |
119 |
var red = data[i]; |
120 |
var green = data[i + 1]; |
121 |
var blue = data[i + 2]; |
122 |
var grey = Math.max( red, green, blue ); |
123 |
|
124 |
data[i] = grey; |
125 |
data[i+1] = grey; |
126 |
data[i+2] = grey; |
127 |
}
|
128 |
newContext.putImageData( imageData, 0, 0 ); |
129 |
|
130 |
//add the gradient overlay
|
131 |
var gradient = newContext.createLinearGradient( mainCanvas.width/2, 0, mainCanvas.width/2, mainCanvas.height ); |
132 |
gradient.addColorStop( 0, '#000' ); |
133 |
gradient.addColorStop( 0.2, '#000' ); |
134 |
gradient.addColorStop( 1, "rgba(0,0,0,0.5)" ); |
135 |
newContext.fillStyle = gradient; |
136 |
newContext.fillRect( 0, 0, mainCanvas.width, mainCanvas.height ); |
137 |
|
138 |
mainContext.save(); |
139 |
mainContext.drawImage( newCanvas, 0, 0, mainCanvas.width, mainCanvas.height ); |
140 |
|
141 |
mainContext.restore(); |
142 |
}
|
143 |
|
144 |
// renders the 'film reel' animation that scrolls across the canvas
|
145 |
function drawFilm(){ |
146 |
var sampleWidth = 116; // the width of a sampled video frame, when painted on the canvas as part of a 'film reel' |
147 |
var sampleHeight = 80; // the height of a sampled video frame, when painted on the canvas as part of a 'film reel' |
148 |
var filmSpeed = 20; // determines how fast the 'film reel' scrolls across the generated canvas animation. |
149 |
var filmTop = 120; //the y co-ordinate of the 'film reel' animation |
150 |
var filmAngle = -10 * Math.PI / 180; //the slant of the 'film reel' |
151 |
var filmRight = ( videoSamples.length > 0 )? videoSamples[0].x + videoSamples.length * sampleWidth : mainCanvas.width; //the right edge of the 'film reel' in pixels, relative to the canvas |
152 |
|
153 |
//here, we check if the first frame of the 'film reel' has scrolled out of view
|
154 |
if( videoSamples.length > 0 ){ |
155 |
var bottomLeftX = videoSamples[0].x + sampleWidth; |
156 |
var bottomLeftY = filmTop + sampleHeight; |
157 |
bottomLeftX = Math.floor( Math.cos(filmAngle) * bottomLeftX - Math.sin(filmAngle) * bottomLeftY ); // the final display position after rotation |
158 |
|
159 |
if( bottomLeftX < 0 ){ //the frame is offscreen, remove it's refference from the film array |
160 |
videoSamples.shift(); |
161 |
}
|
162 |
}
|
163 |
|
164 |
// add new frames to the reel as required
|
165 |
while( filmRight <= mainCanvas.width ){ |
166 |
var newFrame = {}; |
167 |
newFrame.x = filmRight; |
168 |
newFrame.canvas = sampleVideo(); |
169 |
videoSamples.push(newFrame); |
170 |
filmRight += sampleWidth; |
171 |
}
|
172 |
|
173 |
// create the gradient fill for the reel
|
174 |
var gradient = mainContext.createLinearGradient( 0, 0, mainCanvas.width, mainCanvas.height ); |
175 |
gradient.addColorStop( 0, '#0D0D0D' ); |
176 |
gradient.addColorStop( 0.25, '#300A02' ); |
177 |
gradient.addColorStop( 0.5, '#AF5A00' ); |
178 |
gradient.addColorStop( 0.75, '#300A02' ); |
179 |
gradient.addColorStop( 1, '#0D0D0D' ); |
180 |
|
181 |
mainContext.save(); |
182 |
mainContext.globalAlpha = 0.9; |
183 |
mainContext.fillStyle = gradient; |
184 |
mainContext.rotate(filmAngle); |
185 |
|
186 |
// loops through all items of film array, using the stored co-ordinate values of each to draw part of the 'film reel'
|
187 |
for( var i in videoSamples ){ |
188 |
var sample = videoSamples[i]; |
189 |
var punchX, punchY, punchWidth = 4, punchHeight = 6, punchInterval = 11.5; |
190 |
|
191 |
//draws the main rectangular box of the sample
|
192 |
mainContext.beginPath(); |
193 |
mainContext.moveTo( sample.x, filmTop ); |
194 |
mainContext.lineTo( sample.x + sampleWidth, filmTop ); |
195 |
mainContext.lineTo( sample.x + sampleWidth, filmTop + sampleHeight ); |
196 |
mainContext.lineTo( sample.x, filmTop + sampleHeight ); |
197 |
mainContext.closePath(); |
198 |
|
199 |
//adds the small holes lining the top and bottom edges of the 'fim reel'
|
200 |
for( var j = 0; j < 10; j++ ){ |
201 |
punchX = sample.x + ( j * punchInterval ) + 5; |
202 |
punchY = filmTop + 4; |
203 |
mainContext.moveTo( punchX, punchY + punchHeight ); |
204 |
mainContext.lineTo( punchX + punchWidth, punchY + punchHeight ); |
205 |
mainContext.lineTo( punchX + punchWidth, punchY ); |
206 |
mainContext.lineTo( punchX, punchY ); |
207 |
mainContext.closePath(); |
208 |
punchX = sample.x + ( j * punchInterval ) + 5; |
209 |
punchY = filmTop + 70; |
210 |
mainContext.moveTo( punchX, punchY + punchHeight ); |
211 |
mainContext.lineTo( punchX + punchWidth, punchY + punchHeight ); |
212 |
mainContext.lineTo( punchX + punchWidth, punchY ); |
213 |
mainContext.lineTo( punchX, punchY ); |
214 |
mainContext.closePath(); |
215 |
}
|
216 |
mainContext.fill(); |
217 |
}
|
218 |
|
219 |
//loop through all items of videoSamples array, update the x co-ordinate values of each item, and draw its stored image onto the canvas
|
220 |
mainContext.globalCompositeOperation = 'lighter'; |
221 |
for( var i in videoSamples ){ |
222 |
var sample = videoSamples[i]; |
223 |
sample.x -= filmSpeed; |
224 |
mainContext.drawImage( sample.canvas, sample.x + 5, filmTop + 10, 110, 62 ); |
225 |
}
|
226 |
|
227 |
mainContext.restore(); |
228 |
}
|
229 |
|
230 |
// renders the canvas title
|
231 |
function drawTitle(){ |
232 |
mainContext.save(); |
233 |
mainContext.fillStyle = 'black'; |
234 |
mainContext.fillRect( 0, 0, 368, 25 ); |
235 |
mainContext.fillStyle = 'white'; |
236 |
mainContext.font = "bold 21px Georgia"; |
237 |
mainContext.fillText( "SINTEL", 10, 20 ); |
238 |
mainContext.restore(); |
239 |
}
|
240 |
|
241 |
// renders all the text appearing at the top left corner of the canvas
|
242 |
function drawDescription(){ |
243 |
var text = []; //stores all text items, to be displayed over time. the video is 60 seconds, and each will be visible for 10 seconds. |
244 |
text[0] = "Sintel is an independently produced short film, initiated by the Blender Foundation."; |
245 |
text[1] = "For over a year an international team of 3D animators and artists worked in the studio of the Amsterdam Blender Institute on the computer-animated short 'Sintel'."; |
246 |
text[2] = "It is an epic short film that takes place in a fantasy world, where a girl befriends a baby dragon."; |
247 |
text[3] = "After the little dragon is taken from her violently, she undertakes a long journey that leads her to a dramatic confrontation."; |
248 |
text[4] = "The script was inspired by a number of story suggestions by Martin Lodewijk around a Cinderella character (Cinder in Dutch is 'Sintel'). "; |
249 |
text[5] = "Screenwriter Esther Wouda then worked with director Colin Levy to create a script with multiple layers, with strong characterization and dramatic impact as central goals."; |
250 |
text = text[Math.floor( video.currentTime / 10 )]; //use the videos current time to determine which text item to display. |
251 |
|
252 |
mainContext.save(); |
253 |
var alpha = 1 - ( video.currentTime % 10 ) / 10; |
254 |
mainContext.globalAlpha = ( alpha < 5 )? alpha : 1; |
255 |
mainContext.fillStyle = '#fff'; |
256 |
mainContext.font = "normal 12px Georgia"; |
257 |
|
258 |
//break the text up into several lines as required, and write each line on the canvas
|
259 |
text = text.split(' '); |
260 |
var colWidth = mainCanvas.width * .75; |
261 |
var line = ''; |
262 |
var y = 40; |
263 |
for(var i in text ){ |
264 |
line += text[i] + ' '; |
265 |
if( mainContext.measureText(line).width > colWidth ){ |
266 |
mainContext.fillText( line, 10, y ); |
267 |
line = ''; |
268 |
y += 12; |
269 |
}
|
270 |
}
|
271 |
mainContext.fillText( line, 10, y ); |
272 |
|
273 |
mainContext.restore(); |
274 |
}
|
275 |
|
276 |
//updates the bottom-right potion of the canvas with the latest perfomance statistics
|
277 |
function drawStats( average ){ |
278 |
var x = 245.5, y = 130.5, graphScale = 0.25; |
279 |
|
280 |
mainContext.save(); |
281 |
mainContext.font = "normal 10px monospace"; |
282 |
mainContext.textAlign = 'left'; |
283 |
mainContext.textBaseLine = 'top'; |
284 |
mainContext.fillStyle = 'black'; |
285 |
mainContext.fillRect( x, y, 120, 75 ); |
286 |
|
287 |
//draw the x and y axis lines of the graph
|
288 |
y += 30; |
289 |
x += 10; |
290 |
mainContext.beginPath(); |
291 |
mainContext.strokeStyle = '#888'; |
292 |
mainContext.lineWidth = 1.5; |
293 |
mainContext.moveTo( x, y ); |
294 |
mainContext.lineTo( x + 100, y ); |
295 |
mainContext.stroke(); |
296 |
mainContext.moveTo( x, y ); |
297 |
mainContext.lineTo( x, y - 25 ); |
298 |
mainContext.stroke(); |
299 |
|
300 |
// draw the last 50 speedLog entries on the graph
|
301 |
mainContext.strokeStyle = '#00ffff'; |
302 |
mainContext.fillStyle = '#00ffff'; |
303 |
mainContext.lineWidth = 0.3; |
304 |
var imax = speedLog.length; |
305 |
var i = ( speedLog.length > 50 )? speedLog.length - 50 : 0 |
306 |
mainContext.beginPath(); |
307 |
for( var j = 0; i < imax; i++, j += 2 ){ |
308 |
mainContext.moveTo( x + j, y ); |
309 |
mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); |
310 |
mainContext.stroke(); |
311 |
}
|
312 |
|
313 |
// the red line, marking the desired maximum rendering time
|
314 |
mainContext.beginPath(); |
315 |
mainContext.strokeStyle = '#FF0000'; |
316 |
mainContext.lineWidth = 1; |
317 |
var target = y - frameDuration * graphScale; |
318 |
mainContext.moveTo( x, target ); |
319 |
mainContext.lineTo( x + 100, target ); |
320 |
mainContext.stroke(); |
321 |
|
322 |
// current/average speedLog items
|
323 |
y += 12; |
324 |
if( average ){ |
325 |
var speed = 0; |
326 |
for( i in speedLog ){ speed += speedLog[i]; } |
327 |
speed = Math.floor( speed / speedLog.length * 10) / 10; |
328 |
}else { |
329 |
speed = speedLog[speedLog.length-1]; |
330 |
}
|
331 |
mainContext.fillText( 'Render Time: ' + speed, x, y ); |
332 |
|
333 |
// canvas fps
|
334 |
mainContext.fillStyle = '#00ff00'; |
335 |
y += 12; |
336 |
if( average ){ |
337 |
fps = 0; |
338 |
for( i in fpsLog ){ fps += fpsLog[i]; } |
339 |
fps = Math.floor( fps / fpsLog.length * 10) / 10; |
340 |
}else { |
341 |
fps = fpsLog[fpsLog.length-1]; |
342 |
}
|
343 |
mainContext.fillText( ' Canvas FPS: ' + fps, x, y ); |
344 |
|
345 |
// browser frames per second (fps), using window.mozPaintCount (firefox only)
|
346 |
if( window.mozPaintCount ){ |
347 |
y += 12; |
348 |
if( average ){ |
349 |
fps = 0; |
350 |
for( i in paintCountLog ){ fps += paintCountLog[i]; } |
351 |
fps = Math.floor( fps / paintCountLog.length * 10) / 10; |
352 |
}else { |
353 |
fps = paintCountLog[paintCountLog.length-1]; |
354 |
}
|
355 |
mainContext.fillText( 'Browser FPS: ' + fps, x, y ); |
356 |
}
|
357 |
|
358 |
mainContext.restore(); |
359 |
}
|
360 |
|
361 |
//draw the spining blade that appears in the begining of the animation
|
362 |
function drawBlade(){ |
363 |
if( !blade || blade.x > mainCanvas.width ){ return; } |
364 |
blade.x += 2.5; |
365 |
blade.angle = ( blade.angle - 45 ) % 360; |
366 |
|
367 |
//update blade, an ofscreen canvas containing the blade's image
|
368 |
var angle = blade.angle * Math.PI / 180; |
369 |
var bladeContext = blade.getContext('2d'); |
370 |
blade.width = blade.width; //clear the canvas |
371 |
bladeContext.save(); |
372 |
bladeContext.translate( 200, 200 ); |
373 |
bladeContext.rotate(angle); |
374 |
bladeContext.drawImage( bladeSrc, -bladeSrc.width/2, -bladeSrc.height/2 ); |
375 |
bladeContext.restore(); |
376 |
|
377 |
mainContext.save(); |
378 |
mainContext.globalAlpha = 0.95; |
379 |
mainContext.drawImage( blade, blade.x, blade.y + Math.sin(angle) * 50 ); |
380 |
mainContext.restore(); |
381 |
}
|
382 |
})();
|
Крок 3: оптимізація коду: знати правила
Перше правило кодексу оптимізація продуктивності: немає.
Зміст цього правила полягає в запобіганні оптимізація заради оптимізації, так як процес відбувається за ціною.
Оптимізований скрипт буде легше для браузера, щоб проаналізувати і обробити, але, як правило, з тягарем для людей, які буде важче стежити і підтримувати. Всякий раз, коли ви вирішите, що деякі оптимізації необхідно, ставити якісь цілі заздалегідь, так що ви не захоплюйтеся процесом і перестаратися.
Мета оптимізації цей віджет буде мати функції main() виконати менш ніж за 33 мілісекунди, як це передбачається, який буде відповідати частота кадрів відтворення відео файлів (синтэл.MP4 і Синтел.формат WebM). Ці файли були закодовані зі швидкістю 30 кадрів в секунду (тридцяти кадрів в секунду), що означає приблизно 0,33 сек або 33 мілісекунд на Кадр ( 1 секунда ÷ 30 кадрів ).
Оскільки JavaScript малює новий кадр анімації на полотно кожен раз, коли функція main() викликається, мета нашого процесу оптимізації буде зробити цю функцію беруть 33 мілісекунд або менше кожен раз, коли він працює. Ця функція багаторазово викликає сама себе за допомогою методу setTimeout() в JavaScript таймер, як показано нижче.
1 |
|
2 |
var frameDuration = 33; // set the animation's target speed in milliseconds |
3 |
function main(){ |
4 |
if( video.paused || video.ended ){ return false; } |
5 |
setTimeout( main, frameDuration ); |
Друге правило: поки немає.
Це правило підкреслює, що оптимізація завжди повинно бути зроблено наприкінці процесу розвитку, коли ви вже конкретизовані деякі повний робочий код. Поліція оптимізація дозволить нам йти на це, оскільки скрипт віджету-прекрасний приклад повного, робоча програма, готова до процесу.
Третє правило: не ще, а профіль першого.
Це правило про розуміння вашої програми з точки зору продуктивності. Профілювання допоможе вам знати, а не здогадуватися, які функції або галузі сценарію займають більшу частину часу або часто використовуються, так що ви можете зосередитися на тих, в процесі оптимізації. Досить критично важлива для провідних браузерів корабель з вбудований в JavaScript профилометры, або розширення, які надають цю послугу.
Я побіг віджет під профайлером в Firebug, і нижче представлений скріншот результатів.



Крок 4: Встановіть Деякі Показники Продуктивності
Як ви запустили віджет, я впевнений, що ви знайшли всі Синтел речі в порядок, і були абсолютно вражені елемент у правому нижньому кутку полотна, з красивою графічної і блискучий текст.

Це не просто гарне обличчя, що також забезпечує деяку статистику продуктивності в реальному часі на запущеної програми. Його насправді простий, голі кістки профилировщика JavaScript. Це вірно! Я чув, що ти, як профілювання, так що я поклав профілювальник в кіно, так що ви можете профіль під час перегляду.
Цей графік відстежує час рендеринга, розраховується шляхом вимірювання, як довго кожен запуск функції main() у мілісекундах. Оскільки це функція, яка розмальовує кожен кадр анімації, це ефективно частоту кадрів анімації. Кожна вертикальна синя лінія на діаграмі показано час, що витрачається на один кадр. Червона горизонтальна лінія-це швидкість цілі, які ми ставимо на 33ms, щоб відповідати частоту кадрів відео файлів. Трохи нижче графік, швидкість останнього дзвінка main() дається в мілісекундах.
Профайлер-це також зручний браузер візуалізації тест швидкості. На даний момент, середній час рендеринга в Firefox 55ms, 90МС в IE 9, 41ms в Chrome, 148ms в опері і 63ms в Safari. Всі браузери були запущені на Windows XP, за винятком IE 9, який був представлений в Windows Vista.
Наступний метричний нижче, що це полотно кадрів в секунду (полотно кадрів в секунду), отриманих шляхом підрахунку, скільки разів в Main() викликається в секунду. Профайлер відображає останній полотно частоту кадрів коли відео грає досі, та коли вона закінчується, він показує середню швидкість всіх викликів функції main().
Остання метрика браузера ФПС, який вимірює, скільки браузер перемальовує поточне вікно кожну секунду. Це доступно тільки при перегляді віджетів в Firefox, так як він залежить від того, в даний час функція доступна тільки в браузері під назвою Вікно.mozPaintCount., властивість JavaScript, який відстежує, скільки разів у вікні браузера була перефарбована так як веб-сторінка спочатку завантажується.
На перефарбовує, як правило, відбуваються, коли подія або дія, що змінює вигляд сторінки відбувається, наприклад, коли ви прокрутіть сторінку вниз або наведіть вказівник миші на посилання. Це фактично реальна частота кадрів в браузері, який визначається як зайнятий поточної веб-сторінки.
Щоб оцінити, який ефект неоптимізованих анімації на mozPaintCount, я прибрав тег Canvas і JavaScript, так як для відстеження частоти кадрів браузері при програванні тільки відео. Мої тести були зроблені в консолі Firebug, за допомогою функції нижче:
1 |
var lastPaintCount = window.mozPaintCount; |
2 |
setInterval( function(){ |
3 |
console.log( window.mozPaintCount - lastPaintCount ); |
4 |
lastPaintCount = window.mozPaintCount; |
5 |
}, 1000); |
Результати: частота кадрів браузер був між 30 і 32 ФПС, коли відео грає, і впала до 0-1 ФПС, коли відео закінчилося. Це означає, що Firefox був регулювати частоту вікно перемальовується відповідно ігрове відео, закодоване при 30 кадрів в секунду. Коли був виконаний тест з ООН-оптимізований анімації і відео грають разом, він сповільнився до 16fps, як браузер був зараз з усіх сил, щоб виконати всі JavaScript і ще перефарбувати її вікно на час, що відтворення відео і анімації полотно мляво.
Ми зараз почати налаштування нашої програми, і як ми це зробимо, ми будемо відстежувати час рендеринга, полотно ФПС і ФПС браузера, щоб оцінити ефект зміни.
Крок 5: Використовуйте метод requestAnimationFrame()
Останні два вищенаведених фрагментах коду JavaScript використовувати функції setTimeout() і setInterval() функції таймера. Щоб використовувати ці функції, можна вказати інтервал часу в мілісекундах і функції зворотного виклику, ви хочете виконати закінчення цього часу. Різниця між двома полягає в тому, що метод setTimeout() буде викликати функцію тільки один раз, а через setInterval() викликає його повторно.
Хоча ці функції завжди були незамінними інструментами в комплекті на JavaScript аніматора, у них є кілька недоліків:
По-перше, часовий інтервал встановити не завжди надійні. Якщо програма все ще знаходиться в середині виконання чого-то ще, коли інтервал закінчується, функція зворотного виклику буде виконуватися пізніше, ніж початково встановлений, як тільки браузер більше не зайнятий. У функції main (), ми встановили інтервал 33 мс - але як профайлер показує, що функція насправді називається Все 148 мілісекунд в опері.
По-друге, існує проблема з перефарбовує браузера. Якщо б у нас була функція зворотного виклику, яка генерується 20 кадрів анімації в секунду, в той час як браузер перефарбували свого вікна лише 12 разів на секунду, 8 виклики цієї функції будуть витрачені даремно, так як користувач ніколи не отримаєте, щоб побачити результати.
Нарешті, браузер не має можливості дізнатися, що викликається функція анімує елементів в документі. Це означає, що якщо ці елементи прокрутки поза увагою, або користувач натискає на іншу вкладку, зворотний виклик, як і раніше, вам неодноразово виконуватися, витрачати цикли процесора.
Використовуючи метод requestAnimationFrame() вирішує більшість цих проблем, і її можна використовувати замість функції таймера анімації в HTML5. Замість вказівки тимчасового інтервалу, метод requestAnimationFrame() синхронізує викликів функцій у вікні браузера перефарбовує. Це призводить до збільшення об'єму рідини, послідовне анімація як немає кадрів, і браузер може зробити подальше внутрішньої оптимізації знаючи анімація в прогрес.
Щоб замінити функції setTimeout() з допомогою методу requestAnimationFrame в наш віджет, то спочатку додайте такий рядок у верхній частині нашого скрипта:
1 |
requestAnimationFrame = window.requestAnimationFrame || |
2 |
window.mozRequestAnimationFrame || |
3 |
window.webkitRequestAnimationFrame || |
4 |
window.msRequestAnimationFrame || |
5 |
setTimeout; |
Як специфікація є ще досить новою, в деяких браузерах або в різних версіях однієї браузера мають власних експериментальних реалізацій, ця лінія гарантує, що ім'я функції вказує на правильний метод, якщо він доступний, і падає назад в setTimeout (), якщо не. Тоді у функції main (), ми змінюємо цю рядок:
1 |
|
2 |
setTimeout( main, frameDuration ); |
...щоб:
1 |
requestAnimationFrame( main, canvas ); |
Перший параметр приймає функцію зворотного виклику, яка в даному випадку є функція main (). Другий параметр є необов'язковим і вказує на елемент DOM, який містить анімацію. Вона повинна бути використана для розрахунку додаткових оптимізацій.
Зверніть увагу, що getStats() функція також використовує метод setTimeout(), але ми залишимо це в місці, оскільки ця функція не має нічого спільного з анімацією сцени. метод requestAnimationFrame() був створений спеціально для анімації, так що якщо ваша функція зворотного виклику не роблю анімації, ви можете використовувати setTimeout() або setInterval().
Крок 6: Використовуйте сторінку API для видимості
В останньому кроці ми зробили метод requestAnimationFrame потужність полотно анімації, і тепер у нас нова проблема. Якщо у нас почати працювати віджет, а потім згорнути вікно браузера або перейти на нову вкладку, Вікно якої віджета перефарбувати дроселі вниз, щоб зберегти владу. Це також уповільнює полотно анімації, оскільки він тепер синхронізований з темпом перефарбувати - що було б чудово, якщо відео не продовжувати грати до кінця.
Нам потрібен спосіб, щоб визначити, коли сторінка не проглядається, так що ми можете призупинити відтворення відео; це коли сторінка API видимості приходить на допомогу.
API містить набір властивостей, функцій і подій ми можемо використовувати, щоб виявити, якщо веб-сторінка має вигляд або прихована. Потім ми можемо додати код, який регулює поведінку нашої програми відповідно. Ми будемо використовувати цей API, щоб призупинити відтворення відео віджета всякий раз, коли сторінка неактивна.
Ми почнемо з додавання нового прослушивателя подій для нашого скрипта:
1 |
|
2 |
document.addEventListener( 'visibilitychange', onVisibilityChange, false); |
Далі йде функція обробника подій:
1 |
|
2 |
// Adjusts the program behavior, based on whether the webpage is active or hidden
|
3 |
function onVisibilityChange() { |
4 |
if( document.hidden && !video.paused ){ |
5 |
video.pause(); |
6 |
}else if( video.paused ){ |
7 |
video.play(); |
8 |
}
|
9 |
}
|
Крок 7: для нестандартної фігури, намалювати весь шлях відразу
Доріжки використовуються, щоб створити і намалювати власні форми й обриси на елементі елемент, який буде в усі часи мати один активний шлях.
Шлях містить список суб-шляху, і кожен суб-шлях складається з полотна координатою точки пов'язані або лінією, або кривої. Весь шлях створення і малювання функцій властивості об'єкта контексту полотна, і можуть бути класифіковані на дві групи.
Є подпути-функції, які використовуються для визначення подпути і включають кінці lineto(), quadraticCurveTo(), bezierCurveTo () і дугового(). Тоді у нас є інсульт() і заповнити(), шлях/подпуть функції малювання. Використовуючи інсульт() створює контур, а заливку() створює заповнювати форму чи колір, градієнт або візерунок.
При малюванні фігури і начерки на полотні, ефективніше спочатку створити весь шлях, тоді точно інсульт() або заповнити() це один раз, а не визначати і креслення кожної supbath одночасно. Беручи графік профилировщика описано на кроці 4 в якості прикладу, кожна одиночна вертикальна синя лінія-подпуть, а всі вони разом складають весь поточний шлях.

Інсульт() метод в даний час називається цикл, який визначає кожен подпуть:
1 |
|
2 |
mainContext.beginPath(); |
3 |
for( var j = 0; i < imax; i++, j += 2 ){ |
4 |
mainContext.moveTo( x + j, y ); // define the subpaths starting point |
5 |
mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); // set the subpath as a line, and define its endpoint |
6 |
mainContext.stroke(); // draw the subpath to the canvas |
7 |
}
|
Цей графік можна зробити набагато більш ефективно, спочатку потрібно визначити всі подпути, то просто малював весь шлях відразу, як показано нижче.
1 |
|
2 |
mainContext.beginPath(); |
3 |
for( var j = 0; i < imax; i++, j += 2 ){ |
4 |
mainContext.moveTo( x + j, y ); // define the subpaths starting point |
5 |
mainContext.lineTo( x + j, y - speedLog[i] * graphScale ); // set the subpath as a line, and define its endpoint |
6 |
}
|
7 |
mainContext.stroke(); // draw the whole current path to the mainCanvas. |
Крок 8: використовувати внеэкранный полотно для побудови сцени
Цей метод оптимізації відноситься на попередньому кроці, в тому, що вони обидва засновані на тому ж принципі мінімізації сторінці перефарбовує.
Всякий раз, коли щось відбувається, що змінює вигляд чи зміст документа, браузер повинен запланувати операції перефарбовувати незабаром після цього на оновлення інтерфейсу. Перефарбовує може бути дорогою операцією з точки зору циклів процесора і потужність, особливо для щільних сторінок з великою кількістю елементів і анімації відбувається. Якщо ви не вибудовуючи складні сцени анімації шляхом додавання безлічі предметів один раз у елементі , кожне нове доповнення може викликати цілий перефарбовувати.
Краще і набагато швидше будувати сцену на екрані (у пам'яті) і після цього фарбувати всю сцену лише один раз на екрані, видимому елементі .
Трохи нижче код, який отримує посилання на віджета і його контекст, ми додамо п'ять нових ліній, що створити внеэкранный полотно об'єкт DOM і зіставити його розміри з оригіналу, видно елемента .
1 |
|
2 |
var mainCanvas = document.getElementById("mainCanvas"); // points to the on-screen, original HTML canvas element |
3 |
var mainContext = mainCanvas.getContext('2d'); // the drawing context of the on-screen canvas element |
4 |
var osCanvas = document.createElement("canvas"); // creates a new off-screen canvas element |
5 |
var osContext = osCanvas.getContext('2d'); //the drawing context of the off-screen canvas element |
6 |
osCanvas.width = mainCanvas.width; // match the off-screen canvas dimensions with that of #mainCanvas |
7 |
osCanvas.height = mainCanvas.height; |
Потім ми виконаємо пошук і заміна у всіх функціях малювання для всіх посилань на "mainCanvas" і змінити його на "osCanvas". Посилання на "mainContext" буде замінено на "osContext". Все тепер буде звернено на нові закадрові полотно, замість оригінального елемента .
Нарешті, ми додаємо ще одну рядок в Main (), яка малює те, що в даний час на екрані за допомогою елемента в наших оригінальних елемента .
1 |
|
2 |
// As the scripts main function, it controls the pace of the animation
|
3 |
function main(){ |
4 |
requestAnimationFrame( main, mainCanvas ); |
5 |
if( video.paused || video.currentTime > 59 ){ return; } |
6 |
|
7 |
var now = new Date().getTime(); |
8 |
if( frameStartTime ){ |
9 |
speedLog.push( now - frameStartTime ); |
10 |
}
|
11 |
frameStartTime = now; |
12 |
if( video.readyState < 2 ){ return; } |
13 |
|
14 |
frameCount++; |
15 |
osCanvas.width = osCanvas.width; //clear the offscreen canvas |
16 |
drawBackground(); |
17 |
drawFilm(); |
18 |
drawDescription(); |
19 |
drawStats(); |
20 |
drawBlade(); |
21 |
drawTitle(); |
22 |
mainContext.drawImage( osCanvas, 0, 0 ); // copy the off-screen canvas graphics to the on-screen canvas |
23 |
}
|
Крок 9: Шляхи Кешу У Вигляді Растрових Зображень, Коли Це Можливо
Для багатьох видів графіки, за допомогою методу drawImage() буде набагато швидше, ніж будувати одні і ті ж зображення на полотно за допомогою шляху. Якщо ви виявите, що велика зілля скрипта витрачається постійно малював один і той же форм і обрисів, знову і знову, ви можете зберегти певну роботу браузера шляхом кешування отриманий малюнок в якості растрового зображення, малювати його тільки один раз на полотні, коли потрібно за допомогою методу drawImage().
Є два способи зробити це.
Перший-шляхом створення зовнішнього файлу зображення у форматі JPG, GIF або PNG зображення, а потім завантажувати його динамічно за допомогою JavaScript і скопіювавши його на полотно. Єдиний недолік цього методу-це додаткові файли, програму доведеться завантажити з мережі, але в залежності від типу графіка або що ваш додаток робить, це може виявитися гарним рішенням. Віджет анімації використовує цей метод, щоб завантажити спінінг лезо графіки, які неможливо було б відтворити з допомогою всього шляху полотні функції малювання.



Другий спосіб передбачає якраз після малювання графіки на екрані замість завантаження зовнішнього зображення. Ми будемо використовувати цей метод для кешування назва віджета анімації. Спочатку ми створюємо змінну для посилання на нове закадрове елемент canvas, який буде створений. Його значення за замовчуванням-false, так що ми можемо сказати, є чи не кешувати зображення був створений і зберігається після того, як скрипт починає виконуватися:
1 |
|
2 |
var titleCache = false; // points to an off-screen canvas used to cache the animation scene's title |
Потім ми відредагуємо drawTitle (функцій) у перший перевірити titleCache зображення полотно було створено. Якщо це не так, він створює образ поза екрану і зберігає посилання на нього в titleCache:
1 |
|
2 |
// renders the canvas title
|
3 |
function drawTitle(){ |
4 |
if( titleCache == false ){ // create and save the title image |
5 |
titleCache = document.createElement('canvas'); |
6 |
titleCache.width = osCanvas.width; |
7 |
titleCache.height = 25; |
8 |
|
9 |
var context = titleCache.getContext('2d'); |
10 |
context.fillStyle = 'black'; |
11 |
context.fillRect( 0, 0, 368, 25 ); |
12 |
context.fillStyle = 'white'; |
13 |
context.font = "bold 21px Georgia"; |
14 |
context.fillText( "SINTEL", 10, 20 ); |
15 |
}
|
16 |
|
17 |
osContext.drawImage( titleCache, 0, 0 ); |
18 |
}
|
Крок 10: очистити полотно з методу clearrect()
Першим кроком у розробці нового кадру анімації, щоб очистити полотно від поточного. Це можна зробити або скидання ширини елемента canvas, або з допомогою методу clearrect() функція.
Скидання ширину має побічний ефект також, очищення поточного полотно контексту за замовчуванням, який може пригальмувати. За допомогою методу clearrect() завжди швидше і краще, щоб очистити полотно.
У функції main (), ми змінимо це:
1 |
|
2 |
osCanvas.width = osCanvas.width; //clear the off-screen canvas |
...на це:
1 |
|
2 |
osContext.clearRect( 0, 0, osCanvas.width, osCanvas.height ); //clear the offscreen canvas |
Крок 11: Створення Шарів
Якщо ви працювали з зображення або відео-редагування програмного забезпечення, як Gimp або Photoshop раніше, то ви вже знайомі з поняттям "шари", де зображення складається з укладання багато зображень поверх один одного, і кожен з них може бути обраний і змінений окремо.
Наноситься на полотно сцени анімації, кожен шар буде окремий елемент canvas, розміщені на верхній частині один з одним за допомогою CSS, щоб створити ілюзію одного елемента. В якості методу оптимізації, це працює найкраще, коли є чітке розмежування між переднього плану і фону елементів сцени, причому більша частина дій відбувається на передньому плані. Фоновому режимі, потім можна нанести на полотно елемент, який не сильно змінюється між кадрами анімації, а на передньому плані ще більш динамічний елемент canvas над ним. Таким чином, вся сцена не повинна бути знову перефарбували в кожному кадрі анімації.
На жаль, віджет анімації-це хороший приклад сцени, де ми не можемо з користю застосувати цей метод, так як обидва переднього плану і фону елементів сильно пожвавлюються.



Крок 12: оновити тільки зміни областей сцени анімації
Це ще один метод оптимізації, який значною мірою залежить від складу анімацію сцени. Він може бути використаний, коли сцена анімації концентрується навколо певної прямокутну область на полотні. Тоді ми могли б ясно і просто перекроїти перекроїти цей регіон.
Наприклад, назва Синтел залишається незмінним протягом більшої частини анімації, так що ми могли б залишити це куточок незайманої, коли знявши полотно на наступний кадр анімації.



Для реалізації цієї технології, ми замінюємо рядки, які викликає назва функції малювання в Main() з допомогою наступних блоків:
1 |
|
2 |
if( titleCache == false ){ // If titleCache is false, the animation's title hasn't been drawn yet |
3 |
drawTitle(); // we draw the title. This function will now be called just once, when the program starts |
4 |
osContext.rect( 0, 25, osCanvas.width, osCanvas.height ); // this creates a path covering the area outside by the title |
5 |
osContext.clip(); // we use the path to create a clipping region, that ignores the title's region |
6 |
}
|
Крок 13: Мінімізувати Субпиксельный Рендеринг
Субпиксельный рендеринг і згладжування відбувається, коли браузер автоматично застосовує графічні ефекти, щоб видалити нерівні краї. Це призводить до згладжування перегляд зображень і анімації, і автоматично включається всякий раз, коли ви вказати дробові координати, а не ціле число для нанесення на полотно.
Зараз немає стандарту на те, як це повинно бути зроблено, так субпиксельного візуалізації трохи непослідовно в різних браузерах, з точки зору візуалізації. Він також уповільнює швидкість рендеринга в браузері повинен зробити деякі розрахунки, щоб створити ефект. Як полотно згладжування не може бути безпосередньо відключена, єдиний спосіб обійти це з допомогою цілих чисел на кресленні координати.
Ми використовуємо математику.підлогу() для забезпечення цілого числа в наш сценарій, коли це застосовно. Наприклад, наступний рядок у drawFilm():
1 |
|
2 |
punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate |
...переписана як:
1 |
|
2 |
punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate |
Крок 14: виміряти результати
Ми переглянули чимало полотно методи оптимізації анімації, і його зараз час, щоб розглянути результати.



Ця таблиця показує до і після середнього часу render і полотно ФПС. Ми можемо побачити деякі значні поліпшення у всіх браузерах, але це тільки хром, що насправді наближається до досягнення нашої початкової метою максимальної 33ms час рендеринга. Це означає, що ще багато належить зробити для досягнення цієї мети.
Ми могли б продовжити, застосовуючи більш загальні методи оптимізації JavaScript, а якщо і це не допомагає, можливо, розглянути зменшити анімації, видаливши деякі навороти. Але ми не будемо дивитися на інші методи сьогодні, як основну увагу було приділено оптимізації для елемента анімація.
Полотно API-це досить новий і росте кожен день, тому продовжуйте експериментувати, тестування, вивчення і обмін. Спасибі за читання підручника.