From a01110e8cc5fd23f58fc418f226cea519448fc51 Mon Sep 17 00:00:00 2001 From: Tinglyyy Date: Sun, 22 Feb 2026 14:48:37 +0100 Subject: [PATCH] Fixed animations! Updated version-naming scheme (this project doesn't depend on the protocol version) --- pom.xml | 21 ++- .../oacswing/animated/AnimatedComponent.java | 67 +++++++-- .../oacswing/animated/AnimationPath.java | 128 +++++++++--------- .../oacswing/animated/KeyFrame.java | 6 +- .../oacswing/test/AnimationTests.java | 18 +-- 5 files changed, 153 insertions(+), 87 deletions(-) diff --git a/pom.xml b/pom.xml index 8dff179..2502751 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,12 @@ - 4.0.0 org.openautonomousconnection OACSwing - 1.0.0-BETA.1.1 + 0.0.0-STABLE.1.3 Open Autonomous Connection https://open-autonomous-connection.org/ @@ -53,6 +53,21 @@ Open Autonomous Public License (OAPL) https://open-autonomous-connection.org/license.html + + GNU General Public License v3.0 + https://www.gnu.org/licenses/gpl-3.0.html + + Default license: Applies to all users and projects unless an explicit alternative license has been granted. + + + + projectlombok + https://github.com/projectlombok/lombok?tab=License-1-ov-file + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-2.0/ + diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java index 5061724..4c46c82 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java @@ -7,6 +7,8 @@ package org.openautonomousconnection.oacswing.animated; import javax.swing.*; import java.util.concurrent.atomic.AtomicInteger; +// TODO: Implement pause functionality +// (preferably not interfering with play() always reliably playing the animation from the start; aka define it as "setPaused(bool)" or "pause()" and "unpause()") public interface AnimatedComponent { void setCurrentRun(Timer timer); Timer getCurrentRun(); @@ -16,26 +18,48 @@ public interface AnimatedComponent { void setBounds(int x, int y, int width, int height); - default void play(double speed, boolean loop) { + //TODO: implement momentum + /** + * Plays this object's animation path + * @param speed how fast the animation should play percentage (0 to 100; can be more) + * @param loop if should the animation should loop + * @param momentum basically how far an object "shoots off" from a specified keyframe when stopping at proportional speed + * (linear keyframes are excluded from this). Not implemented yet + */ + default void play(double speed, boolean loop, double momentum) { + + // Speed has to be calculated like this since it heavily impacts animations with low inbetweens + // TODO: definetly shouhld fix this, so that speed means the same for every animation and does not depend on framerate + speed /= 100; + AtomicInteger ticksPassed = new AtomicInteger(); + // Cloning the animation path to not mess with the original, + // If an extra frame is to be added because loop is set to true + + AnimationPath playedPath = this.getAnimationPath().clone(); + + if(loop) + playedPath.add(playedPath.getNext()); + + // Finalize for timer + double finalSpeed = speed; this.setCurrentRun(new Timer(0, e -> { - - if(ticksPassed.get() * speed / (100) < 1) { - ticksPassed.addAndGet(this.getAnimationPath().getInbetweens()); - return; - } - - KeyFrame next = this.getAnimationPath().getNext(); - - if(next == null) { - if(loop) - this.getAnimationPath().reset(); + if (!playedPath.anyMore()) { + if (loop) + playedPath.reset(); else ((Timer) e.getSource()).stop(); return; } + if(ticksPassed.get() * finalSpeed / (100) < 1) { + ticksPassed.addAndGet(playedPath.getInbetweens()); + return; + } + + KeyFrame next = playedPath.getNext(); + this.setBounds(next.position().x, next.position().y, next.width(), next.height()); ticksPassed.set(0); @@ -44,14 +68,33 @@ public interface AnimatedComponent { this.getCurrentRun().start(); } + /** + * Plays this object's animation path + * @param speed how fast the animation should play percentage (0 to 100; can be more) + * @param loop if should the animation should loop + */ + default void play(double speed, boolean loop) { + this.play(speed, loop, 0); + } + + /** + * Plays this object's animation path + * @param speed how fast the animation should play percentage (0 to 100; can be more) + */ default void play(double speed) { this.play(speed, false); } + /** + * Plays this object's animation path + */ default void play() { this.play(1, false); } + /** + * Stops and resets replay of this object's animation path + */ default void stop() { if(this.getCurrentRun() != null) if(this.getCurrentRun().isRunning()) diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java index e2e9829..244875c 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java @@ -80,10 +80,6 @@ public class AnimationPath extends ArrayList { } 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 @@ -95,26 +91,16 @@ public class AnimationPath extends ArrayList { 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; + if(nextMethod.equals(KeyFrame.PathMethod.EASE_IN) || nextMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT)) + nextMethod = KeyFrame.PathMethod.EASE_IN; 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; @@ -124,36 +110,7 @@ public class AnimationPath extends ArrayList { 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 this.inBetween(current, next, method, subIterator); } @@ -233,6 +190,7 @@ public class AnimationPath extends ArrayList { return new Point(point.x - subtrahend.x, point.y - subtrahend.y); } + // Unused right now /** * Calculate point between both points * @param p1 first point @@ -248,28 +206,72 @@ public class AnimationPath extends ArrayList { ); } + private static Point onLine(Point p1, Point p2, double scalar) { + return add( + p1, + multiply( + subtract(p2, p1), + scalar + ) + ); + } + /** * 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 + * @param subIterator how far the animation path has proceeded * @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()) + private KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, CombinedPathMethod method, int subIterator) { + double scalar = this.linear(subIterator); + + double remainder = kf1.transition() - kf2.transition(); + + double transition = 0.01; + + if(remainder > 0) { + if(kf1.transition() > kf2.transition()) + transition = kf1.transition() + remainder / 2; + else + transition = 1 - kf2.transition() - remainder / 2; + } + + double threshold = Math.min(kf1.transition(), 1 - kf2.transition()); + + boolean thresholdReached = scalar > threshold; + + // Create point on the line between both keyframes + if(thresholdReached) { + + kf1 = new KeyFrame( + onLine(kf1.position(), kf2.position(), threshold), + + (int) (kf1.width() + (kf2.width() - kf1.width()) * threshold), + (int) (kf1.height() + (kf2.height() - kf1.height()) * threshold) ); - // position + } + scalar = switch (method) { + case LINEAR -> scalar; + + case LINEAR_EASE_IN -> thresholdReached ? this.easeIn(subIterator) * (1 - transition) : scalar; + + case EASE_OUT -> easeOut(subIterator); + + case EASE_OUT_LINEAR -> thresholdReached ? 2 * (scalar - 0.5) : this.easeOut(subIterator); + + case EASE_OUT_AND_IN -> thresholdReached ? this.easeOut(subIterator) : this.easeIn(subIterator) * (1 - transition); + + }; + + // Position Point difference = subtract(kf2.position(), kf1.position()); Point pos = add(kf1.position(), multiply(difference, scalar)); - // scale + // Scale int width, height; @@ -288,25 +290,27 @@ public class AnimationPath extends ArrayList { return new AnimationPath(this.inbetweens, new ArrayList<>(this)); } - private double linear(int subIterator) { - return (double) subIterator/2 / this.inbetweens; + private double linear(double subIterator) { + return subIterator / this.inbetweens; } - private double easeIn(int subIterator) { - return this.linear(subIterator*2) * this.linear(subIterator*2); + private double easeIn(double subIterator) { + return this.linear(subIterator - 1) * this.linear(subIterator - 1) + 1; } - public double easeOut(int subIterator) { - return Math.sqrt(this.linear(subIterator*2)); + private double easeOut(double subIterator) { + return 2 * this.linear(subIterator) * this.linear(subIterator); + } + + /** * 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; diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java index f9c9332..8dbb050 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java @@ -73,11 +73,15 @@ public record KeyFrame(Point position, int width, int height, PathMethod pathMet } - public enum PathMethod { LINEAR, EASE_IN, EASE_OUT, EASE_IN_AND_OUT } + + @Override + public KeyFrame clone() { + return new KeyFrame(this.position, this.width, this.height, this.pathMethod, this.transition); + } } diff --git a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java index a706499..23cb29b 100644 --- a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java +++ b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java @@ -20,9 +20,9 @@ public class AnimationTests { public synchronized void testSimpleAnimatedPanel() throws InterruptedException { DesignManager.setGlobalDesign(Design.DARK); - // OACFrame frame = TestUtils.mockOacFrame(); + OACFrame frame = TestUtils.mockOacFrame(); - // AnimationPath animationPath = new AnimationPath(50); + AnimationPath animationPath = new AnimationPath(100); // This test was too simple // animationPath.add(new KeyFrame(new Point(400, 400), 400, 400)); @@ -30,18 +30,18 @@ public class AnimationTests { // 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, 600), 400, 400, KeyFrame.PathMethod.EASE_OUT)); + animationPath.add(new KeyFrame(new Point(400, 100), 400, 400, KeyFrame.PathMethod.LINEAR)); - // JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath); + JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath); - // animatedPanel.setBackground(Color.BLACK); + animatedPanel.setBackground(Color.BLACK); - // frame.add(animatedPanel); + frame.add(animatedPanel); - // animatedPanel.play(5, true); + animatedPanel.play(10, true); - // frame.setVisible(true); + frame.setVisible(true); wait(10000); } -- 2.49.1