23.Prednaska
23. Prednáška |
Animovaný obrázok
V niekoľkých krokoch budeme vytvárať takúto aplikáciu s animovanými objektmi:
- v grafickej ploche je ako podklad nejaká veľká bitmapa - Image1, ktorá zaberá celú plochu formuláru
- každým kliknutím ľavým tlačidlom myši sa v ploche na tom mieste vytvorí animovaný obrázok, pričom všetky tieto obrázky budú postupne striedať svoje fázy
- každý obrázok sa bude pomaly pohybovať nejakým smerom tak, že, ak na jednej strane "vypadne" z plochy, tak sa objaví na opačnom konci.
- neskôr zabezpečíme, aby sa každý obrázok mohol animovať s rôznou frekvenciou - niektoré obrázky budú striedať fázy častejšie ako iné.
Všetky obrázky z projektu si môžete stiahnuť zo súboru Anim.zip.
Trieda animovaný obrázok
Animáciu budeme zabezpečovať cyklickým striedaním fáz - bitmáp. V prvej verzii budeme predpokladať, že každú fázu animácie máme uloženú v jednom súbore, napr.
Nakoľko chceme zabezpečiť, aby niektoré časti bitmapy boli priesvitné, pre bitmapy v našej aplikácii budeme predpokladať, že "priesvitná farba" je farba bodu v ľavom hornom rohu každej bitmapy. Zadefinujeme triedu (najlepšie v samostatnom unite):
type TObrazok = class Bmp: array of TBitmap; X, Y, Faza: Integer; C: TCanvas; constructor Create(CC: TCanvas; Meno: string; Pocet, XX, YY: Integer); destructor Destroy; override; procedure Kresli; procedure Krok; virtual; end;
a metódy:
constructor TObrazok.Create(CC: TCanvas; Meno: string; Pocet, XX, YY: Integer); var I: Integer; begin SetLength(Bmp, Pocet); for I := 0 to High(Bmp) do begin Bmp[I] := TBitmap.Create; with Bmp[I] do begin LoadFromFile(Meno + IntToStr(I) + '.bmp'); TransparentColor := Canvas.Pixels[0, 0]; Transparent := True; end; end; X := XX; Y := YY; Faza := 0; // prvá fáza C := CC; // zapamätáme si plochu, kde má objekt existovať end; destructor TObrazok.Destroy; var I: Integer; begin for I := 0 to High(Bmp) do Bmp[I].Free; end; procedure TObrazok.Kresli; begin C.Draw(X - Bmp[Faza].Width div 2, Y - Bmp[Faza].Height div 2, Bmp[Faza]); end; procedure TObrazok.Krok; begin Faza := (Faza + 1) mod Length(Bmp); end;
Vo formulári je Image1 a Timer1, ktorý má nastavený Interval na hodnotu 50. Teraz spracovanie udalostí vyzerá takto:
var Pole: array of TObrazok; // zoznam všetkých animovaných objektov Pozadie: TBitmap; // pozadie grafickej plochy procedure TForm1.FormCreate(Sender: TObject); begin Pozadie := TBitmap.Create; Pozadie.LoadFromFile('jazero.bmp'); Image1.Canvas.Draw(0, 0, Pozadie); end; procedure TForm1.FormDestroy(Sender: TObject); var I: Integer; begin Pozadie.Free; for I := 0 to High(Pole) do Pole[I].Free; end; procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var A: TObrazok; begin A := TObrazok.Create(Image1.Canvas, 'vtak', 8, X, Y); SetLength(Pole, Length(Pole) + 1); Pole[High(Pole)] := A; end; procedure TForm1.Timer1Timer(Sender: TObject); var I: Integer; begin Image1.Canvas.Draw(0, 0, Pozadie); for I := 0 to High(Pole) do begin Pole[I].Krok; Pole[I].Kresli; end; end;
Animácia v jednej bitmape
V praxi sa často namiesto viac súborov s bitmapami pre fázy animácie používa jedna bitmapa, ktorá vedľa seba obsahuje všetky fázy a program si túto veľkú bitmapu "rozstrihá", napr. pre
vytvoríme takýto nový konštruktor:
constructor TObrazok.Create1(CC: TCanvas; Meno: string; Pocet, XX, YY: Integer); var I, Sirka, Vyska: Integer; PomBmp: TBitmap; begin SetLength(Bmp, Pocet); PomBmp := TBitmap.Create; try PomBmp.LoadFromFile(Meno + '.bmp'); Sirka := PomBmp.Width div Pocet; Vyska := PomBmp.Height; for I := 0 to High(Bmp) do begin Bmp[I] := TBitmap.Create; with Bmp[I] do begin Width := Sirka; Height := Vyska; Canvas.Draw(- I * Sirka, 0, PomBmp); TransparentColor := Canvas.Pixels[0, 0]; Transparent := True; end; end; finally PomBmp.Free; end; X := XX; Y := YY; Faza := 0; C := CC; end
a kliknutie myšou do plochy napr.
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var A: TObrazok; begin case Random(2) of 0: A := TObrazok.Create(Image1.Canvas, 'vtak', 8, X, Y); 1: A := TObrazok.Create1(Image1.Canvas, 'zajo', 8, X, Y); end; SetLength(Pole, Length(Pole) + 1); Pole[High(Pole)] := A; end;
Pohyb animovaných obrázkov
Každému objektu obrázok pridáme tri nové stavové premenné - informácie o smere a rýchlosti pohybu (DX, DY: Integer) a tiež o obdĺžniku (Rect: TRect), v ktorom sa bude tento obrázok pohybovať. Ďalej urobíme taký efekt, že keď objekt vypadne z obdĺžnika, tak sa objaví na opačnej strane (tzv. wrap - efekt). TRect už poznáme - špecifikuje obdĺžnik (t.j. record Left, Top, Right, Bottom: Integer; end). Ešte pridáme metódu Nastav a vylepšíme metódu Krok:
procedure TObrazok.Nastav(DXX, DYY: Integer; R: TRect); begin DX := DXX; DY := DYY; Rect := R; end; procedure TObrazok.Krok; begin Faza := (Faza + 1) mod Length(Bmp); Inc(X, DX); Inc(Y, DY); with Rect do begin if X < Left then Inc(X, Right - Left); if X >= Right then Dec(X, Right - Left); if Y < Top then Inc(Y, Bottom - Top); if Y >= Bottom then Dec(Y, Bottom - Top); end; end;
Pri vytvorení nového obrázka mu nastavíme pohyb aj jeho obdĺžnik, napr.
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var A: TObrazok; begin case Random(2) of 0: begin A := TObrazok.Create(Image1.Canvas, 'vtak', 8, X, Y); A.Nastav(Random(5), Random(5) - 2, Image1.ClientRect); end; 1: begin A := TObrazok.Create1(Image1.Canvas, 'zajo', 8, X, Y); A.Nastav((- Random(5) - 1, Random(3) - 1, Rect(0, 400, Image1.ClientWidth, Image1.ClientHeight)); end; end; SetLength(Pole, Length(Pole) + 1); Pole[High(Pole)] := A; end;
Animované obrázky sa budú teraz hýbať.
Animovaný obrázok s iným pohybom
Do projektu pridáme nový obrázok - zemeguľu a zmeníme jej správanie tak, že na okraji plochy sa bude namiesto preklápania (wrap) odrážať. Túto triedu môžeme zadefinovať v ďalšom unite:
type
TNovyObrazok = class(TObrazok)
procedure Krok; override;
end;
procedure TNovyObrazok.Krok;
begin
Faza := (Faza + 1) mod Length(Bmp);
Inc(X, DX);
Inc(Y, DY);
with Rect do
begin
if X >= Right then
DX := - Abs(DX);
if X < Left then
DX := Abs(DX);
if Y >= Bottom then
DY := - Abs(DY);
if Y < Top then
DY := Abs(DY);
end;
end;
Potom doplníme aj správanie sa celej aplikácie:
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
var
A: TObrazok;
begin
case Random(3) of
0:
begin
A := TObrazok.Create(Image1.Canvas, 'vtak', 8, X, Y);
A.Nastav(Random(5), Random(5) - 2, Image1.ClientRect);
end;
1:
begin
A := TObrazok.Create1(Image1.Canvas, 'zajo', 8, X, Y);
A.Nastav(- Random(5) - 1, Random(3) - 1,
Rect(0, 400, Image1.ClientWidth, Image1.ClientHeight));
end;
2:
begin
A := TNovyObrazok.Create1(Image1.Canvas, 'zemegula', 21, X, Y);
A.Nastav(Random(5) - 2, Random(5) - 2,
Rect(50, 50, Image1.ClientWidth - 50, Image1.ClientHeight - 50));
end;
end;
SetLength(Pole, Length(Pole) + 1);
Pole[High(Pole)] := A;
end;
Plánovač
Plánovačom bude špeciálny front (rad), do ktorého sa pridáva nie na koniec, ale na správne miesto podľa času. Preto metóda Insert musí najprv vyhladať v rade správne miesto, kam treba zaradiť prichádzajúcu požiadavku, potom na tomto mieste celý rad roztiahne a až na toto nové miesto zaradí túto novú položku. Plánovač teraz vyzerá takto:
unit QueueUnit; {$mode objfpc}{$H+} interface uses Classes, SysUtils; type TQueue = class Pole: array of record Tim: TDateTime; Obj: TObject; end; procedure Insert(Tik: Integer; Obj: TObject); procedure Serve(var Obj: TObject); function Top: TObject; function TopTime: TDateTime; function Empty: Boolean; end; var Q: TQueue; implementation procedure TQueue.Insert(Tik: Integer; Obj: TObject); var I: Integer; Tim: TDateTime; begin Tim := Now + Tik / MSecsPerDay; I := High(Pole); SetLength(Pole, Length(Pole) + 1); while (I >= 0) and (Pole[I].Tim > Tim) do begin Pole[I + 1] := Pole[I]; Dec(I); end; Pole[I + 1].Tim := Tim; Pole[I + 1].Obj := Obj; end; procedure TQueue.Serve(var Obj: TObject); begin if Empty then Obj := nil else begin Obj := Pole[0].Obj; if Length(Pole) = 1 then Pole := nil else Pole := Copy(Pole, 1, MaxInt); end; end; // Top je ako Serve, len hodnotu vráti ako výsledok funkcie function TQueue.Top: TObject; begin if Empty then Result := nil else begin Result := Pole[0].Obj; if Length(Pole) = 1 then Pole := nil else Pole := Copy(Pole, 1, MaxInt); end; end; function TQueue.TopTime: TDateTime; begin if Empty then // čas niekedy v budúcnosti Result := Now + 1 else Result := Pole[0].Tim; end; function TQueue.Empty: Boolean; begin Result := Pole = nil; end; end.
Všimnite si, že sme nepotrebovali vytvoriť konštruktor Create - spoľahli sme sa na to, že v triede sa automaticky inicializujú všetky stavové premenné, pričom dynamické polia, reťazce a objekty - budú mať hodnotu nil, resp. prázdny reťazec.
Pri definovaní takéhoto plánovača sme nešpecifikovali aké objekty budeme do neho ukladať a tiež, aké budeme z neho vyberať - použili sme univerzálne TObject.
Všetkým objektom animovaný obrázok pridáme novú stavovú premennú Tik, ktorá bude obsahovať čas v ms na zmenu ďalšej fázy:
type
TObrazok = class
Bmp: array of TBitmap;
X, Y, Faza, DX, DY: Integer;
Rect: TRect;
Tik: Integer;
constructor Create(CC: TCanvas; Meno: string; Pocet, XX, YY: Integer);
constructor Create1(CC: TCanvas; Meno: string; Pocet, XX, YY: Integer);
destructor Destroy; override;
procedure Nastav(DXX, DYY: Integer; R: TRect);
procedure Kresli(C: TCanvas);
procedure Krok; virtual;
end;
Do metódy Krok doplníme "naplánovanie" ďalšieho volania Krok:
procedure TObrazok.Krok; begin Faza := (Faza + 1) mod Length(Bmp); Inc(X, DX); Inc(Y, DY); with Rect do begin if X < Left then Inc(X, Right - Left); if X >= Right then Dec(X, Right - Left); if Y < Top then Inc(Y, Bottom - Top); if Y >= Bottom then Dec(Y, Bottom - Top); end; if Tik > 0 then Q.Insert(Tik, Self); end;
a tiež pre TNovyObrazok
procedure TNovyObrazok.Krok; begin Faza := (Faza + 1) mod Length(Bmp); ... if Tik > 0 then Q.Insert(Tik, Self); end;
Nezabudneme do FormCreate pridat Q := TQueue.Create. Pri vytváraní objektov im hneď vygenerujeme ich časový interval (rýchlosť animácie):
procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var A: TObrazok; begin case Random(3) of 0: begin A := TObrazok.Create(Image1.Canvas, 'vtak', 8, X, Y); A.Nastav(Random(5), Random(5) - 2, Image1.ClientRect); A.Tik := 10 + 20 * Random(6); end; 1: begin A := TObrazok.Create1(Image1.Canvas, 'zajo', 8, X, Y); A.Nastav(- Random(5) - 1, Random(3) - 1, Rect(0, 400, Image1.ClientWidth, Image1.ClientHeight)); A.Tik := 100 + 10 * Random(6); end; 2: begin A := TObrazok1.Create1(Image1.Canvas, 'zemegula', 21, X, Y); A.Nastav(Random(5) - 2, Random(5) - 2, Rect(50, 50, Image1.ClientWidth - 50, Image1.ClientHeight - 50)); A.Tik := 10; end; end; SetLength(Pole, Length(Pole) + 1); Pole[High(Pole)] := A; Q.Insert(0, A); // naštartovanie animácie end;
ešte časovač:
procedure TForm1.Timer1Timer(Sender: TObject); var I: Integer; // P: TObject; begin while not Q.Empty and (Q.TopTime <= Now) do begin // Q.Serve(P); TObrazok(P).Krok; TObrazok(Q.Top).Krok; end; Image1.Canvas.Draw(0, 0, Pozadie); for I := 0 to High(Pole) do Pole[I].Kresli; end;
Všimnite si spôsob, ako manipulujeme s objektom, ktorý sme vybrali z frontu. Buď použijeme pomocnú premennú P typu TObject:
Q.Serve(P); TObrazok(P).Krok;
alebo použijeme novú metódu funkciu Top triedy TQueue, ktorá pracuje podobne ako Serve, len vybratý prvok nevráti ako parameter ale ako hodnotu funkcie:
TObrazok(Q.Top).Krok;
Ďalšie námety:
- objektu môžeme naplánovať viac rôznych akcií v rôznych časoch - vymyslite, ako plánovaču (teda pre časovač) povedať, že má pre rôzne objekty spúšťať rôzne akcie (nielen Krok)
- stavová premenná Tik, ktorá sa používa ako čas na prechod do nasledujúcej fázy, by mohla byť buď dynamickým poľom (pre každú fázu iný čas) alebo funkciou, ktorá závisí aj od iných okolností - premyslite túto ideu