23.Prednaska

Z Pascal
Prejsť na: navigácia, hľadanie
23. Prednáška

úlohy | cvičenie

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.

8 súborov s animovaným obrázkom

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

jeden súbor s 8 animovanými obrázkami

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


späť | ďalej