diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java index d319cba..5061724 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimatedComponent.java @@ -8,13 +8,11 @@ import javax.swing.*; import java.util.concurrent.atomic.AtomicInteger; public interface AnimatedComponent { + void setCurrentRun(Timer timer); Timer getCurrentRun(); - void setCurrentRun(Timer timer); - - AnimationPath getAnimationPath(); - void setAnimationPath(AnimationPath animationPath); + AnimationPath getAnimationPath(); void setBounds(int x, int y, int width, int height); @@ -23,15 +21,15 @@ public interface AnimatedComponent { this.setCurrentRun(new Timer(0, e -> { - if (ticksPassed.get() * speed / (100) < 1) { + if(ticksPassed.get() * speed / (100) < 1) { ticksPassed.addAndGet(this.getAnimationPath().getInbetweens()); return; } KeyFrame next = this.getAnimationPath().getNext(); - if (next == null) { - if (loop) + if(next == null) { + if(loop) this.getAnimationPath().reset(); else ((Timer) e.getSource()).stop(); @@ -55,8 +53,8 @@ public interface AnimatedComponent { } default void stop() { - if (this.getCurrentRun() != null) - if (this.getCurrentRun().isRunning()) + if(this.getCurrentRun() != null) + if(this.getCurrentRun().isRunning()) this.getCurrentRun().stop(); this.getAnimationPath().reset(); diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java index a9a97b8..e2e9829 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java @@ -13,8 +13,7 @@ import java.util.Arrays; import java.util.Collection; public class AnimationPath extends ArrayList { - @Getter - @Setter + @Getter @Setter private int inbetweens; private int animationIterator = 0; @@ -34,11 +33,175 @@ public class AnimationPath extends ArrayList { public AnimationPath(int inbetweens) { this.inbetweens = inbetweens; } + /** + * Get next keyframe according to current path iterator, depending on the current frame without increasing the animation iterators + * @return next keyframe in order, depending on the current frame + */ + public KeyFrame peekNext() { + int tempSubIterator = this.subFrameIterator, tempAnimationIterator = this.animationIterator; + + if(tempSubIterator >= this.inbetweens) { + tempSubIterator = 0; + tempAnimationIterator++; + } + + if(tempAnimationIterator >= this.size()) + return null; + + tempSubIterator++; + + KeyFrame current = this.get(tempAnimationIterator); + + KeyFrame next = this.getNextInOrder(); + + return getKeyFrame(current, next, tempSubIterator); + } + + /** + * Get next keyframe according to current path iterator, depending on the current frame, and increase the animation iterators + * @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(); + + return getKeyFrame(current, next, this.subFrameIterator); + } + + 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 + currentMethod = current.pathMethod(), + nextMethod = next.pathMethod(); + + // Translate EASE_IN_AND_OUT to its respective currently relevant counterpart + + 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; + + 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; + case EASE_OUT -> CombinedPathMethod.EASE_OUT; + default -> throw new IllegalStateException("Unexpected value: " + nextMethod); + }; + 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); + + } + + + /** + * 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); + } + + /** + * 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 + */ + public boolean anyMore() { + return this.animationIterator + 1 < this.size(); + } + + /** + * 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 point point to multiply * @param scalar scalar to multiply with * @return scalar product point */ @@ -49,10 +212,10 @@ public class AnimationPath extends ArrayList { return new Point(x, y); } + /** * Add two points together; also needed for in-betweens - * - * @param point augend + * @param point augend * @param addend addend * @return sum of both points */ @@ -62,8 +225,7 @@ public class AnimationPath extends ArrayList { /** * Subtracts one point from another; also needed for in-betweens - * - * @param point minuend + * @param point minuend * @param subtrahend subtrahend * @return sum of both points */ @@ -71,15 +233,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 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) { + 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()); @@ -97,55 +280,36 @@ public class AnimationPath extends ArrayList { height = Math.toIntExact(Math.round(kf1.height() + dH * scalar)); - return new KeyFrame(pos, width, height); + return new KeyFrame(pos, width, height, kf2.pathMethod()); + } + + @Override + public AnimationPath clone() { + return new AnimationPath(this.inbetweens, new ArrayList<>(this)); + } + + private double linear(int subIterator) { + return (double) subIterator/2 / this.inbetweens; + } + + 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)); } /** - * Get next keyframe according to current path iterator, depending on the current frame - * - * @return next keyframe in order, depending on the current frame + * Enumerator purely to describe two combined PathMethods, like a 2-dimensional PathMethod enum */ - public KeyFrame getNext() { - if (this.subFrameIterator >= this.inbetweens) { - this.subFrameIterator = 0; - this.animationIterator++; - } + private enum CombinedPathMethod { + LINEAR, + LINEAR_EASE_IN, +// EASE_IN, + EASE_OUT, + EASE_OUT_LINEAR, + EASE_OUT_AND_IN; - 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; } } diff --git a/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java index ee0ed6e..f9c9332 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java +++ b/src/main/java/org/openautonomousconnection/oacswing/animated/KeyFrame.java @@ -7,19 +7,77 @@ 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); +/** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent} + * @param position positional vector (where is the object at this moment?) + * @param width first transformational vector (how wide is the object at this moment) + * @param height second transformational vector (how tall is the object at this moment) + * @param pathMethod how the path from this KeyFrame to the next should look like + * @param transition adds to pathMethod in how much of the path should be defined by this decision (default 0% for linear and 50% for all other methods) + */ +public record KeyFrame(Point position, int width, int height, PathMethod pathMethod, double transition) { + + /** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}. + * @param position positional vector (where is the object at this moment?) + * @param width first transformational vector (how wide is the object at this moment) + * @param height second transformational vector (how tall is the object at this moment) + * @param pathMethod how the path from this KeyFrame to the next should look like + */ + public KeyFrame(Point position, int width, int height, PathMethod pathMethod) { + this(position, width, height, pathMethod, pathMethod.equals(PathMethod.LINEAR) ? 0 : 0.5); } - @Override - public String toString() { - return "KeyFrame{" + - "position=" + position + - ", width=" + width + - ", height=" + height + - '}'; + /** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}. + * Defaults pathMethod to linear and transition percentage to 0% + * @param position positional vector (where is the object at this moment?) + * @param width first transformational vector (how wide is the object at this moment) + * @param height second transformational vector (how tall is the object at this moment) + */ + public KeyFrame(Point position, int width, int height) { + this(position, width, height, PathMethod.LINEAR, 0); + } + + /** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}. + * @param component component that contains necessary vectors + * @param pathMethod how the path from this KeyFrame to the next should look like + * @param transition adds to pathMethod in how much of the path should be defined by this decision (default 50%) + */ + public KeyFrame(JComponent component, PathMethod pathMethod, double transition) { + this( + new Point(component.getX(), component.getY()), + component.getBounds().width, component.getBounds().height, pathMethod, transition); + } + + /** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}. + * @param component component that contains necessary vectors + * @param pathMethod how the path from this KeyFrame to the next should look like + */ + public KeyFrame(JComponent component, PathMethod pathMethod) { + this( + new Point(component.getX(), component.getY()), + component.getBounds().width, component.getBounds().height, pathMethod, + pathMethod.equals(PathMethod.LINEAR) ? 0 : 0.5); + } + + /** + * Record that contains certain vectors and factors for animating in {@link AnimatedComponent}. + * Defaults pathMethod to linear and transition percentage to 50% + * @param component component that contains necessary vectors + */ + public KeyFrame(JComponent component) { + this(component, PathMethod.LINEAR); + } + + + + public enum PathMethod { + LINEAR, + EASE_IN, + EASE_OUT, + EASE_IN_AND_OUT } } diff --git a/src/main/java/org/openautonomousconnection/oacswing/component/design/DesignManager.java b/src/main/java/org/openautonomousconnection/oacswing/component/design/DesignManager.java index e3fd47c..b09e526 100644 --- a/src/main/java/org/openautonomousconnection/oacswing/component/design/DesignManager.java +++ b/src/main/java/org/openautonomousconnection/oacswing/component/design/DesignManager.java @@ -41,9 +41,7 @@ public class DesignManager { Design.DARK.getElements().put(OACRadioButton.class, new DesignFlags(OACColor.DARK_BUTTON)); Design.DARK.getElements().put(OACTitleBar.class, new DesignFlags(OACColor.DARK_SECTION)); - Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 20, 2); - - //Design.DARK.border = new BevelBorder(BevelBorder.LOWERED, OACColor.DARK_BORDERS.getColor(), OACColor.DARK_SHADOW.getColor()); + Design.DARK.border = new RoundedBorder(OACColor.DARK_BORDERS.getColor(), 8, 1); DesignManager.getInstance().registerDesign(Design.DARK); } diff --git a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java index 0639af9..4b5519c 100644 --- a/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java +++ b/src/test/java/org/openautonomousconnection/oacswing/test/AnimationTests.java @@ -8,6 +8,9 @@ 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 org.openautonomousconnection.oacswing.component.OACFrame; +import org.openautonomousconnection.oacswing.component.design.Design; +import org.openautonomousconnection.oacswing.component.design.DesignManager; import javax.swing.*; import java.awt.*; @@ -15,14 +18,20 @@ import java.awt.*; public class AnimationTests { @Test public synchronized void testSimpleAnimatedPanel() throws InterruptedException { - JFrame frame = TestUtils.mockFrame(); + DesignManager.setGlobalDesign(Design.DARK); + + OACFrame frame = TestUtils.mockOacFrame(); AnimationPath animationPath = new AnimationPath(50); - animationPath.add(new KeyFrame(new Point(400, 400), 400, 400)); + // This test was too simple +// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400)); +// animationPath.add(new KeyFrame(new Point(400, 300), 400, 400)); +// 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(100, 100), 400, 400)); - animationPath.add(new KeyFrame(new Point(400, 400), 400, 400)); JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath); @@ -30,10 +39,10 @@ public class AnimationTests { frame.add(animatedPanel); - animatedPanel.play(10, true); + animatedPanel.play(5, true); frame.setVisible(true); - wait(15000); + wait(10000); } }