Fixed animations! Updated version-naming scheme (this project doesn't depend on the protocol version) #1
21
pom.xml
21
pom.xml
@@ -1,12 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>org.openautonomousconnection</groupId>
|
<groupId>org.openautonomousconnection</groupId>
|
||||||
<artifactId>OACSwing</artifactId>
|
<artifactId>OACSwing</artifactId>
|
||||||
<version>1.0.0-BETA.1.1</version>
|
<version>0.0.0-STABLE.1.3</version>
|
||||||
<organization>
|
<organization>
|
||||||
<name>Open Autonomous Connection</name>
|
<name>Open Autonomous Connection</name>
|
||||||
<url>https://open-autonomous-connection.org/</url>
|
<url>https://open-autonomous-connection.org/</url>
|
||||||
@@ -53,6 +53,21 @@
|
|||||||
<name>Open Autonomous Public License (OAPL)</name>
|
<name>Open Autonomous Public License (OAPL)</name>
|
||||||
<url>https://open-autonomous-connection.org/license.html</url>
|
<url>https://open-autonomous-connection.org/license.html</url>
|
||||||
</license>
|
</license>
|
||||||
|
<license>
|
||||||
|
<name>GNU General Public License v3.0</name>
|
||||||
|
<url>https://www.gnu.org/licenses/gpl-3.0.html</url>
|
||||||
|
<comments>
|
||||||
|
Default license: Applies to all users and projects unless an explicit alternative license has been granted.
|
||||||
|
</comments>
|
||||||
|
</license>
|
||||||
|
<license>
|
||||||
|
<name>projectlombok</name>
|
||||||
|
<url>https://github.com/projectlombok/lombok?tab=License-1-ov-file</url>
|
||||||
|
</license>
|
||||||
|
<license>
|
||||||
|
<name>Eclipse Public License v2.0</name>
|
||||||
|
<url>https://www.eclipse.org/legal/epl-2.0/</url>
|
||||||
|
</license>
|
||||||
</licenses>
|
</licenses>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ package org.openautonomousconnection.oacswing.animated;
|
|||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
// TODO: Implement pause functionality
|
||||||
|
// (preferably not interfering with play() always reliably playing the animation from the start; aka define it as "setPaused(bool)" or "pause()" and "unpause()")
|
||||||
public interface AnimatedComponent {
|
public interface AnimatedComponent {
|
||||||
void setCurrentRun(Timer timer);
|
void setCurrentRun(Timer timer);
|
||||||
Timer getCurrentRun();
|
Timer getCurrentRun();
|
||||||
@@ -16,26 +18,48 @@ public interface AnimatedComponent {
|
|||||||
|
|
||||||
void setBounds(int x, int y, int width, int height);
|
void setBounds(int x, int y, int width, int height);
|
||||||
|
|
||||||
default void play(double speed, boolean loop) {
|
//TODO: implement momentum
|
||||||
|
/**
|
||||||
|
* Plays this object's animation path
|
||||||
|
* @param speed how fast the animation should play percentage (0 to 100; can be more)
|
||||||
|
* @param loop if should the animation should loop
|
||||||
|
* @param momentum basically how far an object "shoots off" from a specified keyframe when stopping at proportional speed
|
||||||
|
* (linear keyframes are excluded from this). Not implemented yet
|
||||||
|
*/
|
||||||
|
default void play(double speed, boolean loop, double momentum) {
|
||||||
|
|
||||||
|
// Speed has to be calculated like this since it heavily impacts animations with low inbetweens
|
||||||
|
// TODO: definetly shouhld fix this, so that speed means the same for every animation and does not depend on framerate
|
||||||
|
speed /= 100;
|
||||||
|
|
||||||
AtomicInteger ticksPassed = new AtomicInteger();
|
AtomicInteger ticksPassed = new AtomicInteger();
|
||||||
|
|
||||||
|
// Cloning the animation path to not mess with the original,
|
||||||
|
// If an extra frame is to be added because loop is set to true
|
||||||
|
|
||||||
|
AnimationPath playedPath = this.getAnimationPath().clone();
|
||||||
|
|
||||||
|
if(loop)
|
||||||
|
playedPath.add(playedPath.getNext());
|
||||||
|
|
||||||
|
// Finalize for timer
|
||||||
|
double finalSpeed = speed;
|
||||||
this.setCurrentRun(new Timer(0, e -> {
|
this.setCurrentRun(new Timer(0, e -> {
|
||||||
|
if (!playedPath.anyMore()) {
|
||||||
if(ticksPassed.get() * speed / (100) < 1) {
|
if (loop)
|
||||||
ticksPassed.addAndGet(this.getAnimationPath().getInbetweens());
|
playedPath.reset();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyFrame next = this.getAnimationPath().getNext();
|
|
||||||
|
|
||||||
if(next == null) {
|
|
||||||
if(loop)
|
|
||||||
this.getAnimationPath().reset();
|
|
||||||
else
|
else
|
||||||
((Timer) e.getSource()).stop();
|
((Timer) e.getSource()).stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(ticksPassed.get() * finalSpeed / (100) < 1) {
|
||||||
|
ticksPassed.addAndGet(playedPath.getInbetweens());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyFrame next = playedPath.getNext();
|
||||||
|
|
||||||
this.setBounds(next.position().x, next.position().y, next.width(), next.height());
|
this.setBounds(next.position().x, next.position().y, next.width(), next.height());
|
||||||
|
|
||||||
ticksPassed.set(0);
|
ticksPassed.set(0);
|
||||||
@@ -44,14 +68,33 @@ public interface AnimatedComponent {
|
|||||||
this.getCurrentRun().start();
|
this.getCurrentRun().start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays this object's animation path
|
||||||
|
* @param speed how fast the animation should play percentage (0 to 100; can be more)
|
||||||
|
* @param loop if should the animation should loop
|
||||||
|
*/
|
||||||
|
default void play(double speed, boolean loop) {
|
||||||
|
this.play(speed, loop, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays this object's animation path
|
||||||
|
* @param speed how fast the animation should play percentage (0 to 100; can be more)
|
||||||
|
*/
|
||||||
default void play(double speed) {
|
default void play(double speed) {
|
||||||
this.play(speed, false);
|
this.play(speed, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays this object's animation path
|
||||||
|
*/
|
||||||
default void play() {
|
default void play() {
|
||||||
this.play(1, false);
|
this.play(1, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops and resets replay of this object's animation path
|
||||||
|
*/
|
||||||
default void stop() {
|
default void stop() {
|
||||||
if(this.getCurrentRun() != null)
|
if(this.getCurrentRun() != null)
|
||||||
if(this.getCurrentRun().isRunning())
|
if(this.getCurrentRun().isRunning())
|
||||||
|
|||||||
@@ -80,10 +80,6 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private KeyFrame getKeyFrame(KeyFrame current, KeyFrame next, int subIterator) {
|
private KeyFrame getKeyFrame(KeyFrame current, KeyFrame next, int subIterator) {
|
||||||
// How far the transition should be finished
|
|
||||||
|
|
||||||
double transition = this.linear(subIterator);
|
|
||||||
|
|
||||||
CombinedPathMethod method;
|
CombinedPathMethod method;
|
||||||
|
|
||||||
KeyFrame.PathMethod
|
KeyFrame.PathMethod
|
||||||
@@ -95,26 +91,16 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
if(currentMethod.equals(KeyFrame.PathMethod.EASE_OUT) || currentMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
|
if(currentMethod.equals(KeyFrame.PathMethod.EASE_OUT) || currentMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
|
||||||
currentMethod = KeyFrame.PathMethod.EASE_OUT;
|
currentMethod = KeyFrame.PathMethod.EASE_OUT;
|
||||||
|
|
||||||
if(nextMethod.equals(KeyFrame.PathMethod.EASE_OUT) || nextMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
|
if(nextMethod.equals(KeyFrame.PathMethod.EASE_IN) || nextMethod.equals(KeyFrame.PathMethod.EASE_IN_AND_OUT))
|
||||||
nextMethod = KeyFrame.PathMethod.EASE_OUT;
|
nextMethod = KeyFrame.PathMethod.EASE_IN;
|
||||||
|
|
||||||
method = switch (currentMethod) {
|
method = switch (currentMethod) {
|
||||||
case LINEAR -> switch (nextMethod) {
|
case LINEAR -> switch (nextMethod) {
|
||||||
case LINEAR -> CombinedPathMethod.LINEAR;
|
case LINEAR -> CombinedPathMethod.LINEAR;
|
||||||
case EASE_IN -> CombinedPathMethod.LINEAR_EASE_IN;
|
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);
|
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 EASE_OUT -> switch (nextMethod) {
|
||||||
case LINEAR -> CombinedPathMethod.EASE_OUT_LINEAR;
|
case LINEAR -> CombinedPathMethod.EASE_OUT_LINEAR;
|
||||||
case EASE_IN -> CombinedPathMethod.EASE_OUT_AND_IN;
|
case EASE_IN -> CombinedPathMethod.EASE_OUT_AND_IN;
|
||||||
@@ -124,36 +110,7 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
default -> throw new IllegalStateException("Unexpected value: " + currentMethod);
|
default -> throw new IllegalStateException("Unexpected value: " + currentMethod);
|
||||||
};
|
};
|
||||||
|
|
||||||
double threshold = Math.min(current.transition(), next.transition());
|
return this.inBetween(current, next, method, subIterator);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +190,7 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
return new Point(point.x - subtrahend.x, point.y - subtrahend.y);
|
return new Point(point.x - subtrahend.x, point.y - subtrahend.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unused right now
|
||||||
/**
|
/**
|
||||||
* Calculate point between both points
|
* Calculate point between both points
|
||||||
* @param p1 first point
|
* @param p1 first point
|
||||||
@@ -248,28 +206,72 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Point onLine(Point p1, Point p2, double scalar) {
|
||||||
|
return add(
|
||||||
|
p1,
|
||||||
|
multiply(
|
||||||
|
subtract(p2, p1),
|
||||||
|
scalar
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find in-between with given scalar
|
* Find in-between with given scalar
|
||||||
* @param kf1 first frame
|
* @param kf1 first frame
|
||||||
* @param kf2 next frame
|
* @param kf2 next frame
|
||||||
* @param scalar factor (ideally between 0 and 1, representing 0% transition to 100% transition)
|
* @param subIterator how far the animation path has proceeded
|
||||||
* @param overHalf if this inbetween is over halfway through the transition process
|
|
||||||
* @return in-between frame
|
* @return in-between frame
|
||||||
*/
|
*/
|
||||||
private static KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, double scalar, boolean overHalf) {
|
private KeyFrame inBetween(KeyFrame kf1, KeyFrame kf2, CombinedPathMethod method, int subIterator) {
|
||||||
// create halfway marked point
|
double scalar = this.linear(subIterator);
|
||||||
if(overHalf)
|
|
||||||
kf1.position().setLocation(
|
double remainder = kf1.transition() - kf2.transition();
|
||||||
middle(kf1.position(), kf2.position())
|
|
||||||
|
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
// position
|
}
|
||||||
|
|
||||||
|
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 difference = subtract(kf2.position(), kf1.position());
|
||||||
|
|
||||||
Point pos = add(kf1.position(), multiply(difference, scalar));
|
Point pos = add(kf1.position(), multiply(difference, scalar));
|
||||||
|
|
||||||
// scale
|
// Scale
|
||||||
|
|
||||||
int width, height;
|
int width, height;
|
||||||
|
|
||||||
@@ -288,25 +290,27 @@ public class AnimationPath extends ArrayList<KeyFrame> {
|
|||||||
return new AnimationPath(this.inbetweens, new ArrayList<>(this));
|
return new AnimationPath(this.inbetweens, new ArrayList<>(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private double linear(int subIterator) {
|
private double linear(double subIterator) {
|
||||||
return (double) subIterator/2 / this.inbetweens;
|
return subIterator / this.inbetweens;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double easeIn(int subIterator) {
|
private double easeIn(double subIterator) {
|
||||||
return this.linear(subIterator*2) * this.linear(subIterator*2);
|
return this.linear(subIterator - 1) * this.linear(subIterator - 1) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double easeOut(int subIterator) {
|
private double easeOut(double subIterator) {
|
||||||
return Math.sqrt(this.linear(subIterator*2));
|
return 2 * this.linear(subIterator) * this.linear(subIterator);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enumerator purely to describe two combined PathMethods, like a 2-dimensional PathMethod enum
|
* Enumerator purely to describe two combined PathMethods, like a 2-dimensional PathMethod enum
|
||||||
*/
|
*/
|
||||||
private enum CombinedPathMethod {
|
private enum CombinedPathMethod {
|
||||||
LINEAR,
|
LINEAR,
|
||||||
LINEAR_EASE_IN,
|
LINEAR_EASE_IN,
|
||||||
// EASE_IN,
|
|
||||||
EASE_OUT,
|
EASE_OUT,
|
||||||
EASE_OUT_LINEAR,
|
EASE_OUT_LINEAR,
|
||||||
EASE_OUT_AND_IN;
|
EASE_OUT_AND_IN;
|
||||||
|
|||||||
@@ -73,11 +73,15 @@ public record KeyFrame(Point position, int width, int height, PathMethod pathMet
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public enum PathMethod {
|
public enum PathMethod {
|
||||||
LINEAR,
|
LINEAR,
|
||||||
EASE_IN,
|
EASE_IN,
|
||||||
EASE_OUT,
|
EASE_OUT,
|
||||||
EASE_IN_AND_OUT
|
EASE_IN_AND_OUT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyFrame clone() {
|
||||||
|
return new KeyFrame(this.position, this.width, this.height, this.pathMethod, this.transition);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ public class AnimationTests {
|
|||||||
public synchronized void testSimpleAnimatedPanel() throws InterruptedException {
|
public synchronized void testSimpleAnimatedPanel() throws InterruptedException {
|
||||||
DesignManager.setGlobalDesign(Design.DARK);
|
DesignManager.setGlobalDesign(Design.DARK);
|
||||||
|
|
||||||
// OACFrame frame = TestUtils.mockOacFrame();
|
OACFrame frame = TestUtils.mockOacFrame();
|
||||||
|
|
||||||
// AnimationPath animationPath = new AnimationPath(50);
|
AnimationPath animationPath = new AnimationPath(100);
|
||||||
|
|
||||||
// This test was too simple
|
// This test was too simple
|
||||||
// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
|
// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400));
|
||||||
@@ -30,18 +30,18 @@ public class AnimationTests {
|
|||||||
// animationPath.add(new KeyFrame(new Point(100, 100), 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));
|
||||||
|
|
||||||
// animationPath.add(new KeyFrame(new Point(400, 400), 400, 400, KeyFrame.PathMethod.EASE_OUT));
|
animationPath.add(new KeyFrame(new Point(400, 600), 400, 400, KeyFrame.PathMethod.EASE_OUT));
|
||||||
// animationPath.add(new KeyFrame(new Point(400, 300), 400, 400));
|
animationPath.add(new KeyFrame(new Point(400, 100), 400, 400, KeyFrame.PathMethod.LINEAR));
|
||||||
|
|
||||||
// JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath);
|
JAnimatedPanel animatedPanel = new JAnimatedPanel(animationPath);
|
||||||
|
|
||||||
// animatedPanel.setBackground(Color.BLACK);
|
animatedPanel.setBackground(Color.BLACK);
|
||||||
|
|
||||||
// frame.add(animatedPanel);
|
frame.add(animatedPanel);
|
||||||
|
|
||||||
// animatedPanel.play(5, true);
|
animatedPanel.play(10, true);
|
||||||
|
|
||||||
// frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
|
|
||||||
wait(10000);
|
wait(10000);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user