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śniejszego 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łótno kwadratem (fillRect(x,y,width,height)), narysować obrazek, który przykryje całe płótno (drawImage(obr, x, y)), lub... zmienić rozmiar 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);

    //jeżeli 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 (starX<0 || starX>200-24) {
        stepX = -stepX;
    }
    if (starY<0 || starY>100-24) {
        stepY = -stepY;
    }        
    
    //do x i y gwiazdki dodajemy przesuniecie
    starX += stepX;
    starY += stepY;

    //rysujemy gwiazdkę w nowym miejscu
    c.drawImage(image, starX, starY);
}
    
//x i y gwiazdki
var starX = 0;
var    starY = 0;

//o tyle będzie przesuwała się gwiazdka
var stepX = 2;
var stepY = 2;

var c = document.getElementById('canvas_anim1').getContext('2d');

var image = new Image(24,24); //obrazek ma wymiary 24,24
image.onload = function(){
    //rozpoczynamy animację
    setInterval('draw()',33);
};
image.src = 'gwiazdka.png';
Twoja przeglądarka nie obsługuje 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ę przesuwać, 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 _SuperStar(_x, _y, _speed) {
    this.x = _x;
    this.y = _y;
    this.speed = _speed;
    this.stepX = this.speed;
    this.stepY = this.speed;
    
    //każda gwiazdka ma swój własny obrazek - w naszym przykładzie wszystkie sa takie same
    this.image = new Image(24,24); //obrazek ma wymiary 24,24
    this.image.src = 'gwiazdka.png';
    
    //obiekt super gwiazdki jest super, dlatego sam siebie rysuje
    this.drawStar = function() {
        cAnimacja2.drawImage(this.image, this.x, this.y);
    }
    
    //i sam siebie rusza. Po poruszeniu, rysuje sie.
    this.moved = function() {
        if (this.x<0 || this.x>200-24) {
            this.stepX = -this.stepX;
        }
        if (this.y<0 || this.y>100-24) {
            this.stepY = -this.stepY;
        }        
        this.x = this.x + this.stepX;
        this.y = this.y + this.stepY;

        this.drawStar();
    }
}    

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ę wywołując metodę ruszaj dla każdego obiektu.

    
//funkcja rysujaca animacje
function draw() {    
    c.fillStyle = "#CFEAFD";
    c.fillRect(0, 0, 200, 100);
    for (var i=0; i<stars.length; i++) {
        stars[i].moved();
    }
}
    
var c = document.getElementById('canvas_anim2').getContext('2d');
var starsCount = 10; //liczba gwiazdek do animowania    
var stars = []; //tablica przechowująca obiekty gwiazdki

//tworzymy nowe obiekty gwiazdki i wrzucamy je do tablicy
for (var i=0; i<ileGwiazdek; i++) {
    var newX = 20 + Math.random()*160;
    var newY = 20 + Math.random() * 60;
    var newSpeed = Math.round(1 + Math.random()*2);
    stars.push(new _SuperStar(newX, newY, newSpeed));
}

//odpalamy animację
setInterval('draw()', 33);    
Twoja przeglądarka nie obsługuje 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 frameNr = 1;
var fantomFramesCount = 3;
var frameWidth = fantom.width / fantomFramesCount;
var frameHeight = fantom.height;
var c = document.getElementById('canvas_fantomas').getContext('2d');

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

    frameNr++;
    if (frameNr > fantomFramesCount) {
        frameNr = 1;
    }
    var frameXpos = (frameNr-1)*frameWidth;
    c.drawImage(fantom, frameXpos, 0, frameWidth, frameHeight, 60, 35, frameWidth, frameHeight);
}

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

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 szerokość klatki na wysokość 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 przeglądarka nie obsługuje 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 animować jego przyjaciół, wówczas o wiele lepiej stworzyć je jako obiekty. Podobną klasę już pisaliśmy (_SuperStar), więc wystarczy ją nieco przerobić:


function _animatedObject(_c, _x, _y, _speed, _image, _framesCount) {
    this.x = _x;
    this.y = _y;
    this.speed = _speed;
    this.stepX = this.speed;
    this.stepY = this.speed;
    this.canvas = _c
    
    this.image = _image;
    this.frameNr = 1;
    this.framesCount = _framesCount;
    this.frameWidth = _image.width / _framesCount;
    this.frameHeight = _image.height;
    
    this.draw = function() {
        this.frameNr++;
        if (this.frameNr > this.framesCount) {
            this.frameNr = 1;
        }
        var frameXpos = (this.frameNr-1)*this.frameWidth;
        this.canvas.drawImage(this.image, frameXpos, 0, this.frameWidth, this.frameHeight, this.x, this.y, this.frameWidth, this.frameHeight);
    }        

    this.ruszaj = function() {        
        this.x = this.x + this.stepX;
        this.y = this.y + this.stepY;
        this.draw();
    }
}

Jest to na szybko przerobiona klasa _SuperStar, 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), _image wskazujący na obiekt z obrazkiem do wyświetlenia oraz _framesCount, 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 c = 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 houseB = new Image(66,85);
houseB.src = 'dom.png';

var houseS = new Image(60,90);
houseS.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 _AnimatedObject(c, 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 housesBigCount = 3;
var housesSmallCount = 5;

var housesBig = [];
for (var i=0; i<housesBigCount; i++) {
    housesBig.push([2+Math.random() , Math.random()*200])
}

var housesSmall = [];
for (var i=0; i<housesSmallCount; i++) {
    housesSmall.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() {    
    c.fillStyle = "#111";
    c.fillRect(0,0,200,100);

    //na poczatku rysujemy małe domki w tle
    for (var i=0; i<housesSmallCount; i++) {
        housesSmall[i][1] = housesSmall[i][1] - housesSmall[i][0];
        c.drawImage(houseS, housesSmall[i][1], 20);
        if (housesSmall[i][1] < -housesSmall.width) {
            housesSmall[i][1] = 200;
            housesSmall[i][0] = 1 + Math.random();
        }
    }

    for (var j=0; j<housesBigCount; j++) {
        housesBig[j][1] = housesBig[j][1] - housesBig[j][0];
        c.drawImage(houseB, housesBig[j][1], 20);
        if (housesBig[j][1] < -houseB.width) {
            housesBig[j][1] = 200;
            housesBig[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 _AnimatedObject(_c, _x, _y, _speed, _image, _framesCount) {
    this.x = _x;
    this.y = _y;
    this.speed = _speed;
    this.stepX = this.speed;
    this.stepY = this.speed;
    this.canvas = _c;
    
    this.image = _image;
    this.frameNr = 1;
    this.framesCount = _framesCount;
    this.frameWidth = _image.width / _framesCount;
    this.frameHeight = _image.height;
    
    this.draw = function() {
        this.frameNr++;
        if (this.frameNr>this.framesCount) {this.frameNr = 1;}
        var frameXpos = (this.frameNr-1)*this.frameWidth;
        this.canvas.drawImage(this.image, frameXpos, 0, this.frameWidth, this.frameHeight, this.x, this.y, this.frameWidth, this.frameHeight);
    }        

    this.ruszaj = function() {        
        this.x = this.x + this.stepX;
        this.y = this.y + this.stepY;
        this.draw();
    }

}

//ustawiamy podstawowe zmienne - obrazki, canvas oraz animowanego Fantomasa
var c = document.getElementById('canvas_fantomas_wkracza_do_akcji').getContext('2d');
var houseB = new Image(66,85);
    houseB.src = 'dom.png';
var houseS = new Image(60,90);
    houseS.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 _AnimatedObject(c, 60, 35, 0, fantom, 3);

//tworzymy domki przesuwane w tle
var housesBigCount = 3;
var housesSmallCount = 5;

//domki wstawiamy do tablicy housesBig i housesSmall - dla każdego wybieramy szybkosc i pozycję x
var housesBig = [];
for (var i=0; i<housesBigCount; i++) {
    housesBig.push([2+Math.random() , Math.random()*200])
}

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

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

    //poruszamy domki i w razie czego resetujemy ich pozycję
    //na początku rysujemy małe domki w tle
    for (var i=0; i<housesSmallCount; i++) {
        housesSmall[i][1] = housesSmall[i][1] - housesSmall[i][0];
        c.drawImage(houseS, housesSmall[i][1], 20);
        if (housesSmall[i][1] < -houseS.width) {
            housesSmall[i][1] = 200;
            housesSmall[i][0] = 1 + Math.random();
        }
    }

    for (var j=0; j<housesBigCount; j++) {
        housesBig[j][1] = housesBig[j][1] - housesBig[j][0];
        c.drawImage(houseB, housesBig[j][1], 20);
        if (housesBig[j][1] < -houseB.width) {
            housesBig[j][1] = 200;
            housesBig[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 przeglądarka nie obsługuje canvas
FANTOMAS wkracza do akcji!