Added Animation

This commit is contained in:
Tinglyyy
2026-01-24 12:04:55 +01:00
parent 1b0061a2b7
commit fbaec2464c
8 changed files with 324 additions and 0 deletions

View File

@@ -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();
}

View File

@@ -0,0 +1,26 @@
package org.openautonomousconnection.oacswing;
import lombok.Getter;
import lombok.Setter;
import javax.swing.*;
import java.awt.*;
public class JTitledComponent<C extends JComponent> 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);
}
}

View File

@@ -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<KeyFrame> {
@Getter @Setter
private int inbetweens;
private int animationIterator = 0;
private int subFrameIterator = 0;
public AnimationPath(int inbetweens, Collection<KeyFrame> 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);
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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);
}
}

View File

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