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;
+ }
+}