Strona początkowa

Animacja w canvas

Niestety canvas nie jest idealnym miejscem jeżeli chcemy zająć się animacją. Jego główna bolączka jest taka, że aby coś zaanimować, musimy w każdej klatce przerysowywać cały obraz canvasu. Dlatego zasada działania jest identyczna jak w grach - czyścimy klatkę, rysujemy wszystko od nowa, po czym powtarzamy cały proces.

Zobacz DEMO tego, czego będziemy się uczyli

Aby animować, musimy nasz kod rysujący wywoływać cyklicznie za pomocą funkcji setTimeout albo setInterval. Pierwsza z nich wywołuje dany kod po jakimś czasie, natomiast druga wywołuje cyklicznie dany kod.

	function draw() {
		...kod rysujący
		
		if (!koniec_animacji) {
			setTimeout('draw()',33);
		}
	}
	draw();
	
	function draw() {
        ...//czyścimy obszar canvasu....
		...rysujemy kolejną klatkę animacji...
		
		if (koniec_animacji) {
			clearInterval(play);
		}
	}

	var play = setInterval('draw()',33);
	

Animowanie pojedynczego obiektu

W naszych przykładach będziemy korzystali z drugiej metody, gdyż w tym przypadku jest ona bardziej "logiczna".

W praktyce w skryptach lepiej posługiwać się setTimeout, gdyż metoda ta "czeka" na wykonanie skryptu. Można to sobie bardzo łatwo wyobrazić: setInterval wywołuje cyklicznie daną funkcję, nie patrząc co się w niej znajduje i ile to się wykonuje. setTimeout wstawiany na końcu funkcji logicznie rzecz biorąc musi poczekać na wykonanie wcześniejego kodu tej funkcji.

Do czyszczenia klatki wykorzystamy znaną nam metodę clearRect(x,y,width,height), którą będziemy czyścić całą zawartość klatki. Zamiast tego możemy użyć także innych metod. Możemy zamalować całe płutno kwadratem (fillRect(x,y,width,height)), narysować obrazek, który przykryje całe płutno (drawImage(obr, x, y)), lub... zmienić rozmiac canvasu.

Czyścić canvas możemy także za pomocą zmiany jego rozmiaru. Jeżeli rozmiar canvasu ulega zmianie, wtedy cały jego obraz musi zostać wyczyszczony, by pasował do nowych wymiarów.
Wystarczy więc użyć canvas.width = canvas.width

	function draw() {
        //czyścimy obszar canvasu
		c.clearRect(0,0,200,100);

		//jezeli gwiazdka dotknie krawędzi canvasu, zmieniamy jej kierunek na przeciwny
		//x jest liczony od lewej strony, dlatego dla prawej i dolnej krawędzi musimy odejmować wymiary obrazka (24x24)
		if (x_gwiazdki<0 || x_gwiazdki>200-24) {
			przesuniecieX = -przesuniecieX;
		}
		if (y_gwiazdki<0 || y_gwiazdki>100-24) {
			przesuniecieY = -przesuniecieY;
		}		
		
		//do x i y gwiazdki dodajemy przesuniecie
		x_gwiazdki += przesuniecieX;
		y_gwiazdki += przesuniecieY;

        //rysujemy gwiazdkę w nowym miejscu
		c.drawImage(obr, x_gwiazdki, y_gwiazdki);
	}



    //x i y gwiazdki
	var x_gwiazdki = 0;
	var	y_gwiazdki = 0;

    //o tyle będzie przesówała się gwiazdka
	var przesuniecieX = 2;
	var przesuniecieY = 2;

    var c = document.getElementById('canvas_anim1').getContext('2d');
	
    var obr = new Image(24,24); //obrazek ma wymiary 24,24
		obr.onload = function(){
            //rozpoczynamy animację
			setInterval('draw()',33);
		}
        obr.src = 'gwiazdka.png';
	
Twoja przegladarka nie obsluguje canvas

Ot cała sztuczka.

Animowanie kilku obiektów

Dla większej liczby obiektów będziemy musieli nieco zmodyfikować nasz kod. Wykorzystamy tablice, obiekty itp.

Zasada działania jest bardzo prosta. Wszystkie animowane obiekty trzymamy w tablicy. Podczas pojedynczej klatki robimy pętlę po tablicy, a następnie animujemy każdy obiekt.

Aby działanie nasze było proste i logiczne, zróbmy sobie klasę, z której będziemy tworzyć nowe animowane obiekty. Obiekty takie powinny mieć swoje właściwości, powinny umieć się przesówać, oraz rysować.aniu metody ruszaj() każdego obiektu.
Aby wiedzieć, że nasza klasa to klasa, jej nazwę zacząłem od _ i dużej litery. Prawdopodobnie w internecie znajdziecie sporo standardów odnośnie takiego nazewnictwa, ale ja zastosuję własny :).

	//klasa super gwiazdki
	function _Gwiazdka(_x, _y, _speed) {
		this.x = _x;
		this.y = _y;
		this.speed = _speed;
		this.przesuniecieX = this.speed;
		this.przesuniecieY = this.speed;
		
		//kazda gwiazdka ma swoj wlasny obrazek - w naszym przykladzie wszystkie sa takie same
		this.obr = new Image(24,24); //obrazek ma wymiary 24,24
		this.obr.src = 'gwiazdka.png';
		
		//obiekt super gwiazdki jest super, dlatego sam siebie rysuje
		this.drawGwiazdka = function() {
			cAnimacja2.drawImage(this.obr, this.x, this.y);
		}
		
		//i sam siebie rusza. Po poruszeniu, rysuje sie.
		this.ruszaj = function() {
			if (this.x<0 || this.x>200-24) {
				this.przesuniecieX = -this.przesuniecieX;
			}
			if (this.y<0 || this.y>100-24) {
				this.przesuniecieY = -this.przesuniecieY;
			}		
			this.x = this.x + this.przesuniecieX;
			this.y = this.y + this.przesuniecieY;

			this.drawGwiazdka();
		}
	}	
	

Po utworzeniu klasy gwiazdki wystarczy stworzyć kilka takich obiektów, a następnie przepisać je do tablicy. To właśnie po niej będziemy robili pętlę wowołując metodę ruszaj dla każdego obiektu.

	
	//funkcja rysujaca animacje
	function draw() {	
		cAnimacja2.fillStyle = "#CFEAFD";
		cAnimacja2.fillRect(0,0,200,100);
		for (i=0; i<gwiazdki.length; i++) {
			gwiazdki[i].ruszaj();
		}
	}
		
    var cAnimacja2 = document.getElementById('canvas_anim2').getContext('2d');
	var ileGwiazdek = 10; //liczba gwiazdek do animowania	
	var gwiazdki = []; //tablica przechowujaca obiekty gwiazdki
	
	//tworzymy nowe obiekty gwiazdki i wrzucamy je do tablicy
	for (i=0; i<ileGwiazdek; i++) {
		var new_x = 20 + Math.random()*160;
		var new_y = 20 + Math.random() * 60;
		var new_speed = Math.round(1 + Math.random()*2);
		gwiazdki.push(new _Gwiazdka(new_x, new_y, new_speed));
	}
	
	//odpalamy animację
	setInterval('draw()', 33);    
	
Twoja przegladarka nie obsluguje canvas

Animacja poklatkowa

Jak już wiemy, metoda drawImage w 3 postaci - drawImage(image, sx, sy, swidth, sheight, dx, dy, dWidth, dHeight) - służy do rysowania na płótnie wyciętego kawałka grafiki. Metoda ta idealnie się nadaje do stworzenia poklatkowej animacji. Jeżeli kiedykolwiek tworzyliście gry w programie GameMaker (co osobiście bardzo polecam), jesteście w zasadzie w domu.
Animacja poklatkowa polega na wyświetlaniu kolejnych klatek animacji, które są ułożone obok siebie w jednym pliku.

fantomas klatki

Istnieje pewna zasada mówiąca, by minimalna liczba klatek wynosiła 3, gdyż przy 2 animacja jest za bardzo skokowa, przez co nienaturalna. Poszedłem więc na łatwiznę i zrobiłem ich minimalną liczbę :)

W internecie jest bardzo dużo grafik typu "sprites", które są "wyciągnięte" ze starych gier 2d. Wystarczy w googlach poszukać sformułowań typu "game sprites", by dostać ich pokaźną kolekcję. Przykładowe linki:
http://www.spriters-resource.com/
http://sdb.drshnaps.com/
wyniki w google

Aby teraz zanimować super Fantomasa, powinniśmy kolejno wyświetlać wycięte fragmenty grafiki. Aby to zrobić, powinniśmy znać numer wyświetlanej klatki, i za pomocą tej liczby obliczać w każdej klatce animacji przesunięcie wycinania.

    var fantom = new Image(240,66); //obrazek ma wymiary 24,24
		fantom.src = 'fantom.png';

	var nr_klatki = 1;
	var liczba_klatek_fantom = 3;
	var szerokosc_klatki = fantom.width / liczba_klatek_fantom;
	var wysokosc_klatki = fantom.height;0
	var c = document.getElementById('canvas_anim1').getContext('2d');

	fantom.onload = function(){
		setInterval('draw()',70);			
	}

	function draw() {	
		c.fillStyle = "#111";
		c.fillRect(0,0,200,100);

		nr_klatki++;
		if (nr_klatki>liczba_klatek_fantom) {
			nr_klatki = 1;
		}
		var xklatki = (nr_klatki-1)*szerokosc_klatki;
		c.drawImage(fantom, xklatki, 0, szerokosc_klatki, wysokosc_klatki, 60, 35, szerokosc_klatki, wysokosc_klatki);		
	}
	

Na początku deklarujemy potrzebne zmienne - aktualną klatkę, liczbę klatek, oraz wyliczamy ile jest wszystkich klatek w animacji. Funkcja draw() wylicza skąd ma zacząć wycinanie kawałka grafiki o rozmiarach "szerokosc_klatki na wyokosc_klatki", a następnie wycięty fragment wyświetla na płótnie. Animacja ma być zapętlona, stąd w linijkach 18-21 sprawdzamy numer klatki i w razie czego go cofamy.
Resztę działania już znamy z poprzednich przykładów na tej stronie.

Twoja przegladarka nie obsluguje canvas

Nawet przy 3 klatkach widać, że ruch nie jest płynny. W naszym przypadku jednak "przeskoki" w animacji są spowodowane tym, że Fantomas potrafi poruszać się szybciej niż rejestruje ludzkie oko... Czasami jednak warto nie iść na łatwiznę :)

Powyższy sposób sprawdzi się dla samotnego Fantomasa, jednak gdy będziemy chcieli zanimować jego przyjaciół, wówczas o wiele lepiej stworzyć je jako obiekty. Podobną klasę już pisaliśmy (_Gwiazdka), więc wystarczy ją nieco przerobić:

	function _Animowany_obiekt(_c, _x, _y, _speed, _obr, _liczba_klatek) {
		this.x = _x;
		this.y = _y;
		this.speed = _speed;
		this.przesuniecieX = this.speed;
		this.przesuniecieY = this.speed;
		this.canvas = _c
		
		this.obr = _obr;
		this.nr_klatki = 1;
		this.liczba_klatek = _liczba_klatek;
		this.szerokosc_klatki = _obr.width / _liczba_klatek;
		this.wysokosc_klatki = _obr.height;
		
		this.draw = function() {
			this.nr_klatki++;
			if (this.nr_klatki>this.liczba_klatek) {this.nr_klatki = 1;}
			var xklatki = (this.nr_klatki-1)*this.szerokosc_klatki;
			this.canvas.drawImage(this.obr, xklatki, 0, this.szerokosc_klatki, this.wysokosc_klatki, this.x, this.y, this.szerokosc_klatki, this.wysokosc_klatki);
		}		

		this.ruszaj = function() {		
			this.x = this.x + this.przesuniecieX;
			this.y = this.y + this.przesuniecieY;
			this.draw();
		}

	}
	

Jest to na szybko przerobiona klasa _Gwiazdka, która teraz wyświetla animowany poklatkowo obiekt. Dodatkowe atrybuty przy jej wywołaniu to _c w którym podajemy zmienną zawierającą canvas (dzięki temu nasza metoda jest jeszcze bardziej super), _obr wskazujący na obiekt z obrazkiem do wyświetlenia oraz _liczba_klatek, które dana animacja ma mieć. Metoda draw zawiera kod animacji poklatkowej, który przed chwilą animował Fantomasa. Metodę ruszaj nieco uprościłem, by była bardziej użyteczna dla naszych potrzeb. Sami łatwo możemy tą klasę rozbudować by obiekty się poruszały, skakały czy gryzły - co kto lubi.

Skoro stworzyliśmy naszą klasę, wykorzystajmy ją by jeszcze lepiej animować naszego Fantomasa.

Standardowo więc pobieramy nasz canvas, by za chwilę po nim rysować:

	var cFA = document.getElementById('canvas_fantomas_wkracza_do_akcji').getContext('2d');
	//no cóż - id mówi samo za siebie
	

Przygotujmy kilka grafik, które wykorzystamy. Są to png z przezroczystym tłem:

fantomas klatki
Animowany Fantomas - tą grafikę już znamy
dom
Duży dom - będzie bliżej obrazu
dom
Mały dom - dalej w tle - rozmycie pogłębi pozorną głębię
fire
Super strzał z super pistoletów Fantomasa
	var dom_d = new Image(66,85);
		dom_d.src = 'dom.png';
	var dom_m = new Image(60,90);
		dom_m.src = 'dom2.png';
	var fire = new Image(135,41);
		fire.src = "fire.png";
    var fantom = new Image(240,66);
		fantom.src = 'fantom.png';
	

Kolejny krok to stworzenie naszego animowanego obiektu (fantomas) korzystając z klasy, którą przed chwilą napisaliśmy:

	//canvas, x, y, speed, obrazek_fantomasa, 3 klatki
	var fantomas = new _Animowany_obiekt(cFA, 60, 35, 0, fantom, 3);
	

Utwórzmy teraz kilka domów, które będą się przemieszczać w tle. Ma być ich kilka, więc tworzymy je za pomocą 2 pętli typu for, w których do tablic dla każdego domu wstawiamy randomową szybkość i pozycję x. Dzięki temu domki będą się przesuwać w różnym tempie, a ruch zaczną od różnego x miejsca. Jako, że nie są to poklatkowo animowane obiekty, więc nie stosujemy naszej klasy.

	var ile_domow_d = 3;
	var ile_domow_m = 5;
	
	var domy_d = [];
	for (i=0; i<ile_domow_d; i++) {
		domy_d.push([2+Math.random() , Math.random()*200])
	}

	var domy_m = [];
	for (i=0; i<ile_domow_m; i++) {
		domy_m.push([1+Math.random() , Math.random()*200])
	}
	

Zakończone przygotowania do wkroczenia do akcji, czas więc odpalić sekwencyjnie funkcję drawFantomasAkcja() (jeżeli mamy kilka takich animacji na stronie, pamiętajmy by każda funkcja animująca miała swoją unikalną nazwę).
Funkcja ta wykonuje pętlę po wszystkich domkach i zmienia ich pozycję x. Jeżeli wyjdą poza płótno (czyli ich x będzie mniejsze od -szerokość_domku), wtedy ustawiamy pozycję drzewka na nowo (na prawo) i wybieramy dla niego nową szybkość. Dodatkowo funkcja ta odpala metodę draw() naszego animowanego obiektu fantomas, która automatycznie go animuje. Jest to łopatologiczny kod, i nie ciekawy.

Nas interesują bardziej rzeczy, które dzieją się po tych 2 pętlach. Pierwsza rzecz to odpalenie metody draw() naszego animowanego obiektu.

Kolejna - ostatnia czynność to losowy strzał z pistoletu (wyświetlenie obrazka wystrzału). Losowość otrzymujemy przez równanie Math.random()>0.8. Wiemy, że random() zwraca wartość z przedziału 0-1, więc nasza losowość wynosi 20%.

	function drawFantomasAkcja() {	
		cFA.fillStyle = "#111";
		cFA.fillRect(0,0,200,100);

		//na poczatku rysujemy małe domki w tle
		for (i=0; i<ile_domow_m; i++) {
			domy_m[i][1] = domy_m[i][1] - domy_m[i][0];
			cFA.drawImage(dom_m, domy_m[i][1], 20);
			if (domy_m[i][1] < -domy_m.width) {
				domy_m[i][1] = 200;
				domy_m[i][0] = 1 + Math.random();
			}
		}

		for (j=0; j<ile_domow_d; j++) {
			domy_d[j][1] = domy_d[j][1] - domy_d[j][0];
			cFA.drawImage(dom_d, domy_d[j][1], 20);
			if (domy_d[j][1] < -dom_d.width) {
				domy_d[j][1] = 200;
				domy_d[j][0] = 2 + Math.random()
			}
		}

		fantomas.draw();

		if (Math.random()>0.8) c.drawImage(fire,114,43)
	}

	//odpalamy funkcję draw()
	fantom.onload = function(){
		setInterval('draw()',100);			
	}
	

Cały listing wygląda tak:

	//klasa obiektów animowanych poklatkowo
	function _Animowany_obiekt(_c, _x, _y, _speed, _obr, _liczba_klatek) {
		this.x = _x;
		this.y = _y;
		this.speed = _speed;
		this.przesuniecieX = this.speed;
		this.przesuniecieY = this.speed;
		this.canvas = _c;
		
		this.obr = _obr;
		this.nr_klatki = 1;
		this.liczba_klatek = _liczba_klatek;
		this.szerokosc_klatki = _obr.width / _liczba_klatek;
		this.wysokosc_klatki = _obr.height;
		
		this.draw = function() {
			this.nr_klatki++;
			if (this.nr_klatki>this.liczba_klatek) {this.nr_klatki = 1;}
			var xklatki = (this.nr_klatki-1)*this.szerokosc_klatki;
			this.canvas.drawImage(this.obr, xklatki, 0, this.szerokosc_klatki, this.wysokosc_klatki, this.x, this.y, this.szerokosc_klatki, this.wysokosc_klatki);
		}		

		this.ruszaj = function() {		
			this.x = this.x + this.przesuniecieX;
			this.y = this.y + this.przesuniecieY;
			this.draw();
		}

	}

	//ustawiamy podstawowe zmienne - obrazki, canvas oraz animowanego Fantomasa
	var cFA = document.getElementById('canvas_fantomas_wkracza_do_akcji').getContext('2d');
	var dom_d = new Image(66,85);
		dom_d.src = 'dom.png';
	var dom_m = new Image(60,90);
		dom_m.src = 'dom2.png';
	var fire = new Image(135,41);
		fire.src = "fire.png";
    var fantom = new Image(240,66);
		fantom.src = 'fantom.png';
	
	var fantomas = new _Animowany_obiekt(cFA, 60, 35, 0, fantom, 3);

	//tworzmy domki przesówane w tle
	var ile_domow_d = 3;
	var ile_domow_m = 5;
	
	//domki wstawiamy do tablicy domy_d i domy_m - dla każdego wybieramy szybkosc i pozycję x
	var domy_d = [];
	for (i=0; i<ile_domow_d; i++) {
		domy_d.push([2+Math.random() , Math.random()*200])
	}

	var domy_m = [];
	for (i=0; i<ile_domow_m; i++) {
		domy_m.push([1+Math.random() , Math.random()*200])
	}

	//funkcja rysująca naszą animację
	function drawFantomasAkcja() {	
		cFA.fillStyle = "#111";
		cFA.fillRect(0,0,200,100);

		//poruszamy domki i w razie czego resetujemy ich pozycję
		//na poczatku rysujemy małe domki w tle
		for (i=0; i<ile_domow_m; i++) {
			domy_m[i][1] = domy_m[i][1] - domy_m[i][0];
			cFA.drawImage(dom_m, domy_m[i][1], 20);
			if (domy_m[i][1] < -dom_m.width) {
				domy_m[i][1] = 200;
				domy_m[i][0] = 1 + Math.random();
			}
		}

		for (j=0; j<ile_domow_d; j++) {
			domy_d[j][1] = domy_d[j][1] - domy_d[j][0];
			cFA.drawImage(dom_d, domy_d[j][1], 20);
			if (domy_d[j][1] < -dom_d.width) {
				domy_d[j][1] = 200;
				domy_d[j][0] = 2 + Math.random()
			}
		}

		//rysujemy fantomasa
		fantomas.draw();

		//i jego super strzał z pistoletu
		if (Math.random()>0.8) c.drawImage(fire,114,43)
	}

	//odpalamy funkcję draw()
	fantom.onload = function(){
		setInterval('drawFantomasAkcja()',100);			
	}  
	

Demo

Myślę, że Fantomas byłby zadowolony z wyniku naszego działania:

Twoja przegladarka nie obsluguje canvas
FANTOMAS wkracza do akcji!