mirror of
https://github.com/JFormDesigner/FlatLaf.git
synced 2026-02-11 06:27:13 -06:00
animator and cubic bezier easing classes added (for future animations) (issue #66)
This commit is contained in:
@@ -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 );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 )
|
||||
} )
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user