Fixed issue with looping animation pausing during transition, created new issue because of KeyFrame PathMethods (eases)

This commit is contained in:
Tinglyyy
2026-02-08 22:31:29 +01:00
parent a268926d0b
commit e61dbfa531
5 changed files with 134 additions and 35 deletions

View File

@@ -19,23 +19,34 @@ public interface AnimatedComponent {
default void play(double speed, boolean loop) { default void play(double speed, boolean loop) {
AtomicInteger ticksPassed = new AtomicInteger(); AtomicInteger ticksPassed = new AtomicInteger();
// cloning the animation path to not mess with the original,
// if an extra frame is to be added because loop is set to true
AnimationPath playedPath = this.getAnimationPath().clone();
if(loop)
playedPath.add(playedPath.getNext());
this.setCurrentRun(new Timer(0, e -> { this.setCurrentRun(new Timer(0, e -> {
if (!playedPath.anyMore()) {
System.out.println("called");
if(ticksPassed.get() * speed / (100) < 1) { if (loop)
ticksPassed.addAndGet(this.getAnimationPath().getInbetweens()); playedPath.reset();
return;
}
KeyFrame next = this.getAnimationPath().getNext();
if(next == null) {
if(loop)
this.getAnimationPath().reset();
else else
((Timer) e.getSource()).stop(); ((Timer) e.getSource()).stop();
return; return;
} }
if(ticksPassed.get() * speed / (100) < 1) {
ticksPassed.addAndGet(playedPath.getInbetweens());
return;
}
System.out.println("still executing");
KeyFrame next = playedPath.getNext();
this.setBounds(next.position().x, next.position().y, next.width(), next.height()); this.setBounds(next.position().x, next.position().y, next.width(), next.height());
ticksPassed.set(0); ticksPassed.set(0);

View File

@@ -33,9 +33,32 @@ public class AnimationPath extends ArrayList<KeyFrame> {
public AnimationPath(int inbetweens) { public AnimationPath(int inbetweens) {
this.inbetweens = inbetweens; this.inbetweens = inbetweens;
} }
/**
* Get next keyframe according to current path iterator, depending on the current frame without increasing the animation iterators
* @return next keyframe in order, depending on the current frame
*/
public KeyFrame peekNext() {
int tempSubIterator = this.subFrameIterator, tempAnimationIterator = this.animationIterator;
if(tempSubIterator >= this.inbetweens) {
tempSubIterator = 0;
tempAnimationIterator++;
}
if(tempAnimationIterator >= this.size())
return null;
tempSubIterator++;
KeyFrame current = this.get(tempAnimationIterator);
KeyFrame next = this.getNextInOrder();
return getKeyFrame(current, next, tempSubIterator);
}
/** /**
* Get next keyframe according to current path iterator, depending on the current frame * Get next keyframe according to current path iterator, depending on the current frame, and increase the animation iterators
* @return next keyframe in order, depending on the current frame * @return next keyframe in order, depending on the current frame
*/ */
public KeyFrame getNext() { public KeyFrame getNext() {
@@ -53,11 +76,36 @@ public class AnimationPath extends ArrayList<KeyFrame> {
KeyFrame next = this.getNextInOrder(); KeyFrame next = this.getNextInOrder();
return getKeyFrame(current, next, this.subFrameIterator);
}
private KeyFrame getKeyFrame(KeyFrame current, KeyFrame next, int subIterator) {
// How far the transition should be finished // How far the transition should be finished
double transition = (double) this.subFrameIterator / this.inbetweens; double transition = this.linear(subIterator);
return inBetween(current, next, transition); KeyFrame.PathMethod method;
// Translate EASE_IN_AND_OUT to its respective currently relevant counterpart
//TODO: non-linear keyframes can't behandled this way. This just makes it bug around
if(transition < 0.5)
method = current.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ?
KeyFrame.PathMethod.EASE_OUT : current.pathMethod();
else
method = next.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ?
KeyFrame.PathMethod.EASE_OUT : next.pathMethod();
// Else-case would be linear, which doesn't change anything
if(method.equals(KeyFrame.PathMethod.EASE_IN))
transition = this.easeIn(subIterator);
else if(method.equals(KeyFrame.PathMethod.EASE_OUT))
transition = this.easeOut(subIterator);
return inBetween(current, next, transition, method);
} }
@@ -74,6 +122,13 @@ public class AnimationPath extends ArrayList<KeyFrame> {
return this.get(i); return this.get(i);
} }
/**
* @return true if there are any more frames coming
*/
public boolean anyMore() {
return this.animationIterator + 1 < this.size();
}
/** /**
* Reset animation path iterator to start * Reset animation path iterator to start
*/ */
@@ -123,7 +178,7 @@ public class AnimationPath extends ArrayList<KeyFrame> {
* @param scalar factor (ideally between 0 and 1, representing 0% transition to 100% transition) * @param scalar factor (ideally between 0 and 1, representing 0% transition to 100% transition)
* @return in-between frame * @return in-between frame
*/ */
private static KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, double scalar) { private static KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, double scalar, KeyFrame.PathMethod method) {
// position // position
Point difference = subtract(kf2.position(), kf1.position()); Point difference = subtract(kf2.position(), kf1.position());
@@ -141,6 +196,23 @@ public class AnimationPath extends ArrayList<KeyFrame> {
height = Math.toIntExact(Math.round(kf1.height() + dH * scalar)); height = Math.toIntExact(Math.round(kf1.height() + dH * scalar));
return new KeyFrame(pos, width, height); return new KeyFrame(pos, width, height, method);
}
@Override
public AnimationPath clone() {
return new AnimationPath(this.inbetweens, new ArrayList<>(this));
}
private double linear(int subIterator) {
return (double) subIterator/2 / this.inbetweens;
}
private double easeIn(int subIterator) {
return this.linear(subIterator/2) * this.linear(subIterator/2);
}
public double easeOut(int subIterator) {
return Math.sqrt(this.linear(subIterator*2));
} }
} }

View File

@@ -7,19 +7,28 @@ package org.openautonomousconnection.oacswing.animated;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
public record KeyFrame(Point position, int width, int height) { public record KeyFrame(Point position, int width, int height, PathMethod pathMethod) {
public KeyFrame(JComponent component) {
this( public KeyFrame(Point position, int width, int height) {
new Point(component.getX(), component.getY()), this(position, width, height, PathMethod.LINEAR);
component.getBounds().width, component.getBounds().height);
} }
@Override public KeyFrame(JComponent component, PathMethod pathMethod) {
public String toString() { this(
return "KeyFrame{" + new Point(component.getX(), component.getY()),
"position=" + position + component.getBounds().width, component.getBounds().height, pathMethod);
", width=" + width + }
", height=" + height +
'}'; public KeyFrame(JComponent component) {
this(component, PathMethod.LINEAR);
}
public enum PathMethod {
LINEAR,
EASE_IN,
EASE_OUT,
EASE_IN_AND_OUT
} }
} }

View File

@@ -134,9 +134,7 @@ public class DesignManager {
Design.DARK.getElements().put(OACRadioButton.class, new DesignFlags(OACColor.DARK_BUTTON)); Design.DARK.getElements().put(OACRadioButton.class, new DesignFlags(OACColor.DARK_BUTTON));
Design.DARK.getElements().put(OACTitleBar.class, new DesignFlags(OACColor.DARK_SECTION)); Design.DARK.getElements().put(OACTitleBar.class, new DesignFlags(OACColor.DARK_SECTION));
Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 20, 2); Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 8, 1);
//Design.DARK.border = new BevelBorder(BevelBorder.LOWERED, OACColor.DARK_BORDERS.getColor(), OACColor.DARK_SHADOW.getColor());
DesignManager.getInstance().registerDesign(Design.DARK); DesignManager.getInstance().registerDesign(Design.DARK);
} }

View File

@@ -8,6 +8,9 @@ import org.junit.jupiter.api.Test;
import org.openautonomousconnection.oacswing.animated.AnimationPath; import org.openautonomousconnection.oacswing.animated.AnimationPath;
import org.openautonomousconnection.oacswing.animated.JAnimatedPanel; import org.openautonomousconnection.oacswing.animated.JAnimatedPanel;
import org.openautonomousconnection.oacswing.animated.KeyFrame; import org.openautonomousconnection.oacswing.animated.KeyFrame;
import org.openautonomousconnection.oacswing.component.OACFrame;
import org.openautonomousconnection.oacswing.component.design.Design;
import org.openautonomousconnection.oacswing.component.design.DesignManager;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -15,14 +18,20 @@ import java.awt.*;
public class AnimationTests { public class AnimationTests {
@Test @Test
public synchronized void testSimpleAnimatedPanel() throws InterruptedException { public synchronized void testSimpleAnimatedPanel() throws InterruptedException {
JFrame frame = TestUtils.mockFrame(); DesignManager.setGlobalDesign(Design.DARK);
OACFrame frame = TestUtils.mockOacFrame();
AnimationPath animationPath = new AnimationPath(50); AnimationPath animationPath = new AnimationPath(50);
animationPath.add(new KeyFrame(new Point(400, 400), 400, 400)); // This test was too simple
// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
// animationPath.add(new KeyFrame(new Point(400, 300), 400, 400));
// animationPath.add(new KeyFrame(new Point(100, 100), 400, 400));
// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
animationPath.add(new KeyFrame(new Point(400, 400), 400, 400, KeyFrame.PathMethod.EASE_OUT));
animationPath.add(new KeyFrame(new Point(400, 300), 400, 400)); animationPath.add(new KeyFrame(new Point(400, 300), 400, 400));
animationPath.add(new KeyFrame(new Point(100, 100), 400, 400));
animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath); JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath);
@@ -34,6 +43,6 @@ public class AnimationTests {
frame.setVisible(true); frame.setVisible(true);
wait(15000); wait(10000);
} }
} }