Sprite animations using WPF/Silverlight and JavaFX
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
[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;
}
}
[/JavaFX]
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:
[JavaFX]
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 }
]
}
}
[/JavaFX]
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)
[csharp]
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
{
///
///
Images,
///
///
FileSystem,
///
///
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
private readonly List
private readonly List
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
{
_mode = RunMode.Bytes;
_filmstripBytes = filmstripBytes;
FilmstripImageBrushHost = imageToUpdate;
FilmstripImageBrushHost.ImageSource = CreateImageFromBytes(_filmstripBytes[0]);
CreateStoryboard();
}
public Filmstrip(List
{
_mode = RunMode.Images;
_filmstripImages = filmstripImages;
FilmstripImageHost = new Image { Stretch = Stretch.Fill };
FilmstripImageBrushHost = imageToUpdate;
FilmstripImageBrushHost.ImageSource = filmstripImages[0];
CreateStoryboard();
}
public Filmstrip(List
{
FilmstripImageHost = new Image { Stretch = Stretch.Fill };
_filmstripImagePaths = filmstripImagePaths;
Content = FilmstripImageHost;
CreateStoryboard();
}
public Filmstrip(List
{
_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);
}
///
///
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)
One response to “Sprite animations using WPF/Silverlight and JavaFX”
-
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?
About Me
Chesung Subba
Author/Writer
Hello, I'm Chesung Subba, a passionate writer who loves sharing ideas, stories, and experiences to inspire, inform, and connect with readers through meaningful content.
Follow Me
Connect with me and be part of my social media community.
Leave a Reply