-
Notifications
You must be signed in to change notification settings - Fork 235
How to use Easing
Available since WebLaF v1.2.9 release, updated for WebLaF v1.2.12 release
Requires weblaf-ui module and Java 6 update 30 or any later to run
Easing defines parameter change rate over time. There are multiple popular easing algorithms used across different languages for various purposes. In WebLaF easing is mostly used for creating smooth animations and value transitions.
On a basic level easing simply defines a function describing transition between two parameter values over time. That parameter could be anything - component X coordinate, background transparency, progress bar value etc. WebLaF also offers some additional features to easily cover more complicated data structures and not just integer or double numbers.
In WebLaF all different existing easing algorithms are based on Easing
interface which has a single method that should be implemented to achieve different easing variations:
public double calculate ( double start, double distance, double current, double total );
Here is the short description of each field:
-
start
- starting value for the transition -
distance
- distance that should be covered to reach the end of the transition -
current
- current transition progress, should always be in [0..total] range -
total
- total progress required to finish transition
Any transition using specific easing will always start from start
value and finish at start + distance
value. Easing only provides an algorithm to transform initial value into the final one over time.
Values current
and total
could be anything - time passed, progress percentage, frames done or any other measurement you prefer using, but current
value should always stay in 0
- total
range.
There is also an AbstractEasing
class available which is handy for handling borderland situations in any custom easing algorithm and which is also extended by all existing algorithms.
By the way, here are the existing implementations:
Sinusoidal
Quadratic
Cubic
Quartic
Quintic
Exponential
Circular
Back
Elastic
Bounce
There are also two special ones:
-
Linear
- linear easing for plain linear transitions -
Bezier
- customizable bezier curve -based easing
These easing implementations should be quite sufficient for most common cases, but you are free to create your own or play around with existing ones.
Any easing is pretty much useless on its own as it is simply a function that would return you modified value based on the provided settings. Those settings is something you usually have to manage manually. I said usually because WebLaF actually offers a set of utilities to simplify the task, but let's try to do things manually first.
So, let's try to use raw Exponential
easing implementation for animating a custom drawing transition. First of all let's create a small drawing example:
public class TransitionExample extends WebCanvas
{
@Override
protected void paintComponent ( final Graphics g )
{
// Leave default painting untouched
super.paintComponent ( g );
// Paint circular shape
final Object aa = GraphicsUtils.setupAntialias ( g );
g.setColor ( Color.GRAY );
g.fillOval ( 10, 10, 40, 40 );
GraphicsUtils.restoreAntialias ( g, aa );
}
@Override
public Dimension getPreferredSize ()
{
// Custom preferred size for convenience
return new Dimension ( 500, 60 );
}
public static void main ( final String[] args )
{
WebLookAndFeel.install ();
TestFrame.show ( new TransitionExample () );
}
}
Just a simple canvas with a circle painted on it:
As you can see - I've left some space to move our circle around. So let's do that:
public class TransitionExample extends WebCanvas implements Runnable
{
/**
* Circle position.
* Always in [0..1] values range.
*/
private double position;
/**
* Movement direction.
*/
private CompassDirection direction;
public TransitionExample ()
{
super ();
// Initial position and direction
this.position = 0d;
this.direction = CompassDirection.east;
// Custom thread to perform transition
final Thread thread = new Thread ( this );
thread.setDaemon ( true );
thread.start ();
}
@Override
public void run ()
{
try
{
// Repeat until interrupted
while ( !Thread.interrupted () )
{
// Resetting position
if ( position >= 1d )
{
direction = direction.opposite ();
position = 0;
}
// Modifying circle position
position += 0.01d;
// Updating canvas
// Calling from EDT is not required for repaint() method
repaint ();
// 60 frames per second
Thread.sleep ( 1000 / 60 );
}
}
catch ( final InterruptedException e )
{
e.printStackTrace ();
}
}
@Override
protected void paintComponent ( final Graphics g )
{
// Leave default painting untouched
super.paintComponent ( g );
// Path length in pixels
// We remove borders (2 x 10px) and circle size (40px) which is total of 60px
final int path = getWidth () - 60;
// Calculating X position coordinate
final double pos = direction == CompassDirection.east ? position : 1d - position;
final int x = ( int ) Math.round ( path * pos );
// Paint circular shape
final Object aa = GraphicsUtils.setupAntialias ( g );
g.setColor ( Color.GRAY );
g.fillOval ( 10 + x, 10, 40, 40 );
GraphicsUtils.restoreAntialias ( g, aa );
}
@Override
public Dimension getPreferredSize ()
{
// Custom preferred size for convenience
return new Dimension ( 500, 60 );
}
public static void main ( final String[] args )
{
WebLookAndFeel.install ();
TestFrame.show ( new TransitionExample () );
}
}
That suddenly became quite complicated, but we got what we wanted - linear movement of the circle back and forth.
Linear movement doesn't really appeal to human's eye and you can easily see when GC happens - you get a slight frame drop at that moment. So that is not really something we would want to use in the end. This is where easing kicks in - let's make our transition smooth!
As I mentioned at the start - I will be using Exponential
easing implementation for that, to be more specific - Exponential.Out
one. And I actually only need to add minor modifications to the code above to make use of it:
public class TransitionExample extends WebCanvas implements Runnable
{
/**
* Circle position.
* Always in [0..1] values range.
*/
private double position;
/**
* Movement direction.
*/
private CompassDirection direction;
/**
* Used easing.
*/
private final Easing easing;
public TransitionExample ()
{
super ();
// Initial position and direction
this.position = 0d;
this.direction = CompassDirection.east;
this.easing = new Exponential.Out ();
// Custom thread to perform transition
final Thread thread = new Thread ( this );
thread.setDaemon ( true );
thread.start ();
}
@Override
public void run ()
{
try
{
// Repeat until interrupted
while ( !Thread.interrupted () )
{
// Resetting position
if ( position >= 1d )
{
direction = direction.opposite ();
position = 0;
}
// Modifying circle position
position += 0.01d;
// Updating canvas
// Calling from EDT is not required for repaint() method
repaint ();
// 60 frames per second
Thread.sleep ( 1000 / 60 );
}
}
catch ( final InterruptedException e )
{
e.printStackTrace ();
}
}
@Override
protected void paintComponent ( final Graphics g )
{
// Leave default painting untouched
super.paintComponent ( g );
// Path length in pixels
// We remove borders (2 x 10px) and circle size (40px) which is total of 60px
final int path = getWidth () - 60;
// Calculating X position coordinate
final double start = direction == CompassDirection.east ? 0d : 1d;
final double distance = direction == CompassDirection.east ? 1d : -1d;
final double eased = easing.calculate ( start, distance, position, 1d );
final int x = ( int ) Math.round ( path * eased );
// Paint circular shape
final Object aa = GraphicsUtils.setupAntialias ( g );
g.setColor ( Color.GRAY );
g.fillOval ( 10 + x, 10, 40, 40 );
GraphicsUtils.restoreAntialias ( g, aa );
}
@Override
public Dimension getPreferredSize ()
{
// Custom preferred size for convenience
return new Dimension ( 500, 60 );
}
public static void main ( final String[] args )
{
WebLookAndFeel.install ();
TestFrame.show ( new TransitionExample () );
}
}
Now that is already something! And we can easily replace easing with any other implementation at will:
this.easing = new Bounce.Out ();
to achieve a completely different result with almost no effort!
Though our code is still quite messy and contains a lot of calculations which might become a real pain if we will keep adding new ones. On top of that - it is really hard to control animation duration with this way of performing one as we will have to play with the value we increment and frame rate to achieve descent results. This is where AnimationManager
and transitions kick in to assist us!
Let's rewrite the same example using animated transitions:
public class TransitionExample extends WebCanvas
{
/**
* Circle position.
* Always in [0..1] values range.
*/
private double position;
public TransitionExample ()
{
super ();
// Custom queue transition that will loop animations
final QueueTransition<Double> transition = new QueueTransition<Double> ( true );
// Slow left to right bounce transition
transition.add ( new TimedTransition<Double> ( 0d, 1d, new Bounce.Out (), 2000L ) );
// Quick right to left exponential transition
transition.add ( new TimedTransition<Double> ( 1d, 0d, new Exponential.Out (), 400L ) );
// Listener for value and canvas updates
transition.addListener ( new TransitionAdapter<Double> ()
{
@Override
public void adjusted ( final Transition transition, final Double value )
{
position = value;
repaint ();
}
} );
// Playing transition
transition.play ();
}
@Override
protected void paintComponent ( final Graphics g )
{
// Leave default painting untouched
super.paintComponent ( g );
// Path length in pixels
// We remove borders (2 x 10px) and circle size (40px) which is total of 60px
final int path = getWidth () - 60;
// Calculating X position coordinate
final int x = ( int ) Math.round ( path * position );
// Paint circular shape
final Object aa = GraphicsUtils.setupAntialias ( g );
g.setColor ( Color.GRAY );
g.fillOval ( 10 + x, 10, 40, 40 );
GraphicsUtils.restoreAntialias ( g, aa );
}
@Override
public Dimension getPreferredSize ()
{
// Custom preferred size for convenience
return new Dimension ( 500, 60 );
}
public static void main ( final String[] args )
{
WebLookAndFeel.install ();
TestFrame.show ( new TransitionExample () );
}
}
This is million times more readable than our previous code, right?
On top of that we can easily customize transitions duration, easing, frame rate and even add any amount of additional transitions by modifying our queue transition:
// Custom queue transition that will loop animations
final QueueTransition<Double> transition = new QueueTransition<Double> ( true );
// Slow left to right bounce transition
transition.add ( new TimedTransition<Double> ( 0d, 1d, new Bounce.Out (), 1400L ) );
// Quick right to left exponential transition
transition.add ( new TimedTransition<Double> ( 1d, 0d, new Exponential.Out (), 400L ) );
// Slow left to middle elastic transition
transition.add ( new TimedTransition<Double> ( 0d, 0.5d, new Elastic.Out (), 1400L ) );
// Quick middle to left sinusoidal transition
transition.add ( new TimedTransition<Double> ( 0.5d, 0d, new Sinusoidal.InOut (), 400L ) );
With a minimum effort we get access to unlimited amount of neat transitions for any of our settings.
Let's play around with our example just a bit more - we will add Y coordinate with its own separate transitions. Though to keep circle movement consistent we will synchronize duration for different transitions:
public class TransitionExample extends WebCanvas
{
/**
* Circle X position.
* Always in [0..1] values range.
*/
private double positionX;
/**
* Circle Y position.
* Always in [0..1] values range.
*/
private double positionY;
/**
* Trace points.
*/
private final List<Point> trace;
public TransitionExample ()
{
super ();
// Last traced points
trace = new ArrayList<Point> ( 100 );
// X coordinate transitions
final QueueTransition<Double> transitionX = new QueueTransition<Double> ( true );
transitionX.add ( new TimedTransition<Double> ( 0d, 1d, new Bounce.Out (), 1400L ) );
transitionX.add ( new TimedTransition<Double> ( 1d, 0d, new Quadratic.Out (), 600L ) );
transitionX.add ( new TimedTransition<Double> ( 0d, 0.5d, new Sinusoidal.InOut (), 1400L ) );
transitionX.add ( new TimedTransition<Double> ( 0.5d, 0d, new Sinusoidal.In (), 400L ) );
transitionX.addListener ( new TransitionAdapter<Double> ()
{
@Override
public void adjusted ( final Transition transition, final Double value )
{
positionX = value;
repaint ();
}
} );
// Y coordinate transitions
final QueueTransition<Double> transitionY = new QueueTransition<Double> ( true );
transitionY.add ( new TimedTransition<Double> ( 0d, 1d, new Sinusoidal.InOut (), 1400L ) );
transitionY.add ( new TimedTransition<Double> ( 1d, 0d, new Quadratic.In (), 600L ) );
transitionY.add ( new TimedTransition<Double> ( 0d, 0.5d, new Elastic.Out (), 1400L ) );
transitionY.add ( new TimedTransition<Double> ( 0.5d, 0d, new Sinusoidal.Out (), 400L ) );
transitionY.addListener ( new TransitionAdapter<Double> ()
{
@Override
public void adjusted ( final Transition transition, final Double value )
{
positionY = value;
repaint ();
}
} );
// Playing transitions
transitionX.play ();
transitionY.play ();
}
@Override
protected void paintComponent ( final Graphics g )
{
// Leave default painting untouched
super.paintComponent ( g );
// Using Graphics2D API
final Graphics2D g2d = ( Graphics2D ) g;
final Object aa = GraphicsUtils.setupAntialias ( g2d );
// Calculating X position coordinate
final int x = ( int ) Math.round ( ( getWidth () - 60 ) * positionX );
final int y = ( int ) Math.round ( ( getHeight () - 60 ) * positionY );
// Update traced points
if ( trace.size () == 100 )
{
trace.remove ( trace.size () - 1 );
}
trace.add ( 0, new Point ( 30 + x, 30 + y ) );
// Paint trace line
if ( trace.size () > 1 )
{
final GeneralPath tracePath = new GeneralPath ( Path2D.WIND_EVEN_ODD );
final Point start = trace.get ( 0 );
tracePath.moveTo ( start.x, start.y );
for ( int i = 1; i < trace.size (); i++ )
{
final Point point = trace.get ( i );
tracePath.lineTo ( point.x, point.y );
}
g2d.setPaint ( Color.LIGHT_GRAY );
g2d.draw ( tracePath );
}
// Paint circular shape
g2d.setPaint ( Color.GRAY );
g2d.fillOval ( 10 + x, 10 + y, 40, 40 );
GraphicsUtils.restoreAntialias ( g2d, aa );
}
@Override
public Dimension getPreferredSize ()
{
// Custom preferred size for convenience
return new Dimension ( 500, 500 );
}
public static void main ( final String[] args )
{
WebLookAndFeel.install ();
TestFrame.show ( new TransitionExample () );
}
}
I've picked transitions for X and Y coordinates quite randomly so don't judge me!
Also to make circle movement more visible I've added a trace consisting of last 100 points circle transitioned through - it is a really easy trick to do, but it might be useful for tracking animations when you are not sure whether everything works correctly or something is off.
You might also notice (thanks to the trace) that in this example X and Y coordinates updates are not synchronized which leads to quite rough trace line and some other side effects:
That can be avoided by using different transition implementations, but that is a topic for a separate article so I will leave this example as it is. We will get back to it in the article describing different transition implementations and difference between them.
If you found any mistakes or inconsistency in this article, feel that it is lacking explanation or simply want to request an additional wiki article covering some topic:
I will do my best to answer and provide assistance as soon as possible!