Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java
#	src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java
This commit is contained in:
UnlegitDqrk
2026-02-11 23:28:53 +01:00
5 changed files with 314 additions and 87 deletions

View File

@@ -8,13 +8,11 @@ import javax.swing.*;
import java.util.concurrent.atomic.AtomicInteger;
public interface AnimatedComponent {
void setCurrentRun(Timer timer);
Timer getCurrentRun();
void setCurrentRun(Timer timer);
AnimationPath getAnimationPath();
void setAnimationPath(AnimationPath animationPath);
AnimationPath getAnimationPath();
void setBounds(int x, int y, int width, int height);
@@ -23,15 +21,15 @@ public interface AnimatedComponent {
this.setCurrentRun(new Timer(0, e -> {
if (ticksPassed.get() * speed / (100) < 1) {
if(ticksPassed.get() * speed / (100) < 1) {
ticksPassed.addAndGet(this.getAnimationPath().getInbetweens());
return;
}
KeyFrame next = this.getAnimationPath().getNext();
if (next == null) {
if (loop)
if(next == null) {
if(loop)
this.getAnimationPath().reset();
else
((Timer) e.getSource()).stop();
@@ -55,8 +53,8 @@ public interface AnimatedComponent {
}
default void stop() {
if (this.getCurrentRun() != null)
if (this.getCurrentRun().isRunning())
if(this.getCurrentRun() != null)
if(this.getCurrentRun().isRunning())
this.getCurrentRun().stop();
this.getAnimationPath().reset();

View File

@@ -13,8 +13,7 @@ import java.util.Arrays;
import java.util.Collection;
public class AnimationPath extends ArrayList<KeyFrame> {
@Getter
@Setter
@Getter @Setter
private int inbetweens;
private int animationIterator = 0;
@@ -34,11 +33,175 @@ 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, and increase the animation iterators
* @return next keyframe in order, depending on the current frame
*/
public KeyFrame getNext() {
if(this.subFrameIterator >= this.inbetweens) {
this.subFrameIterator = 0;
this.animationIterator++;
}
if(this.animationIterator >= this.size())
return null;
this.subFrameIterator++;
KeyFrame current = this.get(this.animationIterator);
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 = this.linear(subIterator);
CombinedPathMethod method;
KeyFrame.PathMethod
currentMethod = current.pathMethod(),
nextMethod = next.pathMethod();
// Translate EASE_IN_AND_OUT to its respective currently relevant counterpart
if(currentMethod.equals(KeyFrame.PathMethod.EASE_OUT) || currentMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
currentMethod = KeyFrame.PathMethod.EASE_OUT;
if(nextMethod.equals(KeyFrame.PathMethod.EASE_OUT) || nextMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
nextMethod = KeyFrame.PathMethod.EASE_OUT;
method = switch (currentMethod) {
case LINEAR -> switch (nextMethod) {
case LINEAR -> CombinedPathMethod.LINEAR;
case EASE_IN -> CombinedPathMethod.LINEAR_EASE_IN;
// Removed this case, as ease-out doesn't make sense right before a keyframe
// case EASE_OUT -> CombinedPathMethod.LINEAR_EASE_OUT;
default -> throw new IllegalStateException("Unexpected value: " + nextMethod);
};
// Removed this case, as ease-in doesn't make sense right after a keyframe
// case EASE_IN -> switch (nextMethod) {
// case LINEAR -> CombinedPathMethod.EASE_IN_LINEAR;
// case EASE_IN -> CombinedPathMethod.EASE_IN;
// case EASE_OUT -> CombinedPathMethod.EASE_OUT_LINEAR;
// default -> throw new IllegalStateException("Unexpected value: " + nextMethod);
// };
case EASE_OUT -> switch (nextMethod) {
case LINEAR -> CombinedPathMethod.EASE_OUT_LINEAR;
case EASE_IN -> CombinedPathMethod.EASE_OUT_AND_IN;
case EASE_OUT -> CombinedPathMethod.EASE_OUT;
default -> throw new IllegalStateException("Unexpected value: " + nextMethod);
};
default -> throw new IllegalStateException("Unexpected value: " + currentMethod);
};
double threshold = Math.min(current.transition(), next.transition());
boolean thresholdReached;
if(current.transition() < next.transition())
thresholdReached = transition > threshold;
else
thresholdReached = transition <= next.transition();
if(thresholdReached)
transition = 2 * transition - 1;
transition = switch (method) {
case LINEAR -> transition;
case LINEAR_EASE_IN -> thresholdReached ? easeIn(subIterator) : transition;
// case EASE_IN -> easeIn(subIterator);
case EASE_OUT -> easeOut(subIterator);
case EASE_OUT_LINEAR -> thresholdReached ? transition : easeOut(subIterator);
case EASE_OUT_AND_IN -> thresholdReached ? easeOut(subIterator) : easeIn(subIterator);
};
System.out.println(method + " " + transition + " - linear: " + linear(subIterator));
return inBetween(current, next, transition, thresholdReached);
}
/**
* Get next keyframe according to current path iterator, not depending on the current frame
* @return next keyframe in order, not depending on the current frame
*/
public KeyFrame getNextInOrder() {
int i = this.animationIterator + 1;
if(i >= this.size())
i--;
return this.get(i);
}
/**
* Get previous keyframe according to current path iterator, not depending on the current frame
* @return previous keyframe in order, not depending on the current frame
*/
public KeyFrame getPreviousInOrder() {
int i = this.animationIterator - 1;
if(i < 0)
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
*/
public void reset() {
this.subFrameIterator = 0;
this.animationIterator = 0;
}
/**
* Utility method needed to get in-betweens
*
* @param point point to multiply
* @param point point to multiply
* @param scalar scalar to multiply with
* @return scalar product point
*/
@@ -49,10 +212,10 @@ public class AnimationPath extends ArrayList<KeyFrame> {
return new Point(x, y);
}
/**
* Add two points together; also needed for in-betweens
*
* @param point augend
* @param point augend
* @param addend addend
* @return sum of both points
*/
@@ -62,8 +225,7 @@ public class AnimationPath extends ArrayList<KeyFrame> {
/**
* Subtracts one point from another; also needed for in-betweens
*
* @param point minuend
* @param point minuend
* @param subtrahend subtrahend
* @return sum of both points
*/
@@ -71,15 +233,36 @@ public class AnimationPath extends ArrayList<KeyFrame> {
return new Point(point.x - subtrahend.x, point.y - subtrahend.y);
}
/**
* Calculate point between both points
* @param p1 first point
* @param p2 second point
* @return point in the middle between both points
*/
private static Point middle(Point p1, Point p2) {
return new Point(
multiply(
add(p1, p2),
0.5
)
);
}
/**
* Find in-between with given scalar
*
* @param kf1 first frame
* @param kf2 next frame
* @param kf1 first frame
* @param kf2 next frame
* @param scalar factor (ideally between 0 and 1, representing 0% transition to 100% transition)
* @param overHalf if this inbetween is over halfway through the transition process
* @return in-between frame
*/
private static KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, double scalar) {
private static KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, double scalar, boolean overHalf) {
// create halfway marked point
if(overHalf)
kf1.position().setLocation(
middle(kf1.position(), kf2.position())
);
// position
Point difference = subtract(kf2.position(), kf1.position());
@@ -97,55 +280,36 @@ 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, kf2.pathMethod());
}
@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));
}
/**
* Get next keyframe according to current path iterator, depending on the current frame
*
* @return next keyframe in order, depending on the current frame
* Enumerator purely to describe two combined PathMethods, like a 2-dimensional PathMethod enum
*/
public KeyFrame getNext() {
if (this.subFrameIterator >= this.inbetweens) {
this.subFrameIterator = 0;
this.animationIterator++;
}
private enum CombinedPathMethod {
LINEAR,
LINEAR_EASE_IN,
// EASE_IN,
EASE_OUT,
EASE_OUT_LINEAR,
EASE_OUT_AND_IN;
if (this.animationIterator >= this.size())
return null;
this.subFrameIterator++;
KeyFrame current = this.get(this.animationIterator);
KeyFrame next = this.getNextInOrder();
// How far the transition should be finished
double transition = (double) this.subFrameIterator / this.inbetweens;
return inBetween(current, next, transition);
}
/**
* Get next keyframe according to current path iterator, not depending on the current frame
*
* @return next keyframe in order, not depending on the current frame
*/
public KeyFrame getNextInOrder() {
int i = this.animationIterator + 1;
if (i >= this.size())
i--;
return this.get(i);
}
/**
* Reset animation path iterator to start
*/
public void reset() {
this.subFrameIterator = 0;
this.animationIterator = 0;
}
}

View File

@@ -7,19 +7,77 @@ 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);
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}
* @param position positional vector (where is the object at this moment?)
* @param width first transformational vector (how wide is the object at this moment)
* @param height second transformational vector (how tall is the object at this moment)
* @param pathMethod how the path from this KeyFrame to the next should look like
* @param transition adds to pathMethod in how much of the path should be defined by this decision (default 0% for linear and 50% for all other methods)
*/
public record KeyFrame(Point position, int width, int height, PathMethod pathMethod, double transition) {
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
* @param position positional vector (where is the object at this moment?)
* @param width first transformational vector (how wide is the object at this moment)
* @param height second transformational vector (how tall is the object at this moment)
* @param pathMethod how the path from this KeyFrame to the next should look like
*/
public KeyFrame(Point position, int width, int height, PathMethod pathMethod) {
this(position, width, height, pathMethod, pathMethod.equals(PathMethod.LINEAR) ? 0 : 0.5);
}
@Override
public String toString() {
return "KeyFrame{" +
"position=" + position +
", width=" + width +
", height=" + height +
'}';
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
* Defaults pathMethod to linear and transition percentage to 0%
* @param position positional vector (where is the object at this moment?)
* @param width first transformational vector (how wide is the object at this moment)
* @param height second transformational vector (how tall is the object at this moment)
*/
public KeyFrame(Point position, int width, int height) {
this(position, width, height, PathMethod.LINEAR, 0);
}
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
* @param component component that contains necessary vectors
* @param pathMethod how the path from this KeyFrame to the next should look like
* @param transition adds to pathMethod in how much of the path should be defined by this decision (default 50%)
*/
public KeyFrame(JComponent component, PathMethod pathMethod, double transition) {
this(
new Point(component.getX(), component.getY()),
component.getBounds().width, component.getBounds().height, pathMethod, transition);
}
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
* @param component component that contains necessary vectors
* @param pathMethod how the path from this KeyFrame to the next should look like
*/
public KeyFrame(JComponent component, PathMethod pathMethod) {
this(
new Point(component.getX(), component.getY()),
component.getBounds().width, component.getBounds().height, pathMethod,
pathMethod.equals(PathMethod.LINEAR) ? 0 : 0.5);
}
/**
* Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
* Defaults pathMethod to linear and transition percentage to 50%
* @param component component that contains necessary vectors
*/
public KeyFrame(JComponent component) {
this(component, PathMethod.LINEAR);
}
public enum PathMethod {
LINEAR,
EASE_IN,
EASE_OUT,
EASE_IN_AND_OUT
}
}

View File

@@ -41,9 +41,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);
@@ -30,10 +39,10 @@ public class AnimationTests {
frame.add(animatedPanel);
animatedPanel.play(10, true);
animatedPanel.play(5, true);
frame.setVisible(true);
wait(15000);
wait(10000);
}
}