animator and cubic bezier easing classes added (for future animations) (issue #66)

This commit is contained in:
Karl Tauber
2020-07-21 13:11:46 +02:00
parent 2cdcde8a5e
commit 008ecabd21
4 changed files with 590 additions and 0 deletions

View File

@@ -0,0 +1,299 @@
/*
* Copyright 2020 FormDev Software GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.formdev.flatlaf.util;
import java.util.ArrayList;
import javax.swing.Timer;
/**
* Simple animator based on ideas and concepts from "Filthy Rich Clients" book
* and "Timing Framework" library.
*
* @author Karl Tauber
*/
public class Animator
{
private int duration;
private int resolution = 10;
private Interpolator interpolator;
private final ArrayList<TimingTarget> targets = new ArrayList<>();
private boolean running;
private boolean hasBegun;
private boolean timeToStop;
private long startTime;
private Timer timer;
/**
* Creates an animation that runs duration milliseconds.
* Use {@link #addTarget(TimingTarget)} to receive timing events
* and {@link #start()} to start the animation.
*
* @param duration the duration of the animation in milliseconds
*/
public Animator( int duration ) {
this( duration, null );
}
/**
* Creates an animation that runs duration milliseconds.
* Use {@link #start()} to start the animation.
*
* @param duration the duration of the animation in milliseconds
* @param target the target that receives timing events
*/
public Animator( int duration, TimingTarget target ) {
setDuration( duration );
addTarget( target );
}
/**
* Returns the duration of the animation in milliseconds.
*/
public int getDuration() {
return duration;
}
/**
* Sets the duration of the animation in milliseconds.
*
* @throws IllegalStateException if animation is running
* @throws IllegalArgumentException if duration is <= zero
*/
public void setDuration( int duration ) {
throwExceptionIfRunning();
if( duration <= 0 )
throw new IllegalArgumentException();
this.duration = duration;
}
/**
* Returns the resolution of the animation in milliseconds (default is 10).
* Resolution is the amount of time between timing events.
*/
public int getResolution() {
return resolution;
}
/**
* Sets the resolution of the animation in milliseconds.
*
* @throws IllegalStateException if animation is running
* @throws IllegalArgumentException if resolution is <= zero
*/
public void setResolution( int resolution ) {
throwExceptionIfRunning();
if( resolution <= 0 )
throw new IllegalArgumentException();
this.resolution = resolution;
}
/**
* Returns the interpolator for the animation.
* Default is {@code null}, which means linear.
*/
public Interpolator getInterpolator() {
return interpolator;
}
/**
* Sets the interpolator for the animation.
*
* @throws IllegalStateException if animation is running
*/
public void setInterpolator( Interpolator interpolator ) {
throwExceptionIfRunning();
this.interpolator = interpolator;
}
/**
* Adds a target to the animation that receives timing events.
*
* @param target the target that receives timing events
*/
public void addTarget( TimingTarget target ) {
if( target == null )
return;
synchronized( targets ) {
if( !targets.contains( target ) )
targets.add( target );
}
}
/**
* Removes a target from the animation.
*
* @param target the target that should be removed
*/
public void removeTarget( TimingTarget target ) {
synchronized( targets ) {
targets.remove( target );
}
}
/**
* Starts the animation.
*
* @throws IllegalStateException if animation is running
*/
public void start() {
throwExceptionIfRunning();
running = true;
hasBegun = false;
timeToStop = false;
startTime = System.nanoTime() / 1000000;
timer = new Timer( resolution, e -> {
if( !hasBegun ) {
begin();
hasBegun = true;
}
timingEvent( getTimingFraction() );
} );
timer.setInitialDelay( 0 );
timer.start();
}
/**
* Stops the animation before it normally ends.
* Invokes {@link TimingTarget#end()} on timing targets.
*/
public void stop() {
stop( false );
}
/**
* Cancels the animation before it normally ends.
* Does not invoke {@link TimingTarget#end()} on timing targets.
*/
public void cancel() {
stop( true );
}
private void stop( boolean cancel ) {
if( timer != null ) {
timer.stop();
timer = null;
}
if( !cancel )
end();
running = false;
timeToStop = false;
}
/**
* Returns whether this animation is running.
*/
public boolean isRunning() {
return running;
}
private float getTimingFraction() {
long currentTime = System.nanoTime() / 1000000;
long elapsedTime = currentTime - startTime;
timeToStop = (elapsedTime >= duration);
float fraction = clampFraction( (float) elapsedTime / duration );
if( interpolator != null )
fraction = clampFraction( interpolator.interpolate( fraction ) );
return fraction;
}
private float clampFraction( float fraction ) {
if( fraction < 0 )
return 0;
if( fraction > 1 )
return 1;
return fraction;
}
private void timingEvent( float fraction ) {
synchronized( targets ) {
for( TimingTarget target : targets )
target.timingEvent( fraction );
}
if( timeToStop )
stop();
}
private void begin() {
synchronized( targets ) {
for( TimingTarget target : targets )
target.begin();
}
}
private void end() {
synchronized( targets ) {
for( TimingTarget target : targets )
target.end();
}
}
private void throwExceptionIfRunning() {
if( isRunning() )
throw new IllegalStateException();
}
//---- interface TimingTarget ---------------------------------------------
/**
* Animation callbacks.
*/
@FunctionalInterface
public interface TimingTarget {
/**
* Invoked multiple times while animation is running.
*
* @param fraction the percent (0 to 1) elapsed of the current animation cycle
*/
void timingEvent( float fraction );
/**
* Invoked when the animation begins.
*/
default void begin() {}
/**
* Invoked when the animation ends.
*/
default void end() {}
}
//---- interface Interpolator ---------------------------------------------
/**
* Interpolator used by animation to change timing fraction. E.g. for easing.
*/
@FunctionalInterface
public interface Interpolator {
/**
* Interpolate the given fraction and returns a new fraction.
* Both fractions are in range [0, 1].
*
* @param fraction the percent (0 to 1) elapsed of the current animation cycle
* @return new fraction in range [0, 1]
*/
float interpolate( float fraction );
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2020 FormDev Software GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.formdev.flatlaf.util;
/**
* An interpolator for {@link Animator} that uses a cubic bezier curve.
*
* @author Karl Tauber
*/
public class CubicBezierEasing
implements Animator.Interpolator
{
// common cubic-bezier easing functions (same as in CSS)
// https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function
public static final CubicBezierEasing EASE = new CubicBezierEasing( 0.25f, 0.1f, 0.25f, 1f );
public static final CubicBezierEasing EASE_IN = new CubicBezierEasing( 0.42f, 0f, 1f, 1f );
public static final CubicBezierEasing EASE_IN_OUT = new CubicBezierEasing( 0.42f, 0f, 0.58f, 1f );
public static final CubicBezierEasing EASE_OUT = new CubicBezierEasing( 0f, 0f, 0.58f, 1f );
private final float x1;
private final float y1;
private final float x2;
private final float y2;
/**
* Creates a cubic bezier easing interpolator with the given control points.
* The start point of the cubic bezier curve is always 0,0 and the end point 1,1.
*
* @param x1 the x coordinate of the first control point in range [0, 1]
* @param y1 the y coordinate of the first control point in range [0, 1]
* @param x2 the x coordinate of the second control point in range [0, 1]
* @param y2 the y coordinate of the second control point in range [0, 1]
*/
public CubicBezierEasing( float x1, float y1, float x2, float y2 ) {
if( x1 < 0 || x1 > 1 || y1 < 0 || y1 > 1 ||
x2 < 0 || x2 > 1 || y2 < 0 || y2 > 1 )
throw new IllegalArgumentException( "control points must be in range [0, 1]");
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
@Override
public float interpolate( float fraction ) {
if( fraction <= 0 || fraction >= 1 )
return fraction;
// use binary search
float low = 0;
float high = 1;
while( true ) {
float mid = (low + high) / 2;
float estimate = cubicBezier( mid, x1, x2 );
if( Math.abs( fraction - estimate ) < 0.0005f )
return cubicBezier( mid, y1, y2 );
if( estimate < fraction )
low = mid;
else
high = mid;
}
}
/**
* Computes the x or y point on a cubic bezier curve for a given t value.
*
* https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
*
* The general cubic bezier formula is:
* x = b0*x0 + b1*x1 + b2*x2 + b3*x3
* y = b0*y0 + b1*y1 + b2*y2 + b3*y3
*
* where:
* b0 = (1-t)^3
* b1 = 3 * t * (1-t)^2
* b2 = 3 * t^2 * (1-t)
* b3 = t^3
*
* x0,y0 is always 0,0 and x3,y3 is 1,1, so we can simplify to:
* x = b1*x1 + b2*x2 + b3
* y = b1*x1 + b2*x2 + b3
*/
private static float cubicBezier( float t, float xy1, float xy2 ) {
float invT = (1 - t);
float b1 = 3 * t * (invT * invT);
float b2 = 3 * (t * t) * invT;
float b3 = t * t * t;
return (b1 * xy1) + (b2 * xy2) + b3;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2020 FormDev Software GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.formdev.flatlaf.testing;
import java.awt.*;
import javax.swing.*;
import com.formdev.flatlaf.util.Animator;
import com.formdev.flatlaf.util.CubicBezierEasing;
import net.miginfocom.swing.*;
/**
* @author Karl Tauber
*/
public class FlatAnimatorTest
extends FlatTestPanel
{
private Animator linearAnimator;
private Animator easeInOutAnimator;
public static void main( String[] args ) {
SwingUtilities.invokeLater( () -> {
FlatTestFrame frame = FlatTestFrame.create( args, "FlatAnimatorTest" );
frame.showFrame( FlatAnimatorTest::new );
} );
}
FlatAnimatorTest() {
initComponents();
}
private void start() {
startLinear();
startEaseInOut();
}
private void startLinear() {
if( linearAnimator != null ) {
linearAnimator.stop();
linearAnimator.start();
} else {
linearAnimator = new Animator( 1000, t -> {
linearScrollBar.setValue( Math.round( t * linearScrollBar.getMaximum() ) );
} );
linearAnimator.start();
}
}
private void startEaseInOut() {
if( easeInOutAnimator != null ) {
easeInOutAnimator.stop();
easeInOutAnimator.start();
} else {
easeInOutAnimator = new Animator( 1000, t -> {
easeInOutScrollBar.setValue( Math.round( t * easeInOutScrollBar.getMaximum() ) );
} );
easeInOutAnimator.setInterpolator( CubicBezierEasing.EASE_IN_OUT );
easeInOutAnimator.start();
}
}
private void initComponents() {
// JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
JLabel label1 = new JLabel();
linearScrollBar = new JScrollBar();
JLabel label2 = new JLabel();
easeInOutScrollBar = new JScrollBar();
startButton = new JButton();
//======== this ========
setLayout(new MigLayout(
"ltr,insets dialog,hidemode 3",
// columns
"[fill]" +
"[grow,fill]",
// rows
"[]" +
"[]" +
"[]"));
//---- label1 ----
label1.setText("Linear:");
add(label1, "cell 0 0");
//---- linearScrollBar ----
linearScrollBar.setOrientation(Adjustable.HORIZONTAL);
linearScrollBar.setBlockIncrement(1);
add(linearScrollBar, "cell 1 0");
//---- label2 ----
label2.setText("Ease in out:");
add(label2, "cell 0 1");
//---- easeInOutScrollBar ----
easeInOutScrollBar.setOrientation(Adjustable.HORIZONTAL);
easeInOutScrollBar.setBlockIncrement(1);
add(easeInOutScrollBar, "cell 1 1");
//---- startButton ----
startButton.setText("Start");
startButton.addActionListener(e -> start());
add(startButton, "cell 0 2");
// JFormDesigner - End of component initialization //GEN-END:initComponents
}
// JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
private JScrollBar linearScrollBar;
private JScrollBar easeInOutScrollBar;
private JButton startButton;
// JFormDesigner - End of variables declaration //GEN-END:variables
}

View File

@@ -0,0 +1,62 @@
JFDML JFormDesigner: "7.0.2.0.298" Java: "14.0.2" encoding: "UTF-8"
new FormModel {
contentType: "form/swing"
root: new FormRoot {
auxiliary() {
"JavaCodeGenerator.defaultVariableLocal": true
}
add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) {
"$layoutConstraints": "ltr,insets dialog,hidemode 3"
"$columnConstraints": "[fill][grow,fill]"
"$rowConstraints": "[][][]"
} ) {
name: "this"
add( new FormComponent( "javax.swing.JLabel" ) {
name: "label1"
"text": "Linear:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 0"
} )
add( new FormComponent( "javax.swing.JScrollBar" ) {
name: "linearScrollBar"
"orientation": 0
"blockIncrement": 1
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 0"
} )
add( new FormComponent( "javax.swing.JLabel" ) {
name: "label2"
"text": "Ease in out:"
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 1"
} )
add( new FormComponent( "javax.swing.JScrollBar" ) {
name: "easeInOutScrollBar"
"orientation": 0
"blockIncrement": 1
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 1 1"
} )
add( new FormComponent( "javax.swing.JButton" ) {
name: "startButton"
"text": "Start"
auxiliary() {
"JavaCodeGenerator.variableLocal": false
}
addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "start", false ) )
}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
"value": "cell 0 2"
} )
}, new FormLayoutConstraints( null ) {
"location": new java.awt.Point( 0, 0 )
"size": new java.awt.Dimension( 415, 350 )
} )
}
}