Advertisement
  1. Web Design
  2. Animation

Pengoptimalan Canvas HTML5: Contoh Praktis

Scroll to top
Read Time: 31 min

() translation by (you can also view the original English article)

Jika Anda telah melakukan pengembangan JavaScript cukup lama, kemungkinan besar Anda akan menggagalkan browser Anda beberapa kali. Masalahnya biasanya ternyata beberapa bug JavaScript, seperti perulangan while tanpa henti; jika tidak, tersangka berikutnya adalah transformasi halaman atau animasi - jenis yang melibatkan penambahan dan penghapusan elemen dari halaman web atau menganimasi properti gaya CSS. Tutorial ini berfokus pada pengoptimalan animasi yang dihasilkan dengan menggunakan JS dan elemen <canvas> HTML5

Tutorial ini dimulai dan diakhiri dengan widget animasi HTML5 yang Anda lihat di bawah ini:

Kami akan membawanya bersama kami dalam perjalanan, menjelajahi berbagai kiat dan teknik pengoptimalan canvas dan menerapkannya ke kode sumber widget JavaScript. Tujuannya adalah untuk meningkatkan kecepatan eksekusi widget dan berakhir dengan widget animasi yang lebih halus dan lebih cair, didukung oleh JavaScript yang lebih ramping dan lebih efisien.

Download sumber berisi HTML dan JavaScript dari setiap langkah dalam tutorial, sehingga Anda dapat mengikuti dari manapun juga.

Mari kita ikuti langkah pertama.


Langkah 1: Memainkan Trailer Film

Widget di atas didasarkan pada trailer film untuk Sintel, sebuah film animasi 3D oleh Blender Foundation. Dibuat dengan menggunakan dua tambahan HTML5 yang paling populer: elemen <canvas> dan <video>. dan .

<video> memuat dan memutar file video Sintel, sedangkan <canvas> menghasilkan urutan animasinya sendiri dengan mengambil snapshot dari video yang diputar dan mencampurnya dengan teks dan grafis lainnya. Saat Anda mengklik untuk memutar video, canvas muncul di latar belakang gelap yang merupakan salinan video hitam dan putih yang lebih besar dari video yang sedang diputar. Screenshot video yang lebih kecil dan berwarna disalin ke layar, dan meluncur di atasnya sebagai bagian dari ilustrasi gulungan film.

Di pojok kiri atas, kita memiliki judul dan beberapa baris teks deskriptif yang memudar masuk dan keluar saat animasi dimainkan. Kecepatan kinerja script dan metrik terkait disertakan sebagai bagian dari animasi, di kotak hitam kecil di pojok kiri bawah dengan grafik dan teks yang jelas. Kita akan melihat item khusus ini secara lebih rinci nanti.

Akhirnya, ada pisau besar berputar yang terbang melintasi layar di awal animasi, yang grafisnya diambil dari file gambar PNG eksternal.


Langkah 2: Melihat Sumbernya

Kode sumber berisi campuran HTML, CSS dan Javascript biasa. HTML-nya jarang: hanya tag <canvas> dan <video>, dilampirkan dalam <div> kontainer:

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>

<div> kontainer diberikan sebuah ID (animationWidget), yang bertindak sebagai kait untuk semua aturan CSS yang diterapkan padanya dan isinya (di bawah).

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
}

Sementara HTML dan CSS adalah rempah-rempah dan bumbu yang diasinkan, JavaScript adalah daging dari widget.

  • Di bagian atas, kita memiliki objek utama yang akan sering digunakan melalui skrip, termasuk referensi ke elemen canvas dan konteks 2D-nya.
  • Fungsi init() dipanggil setiap kali video mulai diputar, dan mengatur semua objek yang digunakan dalam skrip.
  • Fungsi sampleVideo() menangkap frame video yang diputar saat ini, sedangkan setBlade() memuat gambar eksternal yang dibutuhkan oleh animasi.
  • Kecepatan dan konten animasi canvas dikendalikan oleh fungsi main(), yang seperti detak jantung dari skrip. Jalankan pada interval reguler begitu video mulai diputar, ia melukis setiap frame animasi dengan terlebih dahulu membersihkan canvas, lalu memanggil masing-masing dari lima fungsi menggambar dari skrip:
    • drawBackground()
    • drawFilm()
    • drawTitle()
    • drawDescription()
    • drawStats()

Seperti namanya, setiap fungsi menggambar bertanggung jawab untuk menggambar item di adegan animasi. Penataan kode dengan cara ini meningkatkan fleksibilitas dan mempermudah perawatan di masa depan.

Skrip lengkap ditunjukkan di bawah ini. Luangkan waktu untuk menilainya, dan lihat apakah Anda bisa menemukan perubahan yang akan Anda lakukan untuk mempercepatnya.

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
})();

Langkah 3: Optimalisasi Kode: Ketahui Aturannya

Aturan pertama dari optimasi kinerja kode adalah: Jangan.

Inti dari peraturan ini adalah untuk mencegah optimasi demi optimalisasi, karena prosesnya sesuai dengan harga tertentu.

Skrip yang sangat optimal akan memudahkan browser untuk mengurai dan mengolahnya, namun biasanya dengan beban bagi manusia yang akan merasa lebih sulit untuk diikuti dan dipelihara. Kapan pun Anda memutuskan bahwa beberapa pengoptimalan diperlukan, tetapkan beberapa tujuan sebelumnya sehingga Anda tidak terbawa oleh proses dan berlebihan.

Tujuan dalam mengoptimalkan widget ini adalah agar fungsi main() berjalan dalam waktu kurang dari 33 milidetik sebagaimana mestinya, yang akan cocok dengan frame rate dari file video yang dimainkan (sintel.mp4 dan sintel.webm). File-file ini dikodekan pada kecepatan pemutaran 30fps (tiga puluh frame per detik), yang berarti sekitar 0,33 detik atau 33 milidetik per frame ( 1 detik ÷ 30 frame ).

Karena JavaScript menggambar frame animasi baru ke canvas setiap kali fungsi main() dipanggil, tujuan proses pengoptimalan kita adalah membuat fungsi ini memakan waktu 33 milidetik atau kurang setiap kali berjalan. Fungsi ini berulang kali memanggil dirinya sendiri menggunakan timer Javascript setTimeout() seperti yang ditunjukkan di bawah ini.

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 );

Aturan kedua: Belum.

Aturan ini menekankan bahwa pengoptimalan harus selalu dilakukan pada akhir proses pengembangan apabila Anda telah menyelesaikan beberapa kode yang bekerja dengan lengkap. Polisi optimalisasi akan membiarkan kita melakukan yang satu ini, karena skrip widget adalah contoh sempurna dari program yang bekerja dengan lengkap yang siap untuk prosesnya.

Aturan ketiga: Belum, dan profil dulu.

Aturan ini adalah tentang memahami program Anda dalam hal kinerja runtime. Profil membantu Anda mengetahui daripada menebak fungsi atau area skrip mana yang paling banyak digunakan atau paling sering digunakan, sehingga Anda dapat berfokus pada proses pengoptimalan. Hal ini cukup penting untuk membuat browser terkemuka dikirimkan dengan profiler JavaScript yang terpasang, atau memiliki ekstensi yang menyediakan layanan ini.

Saya menjalankan widget di bawah profiler di Firebug, dan di bawah adalah screenshot dari hasilnya.


Langkah 4: Tetapkan Beberapa Metrik Kinerja

Saat Anda menjalankan widget, saya yakin Anda menemukan semua barang Sintel baik-baik saja, dan benar-benar terpesona oleh item di sudut kanan bawah canvas, yang memiliki grafik dan teks yang indah.

Ini bukan hanya wajah cantik; kotak itu juga memberikan beberapa statistik kinerja real-time pada program yang sedang berjalan. Itu sebenarnya sederhana, profiler Javascript dasar. Benar! Yo, saya mendengar Anda menyukai profil, jadi saya memasukkan profiler ke dalam film Anda, sehingga Anda bisa mem-profil saat Anda menonton.

Grafiknya melacak Render Time, dihitung dengan mengukur berapa lama setiap menjalankan dari main() yang dibutuhkan dalam milidetik. Karena ini adalah fungsi yang menarik setiap frame dari animasi, secara efektif frame rate dari animasi. Setiap garis biru vertikal pada grafik menggambarkan waktu yang dibutuhkan oleh satu frame. Garis horizontal merah adalah kecepatan target, yang kita atur pada 33ms agar sesuai dengan frame rate file video. Tepat di bawah grafik, kecepatan panggilan terakhir ke main() yang diberikan dalam milidetik.

Profiler juga merupakan uji coba kecepatan rendering browser yang praktis. Saat ini, rata-rata waktu render di Firefox adalah 55ms, 90ms di IE 9, 41ms di Chrome, 148ms di Opera dan 63ms di Safari. Semua browser berjalan pada Windows XP, kecuali IE 9 yang diprofilkan pada Windows Vista.

Metrik berikutnya di bawah itu adalah Canvas FPS (frame per detik dari canvas), yang diperoleh dengan menghitung berapa kali main() dipanggil per detik. Profiler menampilkan tingkat FPS Canvas terbaru saat video masih diputar, dan saat berakhir ia menunjukkan kecepatan rata-rata semua panggilan ke main().

Metrik terakhir adalah Browser FPS, yang mengukur berapa banyak browser menghapus jendela saat ini setiap detiknya. Yang satu ini hanya tersedia jika Anda melihat widget di Firefox, karena bergantung pada fitur yang saat ini hanya tersedia di browser itu yang disebut window.mozPaintCount., sebuah properti JavaScript yang melacak berapa kali jendela browser telah digambar ulang sejak halaman dimuat pertama kali.

Penggambaran ulang biasanya terjadi ketika sebuah peristiwa atau tindakan yang mengubah tampilan halaman, seperti saat Anda menggulir ke bawah halaman atau mouse-over sebuah link. Ini secara efektif merupakan frame rate nyata browser, yang ditentukan oleh seberapa sibuknya halaman web saat ini.

Untuk mengukur efek animasi canvas yang tidak dioptimalkan pada mozPaintCount, saya menghapus tag canvas dan semua JavaScript, sehingga dapat melacak frame rate browser saat memutar video saja. Tes saya dilakukan di konsol Firebug, dengan menggunakan fungsi di bawah ini:

1
	var lastPaintCount = window.mozPaintCount;
2
	setInterval( function(){
3
		console.log( window.mozPaintCount - lastPaintCount );
4
		lastPaintCount = window.mozPaintCount;
5
	}, 1000);

Hasilnya: Frame rate browser adalah antara 30 dan 32 FPS saat video diputar, dan menurun ke FPS 0-1 saat video tersebut berakhir. Ini berarti bahwa Firefox menyesuaikan frekuensi penggambaran ulang jendela agar sesuai dengan video yang diputar, dienkodekan pada 30fps. Ketika tes dijalankan dengan animasi canvas yang tidak dioptimalkan dan video yang diputar bersamaan, ia akan melambat hingga 16fps, karena browser sekarang berjuang untuk menjalankan semua JavaScript dan masih menggambar ulang jendela tepat waktu, membuat pemutaran video dan animasi canvas melamban.

Kita sekarang akan mulai mengutak-atik program kita, dan saat kita melakukannya, kita akan terus melacak Render Time, Canvas FPS dan Browser FPS untuk mengukur dampak perubahan kita.


Langkah 5: Gunakan requestAnimationFrame()

Dua cuplikan JavaScript terakhir di atas menggunakan fungsi timer setTimeout() dan setInterval(). Untuk menggunakan fungsi ini, Anda menentukan interval waktu dalam milidetik dan fungsi callback yang Anda ingin dieksekusi setelah berlalunya waktu. Perbedaan antara keduanya adalah setTimeout() akan memanggil fungsi Anda sekali saja, sementara setInterval() memanggilnya berulang kali.

Sementara fungsi ini selalu menjadi alat yang sangat diperlukan dalam kit animator JavaScript, mereka memiliki beberapa kekurangan:

Pertama, set interval waktu tidak selalu bisa diandalkan. Jika program masih di tengah mengeksekusi sesuatu yang lain saat interval berlalu, fungsi callback akan dieksekusi nantinya dari yang semula ditetapkan, setelah browser tidak lagi sibuk. Pada fungsi main(), kita menetapkan interval ke 33 milidetik - namun seiring profilernya mengungkapkan, fungsi ini sebenarnya dipanggil setiap 148 milidetik di Opera.

Kedua, ada masalah dengan penggambaran ulang browser. Jika kita memiliki fungsi callback yang menghasilkan 20 animasi frame per detik sementara browser menggambar ulang jendelanya hanya 12 kali per detik, 8 panggilan ke fungsi itu akan terbuang karena pengguna tidak akan pernah bisa melihat hasilnya.

Akhirnya, browser tidak memiliki cara untuk mengetahui bahwa fungsi yang dipanggil adalah elemen animasi dalam dokumen. Ini berarti bahwa jika elemen tersebut digeser keluar dari tampilan, atau pengguna mengklik tab lain, callback akan tetap dieksekusi berulang kali, menghabiskan siklus CPU.

Menggunakan requestAnimationFrame() memecahkan sebagian besar masalah ini, dan ini bisa digunakan sebagai pengganti fungsi timer dalam animasi HTML5. Alih-alih menentukan interval waktu, requestAnimationFrame() menyinkronkan fungsi panggilan dengan penggambaran ulang jendela browser. Ini menghasilkan animasi yang lebih cair dan konsisten karena tidak ada frame yang dijatuhkan, dan browser dapat melakukan pengoptimalan internal lebih jauh karena mengetahui animasi sedang berlangsung.

Untuk mengganti setTimeout() dengan requestAnimationFrame di widget kita, pertama-tama tambahkan baris berikut di bagian atas skrip kita:

1
requestAnimationFrame = window.requestAnimationFrame || 
2
						window.mozRequestAnimationFrame || 
3
						window.webkitRequestAnimationFrame || 
4
						window.msRequestAnimationFrame || 
5
						setTimeout;

Karena spesifikasinya masih cukup baru, beberapa browser atau versi browser memiliki implementasi eksperimental mereka sendiri, baris ini memastikan bahwa nama fungsi menunjuk ke metode yang benar jika tersedia, dan berbalik kembali ke setTimeout() jika tidak. Kemudian pada fungsi main(), kita mengubah baris ini:

1
 
2
	setTimeout( main, frameDuration );

...menjadi:

1
	requestAnimationFrame( main, canvas );

Parameter pertama mengambil fungsi callback, yang dalam hal ini adalah fungsi main(). Parameter kedua adalah opsional, dan menentukan elemen DOM yang berisi animasi. Ini seharusnya digunakan untuk menghitung pengoptimalan tambahan.

Perhatikan bahwa fungsi getStats() juga menggunakan setTimeout(), namun kita membiarkannya karena fungsi khusus ini tidak ada hubungannya dengan menganimasikan adegan. requestAnimationFrame() dibuat khusus untuk animasi, jadi jika fungsi callback Anda tidak melakukan animasi, Anda masih bisa menggunakan setTimeout() atau setInterval().


Langkah 6: Gunakan API Page Visibility

Pada langkah terakhir kita membuat requestAnimationFrame menggerakkan animasi canvas, dan sekarang kita memiliki masalah baru. Jika kita mulai menjalankan widget, kemudian minimalkan jendela browser atau beralih ke tab baru, tingkat jendela widget menggambar ulang menurun untuk menghemat daya. Ini juga memperlambat animasi canvas karena sekarang disinkronisasi dengan tingkat menggambar ulang - yang akan sempurna jika video tidak terus diputar sampai akhir.

Kita membutuhkan cara untuk mendeteksi kapan halaman tidak dilihat sehingga kita bisa menghentikan sementara pemutaran video; di sinilah API Page Visibility datang untuk menyelamatkan.

API berisi seperangkat properti, fungsi dan event yang dapat kita gunakan untuk mendeteksi apakah ada halaman web yang dilihat atau disembunyikan. Kita kemudian dapat menambahkan kode yang menyesuaikan perilaku program kita. Kita akan menggunakan API ini untuk mem-pause pemutaran video pada widget setiap kali halaman tidak aktif.

Kita mulai dengan menambahkan event listener baru ke skrip kita:

1
 
2
	document.addEventListener( 'visibilitychange', onVisibilityChange, false);

Selanjutnya datanglah fungsi event handler:

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
}

Langkah 7: Untuk Shape Kustom, Gambarkan Seluruh Path Sekaligus

Path digunakan untuk membuat dan menggambar shape dan garis tepi pada elemen <canvas>, yang setiap saat akan memiliki satu path aktif.

Path berisi daftar sub-path, dan setiap sub-path terdiri dari titik koordinat canvas yang dihubungkan bersama oleh garis atau kurva. Semua fungsi pembuatan dan penggambaran path adalah properti dari objek context canvas, dan dapat digolongkan menjadi dua kelompok.

Ada fungsi pembuatan subpath, yang digunakan untuk menentukan subpath dan menyertakan lineTo()quadraticCurveTo()bezierCurveTo(), dan arc(). Kemudian kita memiliki stroke() dan fill(), fungsi menggambar path/subpath. Menggunakan stroke() akan menghasilkan garis tepi, sedangkan fill() menghasilkan shape yang diisi dengan warna, gradien atau pola.

Saat menggambar shape dan garis tepi pada canvas, lebih efisien untuk membuat keseluruhan path terlebih dulu, lalu hanya stroke() atau fill() padanya sekali, daripada menentukan dan menggambar setiap supbath sekaligus. Dengan mengambil grafik profiler yang dijelaskan pada Langkah 4 sebagai contoh, masing-masing garis vertikal biru adalah subpath, sementara semuanya bersama-sama membentuk path saat ini.

Metode stroke() saat ini dipanggil dalam satu loop yang mendefinisikan setiap subpath:

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
	}

Grafik ini dapat ditarik jauh lebih efisien dengan terlebih dahulu mendefinisikan semua subpath, lalu menggambar keseluruhan path saat ini sekaligus, seperti yang ditunjukkan di bawah ini.

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.

Langkah 8: Gunakan Canvas Off-Screen untuk Membangun Adegan

Teknik pengoptimalan ini terkait dengan yang ada di langkah sebelumnya, bahwa keduanya didasarkan pada prinsip yang sama untuk meminimalkan penggambaran ulang halaman web.

Setiap kali terjadi sesuatu yang mengubah tampilan atau konten dokumen, browser harus menjadwalkan operasi penggambaran ulang segera setelahnya untuk memperbarui antarmukanya. Penggambaran ulang bisa menjadi operasi yang mahal dalam hal siklus dan kekuatan CPU, terutama untuk halaman yang padat dengan banyak elemen dan animasi yang sedang berlangsung. Jika Anda membangun adegan animasi yang kompleks dengan menambahkan banyak item satu per satu ke <canvas>, setiap penambahan baru mungkin akan memicu keseluruhan penggambaran ulang.

Lebih baik dan lebih cepat untuk membangun adegan di luar layar (dalam memori) <canvas>, dan sekali selesai, gambar seluruh adegan hanya sekali ke layar, <canvas> yang terlihat.

Tepat di bawah kode yang mendapatkan rujukan ke widget <canvas> dan konteksnya, kami akan menambahkan lima baris baru yang membuat objek DOM canvas di luar layar dan sesuai dengan dimensi aslinya, <canvas> yang terlihat.

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;

Kita kemudian akan melakukan pencarian dan penggantian di semua fungsi menggambar untuk semua referensi ke "mainCanvas" dan mengubahnya menjadi "osCanvas". Referensi ke "mainContext" akan diganti dengan "osContext". Segalanya sekarang akan tertarik ke canvas di luar layar yang baru, bukannya <canvas> yang asli.

Akhirnya, kita menambahkan satu baris lagi ke main() yang menggambar apa yang saat ini ada di luar layar <canvas> ke dalam <canvas> asli kita.

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
}

Langkah 9: Cache Path Sebagai Gambar Bitmap Bila Memungkinkan

Untuk banyak jenis grafis, menggunakan drawImage() akan jauh lebih cepat daripada membuat gambar yang sama pada kanvas menggunakan path. Jika Anda menemukan bahwa ramuan besar skrip Anda dihabiskan berulang kali menggambar shape dan garis tepi yang sama berulang-ulang, Anda dapat menghemat beberapa pekerjaan browser dengan menyimpan gambar yang dihasilkan sebagai gambar bitmap, lalu melukisnya sekali saja ke canvas kapan pun dibutuhkan dengan menggunakan drawImage().

Ada dua cara untuk melakukan ini.

Yang pertama adalah dengan membuat file gambar eksternal sebagai gambar JPG, GIF atau PNG, lalu memuatnya secara dinamis menggunakan JavaScript dan menyalinnya ke canvas Anda. Satu kelemahan dari metode ini adalah file tambahan yang harus diunduh program Anda dari jaringan, namun tergantung pada jenis grafis atau aplikasi Anda, ini sebenarnya bisa menjadi solusi yang baik. Widget animasi menggunakan metode ini untuk memuat grafik pisau berputar, yang tidak mungkin dibuat ulang hanya dengan menggunakan fungsi menggambar path dari canvas.

Metode kedua melibatkan hanya dengan menggambar grafis sekali ke canvas di luar layar daripada memuat gambar eksternal. Kita akan menggunakan metode ini untuk men-cache judul dari widget animasi. Kita pertama kali membuat variabel untuk referensi elemen canvas di luar layar baru yang akan dibuat. Nilai defaultnya diset menjadi false, sehingga kita dapat mengetahui apakah cache gambar telah dibuat dan disimpan begitu skrip mulai berjalan:

1
 
2
	var titleCache = false; // points to an off-screen canvas used to cache the animation scene's title

Kita kemudian mengedit fungsi drawTitle() untuk pertama-tama memeriksa apakah gambar canvas titleCache telah dibuat. Jika belum, itu akan menciptakan gambar di luar layar dan menyimpan referensi ke 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
}

Langkah 10: Kosongkan Canvas dengan clearRect()

Langkah pertama dalam menggambar frame animasi baru adalah dengan membersihkan canvas saat ini. Ini dapat dilakukan dengan mengatur ulang lebar elemen canvas, atau menggunakan fungsi clearRect().

Mengatur ulang lebar memiliki efek samping juga membersihkan context canvas saat ini kembali ke keadaan standarnya, yang dapat memperlambat segalanya. Menggunakan clearRect() selalu merupakan cara yang lebih cepat dan lebih baik untuk membersihkan canvas.

Pada fungsi main(), kita akan mengubah ini:

1
 
2
	osCanvas.width = osCanvas.width; //clear the off-screen canvas

...menjadi ini:

1
 
2
	osContext.clearRect( 0, 0, osCanvas.width, osCanvas.height ); //clear the offscreen canvas

Langkah 11: Menerapkan Lapisan

Jika Anda pernah bekerja dengan perangkat lunak pengedit gambar atau video seperti Gimp atau Photoshop sebelumnya, maka Anda sudah terbiasa dengan konsep dari lapisan, di mana gambar disusun dengan menumpuk banyak gambar di atas satu sama lain, dan masing-masing dapat dipilih dan diedit secara terpisah.

Diterapkan pada adegan animasi canvas, setiap lapisan akan menjadi elemen canvas terpisah, ditempatkan di atas satu sama lain menggunakan CSS untuk menciptakan ilusi satu elemen. Sebagai teknik pengoptimalan, ia bekerja paling baik bila ada perbedaan yang jelas antara elemen-elemen latar depan dan latar belakang adegan, dengan sebagian besar tindakan berlangsung di latar depan. Latar belakang kemudian dapat ditarik pada elemen canvas yang tidak banyak berubah antara frame animasi, dan latar depan pada elemen canvas yang lebih dinamis di atasnya. Dengan cara ini, keseluruhan adegan tidak harus digambar ulang lagi untuk setiap frame animasi.

Sayangnya, widget animasi adalah contoh bagus dari sebuah adegan dimana kita tidak dapat menggunakan teknik ini dengan baik, karena elemen latar depan dan latar belakang sangat dianimasikan.


Langkah 12: Perbaharui Hanya Area yang Berubah dari Adegan Animasi

Ini adalah teknik optimasi lain yang sangat bergantung pada komposisi adegan animasi. Ini bisa digunakan saat adegan animasi terkonsentrasi di sekitar area persegi tertentu di canvas. Kita kemudian bisa jelas dan menggambar ulang hanya pada daerah itu.

Misalnya, judul Sintel tetap tidak berubah sepanjang sebagian besar animasi, jadi kita bisa meninggalkan area itu tetap utuh saat membersihkan canvas untuk frame animasi berikutnya.

Untuk menerapkan teknik ini, kita mengganti baris yang memanggil fungsi menggambar judul di main() dengan blok berikut:

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
	}

Langkah 13: Minimalkan Rendering Sub-Pixel

Rendering sub-pixel atau anti-aliasing terjadi saat browser secara otomatis menerapkan efek grafis untuk menghilangkan tepi yang bergerigi. Ini menghasilkan gambar dan animasi yang tampak lebih halus, dan otomatis diaktifkan setiap kali Anda menentukan koordinat fraksional daripada jumlah keseluruhan saat menggambar ke canvas.

Saat ini tidak ada standar tentang bagaimana hal itu harus dilakukan, jadi rendering subpiksel agak tidak konsisten di seluruh browser dalam hal output yang di-render. Ini juga memperlambat kecepatan me-render karena browser harus melakukan beberapa perhitungan untuk menghasilkan efeknya. Karena anti-aliasing canvas tidak bisa langsung dimatikan, satu-satunya cara untuk mengatasinya adalah dengan selalu menggunakan bilangan bulat dalam koordinat gambar Anda.

Kita akan menggunakan Math.floor() untuk memastikan bilangan dalam skrip kita bila diperlukan. Sebagai contoh, baris berikut di drawFilm():

1
 
2
	punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate

...ditulis ulang sebagai:

1
 
2
	punchX = sample.x + ( j * punchInterval ) + 5; // the x co-ordinate

Langkah 14: Mengukur Hasilnya

Kita telah melihat beberapa teknik pengoptimalan animasi canvas, dan sekarang saatnya untuk meninjau hasilnya.

Tabel ini menunjukkan rata-rata Render Times sebelum dan sesudah dan Canvas FPS..API Canvas masih cukup baru dan berkembang setiap hari, jadi teruslah bereksperimen, menguji, mengeksplorasi dan berbagi. Terima kasih telah membaca tutorial. Kita dapat melihat beberapa peningkatan yang signifikan di semua browser, meskipun Chrome hanya yang mendekati pencapaian tujuan awal Render Time maksimum 33ms. Ini berarti masih banyak pekerjaan yang harus dilakukan untuk mendapatkan target tersebut.

Kita bisa melanjutkan dengan menerapkan teknik optimasi JavaScript yang lebih umum, dan jika itu masih gagal, mungkin pertimbangkan untuk menurunkan animasinya dengan menghilangkan beberapa peringatan. Tapi kita tidak akan melihat teknik lain hari ini, karena fokusnya adalah optimasi dari animasi <canvas>.

API Canvas masih cukup baru dan berkembang setiap hari, jadi teruslah bereksperimen, menguji, mengeksplorasi dan berbagi. Terima kasih telah membaca tutorial.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.