Transparent animation lazarus / 39-Graphics ---------------------------------------------------------------------- majolika · 2025-02-07 20:29 Hello everyone! It's me again with my newbie's question. I've almost finished my first Lazarus "mine sweeper" project, but I'm stuck trying to make a transparent animation. The idea was like this: there is a TBitmap (FieldBitmap in my code), on which I draw the game field and everything that happens on it, and at the right moment on another transparent TBitmap2 (AnimationBitmap in my code) I draw an animation on a timer, display it on top of the game field and erase what was drawn when animation finished. As always, I reproduced this in a test project and it did not work. I faced two problems: 1. I found out that TBitmap cannot be transparent. But then why does it have the transparency property?! I don't understand. Ok, instead of TBitmap2, I tried using TImage, but this also didn't work. I think, I'm doing something wrong. 2. If, when the application is first launched, the first click on the game field falls on the middle mouse button (I assigned it to start the animation for testing), then part of the animation is drawn in the upper left corner of TBitmap2 for some reason. When restarting the game field or if the first click was the right or left mouse button, then partial rendering in the upper left corner doesn't occur. I assume that the problem is in the initialization of CellXCenter and CellYCenter variables that I use when drawing the animation. How can I deal with both of these problems? I will be grateful for any hints. Here is the code of the test project, and in the attached file is the entire project. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i unit Unit1; {$mode objfpc}{$H+} interface uses  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls, ExtCtrls, LCLType; type   TForm1 = class(TForm)    Button1: TButton;    Button2: TButton;    Label1: TLabel;    Label2: TLabel;    PaintBox1: TPaintBox;    Timer1: TTimer;    procedure Button1Click(Sender: TObject);    procedure Button2Click(Sender: TObject);    procedure FormCreate(Sender: TObject);    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);    procedure PaintBox1MouseMove(Sender: TObject;            Shift: TShiftState; X, Y: Integer);    procedure PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;              Shift: TShiftState; X, Y: Integer);    procedure PaintBox1Paint(Sender: TObject);    procedure Timer1Timer(Sender: TObject);  private    FieldBitmap, AnimationBitmap: TBitmap;    AnimationFrame: Integer;    procedure FieldInit(Cols, Rows: Integer);    procedure StartCaptureAnimation;    procedure StopCaptureAnimation;    procedure ClearAnimationBitmap;   public  end; var  Form1: TForm1;  BoxSize: Integer = 24;  FieldRow: integer = 0;  FieldCol: integer = 0;  Button1Clicked: Boolean = False;  Button2Clicked: Boolean = False;  FieldWidth, FieldHeight: Integer;  ThereWillBeBlood: Boolean = False;  CellXCenter, CellYCenter: Integer;  implementation {$R *.lfm} procedure TForm1.Button1Click(Sender: TObject);begin  Button1Clicked := True;  FieldRow := 5;  FieldCol := 5;  FieldInit(FieldCol, FieldRow);end; procedure TForm1.Button2Click(Sender: TObject);begin    Button2Clicked := True;    FieldRow := 20;    FieldCol := 20;    FieldInit(FieldCol, FieldRow);end; procedure TForm1.FormCreate(Sender: TObject);begin  FieldBitmap := TBitmap.Create;  AnimationBitmap := TBitmap.Create;end; procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);begin  FieldBitmap.Destroy;  AnimationBitmap.Destroy;end; procedure TForm1.PaintBox1MouseMove(Sender: TObject;          Shift: TShiftState; X, Y: Integer);var  CellX, CellY: Integer;begin  Label1.Caption := X.ToString + ', ' + Y.ToString;   CellX := X div BoxSize;  CellY := Y div BoxSize;  Label2.Caption := CellX.ToString + ', ' + CellY.ToString;end;   procedure TForm1.PaintBox1Paint(Sender: TObject);begin  PaintBox1.Canvas.Draw(0,0, FieldBitmap);  if ThereWillBeBlood then     PaintBox1.Canvas.Draw(0, 0, AnimationBitmap);end;   procedure TForm1.FieldInit(Cols, Rows: Integer);var  x, y: Integer;  r: TRect;begin   FieldWidth := Cols * BoxSize;  FieldHeight := Rows * BoxSize;   PaintBox1.Width := FieldWidth;  PaintBox1.Height := FieldHeight;   FieldBitmap.Clear;  AnimationBitmap.Clear;   FieldBitmap.SetSize(FieldWidth, FieldHeight);  FieldBitmap.Canvas.Brush.Color := clNone;  FieldBitmap.Canvas.FillRect(0, 0, FieldWidth, FieldHeight);   AnimationBitmap.SetSize(FieldWidth, FieldHeight);  AnimationBitmap.Transparent := True;  AnimationBitmap.Canvas.Brush.Color := clNone;  AnimationBitmap.Canvas.FillRect(0, 0, FieldWidth, FieldHeight);   for x := 0 to Cols - 1 do  begin    for y := 0 to Rows - 1 do    begin    r := rect(x * BoxSize, y * BoxSize, x * BoxSize + BoxSize, y * BoxSize + BoxSize);      FieldBitmap.Canvas.Frame3d(r, 2, bvRaised);  end;  end;   PaintBox1.Refresh; end;   procedure TForm1.PaintBox1MouseUp(Sender: TObject; Button: TMouseButton;        Shift: TShiftState; X, Y: Integer);var  CellX, CellY: Integer;begin   CellX := (X div BoxSize) * Boxsize;  CellY := (Y div BoxSize) * Boxsize;  CellXCenter := CellX + (Boxsize div 2);  CellYCenter := CellY + (Boxsize div 2);   case Button of     mbLeft: begin       FieldBitmap.Canvas.Brush.Color := RGBToColor(225, 25, 25);       FieldBitmap.Canvas.Brush.Style := bsSolid;       FieldBitmap.Canvas.Pen.Color   := RGBToColor(225, 25, 25);       FieldBitmap.Canvas.Pen.Style   := psSolid;       FieldBitmap.Canvas.Ellipse(CellX + 2, CellY + 2, CellX + BoxSize - 2, CellY + BoxSize - 2);     end;     mbRight: begin       FieldBitmap.Canvas.Brush.Color := RGBToColor(25, 225, 25);       FieldBitmap.Canvas.Brush.Style := bsSolid;       FieldBitmap.Canvas.Pen.Color   := RGBToColor(25, 225, 25);       FieldBitmap.Canvas.Pen.Style   := psSolid;       FieldBitmap.Canvas.Ellipse(CellX + 2, CellY + 2, CellX + BoxSize - 2, CellY + BoxSize - 2);     end;     mbMiddle: begin       ThereWillBeBlood := True;       StartCaptureAnimation;     end;  end;   PaintBox1.Repaint; end;   procedure TForm1.StartCaptureAnimation;begin  AnimationFrame := 0;  Timer1.Interval := 50;               // slower  //Timer1.Interval := 15;             // faster  Timer1.Enabled := True;end;   procedure TForm1.ClearAnimationBitmap;// -----------------------------------// clearing AnimationBitmap.Canvas// -----------------------------------begin   // -- method 1 -- works strange  //AnimationBitmap.TransparentColor := clFuchsia;  //AnimationBitmap.Canvas.Brush.Color := clFuchsia;  //AnimationBitmap.Canvas.FillRect(0, 0, FieldWidth, FieldHeight);   // -- method 2 - works not perfect  //AnimationBitmap.Clear;  //AnimationBitmap.SetSize(FieldWidth, FieldHeight);   // -- method 3 - works not perfect  AnimationBitmap.Canvas.Brush.Color := clNone;  //AnimationBitmap.Canvas.Brush.Color := clFuchsia; // clFuchsia instead of clNone - for clarity  AnimationBitmap.Canvas.FillRect(0, 0, FieldWidth, FieldHeight); end;   procedure TForm1.StopCaptureAnimation;begin  Timer1.Enabled := False;   ClearAnimationBitmap;   PaintBox1.Invalidate;  ThereWillBeBlood := False;end;   procedure TForm1.Timer1Timer(Sender: TObject);var  i, x, y, size: Integer;begin  AnimationFrame := AnimationFrame + 1;   ClearAnimationBitmap;   AnimationBitmap.Canvas.Brush.Color := RGBToColor(150, 0, 0);  AnimationBitmap.Canvas.Pen.Style := psClear;  AnimationBitmap.Canvas.Brush.Style := bsSolid;  AnimationBitmap.Canvas.Ellipse(CellXCenter - AnimationFrame * 4, CellYCenter - AnimationFrame * 4,                                 CellXCenter + AnimationFrame * 4, CellYCenter + AnimationFrame * 4);   Randomize;  for i := 0 to 10 do  begin    x := CellXCenter + Random(AnimationFrame * 15) - (AnimationFrame * 7);    y := CellYCenter + Random(AnimationFrame * 15) - (AnimationFrame * 7);    size := Random(8) + 3;    AnimationBitmap.Canvas.Ellipse(x - size, y - size, x + size, y + size);  end;   PaintBox1.Invalidate;   if AnimationFrame > 10 then    StopCaptureAnimation;end; end. ---------------------------------------------------------------------- TRon · 2025-02-08 07:14 The logic behind it does not work simply because transparency is not the same as an true alpha channel (it is fake transparency providing the illusion of a pixel being opaque or not). I try come up with an example later but I am a bit busy atm so might take me a while. If you want to have something to do in the mean-time try have a lok at 32-bit true alpha graphic formats or as an alternative how to "blend" two normal images together into one by means of bitmasking. That is unless someone else is able to come up with a more decent answer on a shorter notice ---------------------------------------------------------------------- majolika · 2025-02-08 07:33 Quote from: TRon on February 08, 2025, 08:14:31 amI try come up with an example later but I am a bit busy atm so might take me a while. It would be nice of you! I will patiently wait and while I wiil be waiting I definitly Quote from: TRon on February 08, 2025, 08:14:31 amhave a look at 32-bit true alpha graphic formats ---------------------------------------------------------------------- majolika · 2025-02-08 14:05 Allright, I simplified my code to just putting a blue circle on a middle mouse click. No animation for now, just trying to get transparency on the AnimationBitmap. mbLeft — red circle mbRight — green circle mbMiddle — blue circle In FieldInit trying this code: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i   //AnimBitmap.PixelFormat := pf24bit;  AnimBitmap.PixelFormat := pf32bit;  AnimBitmap.SetSize(FieldWidth, FieldHeight);  AnimBitmap.TransparentColor := clBlack;  AnimBitmap.TransparentMode := tmFixed;  //AnimBitmap.TransparentMode := tmAuto;  AnimBitmap.Transparent := True;  AnimBitmap.Canvas.Brush.Color := clBlack;  AnimBitmap.Canvas.FillRect(0, 0, FieldWidth, FieldHeight);  Combination of PixelFormat and TransperentMode gives me two strange behaviors: pf24bit, tmAuto - black background, multiple blue circles pf24bit, tmFixed - transparent background, single blue circle in a first click position pf32bit, tmAuto - black background, multiple blue circles pf32bit, tmFixed - black background, multiple blue circles It's so confusing... P.S. I was trying to use TBGRABitmap instead of TBitmap. It works fine. Just as I expected (transparent background, multiple blue circles). I just need to realize how to draw semi-transparent primitives on a fully transparent background. But what if I don't want to use 3rd party components? P.P.S. Reading this article gives me an idea that using TLazIntfImage is a total mess. You need a TLazIntfImage, TLaxCanvas, TBitmap and Timage (and/or maybe TPaintBox, I don't know) just to get one semi-transparent circle?! Are you kidding me?! ---------------------------------------------------------------------- majolika · 2025-02-08 14:10 I forgot to attach my testing project. Here it is. ---------------------------------------------------------------------- majolika · 2025-02-08 19:17 Ok, now transparent animation works fine with TBGRABitmap (with one exception). But what if I don't want to use 3rd party components? Can I make the same animation with just LCL? Now, to the «one exception»: incorrect initialization of CellXCenter/CellYCenter is still here. If the animation is fast (with a small Timer interval) you can barely see it. But if the animation is slow... at the top-left corner there is a part of animation appears. Steps to reproduce: 1. Launch application: 2. Draw the field with any of two buttons; 3. Middle click on the field. Current phase of the testing project is in the attachment. P.S. Got it! The problem was in Timer handler. When it launches for the first time it doesn't know about Cell(X/Y)Center. But why? It launches _after_ Cell(X/Y)Center initialization. Seems strangr to me. Anyway, this tow lines in FormCreate do the trick. Timer1.Enabled := True; Timer1.Enabled := False; And now I can incorporate animation in my «mine sweeper project». ---------------------------------------------------------------------- TRon · 2025-02-09 18:23 Apologies for the delay. I see that you made quite some progress majolika. Nice ! I have some sort of a solution for when not wanting to use a 3th party implementation but not sure if this is what meets the criteria for your project (please let me know). It is based on your test17 example. PS: in the example  you forgot to disable the timer so that was running a complete cycle of animation in the background. Depending on how quick you pressed the middle mouse button ofc. ---------------------------------------------------------------------- majolika · 2025-02-09 18:52 Quote from: TRon on February 09, 2025, 07:23:48 pmyou forgot to disable the timer so that was running a complete cycle of animation in the background. Oh! That's why the parasite drawings happened! Thanks a lot for your attentiveness! Quote from: TRon on February 09, 2025, 07:23:48 pmI have some sort of a solution for when not wanting to use a 3th party implementation but not sure if this is what meets the criteria for your project (please let me know). As for now I'm ok with BGRABitmap. Although it's documentation is not so detailed as it could be it has a lots of examples. But... It would be interesting to see what and how I can do with only LCL features. My criteria is something in between KISS principle and Zen of Python. ---------------------------------------------------------------------- majolika · 2025-02-09 19:22 For now i'm stuck trying to algorithmize waves after a thrown stone. Alas, such type of algorithmization is not my strong point. Blot was simple enough but waves... P.S. And I'm not sure how to pause application running while Timer do his job. It causes an unwanted behavior: my game shows Win/Lose message although blot is still animating. P.P.S. My testing point is the attachment.