dorkbox.tweenengine.BaseTween Maven / Gradle / Ivy
Show all versions of TweenEngine Show documentation
/*
* Copyright 2012 Aurelien Ribon
* Copyright 2015 dorkbox, llc
*
* 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
*
* http://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 dorkbox.tweenengine;
import java.util.Arrays;
/**
* BaseTween is the base class of Tween and Timeline. It defines the iteration engine used to play animations for any number of times,
* and in any direction, at any speed.
*
*
* It is responsible for calling the different callbacks at the right moments, and for making sure that every callbacks are triggered,
* even if the update engine gets a big delta time at once.
*
*
* @see Tween
* @see Timeline
* @author Aurelien Ribon | http://www.aurelienribon.com/
* @author dorkbox, llc
*/
@SuppressWarnings({"ForLoopReplaceableByForEach", "WeakerAccess", "unused", "ResultOfMethodCallIgnored"})
public
abstract class BaseTween {
private static volatile long lightSyncObject = System.nanoTime();
/**
* Only on public methods.
*
* Flushes the visibility of all tween fields from the cache for access/use from different threads.
*
* This does not block and does not prevent race conditions.
*
* @return the last time (in nanos) that the field modifications were flushed
*/
static long flushRead() {
return lightSyncObject;
}
/**
* Only on public methods.
*
* Flushes the visibility of all tween field modifications from the cache for access/use from different threads.
*
* This does not block and does not prevent race conditions.
*/
static void flushWrite() {
lightSyncObject = System.nanoTime();
}
// if there is a DELAY, the tween will remain inside "START" until it's finished with the delay
protected static final int INVALID = 0;
protected static final int START = 1;
protected static final int RUN = 2;
protected static final int FINISHED = 3;
public static final UpdateAction> NULL_ACTION = new UpdateAction
* If you don't use a TweenManager, you may want to call {@link BaseTween#free()} to reuse the object later.
*/
public
boolean isFinished() {
flushRead();
return isFinished__();
}
/**
* doesn't sync on anything.
*
* Returns true if the Timeline/Tween is finished (i.e. if the tween has reached its end or has been killed). A tween may be restarted
* by a timeline when there is a direction change in the timeline.
*
* If you don't use a TweenManager, you may want to call {@link BaseTween#free()} to reuse the object later.
*/
final
boolean isFinished__() {
return state == FINISHED || isCanceled;
}
/**
* Returns true if the tween automatically reverse when complete.
*/
public
boolean canAutoReverse() {
flushRead();
return canAutoReverse;
}
/**
* Returns true if the tween or timeline is currently paused.
*/
public
boolean isPaused() {
flushRead();
return isPaused;
}
// -------------------------------------------------------------------------
// User Data
// -------------------------------------------------------------------------
/**
* Attaches an object to this tween or timeline. It can be useful in order
* to retrieve some data from a TweenCallback.
*
* @param data Any kind of object.
*
* @return The current tween or timeline, for chaining instructions.
*/
@SuppressWarnings("unchecked")
public
T setUserData(final Object data) {
userData = data;
flushWrite();
return (T) this;
}
/**
* Gets the attached data, or null if none.
*/
@SuppressWarnings("unchecked")
public
T getUserData() {
flushRead();
return (T) userData;
}
// -------------------------------------------------------------------------
// Abstract API
// -------------------------------------------------------------------------
protected abstract
boolean containsTarget(final Object target);
protected abstract
boolean containsTarget(final Object target, final int tweenType);
/**
* Updates a timeline's children. Only called during State.RUN
*/
protected abstract
void update(final boolean updateDirection, final float delta);
/**
* Forces a Timeline/Tween to have it's start/target values
*
* @param updateDirection direction in which the force is happening. Affects children iteration order (timelines) and start/target
* values (tweens)
* @param updateValue this is the start (true) or end/target (false) to set the tween to.
*/
protected abstract
void setValues(final boolean updateDirection, final boolean updateValue);
/**
* Sets the tween or timeline to a specific point in time based on it's duration + delays. Callbacks are not notified and the change is
* immediate.
* For example:
*
* - setProgress(0F, true) : set it to the starting position just after the start delay in the forward direction
* - setProgress(.5F, true) : set it to the middle position in the forward direction
* - setProgress(.5F, false) : set it to the middle position in the reverse direction
* - setProgress(1F, false) : set it to the end position in the reverse direction
*
*
* Caveat: If the timeline/tween is set to end in reverse, and it CANNOT go in reverse, then it will end up in the finished state
* (end position). If the timeline/tween is in repeat mode then it will end up in the same position if it was going forwards.
*
* @param percentage the percentage (of it's duration) from 0-1, that the tween/timeline be set to
* @param direction sets the direction of the timeline when it updates next: forwards (true) or reverse (false).
*/
public
void setProgress(final float percentage, final boolean direction) {
if (percentage < -0.0F || percentage > 1.0F) {
throw new RuntimeException("Cannot set the progress <0 or >1");
}
//flushRead(); // synchronize takes care of this
// have to SAVE all of the callbacks (to stop all from executing).
// ALSO have to prevent anyone from updating/changing callbacks while this is occurring.
synchronized (TEMP_EMPTY) {
// always have to reset, because of issues with delays and repetitions. (also sets the direction to "forwards")
reset();
// how much time is represented by the delta in percentage of time?
final float duration = this.duration;
final float percentageValue = duration * percentage;
final float adjustmentTime;
// Caveat: If the timeline/tween is set to end in reverse, and it CANNOT go in reverse, then it will end up in the finished/end position
// if we specify to "go in reverse" and we are in a "repeat" mode (instead of a "flip-to-reverse" mode), then just pretend we
// specified to "go forwards".
boolean goesReverse = !direction && canAutoReverse;
if (goesReverse) {
// we want the tween/timeline in the REVERSE state when finished, so the next delta update will move it in that direction
// to do this, we "wrap around" the timeline/tween times to the correct time, in a single update.
final float timeSpentToGetToEnd = duration + startDelay;
final float timeSpentInReverseFromEnd = (duration - percentageValue);
adjustmentTime = timeSpentToGetToEnd + timeSpentInReverseFromEnd;
} else {
// we just go from the absolute start (including the delay) to where we should end up
adjustmentTime = percentageValue + startDelay;
}
TweenCallback[] forwards_Begin_saved = forwards_Begin;
TweenCallback[] forwards_Start_saved = forwards_Start;
TweenCallback[] forwards_End_saved = forwards_End;
TweenCallback[] forwards_Complete_saved = forwards_Complete;
TweenCallback[] reverse_Begin_saved = reverse_Begin;
TweenCallback[] reverse_Start_saved = reverse_Start;
TweenCallback[] reverse_End_saved = reverse_End;
TweenCallback[] reverse_Complete_saved = reverse_Complete;
forwards_Begin = TEMP_EMPTY;
forwards_Start = TEMP_EMPTY;
forwards_End = TEMP_EMPTY;
forwards_Complete = TEMP_EMPTY;
reverse_Begin = TEMP_EMPTY;
reverse_Start = TEMP_EMPTY;
reverse_End = TEMP_EMPTY;
reverse_Complete = TEMP_EMPTY;
// update by the timeline/tween this amount (always starting from "scratch"). It will automatically end up in the correct direction.
update__(adjustmentTime);
// have to RESTORE all of the callbacks
forwards_Begin = forwards_Begin_saved;
forwards_Start = forwards_Start_saved;
forwards_End = forwards_End_saved;
forwards_Complete = forwards_Complete_saved;
reverse_Begin = reverse_Begin_saved;
reverse_Start = reverse_Start_saved;
reverse_End = reverse_End_saved;
reverse_Complete = reverse_Complete_saved;
}
// flushWrite(); // synchronize takes care of this
}
// -------------------------------------------------------------------------
// Protected API
// -------------------------------------------------------------------------
protected
void initializeValues() {
}
/**
* Kills every tweens associated to the given target. Will also kill every timelines containing a tween associated to the given target.
*
* @return true if the target was killed, false if we do not contain the target, and it was not killed
*/
protected
boolean killTarget(final Object target) {
if (containsTarget(target)) {
cancel();
return true;
}
return false;
}
/**
* Kills every tweens associated to the given target and tween type. Will also kill every timelines containing a tween associated to
* the given target and tween type.
*
* @return true if the target was killed, false if we do not contain the target, and it was not killed
*/
protected
boolean killTarget(final Object target, final int tweenType) {
if (containsTarget(target, tweenType)) {
cancel();
return true;
}
return false;
}
/**
* Adjust the tween for when repeat + auto-reverse is used
*
* @param newDirection the new direction for all children
*/
protected
void adjustForRepeat_AutoReverse(final boolean newDirection) {
state = START;
if (newDirection) {
currentTime = 0;
}
else {
currentTime = duration;
}
}
/**
* Adjust the current time (set to the start value for the tween) and change state to DELAY.
*
* For timelines, this also changes what the current tween is (for when iterating over tweens)
*
* @param newDirection the new direction for all children
*/
protected
void adjustForRepeat_Linear(final boolean newDirection) {
state = START;
if (newDirection) {
currentTime = 0;
}
else {
currentTime = duration;
}
}
// -------------------------------------------------------------------------
// Update engine
// -------------------------------------------------------------------------
/**
* Updates the tween or timeline state and values.
*
* You may want to use a TweenManager to update objects for you.
*
* Slow motion, fast motion and backward play can be easily achieved by tweaking this delta time.
*
* Multiply it by -1 to play the animation backward, or by 0.5 to play it twice-as-slow than its normal speed.
*
*
* The tween manager doesn't call this method, it correctly calls updateState + updateValues on timeline/tweens
*
* Copyright dorkbox, llc
*
* @param delta the time in SECONDS that has elapsed since the last update
*
* @return true if this tween/timeline is finished (STATE = FINISHED)
*/
// this method was completely rewritten.
@SuppressWarnings({"unchecked", "Duplicates", "ConstantConditions"})
public final
float update(float delta) {
flushRead();
float v = update__(delta);
flushWrite();
return v;
}
/**
* doesn't sync on anything.
*
* Updates the tween or timeline state and values.
*
* You may want to use a TweenManager to update objects for you.
*
* Slow motion, fast motion and backward play can be easily achieved by tweaking this delta time.
*
* Multiply it by -1 to play the animation backward, or by 0.5 to play it twice-as-slow than its normal speed.
*
*
* The tween manager doesn't call this method, it correctly calls updateState + updateValues on timeline/tweens
*
* Copyright dorkbox, llc
*
* @param delta the time in SECONDS that has elapsed since the last update
*
* @return the amount of time remaining (this is the amount of delta that wasn't processed)
*/
// this method was completely rewritten.
@SuppressWarnings({"unchecked", "Duplicates", "ConstantConditions"})
protected final
float update__(float delta) {
isDuringUpdate = true;
if (isPaused || isCanceled) {
return delta;
}
if (isInAutoReverse) {
delta = -delta;
}
// the INITIAL, incoming delta from the app, will be positive or negative.
// Specifically check for +0.0F so that -0.0F will let us go in reverse
boolean direction = delta >= +0.0F;
this.direction = direction;
final float duration = this.duration;
/*
* DELAY - (delay) initial start delay, only happens once, during init
* R.DELAY - (repeatDelay) delay between repeat iterations, if there are more than one.
*
* there are two modes for repeat. LINEAR and AUTO_REVERSE
*
* LINEAR:
* BEGIN COMPLETE
* START END START END
* v v v v
* |---[DELAY]----[XXXXXXXXXX]->>-[R.DELAY]-->>--[XXXXXXXXXX]
*
*
* AUTO_REVERSE
* BEGIN COMPLETE
* START END
* v v
* |---[DELAY]----[XXXXXXXXXX]──────────-─────╮
* ╭╴ [XXXXXXXXXX]-<<-[R.DELAY] <─╯
* │ ^ ^
* │ bEND bSTART
* │ bCOMPLETE bBEGIN
* │
* ╰╴> [R.DELAY]->>-[XXXXXXXXXX] ╶╮
* ╭╴ [XXXXXXXXXX]-<<-[R.DELAY] <─╯
* ╰╴> [R.DELAY]->>-[XXXXXXXXXX] ...
*
* Time must "cross the finish line" in order for the tween to be considered finished.
*/
// The LAST tween (in a timeline) that was modified is what keeps track of "overflow" of time, which is when an animation runs
// longer that the tween duration. This is necessary in order to accurately reverse the animation and have the correct delays
// FORWARDS: 0 > time <= duration
// REVERSE: 0 >= time < duration (reverse always goes from duration -> 0)
startEventCallback.onEvent(this);
do {
float newTime = currentTime + delta;
if (direction) {
// {FORWARDS}
//
// FORWARDS: 0 > time <= duration
switch (state) {
case START: {
if (newTime <= 0.0F) {
// still in start delay
currentTime = newTime;
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
currentTime = 0.0F;
if (canTriggerBeginEvent) {
canTriggerBeginEvent = false;
// initialize during start (but after delay), so that it's at the same point in either direction
if (!isInitialized) {
isInitialized = true;
initializeValues();
}
final TweenCallback[] callbacks = this.forwards_Begin;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.BEGIN, this);
}
}
final TweenCallback[] callbacks = this.forwards_Start;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.START, this);
}
// goto next state
state = RUN;
// -- update is REVERSE so that the FIRST tween data takes priority, if there are
// multiple tweens that have the same target
setValues(REVERSE, START_VALUES);
// adjust the delta so that it is shifted based on the length of (previous) iteration
delta = newTime;
// FALLTHROUGH
}
case RUN: {
if (newTime <= duration) {
// still in running forwards
currentTime = newTime;
update(FORWARDS, delta);
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
state = FINISHED;
currentTime = duration;
final int repeatCountStack = repeatCount;
////////////////////////////////////////////
////////////////////////////////////////////
// 1) we are done running completely
// 2) we flip to auto-reverse repeat mode
// 3) we are in linear repeat mode
if (repeatCountStack == 0) {
// {FORWARDS}{FINISHED}
// -- update is REVERSE so that the FIRST tween data takes priority, if there are
// multiple tweens that have the same target
// "instant" tweens (duration 0) cannot trigger a set-to-startpoint (since they are always [enabled] while
// running). They are [disabled] by their parent timeline when the parent reaches the end of it's duration
// in the FORWARDS direction, this doesn't matter, but in REVERSE, it does.
setValues(REVERSE, TARGET_VALUES);
final TweenCallback[] callbacks = this.forwards_End;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.END, this);
}
final TweenCallback[] callbacks2 = this.forwards_Complete;
for (int i = 0, n = callbacks2.length; i < n; i++) {
callbacks2[i].onEvent(TweenCallback.Events.COMPLETE, this);
}
// don't do this, because it will xfer to the next tween (if a timeline), or will get added in the FINISHED
// case (if not a timeline, to record "overflow" of time)
// currentTime = newTime;
// we're done going forwards
canTriggerBeginEvent = true;
isInAutoReverse = false;
// have to reset our repeat count, so outside repeats will start us in the correct state
repeatCount = repeatCountOrig;
isDuringUpdate = false;
endEventCallback.onEvent(this);
// return the time that is remaining (the remaining amount of delta that wasn't processed)
return newTime - duration;
}
else {
// must always update all of the children
update(FORWARDS, delta);
if (repeatCountStack > 0) {
// -1 means repeat forever
repeatCount--;
}
final TweenCallback[] callbacks = this.forwards_End;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.END, this);
}
if (canAutoReverse) {
// {FORWARDS}{AUTO_REVERSE}
final TweenCallback[] callbacks2 = this.forwards_Complete;
for (int i = 0, n = callbacks2.length; i < n; i++) {
callbacks2[i].onEvent(TweenCallback.Events.COMPLETE, this);
}
// we're done going forwards
canTriggerBeginEvent = true;
isInAutoReverse = !isInAutoReverse; // if we are NOT in autoReverse, then "isInAutoReverse" is true if we reverse
// make sure any checks after this returns accurately reflect the correct REVERSE direction
direction = REVERSE;
// any extra time (what's left in delta) will be applied/calculated on the next loop around
adjustForRepeat_AutoReverse(REVERSE);
currentTime += repeatDelay;
// because we always continue the loop, we must adjust the delta so that it is shifted (in REVERSE)
// delta = newTime - duration;
// delta = -delta
delta = -newTime + duration;
// loop to new state
continue;
}
else {
// {FORWARDS}{LINEAR}
isInAutoReverse = false;
// any extra time (what's left in delta) will be applied/calculated on the next loop around
adjustForRepeat_Linear(FORWARDS);
// because we always continue the loop, we must adjust the delta so that it is shifted
delta = newTime - duration;
currentTime = -repeatDelay + delta;
// loop to new state
continue;
}
}
}
case FINISHED: {
if (newTime <= 0.0F || newTime > duration) {
// still in the "finished" state, and haven't been reversed somewhere
currentTime = newTime;
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
// restart the timeline, since we've had our time adjusted to a point where we are running again.
state = START;
update(FORWARDS, delta);
// loop to new state
continue;
}
default: {
throw new RuntimeException("Unexpected state!! '" + state + "'");
}
}
//
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
else {
// {REVERSE}
//
// REVERSE: 0 >= time < duration (reverse always goes from duration -> 0)
switch (state) {
case START: {
if (newTime >= duration) {
// still in delay
currentTime = newTime;
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
currentTime = duration;
if (canTriggerBeginEvent) {
canTriggerBeginEvent = false;
// initialize during start (but after delay), so that it's at the same point in either direction
if (!isInitialized) {
isInitialized = true;
initializeValues();
}
final TweenCallback[] callbacks = this.reverse_Begin;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.BACK_BEGIN, this);
}
}
final TweenCallback[] callbacks = this.reverse_Start;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.BACK_START, this);
}
// goto next state
state = RUN;
// -- update is FORWARDS so that the LAST tween data takes priority, if there are
// multiple tweens that have the same target
// this is opposite of the logic in FORWARDS.START
setValues(FORWARDS, TARGET_VALUES);
// adjust the delta so that it is shifted based on the length of (previous) iteration
delta = -(duration - newTime);
// FALLTHROUGH
}
case RUN: {
if (newTime >= 0.0F) {
// still in running reverse
currentTime = newTime;
update(REVERSE, delta);
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
state = FINISHED;
currentTime = 0.0F;
final int repeatCountStack = repeatCount;
////////////////////////////////////////////
////////////////////////////////////////////
// 1) we are done running completely
// 2) we flip to auto-reverse
// 3) we are in linear repeat mode
if (repeatCountStack == 0) {
// {REVERSE}{FINISHED}
// set the "start" values, backwards because values are relative to forwards
// -- update is FORWARDS so that the LAST tween data takes priority, if there are
// multiple tweens that have the same target
if (duration <= 0.000001F) {
// "instant" tweens (duration 0) cannot trigger a set-to-startpoint (since they are always [enabled] while
// running). They are [disabled] by their parent timeline when the parent reaches the end of it's duration
// This is why it's always set to target value (even though it's reverse)
setValues(FORWARDS, TARGET_VALUES);
}
else {
// set the "start" values, flipped because we are in reverse
setValues(FORWARDS, START_VALUES);
}
final TweenCallback[] callbacks = this.reverse_End;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.BACK_END, this);
}
final TweenCallback[] callbacks2 = this.reverse_Complete;
for (int i = 0, n = callbacks2.length; i < n; i++) {
callbacks2[i].onEvent(TweenCallback.Events.BACK_COMPLETE, this);
}
// don't do this, because it will xfer to the next tween (if a timeline), or will get added in the FINISHED
// case (if not a timeline, to record "overflow" of time)
// currentTime = newTime;
// we're done going reverse
canTriggerBeginEvent = true;
isInAutoReverse = false;
// have to reset our repeat count, so outside repeats will start us in the correct state
repeatCount = repeatCountOrig;
isDuringUpdate = false;
endEventCallback.onEvent(this);
// return the time that is remaining (the remaining amount of delta that wasn't processed)
return newTime;
}
else {
// must always update all of the children
update(REVERSE, delta);
if (repeatCountStack > 0) {
// -1 means repeat forever
repeatCount--;
}
final TweenCallback[] callbacks = this.reverse_End;
for (int i = 0, n = callbacks.length; i < n; i++) {
callbacks[i].onEvent(TweenCallback.Events.BACK_END, this);
}
if (canAutoReverse) {
// {REVERSE}{AUTO_REVERSE}
final TweenCallback[] callbacks2 = this.reverse_Complete;
for (int i = 0, n = callbacks2.length; i < n; i++) {
callbacks2[i].onEvent(TweenCallback.Events.BACK_COMPLETE, this);
}
// we're done going forwards
canTriggerBeginEvent = true;
isInAutoReverse = !isInAutoReverse; // if we are NOT in autoReverse, then "isInAutoReverse" is true if we reverse
// make sure any checks after this returns accurately reflect the correct FORWARDS direction
direction = FORWARDS;
// any extra time (what's left in delta) will be applied/calculated on the next loop around
adjustForRepeat_AutoReverse(FORWARDS);
currentTime -= repeatDelay;
// because we always continue the loop, we must adjust the delta so that it is shifted (in FORWARDS)
// delta = newTime;
// delta = -delta
delta = -newTime;
// loop to new state
continue;
}
else {
// {REVERSE}{LINEAR}
isInAutoReverse = false;
// any extra time (what's left in delta) will be applied/calculated on the next loop around
adjustForRepeat_Linear(REVERSE);
// because we always continue the loop, we must adjust the delta so that it is shifted
// delta = newTime;
currentTime = repeatDelay + newTime;
// loop to new state
continue;
}
}
}
case FINISHED: {
if (newTime < 0.0F || newTime >= duration) {
// still in the "finished" state, and haven't been reversed somewhere
currentTime = newTime;
isDuringUpdate = false;
endEventCallback.onEvent(this);
return 0.0F;
}
// restart the timeline, since we've had our time adjusted to a point where we are running again.
state = START;
update(REVERSE, delta);
// loop to new state
continue;
}
default: {
throw new RuntimeException("Unexpected state!! '" + state + "'");
}
}
//
}
} while (true);
}
}