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) {
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 -> {
if (!playedPath.anyMore()) {
System.out.println("called");
if(ticksPassed.get() * speed / (100) < 1) {
ticksPassed.addAndGet(this.getAnimationPath().getInbetweens());
return;
}
KeyFrame next = this.getAnimationPath().getNext();
if(next == null) {
if(loop)
this.getAnimationPath().reset();
if (loop)
playedPath.reset();
else
((Timer) e.getSource()).stop();
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());
ticksPassed.set(0);

View File

@@ -33,9 +33,32 @@ public class AnimationPath extends ArrayList<KeyFrame> {
public AnimationPath(int 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
*/
public KeyFrame getNext() {
@@ -53,11 +76,36 @@ public class AnimationPath extends ArrayList<KeyFrame> {
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
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 true if there are any more frames coming
*/
public boolean anyMore() {
return this.animationIterator + 1 < this.size();
}
/**
* 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)
* @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
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));
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 java.awt.*;
public record KeyFrame(Point position, int width, int height) {
public KeyFrame(JComponent component) {
this(
new Point(component.getX(), component.getY()),
component.getBounds().width, component.getBounds().height);
public record KeyFrame(Point position, int width, int height, PathMethod pathMethod) {
public KeyFrame(Point position, int width, int height) {
this(position, width, height, PathMethod.LINEAR);
}
@Override
public String toString() {
return "KeyFrame{" +
"position=" + position +
", width=" + width +
", height=" + height +
'}';
public KeyFrame(JComponent component, PathMethod pathMethod) {
this(
new Point(component.getX(), component.getY()),
component.getBounds().width, component.getBounds().height, pathMethod);
}
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(OACTitleBar.class, new DesignFlags(OACColor.DARK_SECTION));
Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 20, 2);
//Design.DARK.border = new BevelBorder(BevelBorder.LOWERED, OACColor.DARK_BORDERS.getColor(), OACColor.DARK_SHADOW.getColor());
Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 8, 1);
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.JAnimatedPanel;
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 java.awt.*;
@@ -15,14 +18,20 @@ import java.awt.*;
public class AnimationTests {
@Test
public synchronized void testSimpleAnimatedPanel() throws InterruptedException {
JFrame frame = TestUtils.mockFrame();
DesignManager.setGlobalDesign(Design.DARK);
OACFrame frame = TestUtils.mockOacFrame();
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(100, 100), 400, 400));
animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath);
@@ -34,6 +43,6 @@ public class AnimationTests {
frame.setVisible(true);
wait(15000);
wait(10000);
}
}