Animating desktop widgets

Discussion related to the Cross Codebot open source framework
sysrpl
Posts: 105
Joined: Thu Feb 05, 2015 6:31 pm

Animating desktop widgets

Postby sysrpl » Wed Mar 25, 2015 7:05 am


This article describes how to create cross platform animated desktop widgets as demonstrated in the example below. This example is included in the Cross Codebot git repository. Here is a animated clock widget running on Windows.



And here is the same clock widget on Linux.



In previous forum postings I supplied a background for using time to animate with easings as well as he basics of fast hardware accelerated drawing using the ISurface interface. In this post I will walk programmers through cross platform desktop widget creation.

A desktop widget is an overlay window whose shape is defined by a bitmap, rather than traditional rectangular dimensions. The individual pixels of a desktop widget can have varying levels of transparency ranging from totally invisible, to completely opaque, or any value between the two.

Desktop widgets can serve many uses, but generally they are used to display continually updated data, such as the time, weather, stock quotes, sports scores, network activity, or any other information a user might frequently want to check. They provide a convenience to monitoring information, and as a secondary benefit they can add to a generally an aesthetically pleasing computer environment based on the users taste or desire for personalized information attached to his computer desktop.

For programmers interested in creating cross platforms animated widgets, the Cross Codebot library provides an object interface named ISplash. The ISplash object gives programmers a platform independent way to create and overlay window, and a hardware accelerated API to draw to that window. The same ISurface drawing functionality and knowledge you may have gain from previous articles can be simply reused if you would like to pop your code out into and ISplash overlay window, becoming a desktop widget.

A key feature with the ISurface object is that it provides insanely fast hardware accelerated vector graphics on all platforms. These are full 8 bits per channel with an alpha channel graphics. And being a vector API, you can scale your graphics to any size, like in the example video provided at the top of the page.

To get started with a desktop widget, you simply write the following:

var
{ This is our widget }
Widget: ISplash;

procedure TForm1.Create(Sender: TObject);
begin
{ Create the widget }
Widget := NewSplash;
{ Make it 100px by 100px }
Widget.Bitmap.SetSize(100, 100);
{ Fill it with red }
Widget.Bitmap.Surface.Clear(clRed);
{ Show it }
Widget.Visible := True;
end;


To move the widget simply save Widget.Move(LocationX, LocationY), to change data on a widget, simply draw to it and call Widget.Update.

{ Called from our application when a new stock price message 
is available. }
procedure TForm1.StockPricceChanged(StockPriceMessage: string);
const
Radius = 8;
var
B: IBitmap;
C: TColorB;
begin
B := Widget.Bitmap;
{ Clear the previous content }
B.Surface.Clear(clTransparent);
{ Let's use green to draw a round rectangle }
C := clGreen;
{ Use 50% opacity for the green background }
B.Surface.FillRoundRect(NewBrush(C.Fade(0.5)), B.ClientRect, Radius);
{ And 75% opacity for the green border }
B.Surface.StrokeRoundRect(NewPen(C.Fade(0.75)), B.ClientRect, Radius);
{ And draw the pricing message centered in the widget }
B.Surface.TextOut(NewFont(Font), StockPriceMessage, B.ClientRect, drCenter);
{ Finally, tell the widget that we are done with our drawing }
Widget.Update;
end;


This is obviously a very simple example, but you can make the drawing as complex as you want, and rest assured that ISurface can handle it. It's really up to you how far you want to go with your widget visuals. Remember, if your widget has static parts, you can keep them in png resources and use the ISurface.CopyTo method to copy parts of the png resources into you beautiful widget.

Here is the listing of the clock widget from the top of this page.

{ Since we are using vector graphics, we can scale the widget
size using a scaling factor }

var
Factor: Float;
{ For our example Size always equals Round(Factor * 256) }
Size: Integer;

procedure DrawClock(Bitmap: IBitmap);
const
{ Define our colors }
clClockFace: TColorB = (Blue: 255; Green: 248; Red: 248; Alpha: 255);
clLens: TColorB = (Blue: 255; Green: 220; Red: 220; Alpha: 255);
clMinuteHand: TColorB = (Blue: 80; Green: 72; Red: 72; Alpha: 255);
clSecondHand: TColorB = (Blue: 32; Green: 32; Red: 168; Alpha: 255);
clMicroHand: TColorB = (Blue: 220; Green: 100; Red: 100; Alpha: 255);
clShadowHand: TColorB = (Blue: 0; Green: 0; Red: 0; Alpha: 50);

{ Draw the ticks around the clock face }

procedure DrawTicks(Surface: ISurface);
var
M: TMatrix4x4;
A, B, C: TVec3;
P: IPen;
I: Integer;
begin
A := Vec(Size / 2, 35 * Factor, 0);
B := Vec(Size / 2, 43 * Factor, 0);
C := Vec(Size / 2, Size / 2, 0);
{ Use a matrix from Codebot.Geometry to rotate two points
around the clock face }
M := StockMatrix;
{ 12 hours in 360 degrees }
M.RotateAt(0, 0, 360 / 12 , C);
for I := 0 to 11 do
begin
Surface.MoveTo(A.X, A.Y);
Surface.LineTo(B.X, B.Y);
A := M * A;
B := M * B;
end;
{ Use a fat pen for the hour ticks }
P := NewPen(clGray, 3.5 * Factor);
P.Color := P.Color.Darken(0.1);
P.LineCap := cpRound;
{ Stroke the hours }
Surface.Stroke(P);
M := StockMatrix;
{ 60 minutes in 360 degrees }
M.RotateAt(0, 0, 360 / 60 , C);
A := Vec(Size / 2, 35 * Factor, 0);
B := Vec(Size / 2, 37 * Factor, 0);
for I := 0 to 59 do
begin
if I mod 5 > 0 then
begin
Surface.MoveTo(A.X, A.Y);
Surface.LineTo(B.X, B.Y);
end;
A := M * A;
B := M * B;
end;
{ Use a thin pen }
P.Width := Factor;
{ And stroke the minute ticks }
Surface.Stroke(P);
{ The micro second clock is offset to the side }
A := Vec(Size / 2 + 50 * Factor, Size / 2 - 10 * Factor, 0);
B := Vec(Size / 2 + 50 * Factor, Size / 2 - 8 * Factor, 0);
C := Vec(Size / 2 + 50 * Factor, Size / 2, 0);
M := StockMatrix;
{ Draw 12 ticks for the micro seconds }
M.RotateAt(0, 0, 360 / 12 , C);
for I := 0 to 11 do
begin
Surface.MoveTo(A.X, A.Y);
Surface.LineTo(B.X, B.Y);
A := M * A;
B := M * B;
end;
{ Use a thin pen }
P.Width := Factor;
{ And stroke the micro second ticks }
Surface.Stroke(P);
end;

{ Draw the microsecond hand }

procedure DrawMicros(Surface: ISurface; Micro: Double);
var
X, Y: Float;
I: Integer;
begin
{ Clear the previous transform (not really needed, but just in case we
change something later) }
Surface.Matrix.Identity;
X := Size / 2 + 50 * Factor;
Y := Size / 2;
{ To rotate hands, we center the rotation on an x and y coordinate }
Surface.Matrix.Translate(-X, -Y);
{ Rotate the micro second hand }
Surface.Matrix.Rotate(Micro / 12 * PI * 2);
{ Move back to the original position }
Surface.Matrix.Translate(X, Y);
{ Add a microsecond hand to the page }
Surface.MoveTo(X + 0.25 * Factor, Y + -6 * Factor);
Surface.LineTo(X + Factor, Y + 2 * Factor);
Surface.LineTo(X + -Factor, Y + 2 * Factor);
Surface.LineTo(X - 0.25 * Factor, Y + -6 * Factor);
Surface.Path.Close;
{ Close and fill using color clMicroHand }
Surface.Fill(NewBrush(clMicroHand));
{ Reset the transform }
Surface.Matrix.Identity;
end;

procedure DrawHours(Surface: ISurface; Hour: Double);
var
X: Float;
I: Integer;
begin
{ Draw larger hands two times, once for a shadow, then over it again
with the actual hand }
for I := 1 downto 0 do
begin
Surface.Matrix.Identity;
Surface.Matrix.Translate(-Size / 2, -Size / 2);
{ Rotate the hour hand }
Surface.Matrix.Rotate(Hour / 12 * PI * 2);
Surface.Matrix.Translate(Size / 2 - I / 2 * Factor, Size / 2 + I * 2 * Factor);
{ Create the hour hand path }
X := Size / 2;
Surface.MoveTo(X + 3 * Factor, 70 * Factor);
Surface.LineTo(X + 3 * Factor, X + 15 * Factor);
Surface.LineTo(X - 3 * Factor, X + 15 * Factor);
Surface.LineTo(X - 3 * Factor, 70 * Factor);
Surface.Path.Close;
{ and fill the hand or the shadow, depending on the pass }
if I = 0 then
Surface.Fill(NewBrush(clMinuteHand))
else
Surface.Fill(NewBrush(clShadowHand));
Surface.Matrix.Identity;
end;
end;

procedure DrawMinutes(Surface: ISurface; Minute: Double);
var
X: Float;
I: Integer;
begin
{ Same code as the hour hand, but with minor adjustments }
for I := 1 downto 0 do
begin
Surface.Matrix.Identity;
Surface.Matrix.Translate(-Size / 2, -Size / 2);
Surface.Matrix.Rotate(Minute / 60 * PI * 2);
Surface.Matrix.Translate(Size / 2 - I / 2 * Factor, Size / 2 + I * 2 * Factor);
X := Size / 2;
Surface.MoveTo(X + 2 * Factor, 50 * Factor);
Surface.LineTo(X + 2 * Factor, X + 22 * Factor);
Surface.LineTo(X - 2 * Factor, X + 22 * Factor);
Surface.LineTo(X - 2 * Factor, 50 * Factor);
Surface.Path.Close;
if I = 0 then
Surface.Fill(NewBrush(clMinuteHand))
else
Surface.Fill(NewBrush(clShadowHand));
Surface.Matrix.Identity;
end;
end;

procedure DrawSeconds(Surface: ISurface; Second: Double);
var
A, B, C: TPointF;
R: TRectF;
Color: TColorB;
I: Integer;
begin
{ Mostly the same code as the hour hand }
for I := 1 downto 0 do
begin
Surface.Matrix.Identity;
Surface.Matrix.Translate(-Size / 2, -Size / 2);
Surface.Matrix.Rotate(Second / 60 * PI * 2);
Surface.Matrix.Translate(Size / 2 - I / 2 * Factor, Size / 2 + I * 2 * Factor);
{ But this time we define a clipping path }
Surface.MoveTo(0, 0);
Surface.LineTo(0, Size);
Surface.LineTo(Size, Size);
Surface.LineTo(Size, 0);
Surface.Path.Close;
R := TRectF.Create(10 * Factor, 10 * Factor);
R.Center(Size / 2, Size / 2);
R.Inflate(-2 * Factor, -2 * Factor);
{ Because the second hand has a neat hole in it }
Surface.Ellipse(R);
R := TRectF.Create(4 * Factor, 4 * Factor);
R.Center(Size / 2, 60 * Factor);
Surface.Ellipse(R);
{ And clip the hole out }
Surface.Path.Clip;
{ A, B, and C are points on the minute hand when it's positioned at zero }
A := TPointF.Create(Size / 2, 35 * Factor);
B := TPointF.Create(Size / 2 + 1.5 * Factor,
Size / 2 + 25 * Factor);
C := TPointF.Create(Size / 2 - 1.5 * Factor,
Size / 2 + 25 * Factor);
Surface.MoveTo(A.X - 0.25 * Factor, A.Y);
Surface.LineTo(A.X + 0.25 * Factor, A.Y);
Surface.LineTo(B.X, B.Y - 30 * Factor);
Surface.LineTo(B.X, B.Y);
Surface.LineTo(C.X, C.Y);
Surface.LineTo(C.X, C.Y - 30 * Factor);
R := TRectF.Create(10 * Factor, 10 * Factor);
R.Center(Size / 2, Size / 2);
Surface.Ellipse(R);
R := TRectF.Create(6 * Factor, 6 * Factor);
R.Center(Size / 2, 60 * Factor);
{ Another hole }
Surface.Ellipse(R);
{ Color with the hand, or the shadow, depending on the pass }
if I = 0 then
Color := clSecondHand
else
Color := clShadowHand;
{ Fill the path }
Surface.Fill(NewBrush(Color));
{ Unclip }
Surface.Path.Unclip;
{ And reset the matrix }
Surface.Matrix.Identity;
end;
end;

{ Draw a light reflection above and below the center of the clock face }

procedure DrawLens(Surface: iSurface);
var
R: TRectF;
C: TColorB;
G: IGradientBrush;
begin
R := TRectF.Create(Size, Size);
R.Left := -Size * 1.25;
R.Top := R.Left;
R.Offset(0, 25 * Factor);
{ Use a big gradient }
G := NewBrush(R);
{ With an off blue white color }
C := clLens;
G.AddStop(C.Fade(0), 0);
{ Fade in the reflection here }
G.AddStop(C.Fade(0), 0.88);
{ Sharply }
G.AddStop(C.Fade(0.15), 0.885);
G.AddStop(C.Fade(0), 1);
{ Repeat for the top reflections, with different values }
R := TRectF.Create(Size, Size);
R.Inflate(-23 * Factor, -23 * Factor);
Surface.Ellipse(R);
Surface.Fill(G);
R := TRectF.Create(Size, Size);
R.Left := -Size * 0.25;
R.Top := R.Left;
R.Right := R.Right + Size * 1.25;
R.Bottom := R.Right;
R.Offset(0, 25 * -Factor);
G := NewBrush(R);
G.AddStop(C.Fade(0), 0);
G.AddStop(C.Fade(0), 0.75);
G.AddStop(C.Fade(0.1), 0.755);
G.AddStop(C.Fade(0), 0.79);
R := TRectF.Create(Size, Size);
R.Inflate(-23 * Factor, -23 * Factor);
Surface.Ellipse(R);
Surface.Fill(G);
end;

var
Time: Double;
Hour, Minute, Second, Micro: Double;
Surface: ISurface;
R: TRectF;
C: TColorB;
G: IGradientBrush;
begin
{ If the scale factor was changed, make the bitmap match }
Bitmap.SetSize(Size, Size);
Surface := Bitmap.Surface;
{ Erase the last clock }
Surface.Clear(clTransparent);
{ Draw the border ring }
R := Bitmap.ClientRect;
R.Inflate(-2 * Factor, -2 * Factor);
C := clClockFace;
G := NewBrush(R.TopRight, R.BottomLeft);
G.AddStop(C.Darken(0.4), 0);
G.AddStop(C.Fade(0), 0.75);
Surface.Ellipse(R);
Surface.Fill(G);
{ Draw the big ring }
R := Bitmap.ClientRect;
R.Top := R.Top - 200 * Factor;
R.Right := R.Right + 200 * Factor;
G := NewBrush(R);
G.AddStop(C, 0);
G.AddStop(C.Darken(0.8), 1);
R := Bitmap.ClientRect;
R.Inflate(-4 * Factor, -4 * Factor);
Surface.Ellipse(R);
Surface.Fill(G);
{ Draw the inner ring }
R := Bitmap.ClientRect;
R.Left := R.Left - 150 * Factor;
R.Bottom := R.Bottom + 150 * Factor;
G := NewBrush(R);
G.AddStop(C, 0);
G.AddStop(C.Darken(0.8), 1);
R := Bitmap.ClientRect;
R.Inflate(-23 * Factor, -23 * Factor);
Surface.Ellipse(R);
Surface.Fill(G);
{ And finally a solid color for the clock face}
R := Bitmap.ClientRect;
R.Inflate(-28 * Factor, -28 * Factor);
Surface.Ellipse(R);
Surface.Fill(NewBrush(C));
{ Draw the tick marks on top of the clock face }
DrawTicks(Surface);
{ Extract the hours, minutes, seconds, and micro seconds }
Time := Frac(Now);
Hour := Remainder(Time * 24, 12);
Minute := Remainder(Time * 24 * 60, 60);
Second := Remainder(Time * 24 * 60 * 60, 60);
Micro := Remainder(Time * 24 * 60 * 60 * 60, 12);
{ In order, micros, hours, minutes, then seconds }
DrawMicros(Surface, Micro);
DrawHours(Surface, Hour);
DrawMinutes(Surface, Minute);
DrawSeconds(Surface, Second);
{ Draw a shadow creeping across the clock face }
C := clBlack;
R := Bitmap.ClientRect;
R.Left := R.Left - 24.5 * Factor;
R.Bottom := R.Bottom + 24.5 * Factor;
G := NewBrush(R);
G.AddStop(C.Fade(0.12), 0);
G.AddStop(C.Fade(0.16), 0.35);
G.AddStop(C.Fade(0.2), 0.45);
G.AddStop(C.Fade(0.6), 1);
R := Bitmap.ClientRect;
R.Inflate(-28 * Factor, -28 * Factor);
Surface.Ellipse(R);
Surface.Fill(G);
{ And finally draw the clock lense }
DrawLens(Surface);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
{ Default the factor to 1 }
Factor := 1;
{ And Size to 256 * Factor }
Size := Round(256 * Factor);
{ Here is our widget }
FSplash := NewSplash;
{ Here is the bitmap which defines the widget size and pixels }
FClock := FSplash.Bitmap;
{ Draw the clock }
DrawClock(FClock);
{ Move it to the top right of the screen }
FSplash.Move(Screen.Width - Size - 20, 20);
{ Show it }
FSplash.Visible := True;
{ Start a timer to redraw the clock synched with the pc
refresh rate }
FTimer := TAnimationTimer.Create(Self);
FTimer.OnTimer := Tick;
FTimer.Enabled := True;
end;

procedure TForm1.Tick(Sender: TObject);
begin
{ At the screen refresh rate interval, draw a new clock }
DrawClock(FClock);
{ And update the widget }
FSplash.Update;
end;

procedure TForm1.ScaleBarChange(Sender: TObject);
begin
{ Scale the clock using a slider }
Factor := ScaleBar.Position;
Size := Round(256 * Factor);
{ And reposition it }
FSplash.Move(Screen.Width - Size - 20, 20);
end;

Return to “Cross Codebot”

Who is online

Users browsing this forum: No registered users and 1 guest

cron