320 lines
9.4 KiB
Java
320 lines
9.4 KiB
Java
/* 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<KeyFrame> {
|
|
@Getter @Setter
|
|
private int inbetweens;
|
|
|
|
private int animationIterator = 0;
|
|
|
|
private int subFrameIterator = 0;
|
|
|
|
public AnimationPath(int inbetweens, Collection<KeyFrame> 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) {
|
|
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_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;
|
|
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);
|
|
};
|
|
|
|
return this.inBetween(current, next, method, subIterator);
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
// Unused right now
|
|
/**
|
|
* 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
|
|
)
|
|
);
|
|
}
|
|
|
|
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 subIterator how far the animation path has proceeded
|
|
* @return in-between frame
|
|
*/
|
|
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)
|
|
);
|
|
|
|
}
|
|
|
|
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
|
|
|
|
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(double subIterator) {
|
|
return subIterator / this.inbetweens;
|
|
}
|
|
|
|
private double easeIn(double subIterator) {
|
|
return this.linear(subIterator - 1) * this.linear(subIterator - 1) + 1;
|
|
}
|
|
|
|
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_OUT,
|
|
EASE_OUT_LINEAR,
|
|
EASE_OUT_AND_IN;
|
|
|
|
}
|
|
}
|