Skip to content

How to use Easing

Mikle edited this page Jan 30, 2020 · 2 revisions

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


What is it for?

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.


What should I know about it?

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.


How to use it?

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:

Easing

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:

Easing

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.