Macca Blog

My life experiences with code and design

Sprite animations using WPF/Silverlight and JavaFX

Posted on by Mark

Usually when you begin to write any sort of application that requires visual flare, animations are always on the cards.

Since WPF and JavaFX (and Apple’s Cocoa Touch) use a similar animation framework of a declarative nature, meaning, that the developer defines elements at time steps and then the framework works out the rest, it becomes “less intuitive” to find out how to render an image sprite (PNG for example) animation.

This blog post will provide some idea of how to implement a sprite based animation using WPF and JavaFX.

JavaFX

Here is what a basic sprite based animation class looks like in JavaFX

/*
 * SpriteAnimator.fx
 */

package javafxsprites;
import javafx.scene.CustomNode;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

/**
 * @author Mark Macumber
 */

public class SpriteAnimator extends CustomNode {
    var _currentFrame:Integer = 1;

    public var xPos:Number = 0.0;
    public var yPos:Number = 0.0;
    public var FPS:Integer = 30;
    
    var tl = Timeline {
	repeatCount: Timeline.INDEFINITE
	keyFrames : [
		KeyFrame {
			time : bind Duration.valueOf(1000/FPS)
            action: function() {
                _currentFrame++;
                if (_currentFrame > 11)
                _currentFrame = 1;
            }
		}
    ]
    };

    override protected function create () : Node {
        var img = ImageView {
            x: bind xPos,
            y: bind yPos,
            image: bind
                Image { url: "{__DIR__}sprites/{_currentFrame}.png"; }
        };
        tl.play();
        return img;
    }
}

Inside your sprites directory, just add 0.png, 1.png, etc… all the way through to the 11.png (or however many frames you have)

This is a pretty simple implementation, which essentially just updates the image property of the ImageView that is returned as part of the create() method.

Here is how you might use the class:

package javafxsprites;
import javafx.stage.Stage;
import javafx.scene.Scene;

/**
 * @author Mark Macumber
 */

Stage {
    title: "JavaFX and Sprites"
    scene: Scene {
        width: 400
        height: 250
        content: [
            SpriteAnimator{ xPos: 150, yPos: 40, FPS: 15, NumberOfFrames: 10 }
        ]
    }
}

Pretty Simple huh? :)

You might be wondering why I bothered to create a custom class called SpriteAnimator, instead of just using an ImageView which I update. The answer is really simple, “re-use and encapsulation”, you can make this class flexible, rich, and re-usable very easily.

WPF/Silverlight

The following class has been ripped from a project that I was working on, so it is quite involved and very feature rich, and hence, very large :)

Basically it allows you to create a custom UserControl which allows for sprite animations based on file-system images, in memory-byte array images or pre-built up Lists of Image classes.

The reason for this was for performance reasons, sometimes for larger animations in certain areas of the application where lots was going on, the byte[] animations performed better that the filesystem images. Also things like file size of the images, etc… all attributed to the performance improvements/decreases.

Usage

The usage of this class is simple, you create an Image on your page, then pass the Source property of that Image class into the constructor of this class. Then call start() and your image source will be updated using pretty simple dependency property techniques.

Some of the other features of this class are as follows:

  • Create a BitmapImage from a byte array (if, say, you read in an image from the file system or a stream into a byte[]) via the static CreateImageFromBytes method
  • Pause/Unpause the animation
  • Stop/Start the animation
  • Revers the animation
  • Change the framerate
  • Seek within the animation, this is useful is you have an animation that is time based

Warning:

This class was ripped and quickly cleaned up for this blog post, if there are any issues with it, they should be just minor issues, please let me know if you come across any issues and I will do my best to fix it up.

I do NOT provide any guarantees with this class, please use it at your own risk (I know, typical disclaimer)


using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Media;

namespace MarkMacumber.Helpers
{
//Interface for any clean-ups required
public interface ICleanupFilmstrip
{
void AfterCompleted();
}

public class FilmstripAnimator : UserControl
{
private enum RunMode
{
///

/// Run with pre loaded bitmap images
///

Images,

///

/// Run with bitmap images loaded on the fly from the file system
///

FileSystem,

///

/// Using byte arrays for the image source
///

Bytes
}

private static readonly DependencyProperty CurrentFrameProperty = DependencyProperty.Register(“CurrentFrame”, typeof(int), typeof(FilmstripAnimator), new PropertyMetadata(0, OnCurrentFrameChanged));
public static readonly DependencyProperty FrameRateProperty = DependencyProperty.Register(“FrameRate”, typeof(int), typeof(FilmstripAnimator), new PropertyMetadata(30));

//different ways of accessing image data, file system, in memory bitmaps, in memory bytes
private readonly List _filmstripImages;
private readonly List _filmstripBytes;
private readonly List _filmstripImagePaths;

private Storyboard _filmstripStoryboard;

private readonly RunMode _mode = RunMode.FileSystem;

//view components
public Image FilmstripImageHost;
internal ImageBrush FilmstripImageBrushHost;

//clean up tasks
public ICleanUpFilmstrip CleanUp { get; set; }

private static BitmapImage CreateImageFromBytes(byte[] filmstripBytes)
{
var memoryStream = new MemoryStream(filmstripBytes);

var imageSource = new BitmapImage();
imageSource.BeginInit();
imageSource.CacheOption = BitmapCacheOption.None;
imageSource.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
imageSource.StreamSource = memoryStream;
imageSource.EndInit();
return imageSource;
}

public Filmstrip(List filmstripBytes, ImageBrush imageToUpdate)
{
_mode = RunMode.Bytes;
_filmstripBytes = filmstripBytes;
FilmstripImageBrushHost = imageToUpdate;

FilmstripImageBrushHost.ImageSource = CreateImageFromBytes(_filmstripBytes[0]);
CreateStoryboard();
}

public Filmstrip(List filmstripImages, ImageBrush imageToUpdate)
{
_mode = RunMode.Images;
_filmstripImages = filmstripImages;
FilmstripImageHost = new Image { Stretch = Stretch.Fill };
FilmstripImageBrushHost = imageToUpdate;
FilmstripImageBrushHost.ImageSource = filmstripImages[0];

CreateStoryboard();
}

public Filmstrip(List filmstripImagePaths)
{
FilmstripImageHost = new Image { Stretch = Stretch.Fill };
_filmstripImagePaths = filmstripImagePaths;
Content = FilmstripImageHost;
CreateStoryboard();
}

public Filmstrip(List filmstripImages)
{
_mode = RunMode.Images;

FilmstripImageHost = new Image { Stretch = Stretch.Fill };
_filmstripImages = filmstripImages;
Content = FilmstripImageHost;
CreateStoryboard();
}

private static void OnCurrentFrameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var filmstrip = (Filmstrip)d;
if (filmstrip.FrameCount <= 0) return; //If we are using an ImageBrush then use the in memory bitmaps if (filmstrip.FilmstripImageBrushHost != null) { if (filmstrip._mode == RunMode.Images) { filmstrip.FilmstripImageBrushHost.ImageSource = null; BitmapImage newSource = filmstrip._filmstripImages[filmstrip.CurrentFrame]; filmstrip.FilmstripImageBrushHost.ImageSource = newSource; } else { filmstrip.FilmstripImageBrushHost.ImageSource = null; BitmapImage newByteImage = CreateImageFromBytes(filmstrip._filmstripBytes[filmstrip.CurrentFrame]); filmstrip.FilmstripImageBrushHost.ImageSource = newByteImage; } } //If we are NOT using the ImageBrush but we still want to use images from memory else if (filmstrip._filmstripImages != null) { filmstrip.FilmstripImageHost.Source = null; BitmapImage newImageSource = filmstrip._filmstripImages[filmstrip.CurrentFrame]; filmstrip.FilmstripImageHost.Source = newImageSource; } //Otherwise, use the images from the filesystem, which typically are for larger animations else { BitmapImage source = new BitmapImage(new Uri(filmstrip._filmstripImagePaths[filmstrip.CurrentFrame])); RenderOptions.SetBitmapScalingMode(source, BitmapScalingMode.LowQuality); filmstrip.FilmstripImageHost.Source = null; filmstrip.FilmstripImageHost.Source = source; } } private void CreateStoryboard() { CreateStoryboard(0); } private void CreateStoryboard(int from) { int? targetTo; Duration dur; if (_mode == RunMode.FileSystem) { targetTo = new int?((this._filmstripImagePaths.Count == 0) ? 0 : (this._filmstripImagePaths.Count - 1)); dur = TimeSpan.FromSeconds(_filmstripImagePaths.Count); } else if (_mode == RunMode.Images) { targetTo = new int?((_filmstripImages.Count == 0) ? 0 : (_filmstripImages.Count - 1)); dur = TimeSpan.FromSeconds(_filmstripImages.Count); } else { targetTo = new int?((_filmstripBytes.Count == 0) ? 0 : (_filmstripBytes.Count - 1)); dur = TimeSpan.FromSeconds(_filmstripBytes.Count); } var element = new Int32Animation(); element.From = from; element.To = targetTo; element.Duration = dur; Storyboard.SetTargetProperty(element, new PropertyPath(CurrentFrameProperty)); _filmstripStoryboard = new Storyboard {SpeedRatio = this.FrameRate}; _filmstripStoryboard.Children.Add(element); _filmstripStoryboard.Completed += FilmstripStoryboard_Completed; } private void FilmstripStoryboard_Completed(object sender, EventArgs e) { if (CleanUp != null) CleanUp.AfterCompleted(); _filmstripStoryboard.Completed -= FilmstripStoryboard_Completed; } public void Pause() { _filmstripStoryboard.Pause(this); } public void Unpause() { _filmstripStoryboard.Resume(this); } public void Stop() { if (_filmstripStoryboard != null) _filmstripStoryboard.Stop(this); } public void Start() { Start(TimeSpan.Zero, true); } public void Start(TimeSpan beginTime, bool loop) { Start(beginTime, loop, false); } public void Start(TimeSpan beginTime, bool loop, bool reverse) { if (_filmstripStoryboard == null) return; if (beginTime != TimeSpan.Zero) _filmstripStoryboard.Children[0].BeginTime = new TimeSpan?(beginTime); if (loop) _filmstripStoryboard.Children[0].RepeatBehavior = RepeatBehavior.Forever; _filmstripStoryboard.Children[0].AutoReverse = reverse; _filmstripStoryboard.Begin(this, true); } ///

/// From the current position, reverse the animation
///

public void Reverse(int durationOfReverse)
{
_filmstripStoryboard.Pause(this);

var element = _filmstripStoryboard.Children[0] as Int32Animation;

element.From = CurrentFrame;
element.To = 0;
element.Duration = new Duration(new TimeSpan(0,0,0,0, durationOfReverse));
Storyboard.SetTargetProperty(element, new PropertyPath(CurrentFrameProperty));

//speed up reversal
_filmstripStoryboard.SpeedRatio = 1;
_filmstripStoryboard.Begin(this, true);
}

public void MoveFilmstripForward(int secondsToMoveForward)
{
var hasValue = _filmstripStoryboard.GetCurrentTime(this).HasValue;
if (!hasValue) return;

TimeSpan currentTime = (TimeSpan) _filmstripStoryboard.GetCurrentTime(this);
TimeSpan timeToMoveForward = new TimeSpan(0, 0, secondsToMoveForward * this.FrameRate);
TimeSpan newFilmstripLocation = currentTime.Add(timeToMoveForward);

_filmstripStoryboard.Seek(this, newFilmstripLocation, TimeSeekOrigin.BeginTime);
}

public void MoveFilmstripBackward(int secondsToMoveBackward)
{
var hasValue = _filmstripStoryboard.GetCurrentTime(this).HasValue;
if (!hasValue) return;
TimeSpan currentTime = (TimeSpan) _filmstripStoryboard.GetCurrentTime(this);
TimeSpan timeToMoveBackward = new TimeSpan(0, 0, secondsToMoveBackward * this.FrameRate);
TimeSpan newFilmstripLocation = currentTime.Subtract(timeToMoveBackward);

if (newFilmstripLocation.TotalMilliseconds < 1) newFilmstripLocation = new TimeSpan(0, 0, 0); this._filmstripStoryboard.Seek(this, newFilmstripLocation, TimeSeekOrigin.BeginTime); } public void AdjustFrameRate(int frameRate) { _filmstripStoryboard.SpeedRatio = frameRate; FrameRate = frameRate; } private int CurrentFrame { get { return (int)base.GetValue(CurrentFrameProperty); } } public int FrameCount { get { if (_mode == RunMode.FileSystem) return _filmstripImagePaths.Count; if (_mode == RunMode.Bytes) return _filmstripBytes.Count; return _filmstripImages.Count; } } public int FrameRate { get { return (int)base.GetValue(FrameRateProperty); } set { base.SetValue(FrameRateProperty, value); } } } } [/csharp] I have only just been learning about Apples world and the Cocoa-Touch framework, so when I know more about it I will be sure to post about it. As always, please give feedback if you enjoyed this blog post, found some errors, or if you would like more information in general. Thanks -- Macca (Mark)

This entry was posted in animation, Developer, Java, JavaFX, UI, Uncategorized, WPF. Bookmark the permalink.

One Response to Sprite animations using WPF/Silverlight and JavaFX

  1. rodanmuro

    Congratulations for your work, its very intersting. I think that the javafx was created to make the things easier. Tha javafx has many tools to save time writing code ¿Why dont make a tutorial where you can use the sprites and animation but with this tools and less code? ¿What do you think?

Leave a Comment

Your email address will not be published. Required fields are marked *


*