events = null;
if (from.reverse)
@@ -307,33 +270,17 @@ public class AnimationState {
from.totalAlpha = 0;
for (int i = 0; i < timelineCount; i++) {
Timeline timeline = timelines[i];
- boolean fromSetup;
+ int mode = timelineMode[i];
float alpha;
- switch (timelineMode[i]) {
- case SUBSEQUENT -> {
- if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
- fromSetup = false;
- alpha = alphaMix;
- }
- case FIRST -> {
- fromSetup = true;
- alpha = alphaMix;
- }
- case HOLD_SUBSEQUENT -> {
- fromSetup = false;
- alpha = alphaHold;
- }
- case HOLD_FIRST -> {
- fromSetup = true;
- alpha = alphaHold;
- }
- default -> { // HOLD_MIX
- fromSetup = true;
+ if ((mode & HOLD) != 0) {
TrackEntry holdMix = timelineHoldMix[i];
- alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
- }
+ alpha = holdMix == null ? alphaHold : alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
+ } else {
+ if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
+ alpha = alphaMix;
}
from.totalAlpha += alpha;
+ boolean fromSetup = (mode & FIRST) != 0;
if (!shortestRotation && timeline instanceof RotateTimeline rotateTimeline) {
applyRotateTimeline(rotateTimeline, skeleton, applyTime, alpha, fromSetup, timelinesRotation, i << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline attachmentTimeline)
@@ -522,11 +469,6 @@ public class AnimationState {
current.mixingFrom = from;
from.mixingTo = current;
current.mixTime = 0;
-
- // Store the interrupted mix percentage.
- if (from.mixingFrom != null && from.mixDuration > 0)
- current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);
-
from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in.
}
@@ -623,7 +565,7 @@ public class AnimationState {
* {@link #setEmptyAnimations(float)}, or {@link #addEmptyAnimation(int, float, float)}. Mixing to an empty animation causes
* the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation
* transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of
- * 0 still needs to be applied one more time to mix out, so the the properties it was animating are reverted.
+ * 0 still needs to be applied one more time to mix out, so the properties it was animating are reverted.
*
* Mixing in is done by first setting an empty animation, then adding an animation using
* {@link #addAnimation(int, Animation, boolean, float)} with the desired delay (an empty animation has a duration of 0) and on
@@ -689,7 +631,6 @@ public class AnimationState {
entry.trackIndex = trackIndex;
entry.animation = animation;
entry.loop = loop;
- entry.holdPrevious = false;
entry.additive = false;
entry.reverse = false;
@@ -715,8 +656,8 @@ public class AnimationState {
entry.alpha = 1;
entry.mixTime = 0;
entry.mixDuration = last == null ? 0 : data.getMix(last.animation, animation);
- entry.interruptAlpha = 1;
entry.totalAlpha = 0;
+ entry.keepHold = false;
return entry;
}
@@ -756,40 +697,45 @@ public class AnimationState {
entry.timelineHoldMix.clear();
TrackEntry[] timelineHoldMix = entry.timelineHoldMix.setSize(timelinesCount);
ObjectSet propertyIds = this.propertyIds;
- boolean holdPrevious = false, add = entry.additive;
+ boolean add = entry.additive, keepHold = entry.keepHold;
TrackEntry to = entry.mixingTo;
- if (to != null) {
- if (to.additive)
- to = null;
- else
- holdPrevious = to.holdPrevious;
- }
+
outer:
for (int i = 0; i < timelinesCount; i++) {
Timeline timeline = timelines[i];
String[] ids = timeline.propertyIds;
boolean first = propertyIds.addAll(ids)
&& !(timeline instanceof DrawOrderFolderTimeline && propertyIds.contains(DrawOrderTimeline.propertyID));
- if (add && timeline.additive)
+
+ if (add && timeline.additive) {
timelineMode[i] = first ? FIRST : SUBSEQUENT;
- else if (!first)
- timelineMode[i] = holdPrevious ? HOLD_SUBSEQUENT : SUBSEQUENT;
- else if (holdPrevious)
- timelineMode[i] = HOLD_FIRST;
- else if (to == null || timeline.instant || !to.animation.hasTimeline(ids))
- timelineMode[i] = FIRST;
- else {
- for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
- if (next.animation.hasTimeline(ids)) continue;
- if (next.mixDuration > 0) {
- timelineMode[i] = HOLD_MIX;
- timelineHoldMix[i] = next;
- continue outer;
- }
- break;
- }
- timelineMode[i] = HOLD_FIRST;
+ continue;
}
+
+ for (TrackEntry from = entry.mixingFrom; from != null; from = from.mixingFrom) {
+ if (from.animation.hasTimeline(ids)) {
+ // An earlier entry on this track keys this property, isolating it from lower tracks.
+ timelineMode[i] = SUBSEQUENT;
+ continue outer;
+ }
+ }
+
+ // Hold if the next entry will overwrite this property.
+ int mode;
+ if (to == null || timeline.instant || (to.additive && timeline.additive) || !to.animation.hasTimeline(ids))
+ mode = first ? FIRST : SUBSEQUENT;
+ else {
+ mode = first ? HOLD_FIRST : HOLD;
+ // Find next entry that doesn't overwrite this property. Its mix fades out the hold, instead of it ending abruptly.
+ for (TrackEntry next = to.mixingTo; next != null; next = next.mixingTo) {
+ if ((next.additive && timeline.additive) || !next.animation.hasTimeline(ids)) {
+ if (next.mixDuration > 0) timelineHoldMix[i] = next;
+ break;
+ }
+ }
+ }
+ if (keepHold) mode = (mode & ~HOLD) | (timelineMode[i] & HOLD);
+ timelineMode[i] = mode;
}
}
@@ -871,12 +817,17 @@ public class AnimationState {
@Null TrackEntry previous, next, mixingFrom, mixingTo;
@Null AnimationStateListener listener;
int trackIndex;
- boolean loop, holdPrevious, additive, reverse, shortestRotation;
+ boolean loop, additive, reverse, shortestRotation, keepHold;
float eventThreshold, mixAttachmentThreshold, alphaAttachmentThreshold, mixDrawOrderThreshold;
float animationStart, animationEnd, animationLast, nextAnimationLast;
float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale;
- float alpha, mixTime, mixDuration, interruptAlpha, totalAlpha;
+ float alpha, mixTime, mixDuration, totalAlpha;
+ /** For each timeline:
+ * Bit 0, FIRST: 0 = mix from current pose, 1 = mix from setup pose. Timeline is first to set the property.
+ * Bit 1, HOLD: 0 = mix out using alphaMix, 1 = apply full alpha to prevent dipping. Timeline is first on its track to
+ * set the property and the next entry (mixingTo) also sets it. When held, timelineHoldMix's mix controls how the hold fades
+ * out (for 3+ entry chains where the chain eventually stops setting the property). */
final IntArray timelineMode = new IntArray();
final Array timelineHoldMix = new Array(true, 8, TrackEntry[]::new);
final FloatArray timelinesRotation = new FloatArray();
@@ -1167,9 +1118,8 @@ public class AnimationState {
/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
* {@link AnimationStateData#getMix(Animation, Animation)} based on the animation before this animation (if any).
*
- * A mix duration of 0 still needs to be applied one more time to mix out, so the the properties it was animating are
- * reverted. A mix duration of 0 can be set at any time to end the mix on the next {@link AnimationState#update(float)
- * update}.
+ * A mix duration of 0 still needs to be applied one more time to mix out, so the properties it was animating are reverted.
+ * A mix duration of 0 can be set at any time to end the mix on the next {@link AnimationState#update(float) update}.
*
* The mixDuration can be set manually rather than use the value from
* {@link AnimationStateData#getMix(Animation, Animation)}. In that case, the mixDuration can be set for a new
@@ -1232,25 +1182,6 @@ public class AnimationState {
return mixingTo;
}
- public void setHoldPrevious (boolean holdPrevious) {
- this.holdPrevious = holdPrevious;
- }
-
- /** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
- * of being mixed out.
- *
- * When mixing between animations that key the same property, if a lower track also keys that property then the value will
- * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
- * while the second animation mixes from 0% to 100%. Setting holdPrevious to true applies the first animation
- * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
- * keys the property, only when a higher track also keys the property.
- *
- * Snapping will occur if holdPrevious is true and this animation does not key all the same properties as the
- * previous animation. */
- public boolean getHoldPrevious () {
- return holdPrevious;
- }
-
public void setShortestRotation (boolean shortestRotation) {
this.shortestRotation = shortestRotation;
}
diff --git a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java
index 5e2e0775f..c81933e43 100644
--- a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java
+++ b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewer.java
@@ -211,10 +211,8 @@ public class SkeletonViewer extends ApplicationAdapter {
state.setEmptyAnimation(track, 0);
entry = state.addAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked(), 0);
entry.setMixDuration(ui.mixSlider.getValue());
- } else {
+ } else
entry = state.setAnimation(track, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());
- entry.setHoldPrevious(track > 0 && ui.holdPrevCheckbox.isChecked());
- }
entry.setAdditive(track > 0 && ui.addCheckbox.isChecked());
entry.setReverse(ui.reverseCheckbox.isChecked());
entry.setAlpha(ui.alphaSlider.getValue());
diff --git a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewerUI.java b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewerUI.java
index 6a098b179..da067aab0 100644
--- a/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewerUI.java
+++ b/spine-libgdx/spine-skeletonviewer/src/com/esotericsoftware/spine/SkeletonViewerUI.java
@@ -570,10 +570,7 @@ class SkeletonViewerUI {
if (current != null) {
loopCheckbox.setChecked(current.getLoop());
reverseCheckbox.setChecked(current.getReverse());
- if (track > 0) {
- addCheckbox.setChecked(current.getAdditive());
- holdPrevCheckbox.setChecked(current.getHoldPrevious());
- }
+ if (track > 0) addCheckbox.setChecked(current.getAdditive());
}
}
};