From fbaec2464c4510dd9d9f6c21a069042abb20f883 Mon Sep 17 00:00:00 2001 From: Tinglyyy Date: Sat, 24 Jan 2026 12:04:55 +0100 Subject: [PATCH] Added Animation --- pom.xml | 14 ++ .../oacswing/JAnimatedComponent.java | 15 ++ .../oacswing/JTitledComponent.java | 26 ++++ .../oacswing/animated/AnimationPath.java | 142 ++++++++++++++++++ .../oacswing/animated/JAnimatedPanel.java | 56 +++++++ .../oacswing/animated/KeyFrame.java | 21 +++ .../oacswing/test/AnimationTests.java | 34 +++++ .../oacswing/test/TestUtils.java | 16 ++ 8 files changed, 324 insertions(+) create mode 100644 src/main/java/org/openautonomousconnection/oacswing/JAnimatedComponent.java create mode 100644 src/main/java/org/openautonomousconnection/oacswing/JTitledComponent.java create mode 100644 src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java create mode 100644 src/main/java/org/openautonomousconnection/oacswing/animated/JAnimatedPanel.java create mode 100644 src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java create mode 100644 src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java create mode 100644 src/test/java/org/openautonomousconnection/oacswing/test/TestUtils.java diff --git a/pom.xml b/pom.xml index 3234c7d..324202e 100644 --- a/pom.xml +++ b/pom.xml @@ -13,5 +13,19 @@ 23 UTF-8 + + + org.projectlombok + lombok + 1.18.38 + provided + + + org.junit.jupiter + junit-jupiter + 6.0.0 + test + + \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/oacswing/JAnimatedComponent.java b/src/main/java/org/openautonomousconnection/oacswing/JAnimatedComponent.java new file mode 100644 index 0000000..e12ef8b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/oacswing/JAnimatedComponent.java @@ -0,0 +1,15 @@ +package org.openautonomousconnection.oacswing; + +public interface JAnimatedComponent { + void play(double speed, boolean loop); + + default void play(double speed) { + this.play(speed, false); + } + + default void play() { + this.play(1, false); + } + + void stop(); +} diff --git a/src/main/java/org/openautonomousconnection/oacswing/JTitledComponent.java b/src/main/java/org/openautonomousconnection/oacswing/JTitledComponent.java new file mode 100644 index 0000000..510ac63 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/oacswing/JTitledComponent.java @@ -0,0 +1,26 @@ +package org.openautonomousconnection.oacswing; + +import lombok.Getter; +import lombok.Setter; + +import javax.swing.*; +import java.awt.*; + +public class JTitledComponent extends JPanel { + @Getter @Setter + private JLabel title; + + @Getter @Setter + private C component; + + public JTitledComponent(String title, C component) { + this.setLayout(new FlowLayout()); + + this.title = new JLabel(title); + + this.component = component; + + this.add(this.title); + this.add(this.component); + } +} diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java new file mode 100644 index 0000000..5fb90a2 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java @@ -0,0 +1,142 @@ +package org.openautonomousconnection.oacswing.animated; + +import lombok.Getter; +import lombok.Setter; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +public class AnimationPath extends ArrayList { + @Getter @Setter + private int inbetweens; + + private int animationIterator = 0; + + private int subFrameIterator = 0; + + public AnimationPath(int inbetweens, Collection keyFrames) { + this.addAll(keyFrames); + this.inbetweens = inbetweens; + } + + public AnimationPath(int inbetweens, KeyFrame... keyFrames) { + this.addAll(Arrays.stream(keyFrames).toList()); + this.inbetweens = inbetweens; + } + + public AnimationPath(int inbetweens) { + this.inbetweens = inbetweens; + } + + /** + * Get next keyframe according to current path iterator, depending on the current frame + * @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(); + + // 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; + } + + /** + * 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); + } + + /** + * 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); + } +} diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/JAnimatedPanel.java b/src/main/java/org/openautonomousconnection/oacswing/animated/JAnimatedPanel.java new file mode 100644 index 0000000..fc016c9 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/JAnimatedPanel.java @@ -0,0 +1,56 @@ +package org.openautonomousconnection.oacswing.animated; + +import lombok.Getter; +import lombok.Setter; +import org.openautonomousconnection.oacswing.JAnimatedComponent; + +import javax.swing.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class JAnimatedPanel extends JPanel implements JAnimatedComponent { + @Getter @Setter + private AnimationPath animationPath; + + private Timer currentRun = null; + + public JAnimatedPanel(AnimationPath animationPath) { + this.animationPath = animationPath; + } + + + @Override + public void play(double speed, boolean loop) { + + AtomicInteger ticksPassed = new AtomicInteger(); + + this.currentRun = new Timer(0, e -> { + + if(ticksPassed.get() * speed / (100) < 1) { + ticksPassed.addAndGet(animationPath.getInbetweens()); + return; + } + + KeyFrame next = animationPath.getNext(); + + if(next == null) { + ((Timer) e.getSource()).stop(); + return; + } + + setBounds(next.position().x, next.position().y, next.width(), next.height()); + + ticksPassed.set(0); + }); + + this.currentRun.start(); + } + + @Override + public void stop() { + if(this.currentRun != null) + if(this.currentRun.isRunning()) + this.currentRun.stop(); + + this.animationPath.reset(); + } +} diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java new file mode 100644 index 0000000..87ab3f3 --- /dev/null +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java @@ -0,0 +1,21 @@ +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); + } + + @Override + public String toString() { + return "KeyFrame{" + + "position=" + position + + ", width=" + width + + ", height=" + height + + '}'; + } +} diff --git a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java new file mode 100644 index 0000000..55f6162 --- /dev/null +++ b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java @@ -0,0 +1,34 @@ +package org.openautonomousconnection.oacswing.test; + +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 javax.swing.*; +import java.awt.*; + +public class AnimationTests { + @Test + public synchronized void testSimpleAnimatedPanel() throws InterruptedException { + JFrame frame = TestUtils.mockFrame(); + + AnimationPath animationPath = new AnimationPath(50); + + animationPath.add(new KeyFrame(new Point(0, 0), 400, 400)); + animationPath.add(new KeyFrame(new Point(0, 0), 800, 600)); + animationPath.add(new KeyFrame(new Point(0, 0), 400, 400)); + + JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath); + + animatedPanel.setBackground(Color.BLACK); + + frame.add(animatedPanel); + + animatedPanel.play(0.1); + + frame.setVisible(true); + + wait(5000); + } +} diff --git a/src/test/java/org/openautonomousconnection/oacswing/test/TestUtils.java b/src/test/java/org/openautonomousconnection/oacswing/test/TestUtils.java new file mode 100644 index 0000000..05c2d99 --- /dev/null +++ b/src/test/java/org/openautonomousconnection/oacswing/test/TestUtils.java @@ -0,0 +1,16 @@ +package org.openautonomousconnection.oacswing.test; + +import javax.swing.*; +import java.awt.*; + +public class TestUtils { + public static JFrame mockFrame() { + JFrame frame = new JFrame(); + + frame.setBounds(new Rectangle(800, 600)); + + frame.setLocationRelativeTo(null); + + return frame; + } +}