Files
OAC-Swing/src/main/java/org/openautonomousconnection/oacswing/animated/AnimationPath.java

320 lines
9.4 KiB
Java
Raw Normal View History

2026-02-07 18:54:19 +01:00
/* Author: Maple
* Jan. 24 2026
* */
2026-01-24 12:04:55 +01:00
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);
}
2026-01-24 12:04:55 +01:00
/**
* Get next keyframe according to current path iterator, depending on the current frame, and increase the animation iterators
2026-01-24 12:04:55 +01:00
* @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
2026-01-24 12:04:55 +01:00
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);
2026-02-10 22:30:57 +01:00
2026-01-24 12:04:55 +01:00
}
/**
* 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();
}
2026-01-24 12:04:55 +01:00
/**
* 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
)
);
}
2026-01-24 12:04:55 +01:00
/**
* Find in-between with given scalar
* @param kf1 first frame
* @param kf2 next frame
* @param subIterator how far the animation path has proceeded
2026-01-24 12:04:55 +01:00
* @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;
2026-01-24 12:04:55 +01:00
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
2026-01-24 12:04:55 +01:00
Point difference = subtract(kf2.position(), kf1.position());
Point pos = add(kf1.position(), multiply(difference, scalar));
// Scale
2026-01-24 12:04:55 +01:00
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);
2026-01-24 12:04:55 +01:00
}
/**
* 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;
}
2026-01-24 12:04:55 +01:00
}