JavaFX: Creating a Sprite Animation
In this article I will explain how to write custom animations in JavaFX and use this approach to create a class for sprite animations. (This will also be a good practice for one of my sessions at the conference 33rd Degree. I plan to write a game in JavaFX in just one hour. That’s going to be fun!)
The Horse in Motion |
There are many very good articles out there describing predefined Transitions (TranslateTransition, RotateTransition etc.) and Timelines. Most of the time these approaches are sufficient, but in some cases one just needs more flexibility. And that is when the Transition class comes into play, which can be extended to define a custom animation.
To write your own animation class by extending Transition, two steps are required:
- Specify the duration of a single cycle
- Implement the interpolate() method
Duration of a single cycle
You can set the duration of a cycle by calling the protected method setCycleDuration(). Most of the time the duration is either fixed (if the animation is used only once) or configurable by the user. Almost all predefined Transitions in the JavaFX runtime fall into the second category. They expose their cycle duration via the duration property, you probably want to do that in your class, too. In rare cases the duration of a cycle depends on other values. For example the duration of a SequentialTransition and ParallelTransition depend on the duration of their children.
You can change the cycle duration as often as you like, but note that it does not effect a currently running animation. Only after an animation is stopped and started again is the new cycle duration taken into account.
The interpolate() method
The interpolate() method is abstract and needs to be overridden. It defines actual behavior of the animation. The method interpolate() is called by the runtime in every frame while the animation is playing. A value frac, a double between 0.0 and 1.0 (both inclusive) is passed in, which specifies the current position. The value 0.0 marks the start of the animation, the value 1.0 the end. Any value in between defines the relative position. Note that a possible Interpolator is already taken into account when the value of frac is calculated.
The class SpriteAnimation
To demonstrate how a custom transition is defined, we will look at a class that allows us to do Sprite animations. It takes an image that has several frames and moves the viewport from one frame to the other over time. We will test this class with the famous “The Horse in Motion” by Eadweard Muybridge. Enough talk, here is the code:
package sandboxfx; import javafx.animation.Interpolator; import javafx.animation.Transition; import javafx.geometry.Rectangle2D; import javafx.scene.image.ImageView; import javafx.util.Duration; public class SpriteAnimation extends Transition { private final ImageView imageView; private final int count; private final int columns; private final int offsetX; private final int offsetY; private final int width; private final int height; private int lastIndex; public SpriteAnimation( ImageView imageView, Duration duration, int count, int columns, int offsetX, int offsetY, int width, int height) { this.imageView = imageView; this.count = count; this.columns = columns; this.offsetX = offsetX; this.offsetY = offsetY; this.width = width; this.height = height; setCycleDuration(duration); setInterpolator(Interpolator.LINEAR); } protected void interpolate(double k) { final int index = Math.min((int) Math.floor(k * count), count - 1); if (index != lastIndex) { final int x = (index % columns) * width + offsetX; final int y = (index / columns) * height + offsetY; imageView.setViewport(new Rectangle2D(x, y, width, height)); lastIndex = index; } } }
For the sake of simplicity, this example class just takes all parameters in the constructor and does not allow to change them later. In most cases this is sufficient.
The class expects an ImageView, the duration of a single cycle (that is how long it should take to go through all frames), the number of frames, the number of columns (how many frames are in one row in the image), the offset of the first frame, and the width and height of all frames. The duration of the full cycle is passed to the super class by calling setCycleDuration(), all other values are stored. As a last step in the constructor, the interpolator is set to linear. By default an easing interpolator is set for all transitions, because that is what usually gives the best results. But in our case we want to run through all frames with the same speed and an easing interpolator would look weird.
The interpolate() method takes the passed in value and calculates which frame needs to be displayed at the moment. If it changed since the last time interpolate() was called, the position of the new frame is calculated and the viewport of the ImageView set accordingly. That’s it.
The Horse in Motion
To demonstrate the SpriteAnimation class, we will animate The Horse in Motion. The code to do that is straightforward, most of the work is already done. It creates an ImageView with the viewport set to the first frame and instantiates the SpriteAnimation class. The parameters are just estimates, you may want to tweak them a little.
package sandboxfx; import javafx.animation.Animation; import javafx.application.Application; import javafx.geometry.Rectangle2D; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.stage.Stage; import javafx.util.Duration; public class SandboxFX extends Application { private static final Image IMAGE = new Image("http://upload.wikimedia.org/wikipedia/commons/7/73/The_Horse_in_Motion.jpg"); private static final int COLUMNS = 4; private static final int COUNT = 10; private static final int OFFSET_X = 18; private static final int OFFSET_Y = 25; private static final int WIDTH = 374; private static final int HEIGHT = 243; public static void main(String[] args) { launch(args); } public void start(Stage primaryStage) { primaryStage.setTitle("The Horse in Motion"); final ImageView imageView = new ImageView(IMAGE); imageView.setViewport(new Rectangle2D(OFFSET_X, OFFSET_Y, WIDTH, HEIGHT)); final Animation animation = new SpriteAnimation( imageView, Duration.millis(1000), COUNT, COLUMNS, OFFSET_X, OFFSET_Y, WIDTH, HEIGHT ); animation.setCycleCount(Animation.INDEFINITE); animation.play(); primaryStage.setScene(new Scene(new Group(imageView))); primaryStage.show(); } }
Conclusion
Defining your own animation by extending the Transition class is simple and straightforward. It is an extremely powerful approach though, because an animation created this way has all the functionality regular animations have. For example you can play it slower and faster by changing the rate and you can play it even backwards. You can run it in a loop and you can use it within ParallelTransition and SequentialTransition to create even more complex animations.
Reference: Creating a Sprite Animation with JavaFX from our JCG partner Michael Heinrichs at the Mike’s Blog.