Switch On The Code RSS Button - Click to Subscribe
Oct
1

Silverlight Tutorial - Animating HTML Elements

Animation! And more animation! We sure do seem to love animation here at Switch On The Code - and really, I have no idea why. But as long as we keep coming up with cool concepts that deal with animation, we will continue to write about them. Today we are going to take a look at something a little bit weird - we are going to animate HTML elements using Silverlight. Thats right, you read that correctly - HTML elements, the stuff you would normally animate with some plain old javascript - perhaps even the Generic Animation javascript code. Why would you want to do this? I don't have any good reasons at the moment. But it does show off some of the capabilities of silverlight, and that is a good enough reason to write about it.

One thing to note before we get started - you will need the Silverlight 1.1 Alpha Refresh in order for the examples on this page to work. If you don't have it installed, you probably won't see any notification of that fact. This is because, unlike most silverlight apps, the silverlight on this page has no display - so there is no place for the usual "Get Silverlight" image and link to appear. So if the examples on this page fail to work, I would suggest checking out some of our other silverlight tutorials to make sure that your silverlight 1.1 install is working.


So below we have the classic animation example that we use here at Switch On The Code. This time, however, the "Go!" button is not hooked to a javascript function, it is hooked to an function in the silverlight code-behind. And if you set some values in the text boxes and click "Go!", silverlight will move and resize the red box to the values given in the given amount of time.


New X Pos: New Y Pos:
New Width: New Height:
Time (Millisec)

Done playing? If so, we have some code here you might be interested in. I'm not going to explain all the code in extreme detail, because parts of it (especially the math) are extremely similar or identical to the javascript code in the Generic Animation tutorial - so if you haven't read that tutorial, I highly suggest it. For the parts of this code that are different, rest assured, there will be in-depth explanation.

The collapsed version of the AnimationObject is very similar to the javascript version, but there are a couple of added variables and functions:

public class AnimationObject : IDisposable
{
  private const int WorkInterval = 10;
  private const int DrawInterval = 20;

  private List<AnimationFrame> _Frames =
      new List<AnimationFrame>();
  private AnimationFrame _CurrentData = null;
  private NoArgDelegate _Callback = null;
  private HtmlElement _Element = null;
  private HtmlTimer _UITimer = null;
  private Timer _WorkTimer = null;
  private long _LastTick = -1;
  private int _CurrentFI = 0;
  private int _Running = 0;
  private int _PrevDir = 0;

  public AnimationObject(HtmlElement element)

  public void AddFrame(AnimationFrame frame)

  public void SetCallback(NoArgDelegate cb)

  public void ClearFrames()

  public void ResetToStart()

  public void ResetToEnd()

  public void Stop()

  public void RunForward()

  public void RunBackward()

  private void animate(object o)

  private void UITimer_Tick(object sender, EventArgs e)

  private void CallbackHit(object sender, EventArgs e)

  public void Dispose()
}

So lets walk through this, explaining each function as we go. First we have the constructor, which takes in an HtmlElement and initializes the AnimationObject. The code for it looks like the following:

public AnimationObject(HtmlElement element)
{
  _Element = element;
 
  _WorkTimer = new Timer(new TimerCallback(animate),
      null, Timeout.Infinite, WorkInterval);

  _UITimer = new HtmlTimer();
  _UITimer.Enabled = false;
  _UITimer.Interval = DrawInterval;
  _UITimer.Tick += UITimer_Tick;

  ClearFrames();
}

So what is this _WorkTimer and _UITimer business? The javascript animation code did not have anything like this. Well, this is the equivalent way of doing a setTimeout in javascript, with an added twist. Because silverlight uses the .NET framework, we actually have access to multiple threads. So in this animation code, we take advantage of this fact by having two timers - the _WorkTimer and the _UITimer. The _WorkTimer does all the math and figures out the new position for the element, while the _UITimer applies that position to the element. The _UITimer runs on the main silverlight thread (sort of equivalent to the javascript setInterval function, while the _WorkTimer will run on a separate thread.

One thing to remember, only the main silverlight thread can update xaml or dom elements on the actual page. This means that if we tried to do any actual user interface work with the _WorkTimer, silverlight would throw an exception. Also, the _UITimer is an instance of HtmlTimer, which will probably be replaced with a better timer function (or the ability to invoke across threads) in the final Silverlight 1.1 release. But for now, it is the only way to update the user interface on any sort of interval.

We will talk about what these two timers are actually hooked to later on, when we get to those functions. Now on to the next function:

public void AddFrame(AnimationFrame frame)
{ _Frames.Add(frame); }

Almost identical to the AddFrame function in the javascript code. But now let's take a look at the AnimationFrame object, which has changed a little bit since the javascript version:

public class AnimationFrame
{
  public double Left = 0;
  public double Top = 0;
  public double Width = 0;
  public double Height = 0;
  public long Time = 0;

  public AnimationFrame()
  { }

  public AnimationFrame(double left, double top,
      double width, double height, long time)
  {
    Left = left;
    Top = top;
    Width = width;
    Height = height;
    Time = time;
  }

  public AnimationFrame(HtmlElement element, long time)
  {
    SetToElement(element);
    Time = time;     
  }

  public void SetToElement(HtmlElement e)
  {
    Left =
     int.Parse(e.GetStyleAttribute("left").Replace("px", ""));

    Top =
     int.Parse(e.GetStyleAttribute("top").Replace("px", ""));

    Width =
     int.Parse(e.GetStyleAttribute("width").Replace("px", ""));

    Height =
     int.Parse(e.GetStyleAttribute("height").Replace("px", ""));
  }

  public void Copy(AnimationFrame frame)
  {
    Left = frame.Left;
    Top = frame.Top;
    Width = frame.Width;
    Height = frame.Height;
    Time = frame.Time;
  }

  public void Apply(HtmlElement e)
  {
    e.SetStyleAttribute("left", Math.Round(Left) + "px");
    e.SetStyleAttribute("top", Math.Round(Top) + "px");
    e.SetStyleAttribute("width", Math.Round(Width) + "px");
    e.SetStyleAttribute("height", Math.Round(Height) + "px");
  }
}

The form is almost identical to the AnimationFrame that we had in the javascript code - but some of the content is slightly different due to how C# accesses html element properties, and C# syntactical differences. The code here also takes advantage of the possibility of multiple constructors - something you can't do in javascript without a good bit of difficulty. So the main difference is the added function SetToElement, which takes an html element and sets the values in the frame to the values from that element. Sadly, we have to a bit more work here in C# to get the values out (which is why I encapsulated it into a function). Unlike javascript, the integer parsing is very finicky - so we have to get rid of the 'px' that is at the end of the style properties.

Onto the next function, SetCallback, which is again almost identical to the version in the javascript code:

public void SetCallback(NoArgDelegate cb)
{ _Callback = cb; }

The only special thing here is the NoArgDelegate type, which needs to be defined somewhere in the code as a function that takes no arguments and returns nothing:

public delegate void NoArgDelegate();

Next function to look at is ClearFrames:

public void ClearFrames()
{
  if(_Running != 0)
    Stop();
  _Frames.Clear();
  _Frames.Add(new AnimationFrame(_Element, 0));
  _CurrentFI = 0;
  _PrevDir = 0;
  _CurrentData = new AnimationFrame();
}

Since our AnimationFrame object can now take in an HtmlElement as an argument to the constructor, this cleans up the code a little bit as compared to the javascript version. But other than that, they are identical.

The variables _Running, _CurrentFI, _PrevDir, and _CurrentData mean the same things as they do in the javascript animation code. Just as a refresher, for the _Running variable 0 means stopped, -1 means running backward, and 1 means running forward.

The next two functions are ResetToStart and ResetToEnd:

public void ResetToStart()
{
  if(_Running != 0)
    Stop();
  _CurrentFI = 0;
  _PrevDir = 0;
  _CurrentData.Copy(_Frames[0]);
  _CurrentData.Apply(_Element);
}

public void ResetToEnd()
{
  if(_Running != 0)
    Stop();
  _CurrentFI = 0;
  _PrevDir = 0;
  _CurrentData.Copy(_Frames[_Frames.Count - 1]);
  _CurrentData.Apply(_Element);
}

These two functions are again identical to the javascript code. I know, not particularly interesting. But don't worry, we will be getting to the interesting code soon.

Next we have the Stop function, which is a little different from the javascript animation version:

public void Stop()
{
  if (_Running == 0)
    return;
  _WorkTimer.Change(Timeout.Infinite, WorkInterval);
  _PrevDir = _Running;
  _Running = 0;
}

Here, the difference is that instead of clearing the javascript timeout, we have to stop the _WorkTimer. We do this by setting its "dueTime" (the amount of time until the next tick of the timer) to infinity. The _UITimer will actually stop on its own - we will see how that works later.

Now onto the meat of the code - we have the function RunForward next:

public void RunForward()
{
  if(_Running == 1)
    return;
  if(_Running == -1)
    Stop();
  if(_Frames.Count == 1 || _Element == null)
    return;

  _LastTick = Environment.TickCount;

  if(_PrevDir == 0)
  {
    _CurrentFI = 1;
    _CurrentData.SetToElement(_Element);
    _CurrentData.Time = 0;
    _Frames[0].Copy(_CurrentData);
  }
  else if(_PrevDir != 1)
  {
    _CurrentFI++;
    _CurrentData.Time =
      _Frames[_CurrentFI].Time - _CurrentData.Time;
  }

  _Running = 1;
  _WorkTimer.Change(0, WorkInterval);
  _UITimer.Enabled = true;
}

The flow of the code is identical to the javascript code, but there are a couple differences. First, to get the current time in ticks, we use Environment.TickCount (as opposed to new Date().getTime() in javascript). The fact that we have the new method SetToElement on the AnimationFrame object cleans up a little bit of the code in the middle. And finally, instead of just calling animate at the end of the function, we start the two timers. For the _WorkTimer we change its "dueTime" to 0, which means start firing immediately, and with the _UITimer, we just enable it.

The RunBackward function is next:

public void RunBackward()
{
  if (_Running == -1)
    return;
  if (_Running == 1)
    Stop();
  if (_Frames.Count == 1 || _Element == null)
    return;

  _LastTick = Environment.TickCount;

  if (_PrevDir == 0)
  {
    _CurrentFI = _Frames.Count - 2;
    _CurrentData.SetToElement(_Element);
    _CurrentData.Time = _Frames[_Frames.Count - 1].Time;
    _Frames[_Frames.Count - 1].Copy(_CurrentData);
    _CurrentData.Time = 0;
  }
  else if (_PrevDir != -1)
  {
    _CurrentData.Time =
      _Frames[_CurrentFI].Time - _CurrentData.Time;
    _CurrentFI--;
  }

  _Running = -1;
  _WorkTimer.Change(0, WorkInterval);
  _UITimer.Enabled = true;
}

Again, very similar to the javascript RunBackward function, except for the same changes that we made to the RunForward method.

We have finally reached the function where all the action happens - animate:

private void animate(object o)
{
  if (_Running == 0)
    return;
  long curTick = Environment.TickCount;
  long tickCount = curTick - _LastTick;
  _LastTick = curTick;

  long timeLeft =
    _Frames[((_Running == -1) ?
    _CurrentFI + 1 : _CurrentFI)].Time
    - _CurrentData.Time;

  while (timeLeft <= tickCount)
  {
    lock (_CurrentData)
    {
      _CurrentData.Copy(_Frames[_CurrentFI]);
      _CurrentData.Time = 0;
    }
    _CurrentFI += _Running;
    if (_CurrentFI>= _Frames.Count || _CurrentFI <0)
    {
      _UITimer.Tick += CallbackHit;
      _LastTick = -1;
      _Running = 0;
      _PrevDir = 0;
      return;
    }
    tickCount = tickCount - timeLeft;
    timeLeft = _Frames[((_Running == -1) ?
      _CurrentFI + 1 : _CurrentFI)].Time
      - _CurrentData.Time;
  }

  if (tickCount != 0)
  {
    tickCount += _CurrentData.Time;
    float ratio = ((float)tickCount)
      / _Frames[((_Running == -1) ?
      _CurrentFI + 1 : _CurrentFI)].Time;
    lock (_CurrentData)
    {
      _CurrentData.Time = tickCount;
      _CurrentData.Left =
        _Frames[_CurrentFI - _Running].Left
        + (_Frames[_CurrentFI].Left - _Frames[_CurrentFI
        - _Running].Left) * ratio;

      _CurrentData.Top =
        _Frames[_CurrentFI - _Running].Top
        + (_Frames[_CurrentFI].Top - _Frames[_CurrentFI
        - _Running].Top) * ratio;

      _CurrentData.Width =
        _Frames[_CurrentFI - _Running].Width
        + (_Frames[_CurrentFI].Width - _Frames[_CurrentFI
        - _Running].Width) * ratio;

      _CurrentData.Height =
        _Frames[_CurrentFI - _Running].Height
        + (_Frames[_CurrentFI].Height - _Frames[_CurrentFI
        - _Running].Height) * ratio;
    }
  }
}

The math and code flow again are identical to the javascript animate function - but there are some other significant differences. First, the animate function now takes an argument, although the argument is meaningless. This is because this function is attached to the _WorkTimer, and the Timer class expects the callback function to have that method signature. The next major change is that whenever we are writing to the _CurrentData variable, we surround it with a lock. This is because the _UITimer could tick at any point during this function - so we never want the _UITimer to try and read _CurrentData while the code in animate is writing to it.

This leads us into another difference - we never actually apply the results to the html element. This thread can't actually do that, we just calculate them and leave them in the _CurrentData variable. It is the job of the function attached to the _UITimer to actually update the element.

The other significant difference is how the callback function is called when the animation finishes. Again, since this function is running on a seperate thread, we don't want to call the callback directly. So instead, when the animation is complete, we attach the function CallbackHit to the _UITimer - which means that the next time the _UITimer ticks after that point, CallbackHit will be called and it will be running on the main silverlight thread (instead of the current timer thread).

Since we are talking about CallbackHit lets take a look at it:

private void CallbackHit(object sender, EventArgs e)
{
  _UITimer.Tick -= CallbackHit;
  if (_Callback != null)
    _Callback();
}

Pretty simple - as soon as it gets hit, it detaches itself from the _UITimer so it doesn't get hit again, and then calls the callback if one exists.

The final major function is the one always attached to the _UITimer - UITimer_Tick:

private void UITimer_Tick(object sender, EventArgs e)
{
  lock(_CurrentData)
    _CurrentData.Apply(_Element);
  if (_Running == 0)
    _UITimer.Enabled = false;
}

It is also pretty simple - it locks _CurrentData (so the animate function can't write to it), applies it to the element, and then in the animation is no longer running it turns off the timer.

And of course, since this is C#, we now have to worry about disposing things - in this case the timers. So we have the dispose function for when we are done with the animation object:

public void Dispose()
{
  if (_UITimer != null)
  {
    _UITimer.Tick -= UITimer_Tick;
    _UITimer = null;
  }

  if (_WorkTimer != null)
  {
    _WorkTimer.Dispose();
    _WorkTimer = null;
  }
}

And that is all the code! Below is it all together in one chunk, and then below that we go through how to actually use it:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Browser;
using System.Collections.Generic;
using System.Threading;

namespace GenericAnimation
{
  public delegate void NoArgDelegate();

  public class AnimationFrame
  {
    public double Left = 0;
    public double Top = 0;
    public double Width = 0;
    public double Height = 0;
    public long Time = 0;

    public AnimationFrame()
    { }

    public AnimationFrame(double left, double top,
        double width, double height, long time)
    {
      Left = left;
      Top = top;
      Width = width;
      Height = height;
      Time = time;
    }

    public AnimationFrame(HtmlElement element, long time)
    {
      SetToElement(element);
      Time = time;     
    }

    public void SetToElement(HtmlElement element)
    {
      Left = int.Parse(
        element.GetStyleAttribute("left").Replace("px", ""));

      Top = int.Parse(
        element.GetStyleAttribute("top").Replace("px", ""));

      Width = int.Parse(
        element.GetStyleAttribute("width").Replace("px", ""));

      Height = int.Parse(
        element.GetStyleAttribute("height").Replace("px", ""));
    }

    public void Copy(AnimationFrame frame)
    {
      Left = frame.Left;
      Top = frame.Top;
      Width = frame.Width;
      Height = frame.Height;
      Time = frame.Time;
    }

    public void Apply(HtmlElement element)
    {
      element.SetStyleAttribute("left",
        Math.Round(Left) + "px");

      element.SetStyleAttribute("top",
        Math.Round(Top) + "px");

      element.SetStyleAttribute("width",
        Math.Round(Width) + "px");

      element.SetStyleAttribute("height",
        Math.Round(Height) + "px");
    }
  }

  public class AnimationObject : IDisposable
  {
    private const int WorkInterval = 10;
    private const int DrawInterval = 20;

    private List<AnimationFrame> _Frames =
      new List<AnimationFrame>();
    private AnimationFrame _CurrentData = null;
    private NoArgDelegate _Callback = null;
    private HtmlElement _Element = null;
    private HtmlTimer _UITimer = null;
    private Timer _WorkTimer = null;
    private long _LastTick = -1;
    private int _CurrentFI = 0;
    private int _Running = 0;
    private int _PrevDir = 0;

    public AnimationObject(HtmlElement element)
    {
      _Element = element;
     
      _WorkTimer = new Timer(new TimerCallback(animate),
        null, Timeout.Infinite, WorkInterval);

      _UITimer = new HtmlTimer();
      _UITimer.Enabled = false;
      _UITimer.Interval = DrawInterval;
      _UITimer.Tick += UITimer_Tick;

      ClearFrames();
    }

    public void AddFrame(AnimationFrame frame)
    { _Frames.Add(frame); }

    public void SetCallback(NoArgDelegate cb)
    { _Callback = cb; }

    public void ClearFrames()
    {
      if(_Running != 0)
        Stop();
      _Frames.Clear();
      _Frames.Add(new AnimationFrame(_Element, 0));
      _CurrentFI = 0;
      _PrevDir = 0;
      _CurrentData = new AnimationFrame();
    }

    public void ResetToStart()
    {
      if(_Running != 0)
        Stop();
      _CurrentFI = 0;
      _PrevDir = 0;
      _CurrentData.Copy(_Frames[0]);
      _CurrentData.Apply(_Element);
    }

    public void ResetToEnd()
    {
      if(_Running != 0)
        Stop();
      _CurrentFI = 0;
      _PrevDir = 0;
      _CurrentData.Copy(_Frames[_Frames.Count - 1]);
      _CurrentData.Apply(_Element);
    }

    public void Stop()
    {
      if (_Running == 0)
        return;
      _WorkTimer.Change(Timeout.Infinite, WorkInterval);
      _PrevDir = _Running;
      _Running = 0;
    }

    public void RunForward()
    {
      if(_Running == 1)
        return;
      if(_Running == -1)
        Stop();
      if(_Frames.Count == 1 || _Element == null)
        return;

      _LastTick = Environment.TickCount;

      if(_PrevDir == 0)
      {
        _CurrentFI = 1;
        _CurrentData.SetToElement(_Element);
        _CurrentData.Time = 0;
        _Frames[0].Copy(_CurrentData);
      }
      else if(_PrevDir != 1)
      {
        _CurrentFI++;
        _CurrentData.Time =
          _Frames[_CurrentFI].Time - _CurrentData.Time;
      }

      _Running = 1;
      _WorkTimer.Change(0, WorkInterval);
      _UITimer.Enabled = true;
    }

    public void RunBackward()
    {
      if (_Running == -1)
        return;
      if (_Running == 1)
        Stop();
      if (_Frames.Count == 1 || _Element == null)
        return;

      _LastTick = Environment.TickCount;

      if (_PrevDir == 0)
      {
        _CurrentFI = _Frames.Count - 2;
        _CurrentData.SetToElement(_Element);
        _CurrentData.Time = _Frames[_Frames.Count - 1].Time;
        _Frames[_Frames.Count - 1].Copy(_CurrentData);
        _CurrentData.Time = 0;
      }
      else if (_PrevDir != -1)
      {
        _CurrentData.Time =
          _Frames[_CurrentFI].Time - _CurrentData.Time;
        _CurrentFI--;
      }

      _Running = -1;
      _WorkTimer.Change(0, WorkInterval);
      _UITimer.Enabled = true;
    }

    private void animate(object o)
    {
      if (_Running == 0)
        return;
      long curTick = Environment.TickCount;
      long tickCount = curTick - _LastTick;
      _LastTick = curTick;

      long timeLeft =
        _Frames[((_Running == -1) ?
        _CurrentFI + 1 : _CurrentFI)].Time
        - _CurrentData.Time;

      while (timeLeft <= tickCount)
      {
        lock (_CurrentData)
        {
          _CurrentData.Copy(_Frames[_CurrentFI]);
          _CurrentData.Time = 0;
        }
        _CurrentFI += _Running;
        if (_CurrentFI>= _Frames.Count || _CurrentFI <0)
        {
          _UITimer.Tick += CallbackHit;
          _LastTick = -1;
          _Running = 0;
          _PrevDir = 0;
          return;
        }
        tickCount = tickCount - timeLeft;
        timeLeft = _Frames[((_Running == -1) ?
          _CurrentFI + 1 : _CurrentFI)].Time
          - _CurrentData.Time;
      }

      if (tickCount != 0)
      {
        tickCount += _CurrentData.Time;
        float ratio = ((float)tickCount)
          / _Frames[((_Running == -1) ?
         _CurrentFI + 1 : _CurrentFI)].Time;
        lock (_CurrentData)
        {
          _CurrentData.Time = tickCount;
          _CurrentData.Left =
            _Frames[_CurrentFI - _Running].Left
            + (_Frames[_CurrentFI].Left - _Frames[_CurrentFI
            - _Running].Left) * ratio;

          _CurrentData.Top =
            _Frames[_CurrentFI - _Running].Top
            + (_Frames[_CurrentFI].Top - _Frames[_CurrentFI
            - _Running].Top) * ratio;

          _CurrentData.Width =
          _Frames[_CurrentFI - _Running].Width
            + (_Frames[_CurrentFI].Width - _Frames[_CurrentFI
            - _Running].Width) * ratio;

          _CurrentData.Height =
            _Frames[_CurrentFI - _Running].Height
            + (_Frames[_CurrentFI].Height - _Frames[_CurrentFI
            - _Running].Height) * ratio;
        }
      }
    }

    private void UITimer_Tick(object sender, EventArgs e)
    {
      lock(_CurrentData)
        _CurrentData.Apply(_Element);
      if (_Running == 0)
        _UITimer.Enabled = false;
    }

    private void CallbackHit(object sender, EventArgs e)
    {
      _UITimer.Tick -= CallbackHit;
      if (_Callback != null)
        _Callback();
    } 

    public void Dispose()
    {
      if (_UITimer != null)
      {
        _UITimer.Tick -= UITimer_Tick;
        _UITimer = null;
      }

      if (_WorkTimer != null)
      {
        _WorkTimer.Dispose();
        _WorkTimer = null;
      }
    }
  }
}

As usual, here is our fun animation example, the same one we used at the end of the javascript animation tutorial.




So how do we actually get this in silverlight with the code we just created? Well, lets take a look:

AnimationObject example2 = null;

private void makeAnimation()
{
  example2 = new AnimationObject(HtmlPage.Document.GetElementByID("ex2Box"));
  example2.AddFrame(new AnimationFrame(486, 10, 35, 25, 2000));
  example2.AddFrame(new AnimationFrame(496, 10, 25, 35, 100));
  example2.AddFrame(new AnimationFrame(496, 205, 25, 35, 1000));
  example2.AddFrame(new AnimationFrame(486, 215, 35, 25, 100));
  example2.AddFrame(new AnimationFrame(10, 215, 35, 25, 2000));
  example2.AddFrame(new AnimationFrame(10, 205, 25, 35, 100));
  example2.AddFrame(new AnimationFrame(10, 10, 25, 35, 1000));
  example2.AddFrame(new AnimationFrame(10, 10, 35, 25, 100));
}

private void AroundAndAround(object o, EventArgs e)
{
  if (example2 == null)
    makeAnimation();
  example2.SetCallback(example2.RunForward);
  example2.RunForward();
}

private void StopEx2(object o, EventArgs e)
{
  if (example2 == null)
    makeAnimation();
  example2.Stop();
}

private void BackEx2(object o, EventArgs e)
{
  if (example2 == null)
    makeAnimation();
  example2.SetCallback(example2.RunBackward);
  example2.RunBackward();
}

private void ResetEx2(object o, EventArgs e)
{
  if (example2 == null)
    makeAnimation();
  example2.ResetToStart();
}

Again, almost exactly the same as the javascript, with the exception of the syntax. Even setting up the callbacks so that the animation runs forever is the same.

Hope you enjoyed learning about how to do html element animation using silverlight! Look for an article soon with some benchmarks on which method performs better (javascript or silverlight), as well as some other performance comparisons between silverlight and just plain old javascript.



Posted in Silverlight, All Tutorials by The Tallest |

3 Responses

  1. Jason Grunstra Says:

    Slick idea! Very clever. :)

  2. Troy Says:

    This was very cool! Thanks.

  3. Khoa Says:

    Excellent! This article will help me very much. Thanks