Animações em tempo real com a VCL - Parte IV

No artigo anterior, criamos uma animação procedural, onde com um algoritmo simples, um padrão de cores foi gerado. Neste artigo vamos criar um efeito sobre uma imagem já existente utilizando um conceito simples de transparência.

Opacidade

Segundo o Wikipedia:
A opacidade é uma propriedade óptica da matéria. Um material é considerado opaco quando não permite a passagem da luz em quantidades apreciáveis. Geralmente, diz-se que um material é opaco quando bloqueia a passagem da luz visível. (http://pt.wikipedia.org/wiki/Opacidade)

Para nossos estudos, vamos definir opacidade como sendo o nível de transparência de uma imagem cujo valor será representado por um byte, o que nos dá uma faixa de 256 valores possíveis (0..255) onde 0 define uma imagem completamente transparente e 255 uma imagem completamente opaca.
Vamos criar um descendente de TBitmap onde adicionaremos esta propriedade.


TSprite = class(TBitmap)
private
   fOpacity: byte;
   procedure SetOpacity(const Value: byte);
published
   property Opacity: byte read FOpacity write SetOpacity; 
end;

Agora vamos instanciar nosso sprite na inicialização do programa (lembrando que estamos modificando o código do template cirado nos artigos anteriores)

procedure TForm1.FormCreate(Sender: TObject);
begin
   ReportMemoryLeaksOnShutdown := True;
   ClientWidth   := 800;               
   ClientHeight  := 600;               
   fOffScreen := TBitmap.Create;       
   fOffScreen.PixelFormat := pf24bit;  
   fOffScreen.Width  := ClientWidth;   
   fOffScreen.Height := ClientHeight;  
   fOffScreen.Canvas.Font.Color := clGray;
   fOffScreen.Canvas.Font.Size  := 9;

   sprite := TSprite.Create;
   sprite.LoadFromFile('img01.bmp');
   sprite.Opacity := MAXBYTE div 2;
   Application.OnIdle := OnIdle;
end;


Windows.AlphaBlend


Existe uma função na API do windows chamada AlphaBlend que serve para exibir imagens que possuem informações de transparência. Vamos utilizá-la para renderizar nosso sprite com 50% de opacidade sobre um background preto (note que já configuramos opacidade como MAXBYTE div 2).
A primeira coisa que precisamos para usar o Windows.AlphaBlend é configurar um registro do tipo TBlendFunction (Veja a descrição oficial deste struct no MSDN).

procedure TForm1.DrawToBuffer;
var
  Canvas : TCanvas;
  Blendf : TBlendFunction;
begin
  Canvas := fOffScreen.Canvas;
  Canvas.Brush.Color := clBlack;
  Canvas.FillRect(fOffScreen.Canvas.ClipRect);

  Blendf.BlendOp    := AC_SRC_OVER;
  Blendf.BlendFlags := 0;
  Blendf.SourceConstantAlpha := sprite.Opacity;
  Blendf.AlphaFormat := AC_SRC_OVER;
end;

O parâmetro BlendOp representa a opração de mesclagem a executar. Como queremos copiar nosso sprite “por cima” da imagem de fundo, setamos BlendOP com a constante AC_SRC_OVER.
O parâmatro BlendFlags deve ser sempre zero.
SourceConstantAlpha define um valor de transparência a ser aplicado na imagem de origem. Aqui, simplesmente passamos o valor da opacidade de nosso sprite.
Por fim, AlphaFormat deve ser AC_SRC_OVER para que o Windows não tente buscar informações de transparência nos pixels da imagem de origem, aplicando em vez disto, o valor de SourceConstantAlpha.
Pronto. Com nosso TBlendFunction configurado, podemos desenhar o sprite semi-transparente. Complemente o método DrawToBuffer conforme a listagem abaixo e execute o programa para ver o resultado.

procedure TForm1.DrawToBuffer;
var
   Canvas : TCanvas;
   Blendf : TBlendFunction;
begin
   Canvas := fOffScreen.Canvas;
   Canvas.Brush.Color := clBlack;
   Canvas.FillRect(fOffScreen.Canvas.ClipRect);

   Blendf.BlendOp    := AC_SRC_OVER;
   Blendf.BlendFlags := 0;
   Blendf.SourceConstantAlpha := sprite.Opacity;
   Blendf.AlphaFormat := AC_SRC_OVER;

   Windows.AlphaBlend(
      Canvas.Handle,  {manipulador da imagem de destino}
      fOffScreen.Width - sprite.Width, {coordenada X}
      0,                               {coordenada Y}
      sprite.Width,                    {largura da área de desenho}
      sprite.Height,                   {altura da área de desenho} 

      sprite.Canvas.Handle, {manipulador da imagem de origem}
      0,                    {coordenada X}
      0,                    {coordenada Y}
      sprite.Width,         {largura da imagem de origem}
      sprite.Height,        {altura da imagem de origem}

      Blendf                 {parâmetros de executação(TBlendFunction)}
   );

   Canvas.TextOut(10, 10, Format('Opacidade : %d', [sprite.Opacity]));
end;
Se tudo estiver certo, você vai ver uma imagem no canto esquerdo da tela de seu programa. A imagem parece muito escura porque metade a luminosidade foi misturada com o fundo negro. Altere o valor de opacidade do sprite para chegar a resultados diferentes.


Fade In e Fade Out

Uma aplicação muito comum da opacidade em imagens é a criação dos efeitos Fade In e Fade Out. Em um (fade in) uma imagem vai surgindo aos poucos até ficar completamente visível, no outro (fade out) a imagem vai se apagando até sumir completamente. Com o que já temos até aqui, é realmente muito fácil implementar estes efeitos: basta ir alterando o valor da propriedade Opacity ao longo do tempo. Vejamos como ficará o método UpdateAnimation.

procedure TForm1.UpdateAnimation;
begin
  if OpIncreasing then
     sprite.Opacity := sprite.Opacity + 1
  else
     sprite.Opacity := sprite.Opacity - 1;

  case sprite.Opacity of
    0   : OpIncreasing := true;
    255 : OpIncreasing := false;
  end;
end;

Execute o programa e veja os efeitos em funcionamento. Para conseguir novos efeitos, altere os valores dos incrementos e decrementos em cada iteração.
Até o próximo post.

Downloads

Links

Links para as postagens que fazem parte do mini curso "Animações em Tempo real com a VCL"

Animações em tempo real com a VCL - Parte III

No artigo anterior, configuramos um formulário padrão da VCL para que pudéssemos exibir animações em sua superfície. Criamos também uma bola, que podíamos manipular pelas setas do teclado e fazê-la se mover pela tela. Nesta parte, vamos falar um pouco de teoria e criar nossa primeira animação procedural.

Um pouco de teoria

O sistema ocular humano é limitado quanto ao espectro visível de luz e quanto à velocidade de interpretação das imagens. O cérebro, porém, é muito bom no trabalho de contornar estas limitações e nos fazer enxergar imagens corretas, nítidas e sem quebras. Mas este trabalho de interpretação está sujeito a falhas e são essas falhas de interpretação que causam as famosas ilusões de ótica. A animação por computador é um tipo de ilusão projetada para causar ao o observador a impressão de movimento.

Desde o surgimento da animação, o seu fundamento é o mesmo: exibir uma seqüência de imagens estáticas, ligeiramente diferentes entre si, numa a uma velocidade superior a 12 imagens por segundo. Quando isto acontece, o olho envia as imagens paradas ao cérebro, mas este acha que se trata de um objeto animado e une-as, criando os pedaços que faltam para que se tenha a impressão de se estar vendo um objeto animado. Muito embora, não pareça haver um consenso entre os cientistas sobre quantos quadros por segundo são necessários para que a “ilusão da animação” aconteça, 12 quadros parecem ser suficientes para que a maioria dos seres humanos visualize-os como uma animação.

A TV, o cinema, o computador, os desenhos animados e os jogos eletrônicos são todos construídos em cima desta idéia. O sistema PAL-M usado no Brasil usa uma taxa de 30fps (do inglês Frames per Second, literalmente quadros por segundo, é a unidade medida padrão usada na indústria e, portanto iremos utilizá-la de agora em diante), filmes em DVD de boa qualidade usam de 24 a 30fps e, na animação de jogos para computador, uma taxa ideal de 60fps vem se estabelecendo ao longo dos anos.

Quando digo “taxa ideal”, não estou afirmando que é necessário alcançar os 60fps ou que seja errado ultrapassá-los. Na realidade, quanto mais quadros por segundo você puder exibir, mais convincente e suave será sua animação, então, daqui pra frente, vamos escrever nossos programas para que alcancem a maior taxa de fps que conseguirmos.

A composição das cores


Como sabemos, tudo que enxergamos é luz. Os objetos que vemos são, na realidade, luz refletida e/ou emitida por suas superfícies. Sabemos também que a partir da mistura de umas poucas cores primárias, todas as outras cores podem ser obtidas. Pois bem, nos monitores de computador, todas as cores são formadas a partir da combinação de apenas três cores: o vermelho (red), o verde (green) e o azul (blue), o famoso padrão RGB.

Neste formato, utilizado exclusivamente por dispositivos emissores de luz, quando todos os três componentes RGB estão em 0% de sua intensidade obtém-se a cor preta e, no caso inverso, quando todos os três componentes estão em 100%, obtém-se a cor branca. Todas as outras cores são obtidas ao se combinar porções distintas de cada um destes componentes.

Imagens, bitmaps e Pixels


Como dito antes, toda imagem digital é um conjunto de pixels. Pra ser mais preciso, podemos defini-las como sendo uma matriz de pixels que é mapeada, pixel a pixel, para um dispositivo de saída (monitor, plotter, impressora, etc).

Nos primórdios da computação, quando os monitores eram monocromáticos, as imagens eram simplesmente uma seqüência de bits que dizia se um ponto numa determinada coordenada no monitor deveria estar acesa ou apagada. Um mapa de bits, um bitmap.

O Windows utiliza este conceito para representar tudo o que deve aparecer no monitor. Ele sempre mantém uma imagem do tipo bitmap em uma área especial da memória RAM que o sistema de vídeo usa para montar a imagem final na tela. A este espaço dá-se o nome de frame buffer.

O Delphi, através da classe TBitmap, oferece um excelente meio de manipular essas imagens, e o método BitBlit da API do Windows, nos fornece um modo rápido de copiar imagens para o frame buffer.

Um Pouco de Código (finalmente)

Para melhorar um pouco a organização do código escrito no post anterior, vamos criar uma interface IDrawable e fazer com que nosso objeto TBackground a implemente.

IDrawable = interface
  ['{3E41F055-58F6-4990-9C94-AA29FD4D7CF4}']
  function GetVisible: boolean;
  function GetRect : TRect;

  procedure SetVisible(const Value: boolean);
  procedure Draw(Canvas:TCanvas);

  property Rect: TRect read GetRect;
  property Visible: boolean read GetVisible write SetVisible;
end;

A interface é bem intuitiva e dispensa maiores explicações. Ela irá definir o “contrato” para todos os objetos renderizáveis no nosso programa. Agora segue nova definição de TBackground adaptado para implementar IDrawable.

TBackgroung = class(TInterfacedObject, IDrawable)
private
  fStep: byte;
  fSize: uCommon.TSize;
  fVisible: boolean;
  function GetVisible: boolean;
  function GetRect : TRect;
  procedure SetVisible(const Value: boolean);
public
  constructor Create(pWidth: integer=640; pHeight:integer=480);
  destructor Free;
published
  {IDrawable}
  property Rect: TRect read GetRect;
  property Visible: boolean read GetVisible write SetVisible;

  {TBackgroung}
  property Size : uCommon.TSize read fSize;
  property Step : byte read fStep write fStep;
end;

Além das propriedades e métodos da interface, criamos duas outras, Size e Step, que irão nos auxiliar no controle da renderização e no controle da animação. Vamos criar uma série de quadros vermelhos que irão mudar de cor de acordo com o valor da propriedade Step. Para isto, vamos implementar o método Draw.


procedure TBackgroung.Draw(Canvas: TCanvas);
const
  SquareW    = 40; {largura de cada bloco}
  SquareH    = 40; {altura de cada bloco}
  SquaresC   = 640 div SquareW + 1; {colunas}
  SquaresR   = 480 div SquareH + 1; {linhas}
var
  col, row, i : integer;
  r : TRect;
begin
  if fVisible then begin
  for col:=0 to SquaresC-1 do
      for row :=0 to SquaresR-1 do begin
          r.Left   := Col * SquareW;
          r.Top    := Row * SquareH;
          r.Right  := r.Left + SquareW;
          r.Bottom := r.Top + SquareH;

          Canvas.Brush.Color := col * row + Step;
         Canvas.FillRect(r);
      end;
  end;
end;


O que fazemos aqui é dividir a tela em linhas e colunas de acordo com as contantes SquareW (a largura de cada quadrado) e SquareH (altura de cada quadrado), atribuir a cada um destas “células” uma cor diferente, que calculamos com base na linha, na coluna e no valor da propriedade Step para finalmente, desenharmos uma a uma as “células” no Canvas.

Só nos resta agora “animar” a cena alterando a propriedade Step após cada renderização. Faremos isto no nosso loop principal através do método UpdateAnimation.

procedure TForm1.OnIdle(Sender: TObject; var Done: Boolean);
begin
  ProcessInput;
  UpdateAnimation;
  DrawToBuffer;
  Blit;
  Done := False;
end;

E agora, a implementação de UpdateAnimation.

procedure TForm1.UpdateAnimation;
begin
  BackGnd.Step := BackGnd.Step + 1;
end;

Pronto! Nosso background animado está concluído. Execute o código para ver o padrão vermelho mudando de tom e experimente brincar com as constantes do método Draw para chegar a outros resultados. Observe que a classe TBall também foi alterada para implementar a interface IDrawable e que uma outra interface (IMoveable) foi criada para definir os objetos capazes de se mover pela tela.

Downloads

Links

Links para as postagens que fazem parte do mini curso "Animações em Tempo real com a VCL"