com.codename1.ui.animations.Motion Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.ui.animations;
import com.codename1.ui.plaf.UIManager;
import com.codename1.util.MathUtil;
/**
* Abstracts the notion of physical motion over time from a numeric location to
* another. This class can be subclassed to implement any motion equation for
* appropriate physics effects.
* This class relies on the System.currentTimeMillis() method to provide
* transitions between coordinates. The motion can be subclassed to provide every
* type of motion feel from parabolic motion to spline and linear motion. The default
* implementation provides a simple algorithm giving the feel of acceleration and
* deceleration.
*
* @author Shai Almog
*/
public class Motion {
private static boolean slowMotion;
// package protected for the resource editor
static final int LINEAR = 0;
static final int SPLINE = 1;
/**
* Allows debugging motion behavior by slowing motions down 50 fold, doesn't apply to friction motion
* @return the slowMotion
*/
public static boolean isSlowMotion() {
return slowMotion;
}
/**
* Allows debugging motion behavior by slowing motions down 50 fold, doesn't apply to friction motion
* @param aSlowMotion the slowMotion to set
*/
public static void setSlowMotion(boolean aSlowMotion) {
slowMotion = aSlowMotion;
}
int motionType;
private static final int FRICTION = 2;
private static final int DECELERATION = 3;
private static final int CUBIC = 4;
private static final int COLOR_LINEAR = 5;
private static final int EXPONENTIAL_DECAY = 6;
private int sourceValue;
private int destinationValue;
private int targetPosition;
private int duration;
private long startTime;
private double initVelocity, friction;
private int lastReturnedValue;
private int [] previousLastReturnedValue = new int[3];
private long[] previousLastReturnedValueTime = new long[3];
private long currentMotionTime = -1;
private long previousCurrentMotionTime = -1;
private float p0, p1, p2, p3;
/**
* Construct a point/destination motion
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
*/
protected Motion(int sourceValue, int destinationValue, int duration) {
this.sourceValue = sourceValue;
this.destinationValue = destinationValue;
this.duration = duration;
lastReturnedValue = sourceValue;
if(slowMotion) {
this.duration *= 50;
}
previousLastReturnedValue[0] = -1;
previousLastReturnedValueTime[0] = -1;
}
/**
* Sends the motion to the end time instantly which is useful for flushing an animation
*/
public void finish() {
if(!isFinished()) {
startTime = System.currentTimeMillis() - duration;
currentMotionTime = -1;
previousCurrentMotionTime = -1;
}
}
/**
* Construct a velocity motion
*
* @param sourceValue starting value
* @param initVelocity initial velocity
* @param friction degree of friction
*/
protected Motion(int sourceValue, float initVelocity, float friction) {
this.sourceValue = sourceValue;
this.initVelocity = initVelocity;
this.friction = friction;
duration = (int) ((Math.abs(initVelocity)) / friction);
previousLastReturnedValue[0] = -1;
previousLastReturnedValueTime[0] = -1;
}
protected Motion(int sourceValue, double initVelocity, double friction) {
this.sourceValue = sourceValue;
this.initVelocity = initVelocity;
this.friction = friction;
duration = (int) ((Math.abs(initVelocity)) / friction);
previousLastReturnedValue[0] = -1;
previousLastReturnedValueTime[0] = -1;
}
/**
* Creates a standard Cubic Bezier motion to implement functions such as ease-in/out etc.
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
* @param p0 argument to the bezier function
* @param p1 argument to the bezier function
* @param p2 argument to the bezier function
* @param p3 argument to the bezier function
* @return Motion instance
*/
public static Motion createCubicBezierMotion(int sourceValue, int destinationValue, int duration,
float p0, float p1, float p2, float p3) {
Motion m = new Motion(sourceValue, destinationValue, duration);
m.motionType = CUBIC;
m.p0 = p0;
m.p1 = p1;
m.p2 = p2;
m.p3 = p3;
return m;
}
/**
* Equivalent to createCubicBezierMotion with 0, 0.42, 0.58, 1.0 as arguments.
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
* @return Motion instance
*/
public static Motion createEaseInOutMotion(int sourceValue, int destinationValue, int duration) {
return createCubicBezierMotion(sourceValue, destinationValue, duration, 0, 0.42f, 0.58f, 1);
}
/**
* Equivalent to createCubicBezierMotion with 0f, 0.25f, 0.25f, 1 as arguments.
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
* @return Motion instance
*/
public static Motion createEaseMotion(int sourceValue, int destinationValue, int duration) {
return createCubicBezierMotion(sourceValue, destinationValue, duration, 0f, 0.25f, 0.25f, 1.0f);
}
/**
* Equivalent to createCubicBezierMotion with 0f, 0.42f, 1f, 1f as arguments.
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
* @return Motion instance
*/
public static Motion createEaseInMotion(int sourceValue, int destinationValue, int duration) {
return createCubicBezierMotion(sourceValue, destinationValue, duration, 0f, 0.42f, 1f, 1f);
}
/**
* Equivalent to createCubicBezierMotion with 0f, 0f, 0.58f, 1.0f as arguments.
*
* @param sourceValue starting value
* @param destinationValue destination value
* @param duration motion duration
* @return Motion instance
*/
public static Motion createEaseOutMotion(int sourceValue, int destinationValue, int duration) {
return createCubicBezierMotion(sourceValue, destinationValue, duration, 0f, 0f, 0.58f, 1.0f);
}
/**
* Creates a linear motion starting from source value all the way to destination value
*
* @param sourceValue the number from which we are starting (usually indicating animation start position)
* @param destinationValue the number to which we are heading (usually indicating animation destination)
* @param duration the length in milliseconds of the motion (time it takes to get from sourceValue to
* destinationValue)
* @return new motion object
*/
public static Motion createLinearMotion(int sourceValue, int destinationValue, int duration) {
Motion l = new Motion(sourceValue, destinationValue, duration);
l.motionType = LINEAR;
return l;
}
/**
* Creates a linear motion starting from source value all the way to destination value for a color value.
* Unlike a regular linear motion a color linear motion is shifted based on channels where red, green & blue
* get shifted separately.
*
* @param sourceValue the color from which we are starting
* @param destinationValue the destination color
* @param duration the length in milliseconds of the motion (time it takes to get from sourceValue to
* destinationValue)
* @return new motion object
*/
public static Motion createLinearColorMotion(int sourceValue, int destinationValue, int duration) {
Motion l = new Motion(sourceValue, destinationValue, duration);
l.motionType = COLOR_LINEAR;
return l;
}
/**
* Creates a spline motion starting from source value all the way to destination value
*
* @param sourceValue the number from which we are starting (usually indicating animation start position)
* @param destinationValue the number to which we are heading (usually indicating animation destination)
* @param duration the length in milliseconds of the motion (time it takes to get from sourceValue to
* destinationValue)
* @return new motion object
*/
public static Motion createSplineMotion(int sourceValue, int destinationValue, int duration) {
Motion spline = new Motion(sourceValue, destinationValue, duration);
spline.motionType = SPLINE;
return spline;
}
/**
* Creates a deceleration motion starting from source value all the way to destination value
*
* @param sourceValue the number from which we are starting (usually indicating animation start position)
* @param destinationValue the number to which we are heading (usually indicating animation destination)
* @param duration the length in milliseconds of the motion (time it takes to get from sourceValue to
* destinationValue)
* @return new motion object
*/
public static Motion createDecelerationMotion(int sourceValue, int destinationValue, int duration) {
Motion deceleration = new Motion(sourceValue, destinationValue, duration);
deceleration.motionType = DECELERATION;
return deceleration;
}
/**
* Creates a deceleration motion starting from the current position of another motion.
*
* @param motion the number from which we are starting (usually indicating animation start position)
* @param maxDestinationValue The farthest position to allow motion to go.
* @param maxDuration The longest that the duration is allowed to proceed for.
* @return new motion object
*/
public static Motion createDecelerationMotionFrom(Motion motion, int maxDestinationValue, int maxDuration) {
return createDecelerationMotion(
motion.lastReturnedValue,
motion.destinationValue < motion.sourceValue
? Math.min(motion.destinationValue, maxDestinationValue)
: Math.max(motion.destinationValue, maxDestinationValue),
(int)Math.min(maxDuration, motion.duration - (System.currentTimeMillis() - motion.startTime))
);
}
/**
* Creates a friction motion starting from source with initial speed and the friction
*
* @param sourceValue the number from which we are starting (usually indicating animation start position)
* @param maxValue the maximum value for the friction
* @param initVelocity the starting velocity
* @param friction the motion friction
* @return new motion object
*/
public static Motion createFrictionMotion(int sourceValue, int maxValue, float initVelocity, float friction) {
Motion frictionMotion = new Motion(sourceValue, initVelocity, friction);
frictionMotion.destinationValue = maxValue;
frictionMotion.motionType = FRICTION;
return frictionMotion;
}
public static Motion createExponentialDecayMotion(int sourceValue, int maxValue, double initVelocity, double timeConstant) {
Motion decayMotion = new Motion(sourceValue, initVelocity, timeConstant);
decayMotion.destinationValue = maxValue;
decayMotion.targetPosition = sourceValue + (int)(initVelocity * (double)UIManager.getInstance().getThemeConstant("DecayMotionScaleFactorInt", 950));
decayMotion.motionType = EXPONENTIAL_DECAY;
decayMotion.duration = (int)(6 * timeConstant);
return decayMotion;
}
/**
* Sets the start time to the current time
*/
public void start() {
startTime = System.currentTimeMillis();
}
/**
* Returns the current time within the motion relative to start time
*
* @return long value representing System.currentTimeMillis() - startTime
*/
public long getCurrentMotionTime() {
if(currentMotionTime < 0) {
return System.currentTimeMillis() - startTime;
}
return currentMotionTime;
}
/**
* Allows overriding the getCurrentMotionTime method value with a manual value
* to provide full developer control over animation speed/position.
*
* @param currentMotionTime the time in milliseconds for the motion.
*/
public void setCurrentMotionTime(long currentMotionTime) {
this.previousCurrentMotionTime = this.currentMotionTime;
this.currentMotionTime = currentMotionTime;
// workaround allowing the motion to be restarted when manually setting the current time
if(lastReturnedValue == destinationValue) {
lastReturnedValue = sourceValue;
}
}
public boolean isDecayMotion() {
return motionType == EXPONENTIAL_DECAY;
}
/**
* Sets the start time of the motion
*
* @param startTime the starting time
*/
public void setStartTime(long startTime) {
this.startTime = startTime;
}
/**
* Returns true if the motion has run its course and has finished meaning the current
* time is greater than startTime + duration.
*
* @return true if System.currentTimeMillis() > duration + startTime or the last returned value is the destination value
*/
public boolean isFinished() {
return getCurrentMotionTime() > duration || destinationValue == lastReturnedValue || (EXPONENTIAL_DECAY == motionType && previousLastReturnedValue[0] == lastReturnedValue);
}
private int getSplineValue() {
//make sure we reach the destination value.
if(isFinished()){
return destinationValue;
}
float totalTime = duration;
float currentTime = (int) getCurrentMotionTime();
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
currentTime = Math.min(currentTime, totalTime);
int p = Math.abs(destinationValue - sourceValue);
float centerTime = totalTime / 2;
float l = p / (centerTime * centerTime);
int x;
if (sourceValue < destinationValue) {
if (currentTime > centerTime) {
x = sourceValue + (int) (l * (-centerTime * centerTime + 2 * centerTime * currentTime -
currentTime * currentTime / 2));
} else {
x = sourceValue + (int) (l * currentTime * currentTime / 2);
}
} else {
currentTime = totalTime - currentTime;
if (currentTime > centerTime) {
x = destinationValue + (int) (l * (-centerTime * centerTime + 2 * centerTime * currentTime -
currentTime * currentTime / 2));
} else {
x = destinationValue + (int) (l * currentTime * currentTime / 2);
}
}
return x;
}
private int getCubicValue() {
//make sure we reach the destination value.
if(isFinished()){
return destinationValue;
}
float totalTime = duration;
float currentTime = (int) getCurrentMotionTime();
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
currentTime = Math.min(currentTime, totalTime);
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
float dis = Math.abs(destinationValue - sourceValue);
float p = currentTime / totalTime;
float a = (1 - p) * (1 - p) * (1 - p) * p0;
float b = 3 * (1 - p) * (1 - p) * p * p1;
float c = 3 * (1 - p) * p * p * p2;
float d = p * p * p * p3;
int current;
if (destinationValue > sourceValue) {
current = sourceValue + (int)((a + b + c + d) * dis);
} else {
int currentDis = (int)((a + b + c + d) * dis);
current = sourceValue - currentDis;
}
return current;
}
/**
* Returns the value for the motion for the current clock time.
* The value is dependent on the Motion type.
*
* @return a value that is relative to the source value
*/
public int getValue() {
if(currentMotionTime > -1 && startTime > getCurrentMotionTime()) {
return sourceValue;
}
previousLastReturnedValue[0] = previousLastReturnedValue[1];
previousLastReturnedValueTime[0] = previousLastReturnedValueTime[1];
previousLastReturnedValue[1] = previousLastReturnedValue[2];
previousLastReturnedValueTime[1] = previousLastReturnedValueTime[2];
previousLastReturnedValue[2] = lastReturnedValue;
previousLastReturnedValueTime[2] = previousCurrentMotionTime;
if (previousCurrentMotionTime < 0) {
previousCurrentMotionTime = getCurrentMotionTime();
}
switch(motionType) {
case SPLINE:
lastReturnedValue = getSplineValue();
break;
case CUBIC:
lastReturnedValue = getCubicValue();
break;
case FRICTION:
lastReturnedValue = getFriction();
break;
case DECELERATION:
lastReturnedValue = getRubber();
break;
case COLOR_LINEAR:
lastReturnedValue = getColorLinear();
break;
case EXPONENTIAL_DECAY:
lastReturnedValue = getExponentialDecay();
break;
default:
lastReturnedValue = getLinear();
break;
}
return lastReturnedValue;
}
/**
* Gets an approximation of the current velocity in pixels per millisecond.
*
*
NOTE: If {@link #countAvailableVelocitySamplingPoints()} <= 1, then this method will always output {@literal 0}.
* Therefore the output of this method only has meaning if {@link #countAvailableVelocitySamplingPoints()} > {@literal 0}
*
* @return Current velocity in pixels per millisecond.
* @since 8.0
*/
public double getVelocity() {
final long localCurrentMotionTime = getCurrentMotionTime();
final int lastReturnedValueLocal = lastReturnedValue;
double velocity = 0;
boolean firstIteration = true;
for (int i=2; i>= 0; i--) {
final long t = previousLastReturnedValueTime[i];
if (t <= 0 || localCurrentMotionTime == t) {
break;
}
final int valueAtT = previousLastReturnedValue[i];
final double spotVelocity = (lastReturnedValueLocal - valueAtT) / (double)(localCurrentMotionTime - t);
velocity = firstIteration ? spotVelocity : (velocity + spotVelocity)/2.0;
firstIteration = false;
}
return velocity;
}
/**
* Gets the number of sampling points that can be used by {@link #getVelocity()}. A minimum of 2 sampling
* points are required for the result of {@link #getVelocity()} to have any meaning.
*
* @since 8.0
* @return The number of sampling points that can be used by {@link #getVelocity()}.
*/
public int countAvailableVelocitySamplingPoints() {
int count = 1;
final long localCurrentMotionTime = getCurrentMotionTime();
for (int i=2; i>= 0; i--) {
final long t = previousLastReturnedValueTime[i];
if (t <= 0 || localCurrentMotionTime == t) {
break;
}
count++;
}
return count;
}
private int getLinear() {
//make sure we reach the destination value.
if(isFinished()){
return destinationValue;
}
float totalTime = duration;
float currentTime = (int) getCurrentMotionTime();
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
int dis = destinationValue - sourceValue;
int val = (int)(sourceValue + (currentTime / totalTime * dis));
if(destinationValue < sourceValue) {
return Math.max(destinationValue, val);
} else {
return Math.min(destinationValue, val);
}
}
private int getColorLinear() {
if(isFinished()){
return destinationValue;
}
float totalTime = duration;
float currentTime = (int) getCurrentMotionTime();
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
int sourceR = (sourceValue >> 16) & 0xff;
int destR = (destinationValue >> 16) & 0xff;
int sourceG = (sourceValue >> 8) & 0xff;
int destG = (destinationValue >> 8) & 0xff;
int sourceB = sourceValue & 0xff;
int destB = destinationValue & 0xff;
int disR = destR - sourceR;
int disG = destG - sourceG;
int disB = destB - sourceB;
int valR = (int)(sourceR + (currentTime / totalTime * disR));
int valG = (int)(sourceG + (currentTime / totalTime * disG));
int valB = (int)(sourceB + (currentTime / totalTime * disB));
if(destR < sourceR) {
valR = Math.max(destR, valR);
} else {
valR = Math.min(destR, valR);
}
if(destG < sourceG) {
valG = Math.max(destG, valG);
} else {
valG = Math.min(destG, valG);
}
if(destB < sourceB) {
valB = Math.max(destB, valB);
} else {
valB = Math.min(destB, valB);
}
return (((valR) << 16) & 0xff0000) | (((valG) << 8) & 0xff00) | (valB & 0xff);
}
private int getFriction() {
int time = (int) getCurrentMotionTime();
int retVal = 0;
retVal = (int)((Math.abs(initVelocity) * time) - (friction * (((double)time * time) / 2)));
if (initVelocity < 0) {
retVal *= -1;
}
retVal += (int) sourceValue;
if(destinationValue > sourceValue) {
return Math.min(retVal, destinationValue);
} else {
return Math.max(retVal, destinationValue);
}
}
private int getExponentialDecay() {
double elapsed = getCurrentMotionTime();
double timeConstant = friction;
double amplitude = targetPosition - sourceValue;
int position = (int)Math.round(targetPosition - amplitude * MathUtil.exp(-elapsed / timeConstant));
if(destinationValue > sourceValue) {
return Math.min(position, destinationValue);
} else {
return Math.max(position, destinationValue);
}
}
private int getRubber() {
if(isFinished()){
return destinationValue;
}
float totalTime = duration;
float currentTime = (int) getCurrentMotionTime();
if(currentMotionTime > -1) {
currentTime -= startTime;
totalTime -= startTime;
}
currentTime = Math.min(currentTime, totalTime);
int p = Math.abs(destinationValue - sourceValue);
float centerTime = totalTime/2;
float l = p / (centerTime * centerTime);
int x;
int dis = (int) (l * (-centerTime * centerTime + 2 * centerTime * currentTime -
currentTime * currentTime / 2));
if (sourceValue < destinationValue) {
x = Math.max(sourceValue, sourceValue + dis);
x = Math.min(destinationValue, x);
} else {
x = Math.min(sourceValue, sourceValue - dis);
x = Math.max(destinationValue, x);
}
return x;
}
/**
* The number from which we are starting (usually indicating animation start position)
*
* @return the source value
*/
public int getSourceValue() {
return sourceValue;
}
/**
* The number to which we will reach when the motion is finished
*
* @return the source value
*/
public int getDestinationValue() {
return destinationValue;
}
/**
* The number from which we are starting (usually indicating animation start position)
*
* @param sourceValue the source value
*/
public void setSourceValue(int sourceValue) {
this.sourceValue = sourceValue;
}
/**
* The value of System.currentTimemillis() when motion was started
*
* @return the start time
*/
protected long getStartTime() {
return startTime;
}
/**
* Returns the animation duration
*
* @return animation duration in milliseconds
*/
public int getDuration() {
return duration;
}
}