Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Vector Graphics Rendered Animated Clock

0.00/5 (No votes)
18 Apr 2004 2  
Demonstrates Using MyXaml With A Vector Graphics Engine to Create an Analog Clock

Contents

Introduction

In my last article, I demonstrated using MyXaml to create a simple blog reader. In this article, I'd like to demonstrate how to create vector graphics applications using VG.net's runtime engine in conjunction with declarative markup. In particular, I will be demonstrating the code that creates this working clock:

The Initial Markup

<?xml version="1.0" encoding="utf-8" standalone="no"?>
<MyXaml

  xmlns:def="Definition"

  xmlns="Prodige.Drawing, Prodige.Drawing"

  xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing">
  <Picture Name="Clock">
  </Picture>
</MyXaml>

The initial markup declares the assembly namespaces and associates them with xmlns prefixes. The default namespace in this case is the Prodige.Drawing vector graphics runtime engine. The Picture class is a container for vector graphic elements.

Creating The Container Form

Normally, one or more Picture objects are drawn on a Canvas, which is a user control and can be added to a Form. Because the VG.net designer can generate the MyXaml markup directly, I've put together a small loader vgLoader.exe. The loader tells the parser to instantiate the Picture and then invokes Picture's DisplayInForm method. This returns an initially sized Form which can then be displayed. The code for the actual loading is as follows:

Parser parser=new Parser();
object picture=parser.LoadForm(filename, "*", null, null);
Type type=picture.GetType();
MethodInfo mi=type.GetMethod("DisplayInForm");
Form form=mi.Invoke(picture, new object[] {new Size(10, 10)}) as Form;
form.ShowDialog();

Since the loader doesn't know the name of the Picture, it uses the "*" wildcard to tell the parser to instantiate the first class that it encounters after the <MyXaml> tag.

The Clock Features

The following sections discuss how the pieces of the clock are constructed.

The Frame

<?xml version="1.0" encoding="utf-8" standalone="no"?>
MyXaml
  xmlns:def="Definition"
  xmlns="Prodige.Drawing, Prodige.Drawing"
  xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing">
  <Picture Name="Clock">
    <Elements>
      <Ellipse Name="outerRim" Location="100, 100" Size="200, 200"

               StyleReference="Rim" DrawAction="Fill" />
      <Ellipse Name="innerRim" Location="110, 110" Size="180, 180"

               StyleReference="Rim" DrawAction="Fill">
        <Fill>
          <pds:LinearGradientFill Angle="225" />
        </Fill>
      </Ellipse>
    </Elements>
    <Styles>
      <pds:Style Name="Rim">
        <pds:Fill>
          <pds:LinearGradientFill GradientType="TwoColor" Angle="45"

               StartColor="192, 192, 255" />
        </pds:Fill>
      </pds:Style>
    </Styles>
  </Picture>
</MyXaml>

The clock frame consists of two circles (Ellipse classes), one drawn inside the other. A linear gradient fill is used to create the shadow effect of light being cast on the clock from the upper left. In this markup, both the outer and inner rim use the same style, however the inner rim overrides the Angle property.

The Face

<Ellipse Name="face" Location="114, 114" Size="173, 173"

         StyleReference="Face" DrawAction="Fill" />
<pds:Style Name="Face">
  <pds:Fill>
    <pds:LinearGradientFill GradientType="TwoColor" Angle="45"

         EndColor="0, 0, 0" StartColor="125, 123, 168" />
  </pds:Fill>
</pds:Style>

A third ellipse and an additional style is created to add the clock face.

The Shadow

<Ellipse Name="shadow" Location="102, 102" Size="200, 200"

         StyleReference="ClockShadow" DrawAction="Fill" />
<pds:Style Name="ClockShadow">
  <pds:Fill>
    <pds:SolidFill Color="63, 0, 0, 0" Opacity="0.247058824" />
  </pds:Fill>
</pds:Style>

A shadow is another ellipse and style.

Objects are drawn one on top of the other, so, the actual order of the vector graphics elements so far is:

<Elements>
  <Ellipse Name="shadow" Location="102, 102" Size="200, 200"

           StyleReference="ClockShadow" DrawAction="Fill" />
  <Ellipse Name="outerRim" Location="100, 100" Size="200, 200"

           StyleReference="Rim" DrawAction="Fill" />
  <Ellipse Name="innerRim" Location="110, 110" Size="180, 180"

           StyleReference="Rim" DrawAction="Fill">
    <Fill>
      <pds:LinearGradientFill Angle="225" />
    </Fill>
  </Ellipse>
  <Ellipse Name="face" Location="114, 114" Size="173, 173"

           StyleReference="Face" DrawAction="Fill" />
</Elements>

The Numerals

<Picture>
  <TextAppearance>
    <pds:TextAppearance Color="192, 192, 255" Size="18"

         FaceName="Maiandra GD" RenderingHint="AntiAliasGridFit"

         SizeUnit="World" />
  </TextAppearance>
  <Elements>
  ...

The TextAppearance property of the Picture object is instantiated to the default text appearance, which is used for each numeral:

<Rectangle Name="one" Text="1" Location="220, 124.3782" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="two" Text="2" Location="245.6218, 150" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="three" Text="3" Location="255, 185" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="four" Text="4" Location="245.6218, 220" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="five" Text="5" Location="220, 245.6218" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="six" Text="6" Location="185, 255" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="seven" Text="7" Location="150, 245" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="eight" Text="8" Location="124.3782, 220" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="nine" Text="9" Location="115, 185" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="ten" Text="10" Location="124.3782, 150" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="eleven" Text="11" Location="150, 124.3782" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill" />
<Rectangle Name="twelve" Text="12" Location="185, 115" Size="30, 30"

           StyleReference="Numeral" DrawAction="Fill"/>
<pds:Style Name="Numeral">
  <pds:Fill>
    <pds:SolidFill Opacity="0" />
  </pds:Fill>
</pds:Style>

Each numeral is placed on top of the clock face, and therefore appears after the "face" Ellipse in the Elements list. If you're wondering about whether I hand coded the precision of the placement to the ten-thousandth's decimal, the answer is no. Since my understanding of vector graphics is rather new, Frank Hileman drew the clock in the VG.net designer. I asked Frank how he did it, and this is what he said:

First I created "twelve" and positioned it at the top. I selected "twelve" and set the TransformationReference Type property to "Absolute". Then I changed the TransformationReference Location to the center of the circles:

<TransformationReference>
  <TransformationReference Location="200, 200" Type="Absolute"/>
</TransformationReference>

Now any change to Rotation will go about that Location. I did a copy/paste of "twelve", creating an identical object in the same place. Let's make that one "three". Change the Rotation property to 90, and the Text property to "3". You now have text rotated about the center of the clock.

Now we need to remove the Rotation, but relative to the text center, and not the clock center. Select "three". Right click on the TransformationReference property, and click "Reset". The reference point goes back to Center, but the object does not change position. Now right click on Rotation, and click "Reset". The rotation is gone, but the text does not move.

I copied the "twelve" object 11 times, each time setting the Rotation property by a multiple of 30 degrees, changing the Name and Text properties, and doing a Reset on the TransformationReference and Rotation, in that order.

The Minute Hand

<Group Name="minute" StyleReference="Minute">
  <TransformationReference>
    <TransformationReference Location="200, 200" Type="Absolute" />
  </TransformationReference>
  <Elements>
    <Path Name="leftMinute" DrawAction="Fill">
      <PathPoints>
        <PathPoint Point="200.101, 120" Type="Start" />
        <PathPoint Point="199.7635, 131.6271" Type="Control1" />
        <PathPoint Point="195, 194.9518" Type="Control2" />
        <PathPoint Point="195.0503, 198.8948" Type="EndBezier" />
        <PathPoint Point="195.1006, 202.8378" Type="Control1" />
        <PathPoint Point="197.7291, 205.1745" Type="Control2" />
        <PathPoint Point="200, 204.9767" Type="EndBezier" />
        <PathPoint Point="200.2, 204.5694" Type="EndLine" />
      </PathPoints>
    </Path>
    <Path Name="rightMinute" DrawAction="Fill" Scaling="1, -1.213767"

          Rotation="180">
      <PathPoints>
        <PathPoint Point="205.09, 197.4994" Type="Start" />
        <PathPoint Point="202.8521, 197.6623" Type="Control1" />
        <PathPoint Point="200.1495, 195.748" Type="Control2" />
        <PathPoint Point="200.0996, 192.4994" Type="EndBezier" />
        <PathPoint Point="200.0498, 189.2508" Type="Control1" />
        <PathPoint Point="204.7664, 137.0788" Type="Control2" />
        <PathPoint Point="205.09, 127.4994" Type="EndBezier" />
      </PathPoints>
    </Path>
  </Elements>
</Group>
<pds:Style Name="Minute">
  <pds:Fill>
    <pds:LinearGradientFill Bounds="0, 0.8, 1.3, 1.3"

         GradientType="TwoColorBell" Angle="140" EndColor="202, 222, 255"

         StartColor="0, 0, 128" />
  </pds:Fill>
</pds:Style>

I asked Frank how he made the hands, and here's what he said:

I have to admit, there is a Hack in there. Here is how I did it: I created a 3-point spline for one half of the hand. Then I converted that to a Path, and tweaked the control points a bit. Then I did a copy/paste, creating an identical object in the same place. To mirror, I set the scale X property to -1 (for the minute hand). Then I moved the hand over to the right, using grid snap to align with the other. Since each Path displays a linear gradient, but one displays in the opposite direction (because of negative scaling), together they give it that 3D effect.

Now why to you see that weird scaling in the generated xml?  This is because I resized the two halves after I created them. Since the left half did not need a -1 scaling, the designer transformed the points in the path. I could have removed the weird scaling on the right half with ApplyTransformation, but I needed to leave the negative scaling in the right half, to reverse the gradient (so I can keep the same Style for both). By default, when you resize, the designer does not apply the transform to the points if the object already has a scaling. So the right half kept its scaling, but it is modified. If I had just resized the left half correctly before the copy paste, you would not see that weird scaling.

The hour hand was done similarly but I did it horizontally.

The Bounds and Angle on the LinearGradientFill were carefully chosen to line the darker edge of the gradient up with the angle of the Path.

Now the Hack. There was a small line visible up the middle of the arrows (still is at smaller scales, didn't get rid of it completely). This is caused by the fill algorithms in GDI+ not aligning edges of filled areas perfectly, so you see the background a bit. I went and added an extra point to the left half to cover that. I also tried to cover it by tweaking the end points a bit but that never really worked. A better choice I realize now would be to draw a single pixel line up the middle, behind the two filled halves.

We want the minute hand to be the bottom-most hand, so it gets declared immediately after the numerals, and has its own style. In this markup, a group (a composite of elements) is being declared, one for each half of the minute hand. The Path class defines a set of figures each of which contains a set of straight and curved segments (the path points were determined by the designer, not by me!).

Also note that the entire group uses a TransformationReference to specify an absolute reference point for the group. This allows us to rotate the starting point of the minute hand paths about the center of the clock.

The Hour Hand

<Group Name="hour" StyleReference="Hour">
  <TransformationReference>
    <TransformationReference Location="200, 200" Type="Absolute" />
  </TransformationReference>
  <Elements>
    <Path Name="leftHour" DrawAction="Fill" Rotation="270">
      <PathPoints>
        <PathPoint Point="225.1051, 179.8949" Type="Start" />
        <PathPoint Point="217.4753, 179.0615" Type="Control1" />
        <PathPoint Point="188.4821, 174.8949" Type="Control2" />
        <PathPoint Point="179.3263, 174.8949" Type="EndBezier" />
        <PathPoint Point="170.1706, 174.8949" Type="Control1" />
        <PathPoint Point="170.1053, 178.2542" Type="Control2" />
        <PathPoint Point="170.1706, 179.8949" Type="EndBezier" />
        <PathPoint Point="170.4581, 180.1053" Type="EndLine" />
      </PathPoints>
    </Path>
    <Path Name="rightHour" DrawAction="Fill" Scaling="1.831152, -1"

          Rotation="270">
      <PathPoints>
        <PathPoint Point="217.3672, 179.8948" Type="Start" />
        <PathPoint Point="213.2005, 179.0614" Type="Control1" />
        <PathPoint Point="197.3672, 174.8948" Type="Control2" />
        <PathPoint Point="192.3672, 174.8948" Type="EndBezier" />
        <PathPoint Point="187.3672, 174.8948" Type="Control1" />
        <PathPoint Point="187.3315, 178.2541" Type="Control2" />
        <PathPoint Point="187.3672, 179.8948" Type="EndBezier" />
        <PathPoint Point="187.5243, 180.1052" Type="EndLine" />
      </PathPoints>
    </Path>
  </Elements>
</Group>
<pds:Style Name="Hour">
  <pds:Fill>
    <pds:LinearGradientFill Bounds="0, 0.8, 1.3, 1.3"

         GradientType="TwoColorBell" Angle="140" EndColor="202, 222, 255"

         StartColor="0, 0, 128" />
  </pds:Fill>
</pds:Style>

The hour hand is almost identical to the minute hand--it consists of a group of elements containing two paths, one for the left side and one for the right side of the hour hand. A separate style is used.

The Second Hand

<Polyline Name="second" StyleReference="Second" DrawAction="Edge">
  <TransformationReference>
    <TransformationReference Location="200, 200" Type="Absolute" />
  </TransformationReference>
  <Points>
    <Vector X="200" Y="200" />
    <Vector X="200" Y="135" />
  </Points>
</Polyline>
<pds:Style Name="Second">
  <pds:Stroke>
    <pds:Stroke Color="255, 255, 255" EndCap="Round" StartCap="Round"

         Width="1" />
  </pds:Stroke>
</pds:Style>

Being a straight line, the second hand is simpler and is implemented as a PolyLine with a start point and an end point.

Animating The Clock

The only thing left now is to have the clock tell the time!  We need to do three things:

  1. Add an xmlns to the System.Windows.Forms namespace, so that the complete namespace list now reads:
    <MyXaml
    
      xmlns:def="Definition"
    
      xmlns="Prodige.Drawing, Prodige.Drawing"
    
      xmlns:pds="Prodige.Drawing.Styles, Prodige.Drawing"
    
      xmlns:wf="System.Windows.Forms">
  2. Instantiate a Timer (the reason for the System.Windows.Forms namespace):
    <Picture Name="Clock">
      <wf:Timer Tick='OnTick' Interval='10' Enabled='true'/>
      ...
  3. Implement the event handler:
    <def:Code language='C#'>
      <reference assembly="System.Windows.Forms.dll"/>
      <reference assembly="System.Xml.dll"/>
      <reference assembly="myxaml.dll"/>
      <reference assembly="Prodige.Drawing.dll"/>
      <![CDATA[
    using System;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Windows.Forms;
    
    using MyXaml;
    using Prodige.Drawing;
    
    class AppHelpers
    {
      public Parser parser;
    
      public AppHelpers()
      {
        parser=Parser.CurrentInstance;
      }
    
      public void OnTick(object sender, EventArgs e)
      {
        DateTime n = DateTime.Now;
    
        Polyline second=(Polyline)picture.Elements["second"];
        Group minute=(Group)picture.Elements["minute"];
        Group hour=(Group)picture.Elements["hour"];
    
        second.Rotation = 360F * ((n.Second+ n.Millisecond/1000F)/60F);
        minute.Rotation = 360F * (n.Minute + n.Second/60F)/60F;
        hour.Rotation = 360F * (n.Hour + n.Minute/60F)/12F;
      }
    }
    ]]>
    </def:Code>

Wiring Up The Handler in a Compiled Assembly

If you don't like the in-line code intermingled with the markup, you can wire up the event handler in your own assembly. At the beginning of this article, I showed a code snippet for the loader:

Parser parser=new Parser();
object picture=parser.LoadForm(filename, "*", null, null);
Type type=picture.GetType();
MethodInfo mi=type.GetMethod("DisplayInForm");
Form form=mi.Invoke(picture, new object[] {new Size(10, 10)}) as Form;
form.ShowDialog();

By specifying a target object for events (change the first null to a "this" or any other instance of a class that contains the event handler):

object picture=parser.LoadForm(filename, "*", this, null);

You can copy the OnTick method directly into your assembly, and the parser will automatically wire up the event to your assembly.

Conclusion

The ability for MyXaml to work with third party runtimes provides an exceptionally easy way of plugging in functionality to an application. Compared to the C# code, the markup is less than 1/10th the size, and in my opinion is a lot more readable and easier to edit.

And some truly amazing applications can be written in conjunction with VG.net's free runtime vector graphics engine. I hope this article stimulates a lot of discussion and whets your appetite for the beauty of vector graphics!

Additional examples of vector graphics are provided in the download.

Notes

  1. The version of MyXaml included in this demo is a pre-release of the next beta (0.95). You can download the source from the CVS site on http://myxaml.tigris.org/ or wait for me to release the next version.
  2. The vector graphics engine in this demo is not necessarily the most current. You can download the latest runtime vector graphics engine at http://www.vgdotnet.com/. (Obviously, there is no source for the VG engine. But the runtime is free and fully documented). VG.net is also offering a 30-day time limited beta version of their designer.

Further Reading

  1. MyXaml--XAML-style GUI Generator
  2. Vector Graphics and Declarative Animation with Avalon - the Analog Clock
  3. Adobe SVG Analog Clock (requires an SVG viewer)
  4. Scalable Vector Graphics (SVG) 1.1. Specification

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here