/* 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); 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); } /** * 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); } /** * @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); } /** * 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, KeyFrame.PathMethod method) { // 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, method); } @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)); } }