Turn the page !

by gleblanc 29. September 2008 19:24

!!!!!!! This page has been updated for Silverlight 2 RTM : see post of Nov 18th (http://www.gleblanc.eu/Blog/post/Page-turner-updated-for-Silverlight-2-TRM.aspx)  !!!!! 

In this post, we'll turn pages, thanks to an outstanding component developed and made freely available by Mitsu Furuta of Microsoft France.

Turn the pages using the mouse, as if it was a book ! There are five paintings of Brueghel (a painter of 16th century) to be seen.
 


Project / source code 

To add such a "page turner" into your Silverlight program, download the DLL that does the job. First, access the project on Codeplex. The component was developed by Mitsu Furuta of Microsoft France. Incidentally, Mitsu is blogging in French but also in English.

Download the zip file, unzip it, compile the project in the SLBookDemoApp directory and extract the SLMitsuControls.dll file that is created by the compiler in the ClientBin sub-directory.

The use of this component is under "Microsoft Public License" (MS-PL). I can read a computer book as if it was a novel but reading just a few paragraphs of lawyer's jargon always gives me headache. But here it's rather clear : you can use the component "as it is", even in commercial applications, royalty-free (I mean the component described here is royalty-free, not necessarily your application). Of course, you can't claim rights on this component... For me, it was obvious. Programmers and lawyers seem to have a brain somewhat different. 

In your application, add a reference to the DLL you have just extracted and saved somewhere. In the UserControl tag in the xaml file, add :
 xmlns:local="clr-namespace:SLMitsuControls;assembly=SLMitsuControls"
I chose local for future reference to the component but any name could be chosen.

Now, you need to add a component that has a visual appearence :
 <local:UCBook x:Name="book" ..... />
where ..... can be replaced by any property you usually find in a component (Grid.Row, Canvas.Left, etc.).

Now, let's see the changes that have to be made in your Page.xaml.cs file. Your Page class must implement the IDataProvider interface (and consequently implement GetItem and GetCount functions). Some initialization (one line) has also to be done in the function that handles the Loaded event :
 using SLMitsuControls;
 .....
 public partial class Page : UserControl,IDataProvider
 {
  .....
  private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
  {
   book.SetData(this);
  }
  public object GetItem(int index)
  {
   return pages.Items[index];
  }
  public int GetCount()
  {
   return pages.Items.Count;
  }
  .....
 }

The images (here five) have to be mentionned in Grid.Resources (or Canvas.Resources, depending on the container) :
 <Grid .... >
  <Grid.Resources>
  <ItemsControl x:Name="pages" >
   <Image Source="Brueghel1.jpg" Stretch="UniformToFill"  />
   <Image Source="Brueghel2.jpg" Stretch="UniformToFill"    />
   <Image Source="Brueghel3.jpg" Stretch="UniformToFill" />
   <Image Source="Brueghel4.jpg" Stretch="UniformToFill"   />
   <Image Source="Brueghel5.jpg" Stretch="UniformToFill"  />
  </ItemsControl>
 </Grid.Resources>
 ....
 </Grid>

The images (here 5 images, from Brueghel1.jpg to Brueghel5.jpg) need to be copied in the ClientBin directory (the directory of the xap file). This is often an overlooked step : adding the images in the project (as often done for images) doesn't help. The images need to be explicitly copied into the ClientBin directory. 

To know which pages are visible, just handle the OnPageTurned event :
 <local:UCBook x:Name="book" ..... OnPageTurned="book_OnPageTurned" />
 .....
 private void book_OnPageTurned(int leftPageIndex, int rightPageIndex)
 {
  .....
 }

The leftPageIndex and rightPageIndex arguments get the value -1 when a page is not displayed (for instance there is no page at the left when the book is initially opened).

A page is not restricted to images : it could contain a set of components. Just replace the <Image ..... /> tag by any other tag, for instance a grid tag (container for all the elements of a page).  

Tags:

Silverlight 2 RC0 and the programs presented here

by gleblanc 26. September 2008 22:22

In his blog, Scott Guthrie announces the availability of Silverlight 2 Release Candidate 0.

Please read carefully the terms, still more explicitely stated in Tim Heuer's blog. RC0 is for developers only (well, I guess my readers are), so they can prepare themselves to have their programs ready the day Silverlight 2 comes to the light : there are breaking changes and current Silverlight 2 beta 2 (as my programs currently are) will not run unchanged with Silverlight 2 (RC0 included).

As Tim states with insistance : applications developed using RC0 should not be deployed and there is no RC0 run-time available for download ! In other words, use RC0 just to update and test your own Silverlight 2 programs and only on your development machine.

Though I intend to document changes that will be necessary in my programs (very few, I hope), I am currently sticking to Silverlight 2 Beta 2. I will switch to SL2 RTM and update my programs (included the SL2 programs mentionned in my book) the day Silverlight 2 is officially out.

 Thanks for your comprehension.

Tags:

Rotating images

by gleblanc 26. September 2008 12:40

In this post, we will show how easy it is to implement spectacular rotating effects to images.


Project / source code 
First, we inserted five images (all 250x200) as resource into the project. The carrousel is a canvas made of two Image components and a TextBlock. All these three components will be animated : 
 <Canvas x:Name="Carrousel" Width="250" Height="200" Background="Black"
         Loaded="Carrousel_Loaded" >
  <Image x:Name="img1" Source="London.jpg" Width="250" Height="200" .... ></Image>
  <Image x:Name="img2" Source="Brussels.jpg" Width="250" Height="200" .... ></Image>
  <Canvas .... >
   <TextBlock x:Name="lCityName" ..... ></TextBlock>
  </Canvas> 
 </Canvas>

We apply a ScaleTransform to the two Image components :
 <Image x:Name="img1" Source="London.jpg" Width="250" Height="200" 
        RenderTransformOrigin="0.5,0.5"
        MouseEnter="img_MouseEnter" MouseLeave="img_MouseLeave" >
  <Image.RenderTransform>
   <ScaleTransform x:Name="scale1" CenterX="0" />
  </Image.RenderTransform>
 </Image>
 <Image x:Name="img2" Source="Brussels.jpg" Width="250" Height="200"
        MouseEnter="img_MouseEnter" MouseLeave="img_MouseLeave" >
  <Image.RenderTransform>
   <ScaleTransform x:Name="scale2" CenterX="250"/>
  </Image.RenderTransform>
 </Image>

The MouseEnter and MouseLeave events are intercepted just to pause and resume the animation.
We now define the 3-second animation (simultaneously, a ScaleTransform and an horizontal move on img1 and a ScaleTransform on img2) :
 <Grid x:Name="LayoutRoot" >
  <Grid.Resources>
   <Storyboard x:Name="stb" Completed="stb_Completed" >
    <DoubleAnimation Storyboard.TargetName="scale1" Storyboard.TargetProperty="ScaleX"
                     From="1" To="0" Duration="0:0:3" />
    <DoubleAnimation Storyboard.TargetName="img1" Storyboard.TargetProperty="(Canvas.Left)"
                     From="0" To="-125" Duration="0:0:3" />
    <DoubleAnimation Storyboard.TargetName="scale2" Storyboard.TargetProperty="ScaleX"
                     From="0" To="1" Duration="0:0:3" />
   </Storyboard>
  </Grid.Resources>
  .....
 </Grid>
The animation is fired in the Carrousel_Loaded function. In the stb_Completed function, we reload the img1 and img2 components with images (two images in succession in the ts array) and fire again the animation :
 using System.Windows.Media.Imaging;
 .....
 string[] ts = { "London", "Brussels", "Venice", "Moskow", "Amsterdam" };
 int N, nImg1, nImg2;
 private void stb_Completed(object sender, EventArgs e)
 {
  N++;
  nImg1 = N % ts.Length;
  nImg2 = (nImg1 + 1) % ts.Length;
  img1.Source = new BitmapImage(new Uri(ts[nImg1] + ".jpg", UriKind.Relative));
  img2.Source = new BitmapImage(new Uri(ts[nImg2] + ".jpg", UriKind.Relative));
  stb.Begin();
  .....
 }

It's all for images. Now, the animation on text. The lCityName TextBlock is inserted in a clipped canvas. The stbCityName animation just changes the Canvas.Left property, giving the right to left effect :
 <Canvas Canvas.Left="10" Canvas.Top="220" Width="500" Height="35"
         Clip="M0,0 h250 v35 h-250 z">
  <TextBlock x:Name="lCityName" Canvas.Left="260" Canvas.Top="0"
             FontSize="30" FontWeight="Bold" Foreground="Blue" Width="250" >
   <TextBlock.Resources>
    <Storyboard x:Name="stbCityName">
     <DoubleAnimation Storyboard.TargetName="lCityName"
                      Storyboard.TargetProperty="(Canvas.Left)" From="220" To="50"
                      Duration="0:0:2.5" BeginTime="0:0:0.5" />
    </Storyboard>
   </TextBlock.Resources>
  </TextBlock>
 </Canvas>

City names are also changed in the stb_Completed function, as we did for images :
 private void stb_Completed(object sender, EventArgs e)
 {
  .....
  lCityName.SetValue(Canvas.LeftProperty, 250.0); lCityName.Text = ts[nImg2];
  stbCityName.Begin();
 }

Instead of :
 <DoubleAnimation Storyboard.TargetName="scale1" Storyboard.TargetProperty="ScaleX" ..... />
it's possible to write :
 <DoubleAnimation Storyboard.TargetName="img1"
   Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
   ..... />


And now another rotating effect :


Project / source code
We first inserted five images (all 128x152) of outstanding scientists as resource into the project. The carrousel is made of an Image component and a TextBlock. These two elements will be animated :
 <Canvas Width="128" Height="152" >
  <Image x:Name="img" Source="Marie Curie.jpg" Width="128" Height="152" .... ></Image>
  <TextBlock x:Name="sName" .....></TextBlock>
 </Canvas>

The image is clipped to an ellipse and we apply a ScaleTransform to it :
 <Image x:Name="img" Source="Marie Curie.jpg" Width="128" Height="152"
        RenderTransformOrigin="0.5, 0.5">
  <Image.Clip>
   <EllipseGeometry Center="64,76" RadiusX="64" RadiusY="76" />
  </Image.Clip>
  <Image.RenderTransform>
   <ScaleTransform x:Name="imgScale"  />
  </Image.RenderTransform>
  .....
 </Image>
We define an animation for the Image :
 <Image x:Name="img" Source="Marie Curie.jpg" ..... >
  .....
  <Image.Resources>
   <Storyboard x:Name="stb" Completed="stb_Completed" >
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="imgScale"
                                   Storyboard.TargetProperty="ScaleX"  >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
     <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
     <LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </Image.Resources>
 </Image>
In the stb_Completed function, we load the img component with another image, whose names are found in the ts array :
 using System.Windows.Media.Imaging;
 .....
 string[] ts = { "Marie Curie", "Thomas Edison", "Alexander Fleming",
                 "Guglielmo Marconi", "Albert Einstein" };
 int N;
 private void stb_Completed(object sender, EventArgs e)
 {
  N++;
  int nImg = N % ts.Length;
  img.Source = new BitmapImage(new Uri(ts[nImg] + ".jpg", UriKind.Relative));
  stb.Begin();
 .....
 }
And now for the animation on names (similar, though applied to a TextBlock) :
 <TextBlock x:Name="sName" Width="128" Canvas.Left="0" Canvas.Top="180" TextAlignment="Center"
            RenderTransformOrigin="0.5, 0.5">
  <TextBlock.RenderTransform>
   <ScaleTransform x:Name="nameScale" />
  </TextBlock.RenderTransform>
  <TextBlock.Resources>
   <Storyboard x:Name="stbName">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="nameScale"
                                   Storyboard.TargetProperty="ScaleX">
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="0" />
     <LinearDoubleKeyFrame KeyTime="0:0:2" Value="1" />
     <LinearDoubleKeyFrame KeyTime="0:0:4" Value="0" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </TextBlock.Resources>
 </TextBlock>
In the stb_Completed function, we change names, as we did for images :
 private void stb_Completed(object sender, EventArgs e)
 {
  .....
  sName.Text = ts[nImg];
  stbName.Begin();
 }


In the next post, we will turn pages.

Tags:

Visual effects while displaying an image. Part III

by gleblanc 23. September 2008 17:12

In this post, we will animate the clipping area using the Silverlight animation technique (ie, no more the timer).
This time, the painting is "The station in the forest" from Paul Delvaux, a Belgian surrealist painter (1897-1994).


Project / source code
Again, as in part II, we display two (identical) images at the same location : the first one, in the background, with an opacity of 0.2 and the second one, on top, with a clipping area. The clipping area, representing a keyhole, is a closed figure (named pf) made of an arc segment and two line segments (since it's a closed figure, there is no need to specify the closing line segment) :
 <Image Source="Delvaux.jpg" Width="400" Height="270" Opacity="0.2" />
 <Image Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Clip>
   <PathGeometry >
    <PathFigure x:Name="pf" StartPoint="20,35" IsClosed="True" >
     <ArcSegment IsLargeArc="True" Point="40,35" Size="20,20" SweepDirection="Clockwise" />
     <LineSegment Point="45,75" />
     <LineSegment Point="15,75" />
    </PathFigure>
   </Image.Clip>
 </Image>

We'll move and scale the clipping area, ie the keyhole. To do so, we first define two transforms for the second image : a TranslateTranform (name keyholeXlate) and a ScaleTransform (named keyholeScale) :
 <Image Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Clip>
   <PathGeometry>
    <PathFigure ..... >
     .....
    </PathFigure>
    <PathGeometry.Transform>
     <TransformGroup>
      <TranslateTransform x:Name="keyholeXlate"/>
      <ScaleTransform x:Name="keyholeScale" />
     </TransformGroup>
    </PathGeometry.Transform>
   </PathGeometry>
  </Image.Clip>
 </Image>

We now define the animation. We have four elements to animate (not counting a last and dummy - though important - animation, whose purpose will be explained later on) :

  • we make the keyhole bigger and bigger : two ScaleTransform with ScaleX and ScaleY jumping from 1 to 5 in 15 seconds,
  • an horizontal move (TranslateTransform) in three steps (thus with four DoubleAnimationUsingKeyFrames) to sweep the painting in zigzag,
  • a vertical move (TranslateTransform).

<Grid x:Name="LayoutRoot" Background="White" Loaded="LayoutRoot_Loaded">
 <Grid.Resources>
  <Storyboard x:Name="stb" RepeatBehavior="Forever" >
   <DoubleAnimation Storyboard.TargetName="keyholeScale" Storyboard.TargetProperty="ScaleX"
                    From="1" To="5" Duration="0:0:15" />
   <DoubleAnimation Storyboard.TargetName="keyholeScale" Storyboard.TargetProperty="ScaleY"
                    From="1" To="5" Duration="0:0:15" />
   <DoubleAnimationUsingKeyFrames Storyboard.TargetName="keyholeXlate"
                                  Storyboard.TargetProperty="X" >
    <LinearDoubleKeyFrame KeyTime="0:0:0" Value="1" />
    <LinearDoubleKeyFrame KeyTime="0:0:5" Value="120" />
    <LinearDoubleKeyFrame KeyTime="0:0:10" Value="-17" />
    <LinearDoubleKeyFrame KeyTime="0:0:15" Value="50" />
   </DoubleAnimationUsingKeyFrames >
   <DoubleAnimation Storyboard.TargetName="keyholeXlate" Storyboard.TargetProperty="Y"
                    From="0" To="30" Duration="0:0:15"  />
   <PointAnimation Storyboard.TargetName="pf" Storyboard.TargetProperty="StartPoint"
                   From="20,35" To="20,35.001" Duration="0:0:15"  />
  </Storyboard>
 </Grid.Resources>
 .....
</Grid>

Why do we move the StartPoint vertically from 35 to 35.001 in 15 seconds ? Such an unnoticeable animation seems to be ridiculous, to say the least ! This line is just there to make things work ! There is a bug in beta 2 : without such a dummy move, nothing is ever redrawn and nothing happens !

Of course, we still need to execute stb.Begin() in the function handling the Loaded event.

Now, let's change things to have the keyhole fixed and the painting animated :


Project / source code
The image in front is a painting named "Castel in the Pyrrhénées" from René Magritte, another Belgian surrealist painter (1898-1967). A black keyhole was inserted in this masterpiece (shame on me). Using Microsoft Photo Editor, we force black as the transparent color and save the image in png format.
The two images (Delvaux in the background and Magritte in the foreground) are inserted in a canvas whose size is the size of the foreground image. The canvas is clipped to the foreground image :
 <Canvas Width="266" Height="460" Clip="M0,0 h266 v460 h-266 z" >
  <Image x:Name="img" Source="Delvaux.jpg" Width="400" Height="270" >
   .....
  </Image>
  <Image Source="Magritte.png" Width="266" Height="460" />
 </Canvas> 

We now define an animation for the background image, in four step : from left to right, from top to bottom, from right to left and finally from bottom to top :
 <Image x:Name="img" Source="Delvaux.jpg" Width="400" Height="270" >
  <Image.Resources>
   <Storyboard x:Name="stb" RepeatBehavior="Forever">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="img" 
        Storyboard.TargetProperty="(Canvas.Left)" >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:5" Value="-210" />
     <LinearDoubleKeyFrame KeyTime="0:0:7" Value="-210" />
     <LinearDoubleKeyFrame KeyTime="0:0:12" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:14" Value="105" />
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="img"
       Storyboard.TargetProperty="(Canvas.Top)" >
     <LinearDoubleKeyFrame KeyTime="0:0:0" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:5" Value="105" />
     <LinearDoubleKeyFrame KeyTime="0:0:7" Value="10" />
     <LinearDoubleKeyFrame KeyTime="0:0:12" Value="10" />
     <LinearDoubleKeyFrame KeyTime="0:0:14" Value="105" />
    </DoubleAnimationUsingKeyFrames>
   </Storyboard>
  </Image.Resources>
 </Image>

See you for a forthcoming post on visual effects.

Tags:

Visual effects while displaying an image. Part II

by gleblanc 21. September 2008 18:13

In this second (but not last) post, we will discuss different techniques for animating the clipping area.
The image is a painting ("The persistence of memory") from Salvador Dali (1904-1989), the famous Spanish (Catalan) surrealist painter.

In this post, we'll animate the clipping area using an old animation technique, ie the timer.


Project / source code

We have two identical images : one, in the background, with an opacity of 0.3 and the other, on top, with a clipping area limited to a growing ellipse that will appear (at random location) at regular interval :
 <Image Source="Dali.jpg" Width="500" Height="363" Opacity="0.3" />
 <Image Source="Dali.jpg" Width="500" Height="363" >
  <Image.Clip>
   <EllipseGeometry x:Name="ellGeo" />
  </Image.Clip>
 </Image>

The animation is here based on the timer, a technique used by veterans of animations in computer programs. Though animation techniques introduced in WPF and Silverlight have to be privileged (see next post), the technique based on the timer is still there (do not throw it away too fast). To use the timer :
 using System.Windows.Threading;
 .....
 DispatcherTimer timer;  // outside of function

In the function that handles the Loaded event :
 private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
 {
  timer = new DispatcherTimer();
  timer.Interval = new TimeSpan(0, 0, 0, 0, 50); // 50 millisec
  timer.Tick += new EventHandler(timer_Tick);
  timer.Start();
 }
The following function is executed at regular interval :   
  void timer_Tick(object sender, EventArgs e)
  {
   .....  
  }

Our timer_Tick function is simple :

  • we reinitialize the EllipseGeometry properties every 100 ticks : new Center (at random location) and reduced radius (also a random small value), 
  • we make this ellipse bigger every tick.

  Random rndm;
  .....
  rndm = new Random();
  .....
  int N;
  void timer_Tick(object sender, EventArgs e)
  {
   if (N % 100==0)
   {
    X = rndm.Next(500); Y = rndm.Next(380);
    W = rndm.Next(10);
    ellGeo.Center = new Point(X, Y);
   }
   W += 5; ellGeo.RadiusX = ellGeo.RadiusY = W;
   N++;
  }

Now, let's go a step further : the clipping area is now made from several basic figures, here several growing ellipses, that are dynamically added to the clipping area :


Project / source code

Since the clipping area is build dynamically (in the timer_Tick function), the XAML is just :
  <Image Source="Dali.jpg" Width="500" Height="363" Opacity="0.3" />
  <Image Source="Dali.jpg" Width="500" Height="363" >
   <Image.Clip>
    <GeometryGroup x:Name="geoGroup" FillRule="Nonzero"  />
   </Image.Clip>
  </Image>

It's important to specify NonZero in the FillRule property. Otherwise, the intersection of two or several ellipses is not part of the clipping area.

The timer_Tick function becomes :
   void timer_Tick(object sender, EventArgs e)
   {
    if (N % 100 == 0) geoGroup.Children.Clear(); // reset clipping area
    if (N % 2 == 0)
    {
     // create a new hole in the clipping area
     EllipseGeometry eg = new EllipseGeometry();
     eg.Center = new Point(rndm.Next(0, 500), rndm.Next(0, 360)); 
     eg.RadiusX = eg.RadiusY = rndm.Next(5, 20);
     geoGroup.Children.Add(eg);
    }
    // grow and move slightly each ellipse
    foreach (EllipseGeometry g in geoGroup.Children)
    {
     g.RadiusX += 2; g.RadiusY += 2;
     g.Center = new Point(g.Center.X, g.Center.Y - 1);
    }
    N++;
   }

In the next post, we will use the Silverlight animation technique to animate the clipping area.

Tags:

Visual effects while displaying an image. Part I

by gleblanc 17. September 2008 13:45

Let's review different visual effects while displaying an image, from simple to more sophisticated. In this serie, we'll discuss animation, clipping area and opacity mask. Here, a first post dedicated to clipping. 

The image used in this post is a painting from Alfred Sisley (1839-1899), a French impressionnist painter, though British citizen (he asked for French citizenship but died before the Administration took a decision, due to missing papers - nothing is never new in this world).


Project / source code

The effect is based on a clipping area. Nothing is displayed outside the clipping area, here an ellipse. We give a name (ellGeo) to this ellipse and animate the RadiusX and RadiusY properties. When stb1.Begin() is executed, RadiusX jumps from 0 to 400 in two seconds. Simultaneously, RadiusY jumps from 0 to 300 (to be sure that the 400x300 rectangle is fully inside the ellipse).

 <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
  <Image.Clip>
   <EllipseGeometry x:Name="ellGeo" Center="200, 150" RadiusX="400" RadiusY="300" />
  </Image.Clip>
  <Image.Resources>
   <Storyboard x:Name="stb1">
    <DoubleAnimation Storyboard.TargetName="ellGeo" Storyboard.TargetProperty="RadiusX" 
                     From="0" To="400" Duration="0:0:2" />
    <DoubleAnimation Storyboard.TargetName="ellGeo" Storyboard.TargetProperty="RadiusY" 
                     From="0" To="300" Duration="0:0:2" />
   </Storyboard>
  </Image.Resources>
 </Image>

Instead of giving a name to EllipseGeometry, we could write :

 <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
  <Image.Clip>
   <EllipseGeometry Center="200, 150" RadiusX="400" RadiusY="300" />
  </Image.Clip>
  <Image.Resources>
   <Storyboard x:Name="stb1">
    <DoubleAnimation Storyboard.TargetName="img"
                     Storyboard.TargetProperty="(UIElement.Clip).(EllipseGeometry.RadiusX)"
                     From="0" To="400" Duration="0:0:2" />
    <DoubleAnimation Storyboard.TargetName="img"
                     Storyboard.TargetProperty="(UIElement.Clip).(EllipseGeometry.RadiusY)"
                     From="0" To="300" Duration="0:0:2" />      
   </Storyboard>
  </Image.Resources>
 </Image> 

Fine for the EllipseGeometry, thanks to its two properties (RadiusX and RadiusY) that are of double type. Not so easy with RectangleGeometry that has a property (Rect) with four values ! It's easier to use the PathGeometry.


Project / source code

As PathGeometry, we define one PathFigure (closed figure starting at upper-left corner) and made of three line segments (remember, it's a closed figure). Each line ending point is specified in the Point property. of course of type Point. Our animation will now be based on PointAnimation :    

  <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
   <Image.Clip>
    <PathGeometry>
      <PathFigure StartPoint="0,0">
       <LineSegment x:Name="ls1" Point="400, 0" />
       <LineSegment x:Name="ls2" Point="400, 300" />
       <LineSegment Point="0, 300" />
      </PathFigure>
    </PathGeometry>
   </Image.Clip>
   <Image.Resources>
    <Storyboard x:Name="stb2">
     <PointAnimation Storyboard.TargetName="ls1" Storyboard.TargetProperty="Point"
                     From="0, 0" To="400, 0" Duration="0:0:2" />
     <PointAnimation Storyboard.TargetName="ls2" Storyboard.TargetProperty="Point"
                     From="0, 300" To="400, 300" Duration="0:0:2" />
    </Storyboard>
   </Image.Resources>
  </Image>

Since a path can be made of a succession of paths, we can do more complex animations. For instance :


Project / source code

The clipping path is made of five triangles, all having in common the image's center :
The five clipping areas
The animation is now made of five animations in succession, each elementary triangle being drawn in one second. The initial Point values in the lsxB LineSegment are such that the image is initially invisible. The values are changed in the corresponding PointAnimation :

  <Image x:Name="img" Source="Sisley.jpg" Width="400" Height="300" >
   <Image.Clip>
    <PathGeometry>
     <PathGeometry.Figures>
      <PathFigure StartPoint="200,150">
       <LineSegment x:Name="ls1A" Point="200, 0" />
       <LineSegment x:Name="ls1B" Point="200, 0" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls2A" Point="400, 0" />
       <LineSegment x:Name="ls2B" Point="400, 0" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls3A" Point="400, 300" />
       <LineSegment x:Name="ls3B" Point="400, 300" />
      </PathFigure>
       <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls4A" Point="0, 300" />
      <LineSegment x:Name="ls4B" Point="0, 300" />
      </PathFigure>
      <PathFigure StartPoint="200,150" >
       <LineSegment x:Name="ls5A" Point="0, 0" />
       <LineSegment x:Name="ls5B" Point="0, 0" />
      </PathFigure>
     </PathGeometry.Figures>
    </PathGeometry>
   </Image.Clip>
   <Image.Resources>
    <Storyboard x:Name="stb3">
     <PointAnimation Storyboard.TargetName="ls1B" Storyboard.TargetProperty="Point"
                     From="200, 0" To="400, 0" Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:1" Storyboard.TargetName="ls2B"
                     Storyboard.TargetProperty="Point" From="400, 0" To="400, 300"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:2" Storyboard.TargetName="ls3B"
                     Storyboard.TargetProperty="Point" From="400, 300" To="0, 300"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="ls4B"
                     Storyboard.TargetProperty="Point" From="0, 300" To="0, 0"
                     Duration="0:0:1" />
     <PointAnimation BeginTime="0:0:4" Storyboard.TargetName="ls5B"
                     Storyboard.TargetProperty="Point" From="0, 0" To="200, 0" 
                     Duration="0:0:1" />
    </Storyboard>
   </Image.Resources>
  </Image>

To restart an animation (just before executing st3.Begin), we need to reinitialize the clipping path, to have the image disappearing :
 ls1A.Point = ls1B.Point = new Point(200, 0);
 ls2A.Point = ls2B.Point = new Point(400, 0);
 ls3A.Point = ls3B.Point = new Point(400, 300);
 ls4A.Point = ls4B.Point = new Point(0, 300);
 ls5A.Point = ls5B.Point = new Point(0, 0);

Instead of writing 
 <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="ls4B"
                 Storyboard.TargetProperty="Point" From="0, 300" To="0, 0"
                 Duration="0:0:1" /> >

it's possible to write (remember, it's the fourth PointAnimation and we animate the second LineSegment) :
 <PointAnimation BeginTime="0:0:3" Storyboard.TargetName="img"
                 Storyboard.TargetProperty="(UIElement.Clip)
                                            .(PathGeometry.Figures)[3]
                                            .(PathFigure.Segments)[1]
                                            .(LineSegment.Point)"
                 From="0, 300" To="0, 0" Duration="0:0:1" />

As said Lewis Carroll in "Alice in Wonderland" : "I could have thought of a much more complicated way of doing it, said the Red Queen, immensely proud". 

In a forthcoming post, we will show more sophisticated animations, still based on the clipping area.

 

Tags:

Simultaneous and dynamic animations

by gleblanc 15. September 2008 10:47
In this post, we'll show how to perform several animations at the same time. To illustrate this, we will implement a carrousel with images that remain permanently vertical :

Project / source code

So, let's write the Silverlight application. We'll write it as if we didn't know the number of images at time of writing / compiling the program. Due to that constraint, adding images and forcing animations has to be done dynamically, by program, at run-time. I know the program presented here is not be 100% independant of number of images (since images are inserted as resource at development time) but we accept such a restriction to keep things simple. We are discussing here animations, not remote access to images.

We are changing some dimensions (to take care of my BlogEngine restrictions) and put an image (SilverlightLogo.jpg) in the background of our main container (a grid named LayoutRoot as usual). Nothing very special yet. 

<UserControl x:Class="Anim1.Page"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="500" Height="500">
    <Grid x:Name="LayoutRoot" >
     <Grid.Background>
      <ImageBrush ImageSource="SilverlightLogo.jpg" Stretch="Uniform" />
     </Grid.Background>
 
    </Grid>
</UserControl> 

We insert a transparent canvas in front of our main LayoutRoot container. Since this canvas will be in rotation, we name it RotatingSurface. Inside this canvas, we insert a rail. Images will be fixed on this rail. We also decide to react to the Loaded event, in order to initialize things. 

  <Grid x:Name="LayoutRoot" >
    .....
    <Canvas x:Name="RotatingSurface" Width="400" Height="400"
            Background="Transparent" Loaded="RotatingSurface_Loaded" >
 
     <Ellipse x:Name="rail" Canvas.Left="50" Canvas.Top="50" Width="300"
              Height="300" Stroke="Gray" StrokeThickness="10" />
    </Canvas>
  </Grid>

We are here adding five images as resource (DaVinci, Newton, Mozart, Shakespeare and Einstein, all of same size, 90x110) but we code our program to be more general (doing so, adding more images will just require a change in the tabNames array).  

In Page.xaml.cs, we add (outside of functions) :

   string[] tabNames = {"DaVinci", "Shakespeare", "Newton", "Mozart", "Einstein" };
   Image[] tabImages;

In the RotateSurface_Loaded function :

  • we create an array of images (remember, to keep things simple, these images were inserted in the project and are thus automatically copied into the XAP file),
  • place them along the the rail (a bit of trigonometry, with Sin and Cos, is needed but nothing worth a Nobel prize),
  • finally add each of these images as children of the canvas.

  private void RotatingSurface_Loaded(object sender, RoutedEventArgs e)
  { 
   int N = tabNames.Length;                // number of images
   // get radius and position of rail
   double wRail = rail.ActualWidth / 2;    // width of rail
   double hRail = rail.ActualHeight / 2;   // height of rail
   double xRail = (double)rail.GetValue(Canvas.LeftProperty);
   double yRail = (double)rail.GetValue(Canvas.TopProperty); 
   double wImage = 90, hImage = 110;       // same size for all images 
   // create an array of images (N depending on tabNames array)
   tabImages = new Image[N];
   for (int i = 0; i < N; i++)
   {
    tabImages[i] = new Image();
    tabImages[i].Source=new BitmapImage(new Uri(tabNames[i]+".jpg",
                                                UriKind.Relative));
    tabImages[i].Width = wImage;
    tabImages[i].Height = hImage;

    // put image at the right place
    double X = wRail * Math.Cos(i * 2 * 3.14 / N) + xRail + wRail - wImage / 2;
    double Y = wRail * Math.Sin(i * 2 * 3.14 / N) + yRail + hRail - hImage / 2;
    tabImages[i].SetValue(Canvas.LeftProperty, X);
    tabImages[i].SetValue(Canvas.TopProperty, Y);

    RotatingSurface.Children.Add(tabImages[i]);
   }
  }

We need to add

 using System.Windows.Media.Imaging;

to make BitmapImage known by Visual Studio. 

At this point, images are displayed but the result is still very static (nothing in rotation yet) :

So, let's animate the canvas. We define for the RotatingSurface canvas :

  • the rotation point, at the center (coordinates are relative in RenderTransformOrigin),
  • a rotation transform named rotCanvas (the storyboard will animate its Angle property),
  • a storyboard (named stb) that animates Angle forever from 0 to 360° in 10 seconds :

  <Canvas x:Name="RotatingSurface" Width="400" Height="400" 
          Background="Transparent" Loaded="RotatingSurface_Loaded"
          RenderTransformOrigin="0.5, 0.5">
   <Canvas.RenderTransform>
    <RotateTransform x:Name="rotCanvas" />
   </Canvas.RenderTransform>
   <Canvas.Resources>
    <Storyboard x:Name="stb" RepeatBehavior="Forever">
     <DoubleAnimation Storyboard.TargetName="rotCanvas"
                      Storyboard.TargetProperty="Angle" From="0" To="360"
                      Duration="0:0:10" />
    </Storyboard>
   </Canvas.Resources>
   <Ellipse x:Name="rail" Canvas.Left="50" Canvas.Top="50"
            Width="300" Height="300" Stroke="Gray" StrokeThickness="10" />
  </Canvas>

As last instruction in the RotatingSurface_Loaded function, we start the stb storyboard :

 stb.Begin();

 

Here is the result of the current implementation. It doesn't give full satisfaction :

To keep images vertical, we need to add a rotation to each image : forever, from 0 to -360° in 10 seconds. Such a storyboard has to be added dynamically (remember, we don't know the number of images and these images were created dynamically).

To be able, later on, to start / pause these storyboards, we declare and initialize an array of N storyboards. For each image, we create by program :

  • a RotateTransform (we'll animate its Angle property),
  • a storyboard,
  • a DoubleAnimation that will animate Angle and that will pertain to the storyboard just created.

   Storyboard[] tabStoryboards; // outside of functions
   .....
   tabStoryboards = new Storyboard[N];
   for (int i = 0; i < N; i++)
   {
    .....       // create an Image in tabImages[i]
    RotateTransform rt = new RotateTransform();
    tabImages[i].RenderTransform = rt;
    tabImages[i].RenderTransformOrigin = new Point(0.5, 0.5);
    Storyboard st = new Storyboard();
    DoubleAnimation da = new DoubleAnimation();
    da.Duration = new Duration(new TimeSpan(0, 0, 10));
    da.To = -360;
    da.RepeatBehavior = RepeatBehavior.Forever;
   
    Storyboard.SetTargetProperty(da, new PropertyPath("Angle"));
    Storyboard.SetTarget(da, rt);
   
    st.Children.Add(da);
    tabStoryboards[i] = st;
    RotatingSurface.Children.Add(tabImages[i]);
   }


And now, to start the animation :

  stb.Begin();
  for (int i=0; i<N; i++) tabStoryboards[i].Begin(); 

You know the result, at the beginning of this post.

 



Tags:

"Gerard Leblanc on Silverlight", reloaded

by gleblanc 13. September 2008 23:51

A few months ago, I stopped my blog (Gerard Leblanc on Silverlight) to devote myself to my book "Silverlight 2", published in French by Eyrolles, Paris (see here).

This book is now in bookstores (for instance, see here or here). Incidently, my book's companion web site contains sample programs (see here) that could be instructive even if your French doesn't go further than "Liberté, égalité, fraternité" (in case you don't know, it's the national motto for France). These programs were written to be self-explanatory and illustrate one thing at a time. The C# and VB versions are always provided.
 


This blog will contain additional information. This time, English will be used most of the time since this language is, no doubt, the universal language for professional programmers.


I hope you will find this blog instructive.

 

Tags:

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Gerard Leblanc is the author of several books (in french) on C++, C#, .NET and Silverlight (Eyrolles, Paris as publisher). See www.gleblanc.eu as the companion web site for these books (included sample programs).
He is Microsoft MVP for Silverlight.

MVP logo