5.12.2002
|
čo sa budeme dnes učiť
- ako zabezpečiť striedanie fáz nejakej
animácie
- čo je to plánovací kalendár
a ako sa dá použiť na animácie
Malá animačná aplikácia
Postupne budeme vytvárať takúto aplikáciu:
- v grafickej ploche je ako podklad nejaká veľká
bitmapa - Image1 zaberá celú plochu formuláru
- nastavili sme mu Align na alClient
- každým kliknutím myšou do plochy sa
na tom mieste vytvorí animovaný obrázok,
tieto budú postupne striedať svoje fázy
- každý obrázok sa bude pomaly pohybovať
nejakým smerom, pričom, 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ť z bitmapy.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 sa dohodneme, že "priesvitná
farba" je farba bodu v ľavom hornom rohu prvej
bitmapy (t.j. bitmapa.Canvas.Pixels[0,0])
zadefinujeme triedu:
|
type
TObrazok = class
bm:array of TBitmap;
x,y,f:integer;
constructor Create(meno:string; pocet,xx,yy:integer);
destructor Destroy; override;
procedure kresli(c:TCanvas); virtual;
procedure krok; virtual;
end;
|
a metódy:
|
constructor TObrazok.Create(meno:string; pocet,xx,yy:integer);
var
i:integer;
tc:TColor; // priesvitná farba
begin
SetLength(bm,pocet);
for i:=0 to High(bm) do begin
bm[i]:=TBitmap.Create;
bm[i].LoadFromFile(meno+IntToStr(i)+'.bmp');
if i=0 then tc:=bm[i].Canvas.Pixels[0,0];
bm[i].TransparentColor:=tc;
bm[i].Transparent:=true;
end;
x:=xx; y:=yy;
f:=0; // prvá fáza
end;
destructor TObrazok.Destroy;
var
i:integer;
begin
for i:=0 to High(bm) do
bm[i].Free;
end;
procedure TObrazok.kresli(c:TCanvas);
begin
c.Draw(x - bm[f].Width div 2,y - bm[f].Height div 2,bm[f]);
end;
procedure TObrazok.krok;
begin
f:=(f+1) mod Length(bm);
end;
|
- vo formulári je Image1 a Timer1, ktorý
má nastavený Interval na hodnotu 50:
spracovanie udalostí:
|
var
a:array of TObrazok;
bg:TBitmap;
procedure TForm1.FormCreate(Sender: TObject);
begin
bg:=TBitmap.Create;
bg.LoadFromFile('jazero.bmp');
Image1.Canvas.Draw(0,0,bg);
DoubleBuffered:=true;
end;
procedure TForm1.FormDestroy(Sender: TObject);
var
i:integer;
begin
bg.Free;
for i:=0 to high(a) do a[i].Free;
end;
procedure TForm1.Image1MouseDown(...);
begin
SetLength(a,Length(a)+1);
a[High(a)]:=TObrazok.Create('vtak',8,x,y);
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:integer;
begin
if a=nil then exit;
Image1.Canvas.Draw(0,0,bg);
for i:=0 to high(a) do
with a[i] do begin
krok;
kresli(Image1.Canvas);
end;
end;
|
- vďaka nastaveniu DoubleBuffered zabezpečíme, že
animovanie obrázkov nebude blikať
- 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 nový konštruktor:
|
constructor TObrazok.Create1(meno:string; pocet,xx,yy:integer);
var
i,w,h:integer;
tc:TColor;
b:TBitmap;
begin
SetLength(bm,pocet);
b:=TBitmap.Create;
try
b.LoadFromFile(meno+'.bmp');
tc:=b.Canvas.Pixels[0,0];
w:=b.Width div pocet; h:=b.Height;
for i:=0 to High(bm) do begin
bm[i]:=TBitmap.Create;
with bm[i] do begin
Width:=w; Height:=h;
Canvas.CopyRect(Rect(0,0,w,h),b.Canvas,Rect(i*w,0,i*w+w,h));
TransparentColor:=tc;
Transparent:=true;
end;
end;
finally
b.Free;
end;
x:=xx; y:=yy; f:=0;
end;
|
a kliknutie myšou do plochy napr.
|
procedure TForm1.Image1MouseDown(...);
var
aa:TObrazok;
begin
case random(2) of
0: aa:=TObrazok.Create('vtak',8,x,y);
1: aa:=TObrazok.Create1('zajo',8,x,y);
end;
SetLength(a,Length(a)+1); a[High(a)]:=aa;
end;
|
- použili sme tu metódu triedy TCanvas:
CopyRect(const
Dest: TRect; Canvas: TCanvas; const Source: TRect); ktorá
kopíruje časť z iného obrázka
(v druhom parametri Canvas) do našej grafickej
plochy, resp. bitmapy - Dest špecifikuje
obdĺžnik (TRect: Left,Top,Right,Bottom), do ktorého
sa bude kopírovať a Source špecifikuje
obdĺžnik, z ktorého sa bude kopírovať
Uloženie bitmáp do projektu
- už viackrát by sa nám hodilo, keby
sme s našim projektom, napr. Project1.exe, nemuseli
nosiť niekedy aj väčšie množstvo bitmáp
(a aj iných súborov); možností
je niekoľko, napr.
- do formulára hocikam položíme niekoľko
komponentov Image a nastavíme im vlastnosť Visible
na false (počas behu ich nebude vidieť) - nastavíme
im vlastnosť Picture (dvojklikom do vnútra) na
nejakú konkrétnu bitmapu a počas behu
túto bitmapu môžeme z tohoto Image jednoducho
kopírovať;
- druhou možnosťou je vytvoriť .RES súbor, ktorý
bude obsahovať všetky súbory, ktoré chceme
uložiť do projektu .EXE - takéto súbory
nebudeme čítať pomocou LoadFromFile ale pomocou
LoadFromResourceName
ukážeme druhú možnosť:
- najprv pripravíme
textový súbor Bitmapy.rc s týmto
obsahom:
Bitmapy.rc je obyčajný textový
súbor - môžeme ho vytvoriť
aj v Delphi editore:
|
jazero BITMAP jazero.bmp
vtak0 BITMAP vtak0.bmp
vtak1 BITMAP vtak1.bmp
vtak2 BITMAP vtak2.bmp
vtak3 BITMAP vtak3.bmp
vtak4 BITMAP vtak4.bmp
vtak5 BITMAP vtak5.bmp
vtak6 BITMAP vtak6.bmp
vtak7 BITMAP vtak7.bmp
zajo BITMAP zajo.bmp
|
- v každom riadku je trojica reťazcov: meno (pomocou
neho sa budeme na toto odvolávať), BITMAP
znamená typ súboru
- pomocou identifikátora pred slovom BITMAP
sa budeme v programe odvolávať na danú
bitmapu
najprv do Project1.dpr pridáme jeden riadok:
|
program Project1;
{$R 'Bitmapy.res' 'Bitmapy.rc'}
uses
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.RES}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
|
a všetky LoadFromFile pozmeníme takto:
|
constructor TObrazok.Create(meno:string; pocet,xx,yy:integer);
...
bm[i]:=TBitmap.Create;
bm[i].LoadFromResourceName(HInstance,meno+IntToStr(i));
...
constructor TObrazok.Create1(const meno:string;pocet,xx,yy:integer);
...
try
b.LoadFromResourceName(HInstance,meno);
...
procedure TForm1.FormCreate(Sender: TObject);
begin
bg:=TBitmap.Create;
bg.LoadFromResourceName(HInstance,'jazero');
...
|
- všimnite si, že po prvej kompilácii sa automaticky
vytvoril dosť veľký súbor Bitmapy.res
- tento už obsahuje všetky bitmapy - výsledná
aplikácia (Project1.exe) už nepotrebuje
na spustenie veľké množstvo bitmáp,
už ich má uložené v sebe
- súbor Bitmapy.rc aj riadok v projekte {$R
'Bitmapy.res' 'Bitmapy.rc'} môžeme teraz
vyhodiť a namiesto toho niekam nižšie buď v Project1.dpr
alebo do Unit1.pas dáme riadok {$R
Bitmapy.res}
Pohyb animovaných obrázkov
- každému objektu obrázok pridáme
informácie o smere a rýchlosti pohybu
(dx,dy:integer) a tiež o obdĺžniku, v ktorom sa bude
tento obrázok pohybovať: urobíme taký
efekt, že keď objekt vypadne z obdĺžnika, tak sa objaví
na opačnej strane (tzv. wrap - efekt)
prídáme metódu ZmenDXY a vylepšíme
metódu krok:
|
procedure TObrazok.ZmenDXY(dxx,dyy:integer; r:TRect);
begin
dx:=dxx; dy:=dyy; obd:=r;
end;
procedure TObrazok.krok;
begin
f:=(f+1) mod Length(bm);
if (dx=0) and (dy=0) then exit;
inc(x,dx); inc(y,dy);
with obd 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;
|
- zrejme konštruktor bude inicializovať dx a dy na
0
pri vytvorení nového obrázka
mu nastavíme pohyb aj obdĺžnik, napr.
|
procedure TForm1.Image1MouseDown(...);
var
aa:TObrazok;
begin
case random(2) of
0:
begin
aa:=TObrazok.Create('vtak',8,x,y);
aa.ZmenDXY(random(5),random(5)-2,Image1.ClientRect);
end;
1:
begin
aa:=TObrazok.Create1('zajo',8,x,y);
aa.ZmenDXY(-random(5)-1,random(3)-1,
Rect(0,400,Image1.ClientWidth,Image1.ClientHeight));
end;
end;
SetLength(a,Length(a)+1); a[High(a)]:=aa;
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:
na
okraji plochy sa bude odrážať:
|
type
TObrazok1 = class(TObrazok)
procedure krok; override;
end;
procedure TObrazok1.krok;
begin
f:=(f+1) mod Length(bm);
if (dx=0) and (dy=0) then exit;
inc(x,dx); inc(y,dy);
with obd do begin
if (x<left) or (x>=right) then dx:=-dx;
if (y<top) or (y>=bottom) then dy:=-dy;;
end;
end;
...
procedure TForm1.Image1MouseDown(...);
var
aa:TObrazok;
begin
case random(3) of
0:
begin
aa:=TObrazok.Create('vtak',8,x,y);
aa.ZmenDXY(random(5),random(5)-2,Image1.ClientRect);
end;
1:
begin
aa:=TObrazok.Create1('zajo',8,x,y);
aa.ZmenDXY(-random(5)-1,0,
Rect(0,400,Image1.ClientWidth,Image1.ClientHeight));
end;
2:
begin
aa:=TObrazok1.Create1('zemegula',21,x,y);
aa.ZmenDXY(random(5)-2,random(5)-2,
Rect(50,50,Image1.ClientWidth-50,Image1.ClientHeight-50));
end;
end;
SetLength(a,Length(a)+1); a[High(a)]:=aa;
end;
|
- nezabudneme aj zemegula.bmp pridať do
Bitmapy.rc
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 metóda
insert:
definícia plánovača:
|
unit QueueUnit;
interface
type
TQueue = class
q:array of record
tim:TDateTime;
ob:TObject;
end;
procedure insert(tik:integer; ob:TObject);
procedure serve(var ob:TObject);
function first:TObject;
function toptime:TDateTime;
function empty:boolean;
end;
var
q:TQueue;
implementation
uses
SysUtils;
procedure TQueue.insert(tik:integer; ob:TObject);
var
i:integer;
tim:TDateTime;
begin
tim:=Now + tik/MSecsPerDay;
i:=0; while (i<=High(q)) and (q[i].tim<=tim) do inc(i);
SetLength(q,Length(q)+1);
move(q[i],q[i+1],(Length(q)-i-1)*SizeOf(q[0]));
q[i].tim:=tim;
q[i].ob:=ob;
end;
procedure TQueue.serve(var ob:TObject);
begin
if empty then ob:=nil
else begin
ob:=q[0].ob;
move(q[1],q[0],(Length(q)-1)*SizeOf(q[0]));
SetLength(q,Length(q)-1);
end;
end;
// first je ako serve, len hodnotu vráti ako výsledok funkcie
function TQueue.first:TObject;
begin
if empty then Result:=nil
else begin
Result:=q[0].ob;
move(q[1],q[0],(Length(q)-1)*SizeOf(q[0]));
SetLength(q,Length(q)-1);
end;
end;
function TQueue.toptime:TDateTime;
begin
if empty then Result:=now+1 // čas niekedy v budúcnosti
else Result:=q[0].tim;
end;
function TQueue.empty:boolean;
begin
Result:=q=nil;
end;
end.
|
- všimnite si, že sme nevytvorili konštruktor
Create - spoľahli sme sa na to, že Delphi
automaticky inicializujú stavové premenné,
ktoré sú dynamické polia, reťazce
a objekty - tieto majú hodnotu nil,
resp. prázdny reťazec
- 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:
pridáme tik:
|
type
TObrazok = class
bm:array of TBitmap;
x,y,f,dx,dy:integer;
obd:TRect;
tik:integer;
constructor Create(meno:string; pocet,xx,yy:integer);
constructor Create1(meno:string; pocet,xx,yy:integer);
destructor Destroy; override;
procedure ZmenDXY(dxx,dyy:integer; r:TRect); virtual;
procedure kresli(c:TCanvas); virtual;
procedure krok; virtual;
end;
constructor TObrazok.Create(meno:string; pocet,xx,yy:integer);
...
tik:=0;
end;
constructor TObrazok.Create1(meno:string; pocet:integer);
...
tik:=0;
end;
|
do metódy krok doplníme
"naplánovanie" ďalšieho
volania krok:
|
procedure TObrazok.krok;
begin
f:=(f+1) mod Length(bm);
if (dx<>0) or (dy<>0) then begin
inc(x,dx); inc(y,dy);
with obd 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;
if tik>0 then
q.insert(tik,self);
end;
procedure TObrazok1.krok;
...
if tik>0 then
q.insert(tik,self);
end;
|
- nezabudneme do FormCreate pridať q:=TQueue.Create;
a pri vytvorení objektov im hneď vygenerujeme
časový interval:
|
procedure TForm1.Image1MouseDown(...);
var
aa:TObrazok;
begin
case random(3) of
0:
begin
aa:=TObrazok.Create('vtak',8,x,y);
aa.ZmenDXY(random(5),random(5)-2,Image1.ClientRect);
aa.tik:=50+50*random(6);
end;
1:
begin
aa:=TObrazok.Create1('zajo',8,x,y);
aa.ZmenDXY(-random(5)-1,random(5)-2,Image1.ClientRect);
aa.tik:=100+10*random(6);
end;
2:
begin
aa:=TObrazok1.Create1('zemegula',21,x,y);
aa.ZmenDXY(random(5)-2,random(5)-2,
Rect(50,50,Image1.ClientWidth-50,Image1.ClientHeight-50));
aa.tik:=10;
end;
end;
SetLength(a,Length(a)+1); a[High(a)]:=aa;
q.insert(0,aa); // aby sa naštartoval
end;
|
ešte Timer:
|
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:integer;
b:boolean;
// p:TObject;
begin
b:=false;
while not q.empty and (q.toptime<=Now) do begin
// q.serve(p); TObrazok(p).krok;
TObrazok(q.first).krok;
b:=true;
end;
if b then begin
Image1.Canvas.Draw(0,0,bg);
for i:=0 to high(a) do
a[i].kresli(Image1.Canvas);
end;
end;
|
- plochu prekresľujeme, len ak sa na nej niečo zmenilo
(premenná b)
NDÚ:
- objektu môžeme naplánovať viac rôznych
akcií v rôznych časoch - vymyslite, ako
plánovaču (timeru) 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
Ešte jeden príklad na použitie radu
ukážeme riešenie takejto úlohy:
- do grafickej plochy môžeme kresliť čierne čiary
pomocou ľavého tlačidla myši
- keď niekam klikneme pravým tlačidlom myši,
tak sa od tohoto bodu začne na všetky strany šíriť
nejaká farba a toto vypĺňanie sa zastaví
až na čiernej čiare alebo nejako zafarbenej oblasti
- pravým tlačidlom myši môžeme naraz naštartovať
viac vypĺňacích akcií
- využijeme obyčajnú dátovú štruktúru
rad
najprv klasický rad (pamätáme
si súradnice bodu a farbu):
|
type
TQueue = class
q:array of record
x,y:integer;
col:TColor;
end;
procedure append(x,y:integer; col:TColor);
procedure serve(var x,y:integer; var col:TColor);
function empty:boolean;
end;
procedure TQueue.append(x,y:integer; col:TColor);
begin
SetLength(q,Length(q)+1);
q[High(q)].x:=x; q[High(q)].y:=y; q[High(q)].col:=col;
end;
procedure TQueue.serve(var x,y:integer; var col:TColor);
begin
if empty then begin showmessage('empty'); exit end;
x:=q[0].x; y:=q[0].y; col:=q[0].col;
move(q[1],q[0],(Length(q)-1)*Sizeof(q[0]));
SetLength(q,Length(q)-1);
end;
function TQueue.empty:boolean;
begin
Result:=q=nil;
end;
|
- využijeme komponent TTimer s nastaveným
Interval napr. na 10
metóda Timer1Timer:
|
var
q:TQueue;
g:TCanvas;
b:boolean; // či sa kreslí ťahaním myši
procedure TForm1.FormCreate(Sender: TObject);
begin
g:=Image1.Canvas;
q:=TQueue.Create;
randomize;
DoubleBuffered:=true;
end;
procedure TForm1.Image1MouseDown(...);
const
col:array [0..5] of TColor =
(clRed, clBlue, clGreen, clYellow, clAqua, clLime);
begin
b:=Shift=[ssLeft];
if b then g.MoveTo(x,y)
else if Shift=[ssRight] then
q.append(x,y,col[random(Length(col))]);
end;
procedure TForm1.Image1MouseMove(...);
begin
if b then g.LineTo(x,y);
end;
procedure TForm1.Image1MouseUp(...);
begin
b:=false;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
x,y:integer;
col:TColor;
begin
if q.empty then exit;
q.serve(x,y,col);
if g.Pixels[x,y]<>clWhite then exit;
g.Pixels[x,y]:=col;
q.append(x-1,y,col);
q.append(x+1,y,col);
q.append(x,y-1,col);
q.append(x,y+1,col);
end;
|
- takéto vyfarbovanie plochy nejakými
farbami je veľmi pomalé, ale rôznymi
vylepšeniami sa môže urýchliť
ApplicationEvents onIdle
- túto úlohu môžeme riešiť
tak, že namiesto TTimer použijeme nový
komponent TApplicationEvents:
- Timer1 vyhodíme a nahradíme
ho TApplicationEvents z palety Additional
- rovnako ako Timer je to nevizuálny komponent,
teda ho za behu programu nebude vidieť
- využijeme jeho udalosť onIdle, ktorý
bude zavolaný podobne ako onTimer pre
časovač, ale vždy vtedy, keď "Windows nemajú
čo robiť" - t.j. keď momentálne
nie sú zaťažované inými
úlohami - hneď ako príde
nejaká iná udalosť (napr. pohneme
alebo klikneme myšou, alebo prišiel tik od časovača
a pod.), ďalšie volanie onIdle sa už nezrealizuje
- teda do onIdle dáme spracovanie
jedného bodu z frontu čakajúcich
bodov a oznámime systému, že nás
má volať stále, kým systém
nemá čo robiť (nastavíme premennú
Done na false) - Done s
hodnotou true znamená, že onIdle
sa najbližšie zavolá až po nejakej inej
udalosti, keď už systém opäť nebude
mať čo robiť...
namiesto metódy Timer1Timer napíšeme
ApplicationEvents1Idle:
|
procedure TForm1.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
var
x,y:integer;
col:TColor;
begin
Done:=q.empty; if Done then exit;
q.serve(x,y,col);
if g.Pixels[x,y]<>clWhite then exit;
g.Pixels[x,y]:=col;
q.append(x-1,y,col);
q.append(x+1,y,col);
q.append(x,y-1,col);
q.append(x,y+1,col);
end;
|
NDÚ:
- urýchlite prácu s radom - pole zväčšujte
napr. s krokom 100, pamätajte si začiatok a počet
prvkov a pole posúvajte len v nutných
prípadoch
- ďalšie urýchlenie: v metóde Timer1Timer,
resp. ApplicationEvents1Idle
spracujte nielen jeden bod, ale napr. v cykle naraz
20
- vymyslite iné pravidlá farbenia: nielen
keď je biela farba, ale napr. nie čierna a rôzna
od aktuálnej a pod.
|