/* Author: Maple * Jan. 24 2026 * */ 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 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 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); } /** * 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, boolean overHalf) { // create halfway marked point if(overHalf) kf1.position().setLocation( middle(kf1.position(), kf2.position()) ); // 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, 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)); } /** * 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; } }