Effects and Animation - jMonkeyEngine-Contributions/Lemur GitHub Wiki
As of version 1.6, Lemur includes a built-in animation system and a GUI element effects framework that can utilize it to add effects to GUI elements, either directly or through styling. The base Animation system allows any type of time-based repetitive tasking to be invoked. This in turn can be used to call a set of Tween objects which perform interpolation over time. The effects system then taps into these layers to provide per-GUI-element event based animation. (For example, a button click might run a 'click' animation that creates the appropriate animation tasks to wiggle the button, flash its color, and so on.)
The above diagram illustrates the separation between the various layers of the animation system. At the bottom, you have Tweens which are reusable chunks that actually do work, can be chained together, or run in parallel. These are usually executed by TweenAnimations in the AnimationState. The AnimationState may also be executing any arbitrary custom animations that the application registered. Finally, Effect objects in the EffectControl of a Spatial may execute certain animations on demand and usually these are TweenAnimations (though they don't have to be).
The core of animation is simply doing something over a period of time. Lemur provides an AnimationState app state to provide this capability. Individual Animation objects can be added to the AnimationState and will all be called once a frame until that particular animation signals that it is complete.
A custom animation implementation need only implement the animate() and cancel() methods.
Example:
public class MyCustomAnimation implements Animation {
public boolean animate( double tpf ) {
// perform some animation task
return true; // to keep going
}
public void cancel() {
}
}
A built in Animation implementation called TweenAnimation is setup to automatically call Tween objects. Tweens are sort of units of interpolation. A single Tween might move a Spatial from one location to another or rotate it from one angle to another, or both. The TweenAnimation object tracks time from when it was started and passes that time onto the Tween delegate until the full duration has passed.
TweenAnimation also supports looping, queries about the time remaining/expired, and the ability to fast-forward the task.
TweenAnimation:
- isLooping(): true if the animation is looping.
- isRunning(): true if the animation is currently running and has not finished.
- getLength(): returns the configured duration of this animation.
- getTime(): returns the amount of time that the animation has been run so far.
- getRemaining(): returns the time left to run. If the animation is looping then this returns the time left in this loop iteration.
- getRemainingPercent(): returns the time remaining scaled to be between 0 and 1.0 based on the total length of the animation.
- fastForward(): fast forwards the animation to a particular time.
- fastForwardPercent(): fast forwards the animation to a particular time as a length relative value between 0 and 1.
Tween objects are small animation units that compose the core of Lemur's reusable animation system. Individual Tween objects perform simple interpolation operations based on input and then can be composed into sequences or parallel tweens for building up more complex animations.
A Tween does one thing. It interpolates given some 'time' value between 0 and length. For example, a Tween that moves a spatial from (0, 0, 0) to (100, 0, 0) over a period of 5.0 would interpolate location and apply it to the spatial based on a 'time' input of 0 to 5.0.
myMoveTween.interpolate(1); // puts the spatial at 20, 0, 0
myMoveTween.interpolate(2.5); // puts the spatial at 50, 0, 0
myMoveTween.interpolate(5); // puts the spatial at 100, 0, 0
/// and it's clamped so
myMoveTween.interpolate(6); // also puts the spatial at 100, 0, 0
Lemur includes a bunch of built in Tween implementations in sets of static factory methods.
The SpatialTweens javadoc class provides a bunch of factory methods for manipulating spatials through interpolation.
Factory Methods:
- move: creates a Tween that moves a spatial from one location to another.
- rotate: creates a Tween that rotates a spatial from one orientation to another.
- scale: creates a Tween that scales a spatial from one scale to another.
- detach: creates an instant Tween that detaches the spatial at time >= 0.
- attach: creates an instant Tween that attaches the spatial to a specified parent at time >= 0.
Each of these can use the Spatial's current value as the starting or ending point at the time of Tween creation.
Note: additional factories will be added in the future for spline following, etc.
On their own, these are only a tiny bit useful. The real power comes in how they can be composed and manipulated using the standard base Tweens.
This class is real the real power starts to show. The Tweens set of factory methods provides a built in set of Tween implementations for composing or rescaling/filtering other tween objects. For example, a sequence of tweens can be put together to run one at a time.
Another feature is the ability to smooth step, or apply a curve filter to, another Tween. For example, a move Tween will linearly interpolate from on location to another. Often it would be nicer to perform a smoother interpolation where the object starts to move slow, speeds up, then slows down again at the other end. The Tweens class includes both a smoothStep() wrapper and a sineStep() wrapper that filters 'time' through a curve before sending it onto the delegate Tween. The beauty of this approach is that the curve could be supplied to a whole composite Tween that might include moving, rotation, scaling, fading, etc. all at once, in sequence, or in parallel. Being able to smooth step the whole thing is very powerful for making animations a little 'juicier'.
Factory Methods:
- sequence() : creates a Tween that contains a set of delegate tweens that will be run in sequence.
- parallel() : creates a Tween that contains a set of delegate tweens that will be run in parallel until each one finishes.
- delay() : creates a no-op Tween that does nothing until the specified time has passed. Useful in sequences.
- stretch() : creates a Tween that changes the length of another tween by shrinking or stretching. (Basically scales the time value.)
- sineStep() : creates a Tween that filters time through a sine function to smooth out the delegate Tween's interpolation.
- smoothStep() : creates a Tween that filters time through a hermite function to smooth out the delegate Tween's interpolation. This is similar to the GLSL smoothstep() function.
- callMethod() : creates an instant Tween that uses reflection to call a method when time >= 0.
- callTweenMethod() : creates a Tween that will pass the time on to a specified method using reflection.
This class provides some standard tweens for manipulating Lemur's GUI elements based on Panels. Right now there is only a fade() method that supports changing a Panel's alpha value over time.
On the plan are additional factory method sets for manipulating the camera, material parameters, and sound.
At the GUI element level, effects are the ability to associate some predefined animations to names. Some GUI elements like Button will have a built in set of events that trigger effects (press, release, click, activate, and deactivate). A caller can register Effect objects for these names that will be used to create an Animation when the effect is executed. In this sense, an Effect is like a reusable animation factory.
Often, a GUI element will have reciprocal effects (like open/close, activate/deactivate) that should interrupt each other. For example, if a window is in the middle of its open animation then the close animation should either wait for it to finish or replace it. To facilitate this, an effect can provide a channel name. This channel name is used to replace any existing effect running on the channel at the time of the new effect's invocation.
Replaced effects are automatically canceled and the new effect is 'fast-forward' to proportionally the same time. For example, if the window open effect is scaling and moving the window over a 2 second period and is only half way through then replacing it with the close effect would cancel the open in mid-operation. The close effect would then be fast-forwarded halfway through its own timeline (with all intervening events being executed.) So even if the open animation will take 2 seconds but the close animation will take only 1 second, the proportional fast-forward makes sure that the replacement is smooth (presuming the two effects are truly opposites).
Effects can be setup through style attributes similar to any other styleable attribute. The different here is that the effects value is a Map of names to Effect objects. The styling API will automatically merge these maps in an intelligent way based on the styling containment that is configured.
For example, if a general button effect is registered like (in style language form):
selector("button", "myStyle") {
effects = [
click:new PlayBeepEffect(),
activate:new MyHighlightEffect(),
deactivate:new MyUnhighlightEffect()
]
}
That registers a set of effects that will be applied to all buttons by default.
It is then possible to override this for just slider buttons in the normal style way:
selector("slider", "button", "myStyle") {
effects = [
click:new PlayClickEffect(),
activate:null,
deactivate:null
]
}
In that case, by default Sliders will only have the new click effect and none of the default button effects.
This holds true even when dealing directly with the Styles class and grabbing Attributes for selectors. One must just take care to create a Map value and/or add settings to any existing map values for that selector.
All Lemur GUI elements based on Panel will automatically have support for running effects. This is done by internally calling the EffectControl. EffectControl is an independent module that can be used for any JME Spatial. General Lemur users do not need to worry about this control as full access to effects is provided through the Panel base class.
Without drilling down too deeply, here are some brief examples to give a feel for how the parts work together.
AnimationState anim = getState(AnimationState.class);
Node someSpatial...
Tween move1 = SpatialTweens.move(someSpatial, Vector3f.ZERO, new Vector3f(100, 100, 0), 2);
Tween move2 = SpatialTweens.move(someSpatial, new Vector3f(100, 100, 0), new Vector3f(200, 0, 0), 2);
Tween rotate = SpatialTweens.rotate(someSpatial, null, new Quaternion().fromAngles(0, 0, FastMath.HALF_PI, 2);
Tween unrotate = SpatialTweens.rotate(someSpatial, new Quaternion().fromAngles(0, 0, FastMath.HALF_PI, null, 1);
// Now put it all together
Tween overall = Tweens.sequence(move1, rotate, Tweens.parallel(move2, unrotate));
anim.add(overall);
The above example will move a spatial from (0, 0, 0) to (100, 100, 0) over a 2 second period. It will then stop and rotate 90 degrees over a 2 second. After that it will simultaneously rotate back 90 degrees and move to a new location. The reverse rotation only lasts for half of the final journey because its length is set to 1 second.
// All of the same stuff from above... but now with smoothing of the movement here:
Tween overall = Tweens.sequence(Tweens.smoothStep(move1), rotate, Tweens.parallel(Tweens.smoothStep(move2), unrotate));
anim.add(overall);
This is essentially the same example except that the spatial will move much more smoothly, slowing starting up to a slightly higher speed before slowing down again at the end. The overall time will remain the same.
In this example, we'll configure open and close effects and apply them to a Panel. This is an example of reciprocal effects that will interrupt each other.
Effect<Panel> open = new AbstractEffect<>("open/close") {
public Animation create( Panel target, EffectInfo existing ) {
Tween move = SpatialTweens.move(target, Vector3f.ZERO, new Vector3f(200, 200, 0), 2);
Tween scale = SpatialTweens.scale(target, 0, 1, 2);
return new TweenAnimation(Tweens.smoothStep(Tweens.parallel(move, scale)));
}
}
Effect<Panel> close = new AbstractEffect<>("open/close") {
public Animation create( Panel target, EffectInfo existing ) {
Tween move = SpatialTweens.move(target, new Vector3f(200, 200, 0), Vector3f.ZERO, 1);
Tween scale = SpatialTweens.scale(target, 1, 0, 1);
return new TweenAnimation(Tweens.smoothStep(Tweens.parallel(move, scale)));
}
}
// Now that we've created the effects we can set them to our Panel.
myPanel.addEffect("open", open);
myPanel.addEffect("close", close);
That sets up the panel to support runEffect("open") and runEffect("close") (presuming it is already attached to something). The open will take 2 seconds and the close will take 1 second.
But wait! Don't we have to do something to handle the interrupting and stuff?!? Nope. In this case for simple tween-based animations, the right thing will be done.
If myPanel.runEffect("close") happens before the open has completed, the open animation will be canceled and the close animation will be fast forwarded to skip what was remaining in the open animation.
So if open is only 1 second into its animation when runEffect("close") is called then the close animation will be fast-forwarded to 0.5 seconds (half of its time because half of open had completed).
The cool thing is that the same thing goes in reverse also. If close is partially run and runEffect("open") happens then open will skip ahead also. Integrity is maintained.
Note: this type of interruption only happens because the Effects have specified a channel (in this case "open/close"). Effects that do not specify a channel will always run independently.