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; import java.util.concurrent.atomic.AtomicInteger;
public interface AnimatedComponent { public interface AnimatedComponent {
void setCurrentRun(Timer timer);
Timer getCurrentRun(); Timer getCurrentRun();
void setCurrentRun(Timer timer);
AnimationPath getAnimationPath();
void setAnimationPath(AnimationPath animationPath); void setAnimationPath(AnimationPath animationPath);
AnimationPath getAnimationPath();
void setBounds(int x, int y, int width, int height); void setBounds(int x, int y, int width, int height);

View File

@@ -13,8 +13,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
public class AnimationPath extends ArrayList<KeyFrame> { public class AnimationPath extends ArrayList<KeyFrame> {
@Getter @Getter @Setter
@Setter
private int inbetweens; private int inbetweens;
private int animationIterator = 0; private int animationIterator = 0;
@@ -34,75 +33,32 @@ public class AnimationPath extends ArrayList<KeyFrame> {
public AnimationPath(int inbetweens) { public AnimationPath(int inbetweens) {
this.inbetweens = inbetweens; this.inbetweens = inbetweens;
} }
/** /**
* Utility method needed to get in-betweens * 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
* @param point point to multiply
* @param scalar scalar to multiply with
* @return scalar product point
*/ */
private static Point multiply(Point point, double scalar) { public KeyFrame peekNext() {
int x = Math.toIntExact(Math.round(point.x * scalar)); int tempSubIterator = this.subFrameIterator, tempAnimationIterator = this.animationIterator;
int y = Math.toIntExact(Math.round(point.y * scalar));
return new Point(x, y); 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);
} }
/** /**
* Add two points together; also needed for in-betweens * Get next keyframe according to current path iterator, depending on the current frame, and increase the animation iterators
*
* @param point augend
* @param addend addend
* @return sum of both points
*/
private static Point add(Point point, Point addend) {
return new Point(point.x + addend.x, point.y + addend.y);
}
/**
* Subtracts one point from another; also needed for in-betweens
*
* @param point minuend
* @param subtrahend subtrahend
* @return sum of both points
*/
private static Point subtract(Point point, Point subtrahend) {
return new Point(point.x - subtrahend.x, point.y - subtrahend.y);
}
/**
* Find in-between with given scalar
*
* @param kf1 first frame
* @param kf2 next frame
* @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) {
// position
Point difference = subtract(kf2.position(), kf1.position());
Point pos = add(kf1.position(), multiply(difference, scalar));
// scale
int width, height;
int dW = kf2.width() - kf1.width();
int dH = kf2.height() - kf1.height();
width = Math.toIntExact(Math.round(kf1.width() + dW * scalar));
height = Math.toIntExact(Math.round(kf1.height() + dH * scalar));
return new KeyFrame(pos, width, height);
}
/**
* Get next keyframe according to current path iterator, depending on the current frame
*
* @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() {
@@ -120,16 +76,90 @@ 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);
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);
return inBetween(current, next, transition);
} }
/** /**
* Get next keyframe according to current path iterator, not depending on the current frame * 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 * @return next keyframe in order, not depending on the current frame
*/ */
public KeyFrame getNextInOrder() { public KeyFrame getNextInOrder() {
@@ -141,6 +171,26 @@ public class AnimationPath extends ArrayList<KeyFrame> {
return this.get(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 * Reset animation path iterator to start
*/ */
@@ -148,4 +198,118 @@ public class AnimationPath extends ArrayList<KeyFrame> {
this.subFrameIterator = 0; this.subFrameIterator = 0;
this.animationIterator = 0; this.animationIterator = 0;
} }
/**
* Utility method needed to get in-betweens
* @param point point to multiply
* @param scalar scalar to multiply with
* @return scalar product point
*/
private static Point multiply(Point point, double scalar) {
int x = Math.toIntExact(Math.round(point.x * scalar));
int y = Math.toIntExact(Math.round(point.y * scalar));
return new Point(x, y);
}
/**
* Add two points together; also needed for in-betweens
* @param point augend
* @param addend addend
* @return sum of both points
*/
private static Point add(Point point, Point addend) {
return new Point(point.x + addend.x, point.y + addend.y);
}
/**
* Subtracts one point from another; also needed for in-betweens
* @param point minuend
* @param subtrahend subtrahend
* @return sum of both points
*/
private static Point subtract(Point point, Point subtrahend) {
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 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, boolean overHalf) {
// create halfway marked point
if(overHalf)
kf1.position().setLocation(
middle(kf1.position(), kf2.position())
);
// position
Point difference = subtract(kf2.position(), kf1.position());
Point pos = add(kf1.position(), multiply(difference, scalar));
// scale
int width, height;
int dW = kf2.width() - kf1.width();
int dH = kf2.height() - kf1.height();
width = Math.toIntExact(Math.round(kf1.width() + dW * scalar));
height = Math.toIntExact(Math.round(kf1.height() + dH * scalar));
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));
}
/**
* Enumerator purely to describe two combined PathMethods, like a 2-dimensional PathMethod enum
*/
private enum CombinedPathMethod {
LINEAR,
LINEAR_EASE_IN,
// EASE_IN,
EASE_OUT,
EASE_OUT_LINEAR,
EASE_OUT_AND_IN;
}
} }

View File

@@ -7,19 +7,77 @@ 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 KeyFrame(JComponent component) { * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}
this( * @param position positional vector (where is the object at this moment?)
new Point(component.getX(), component.getY()), * @param width first transformational vector (how wide is the object at this moment)
component.getBounds().width, component.getBounds().height); * @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() { * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}.
return "KeyFrame{" + * Defaults pathMethod to linear and transition percentage to 0%
"position=" + position + * @param position positional vector (where is the object at this moment?)
", width=" + width + * @param width first transformational vector (how wide is the object at this moment)
", height=" + height + * @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(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);
@@ -30,10 +39,10 @@ public class AnimationTests {
frame.add(animatedPanel); frame.add(animatedPanel);
animatedPanel.play(10, true); animatedPanel.play(5, true);
frame.setVisible(true); frame.setVisible(true);
wait(15000); wait(10000);
} }
} }