diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java index 988553b..2736e66 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,7 +18,15 @@ 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 + * @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) + */ + default void play(double speed, boolean loop, double momentum) { AtomicInteger ticksPassed = new AtomicInteger(); // cloning the animation path to not mess with the original, @@ -29,8 +39,6 @@ public interface AnimatedComponent { this.setCurrentRun(new Timer(0, e -> { if (!playedPath.anyMore()) { - System.out.println("called"); - if (loop) playedPath.reset(); else @@ -43,8 +51,6 @@ public interface AnimatedComponent { return; } - System.out.println("still executing"); - KeyFrame next = playedPath.getNext(); this.setBounds(next.position().x, next.position().y, next.width(), next.height()); @@ -55,14 +61,33 @@ public interface AnimatedComponent { this.getCurrentRun().start(); } + /** + * Plays this object's animation path + * @param speed how fast the animation should play + * @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 + */ 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 75c5ec6..fe5b520 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java @@ -84,28 +84,95 @@ public class AnimationPath extends ArrayList { double transition = this.linear(subIterator); - KeyFrame.PathMethod method; + CombinedPathMethod method; + + KeyFrame.PathMethod + currentMethod = current.pathMethod(), + nextMethod = next.pathMethod(); // Translate EASE_IN_AND_OUT to its respective currently relevant counterpart - //TODO: non-linear keyframes can't behandled this way. This just makes it bug around + if(currentMethod.equals(KeyFrame.PathMethod.EASE_OUT) || currentMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT)) + currentMethod = KeyFrame.PathMethod.EASE_OUT; - if(transition < 0.5) - method = current.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ? - KeyFrame.PathMethod.EASE_OUT : current.pathMethod(); - else - method = next.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ? - KeyFrame.PathMethod.EASE_OUT : next.pathMethod(); + if(nextMethod.equals(KeyFrame.PathMethod.EASE_OUT) || nextMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT)) + nextMethod = KeyFrame.PathMethod.EASE_OUT; - // Else-case would be linear, which doesn't change anything + method = switch (currentMethod) { + case LINEAR -> switch (nextMethod) { + case LINEAR -> CombinedPathMethod.LINEAR; + case EASE_IN -> CombinedPathMethod.LINEAR_EASE_IN; + case EASE_OUT -> CombinedPathMethod.LINEAR_EASE_OUT; + default -> throw new IllegalStateException("Unexpected value: " + nextMethod); + }; - if(method.equals(KeyFrame.PathMethod.EASE_IN)) - transition = this.easeIn(subIterator); + 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); + }; - else if(method.equals(KeyFrame.PathMethod.EASE_OUT)) - transition = this.easeOut(subIterator); + 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); + }; - return inBetween(current, next, transition, method); + boolean overHalf = transition > 0.5; + + if(overHalf) + transition = 2 * transition - 1; + + transition = switch (method) { + case LINEAR -> transition; + + case LINEAR_EASE_IN -> overHalf ? easeIn(subIterator) : transition; + + case EASE_IN -> easeIn(subIterator); + + case EASE_IN_LINEAR -> overHalf ? transition : easeIn(subIterator); + + case LINEAR_EASE_OUT -> overHalf ? easeOut(subIterator) : transition; + + case EASE_OUT -> easeOut(subIterator); + + case EASE_OUT_LINEAR -> overHalf ? transition : easeOut(subIterator); + + case EASE_OUT_AND_IN -> overHalf ? easeOut(subIterator) : easeIn(subIterator); + + }; + + System.out.println(method + " " + transition + " - linear: " + linear(subIterator)); + + return inBetween(current, next, transition, overHalf); + + +// KeyFrame.PathMethod method; +// +// // Translate EASE_IN_AND_OUT to its respective currently relevant counterpart +// +// //TODO: non-linear keyframes can't behandled this way. This just makes it bug around +// +// if(transition < 0.5) +// method = current.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ? +// KeyFrame.PathMethod.EASE_OUT : current.pathMethod(); +// else +// method = next.pathMethod().equals(KeyFrame.PathMethod.EASE_IN_AND_OUT) ? +// KeyFrame.PathMethod.EASE_OUT : next.pathMethod(); +// +// // Else-case would be linear, which doesn't change anything +// +// if(method.equals(KeyFrame.PathMethod.EASE_IN)) +// transition = this.easeIn(subIterator); +// +// else if(method.equals(KeyFrame.PathMethod.EASE_OUT)) +// transition = this.easeOut(subIterator); +// +// return inBetween(current, next, transition, method); } @@ -122,6 +189,19 @@ public class AnimationPath extends ArrayList { 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 */ @@ -171,14 +251,36 @@ public class AnimationPath extends ArrayList { 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, KeyFrame.PathMethod method) { + 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()); @@ -196,7 +298,7 @@ public class AnimationPath extends ArrayList { height = Math.toIntExact(Math.round(kf1.height() + dH * scalar)); - return new KeyFrame(pos, width, height, method); + return new KeyFrame(pos, width, height, kf2.pathMethod()); } @Override @@ -208,11 +310,26 @@ public class AnimationPath extends ArrayList { return (double) subIterator/2 / this.inbetweens; } - private double easeIn(int subIterator) { - return this.linear(subIterator/2) * this.linear(subIterator/2); + 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_IN_LINEAR, + LINEAR_EASE_OUT, + EASE_OUT, + EASE_OUT_LINEAR, + EASE_OUT_AND_IN; + + } } diff --git a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java index d365a83..4b5519c 100644 --- a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java +++ b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java @@ -39,7 +39,7 @@ public class AnimationTests { frame.add(animatedPanel); - animatedPanel.play(10, true); + animatedPanel.play(5, true); frame.setVisible(true);