22. Jednoduchá animácia


posledná zmena: 5.12.2002

Banner Text 5.12.2002

    čo sme sa doteraz naučili

    • vieme kresliť a hýbať bitmapy v grafickej ploche

    č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.


© 2002 AB, KVI
blaho@fmph.uniba.sk