diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b7ab68..69cb1ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## Added
- Interface `Audio` extrahiert, mit Basisfunktionen von `Sound` und `Music`.
- Klasse `Mixer` steuert mehrere Audio-Objekte gleichzeitig.
+- Klasse `tasks.RateLimitedTask`, `tasks.FramerateLimitedTask`, `tasks.FrameSynchronizedTask` und `tasks.DelayedTask`.
## Changed
- Neue Package-Struktur:
diff --git a/src/main/java/schule/ngb/zm/Zeichenmaschine.java b/src/main/java/schule/ngb/zm/Zeichenmaschine.java
index f5849cb..5f44473 100644
--- a/src/main/java/schule/ngb/zm/Zeichenmaschine.java
+++ b/src/main/java/schule/ngb/zm/Zeichenmaschine.java
@@ -1277,10 +1277,37 @@ public class Zeichenmaschine extends Constants {
// Zeichenthread
////
+ /**
+ * Globaler Monitor, der einmal pro Frame vom Zeichenthread freigegeben
+ * wird. Andere Threads können {@link Object#wait()} auf dem Monitor
+ * aufrufen, um sich mit dem Zeichenthread zu synchronisieren. Der
+ * {@code wait()} Aufruf sollte sich zur Sicherheit in einer Schleife
+ * befinden, die prüft, ob sich der Aktuelle {@link #tick} erhöht hat.
+ *
+ * int lastTick = Constants.tick;
+ *
+ * // Do some work
+ *
+ * while( lastTick >= Constants.tick ) {
+ * synchronized( Zeichenmaschine.globalSyncLock ) {
+ * try {
+ * Zeichenmaschine.globalSyncLock.wait();
+ * } catch( InterruptedException ex ) {}
+ * }
+ * }
+ * // Next frame has started
+ *
+ *
+ * Die {@link schule.ngb.zm.tasks.FrameSynchronizedTask} implementiert eine
+ * {@link schule.ngb.zm.tasks.Task}, die sich automatisch auf diese Wiese
+ * mit dem Zeichenthread synchronisiert.
+ */
+ public static final Object globalSyncLock = new Object[0];
+
class Zeichenthread extends Thread {
public Zeichenthread() {
- super(Zeichenthread.class.getSimpleName());
+ super(APP_NAME);
//super(APP_NAME + " " + APP_VERSION);
}
@@ -1329,6 +1356,9 @@ public class Zeichenmaschine extends Constants {
}
runTasks();
+ synchronized( globalSyncLock ) {
+ globalSyncLock.notifyAll();
+ }
// delta time in ns
long afterTime = System.nanoTime();
@@ -1414,7 +1444,8 @@ public class Zeichenmaschine extends Constants {
}
- class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener{
+ class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener {
+
@Override
public void mouseClicked( MouseEvent e ) {
enqueueEvent(e);
diff --git a/src/main/java/schule/ngb/zm/anim/Animations.java b/src/main/java/schule/ngb/zm/anim/Animations.java
new file mode 100644
index 0000000..d9f63a2
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/anim/Animations.java
@@ -0,0 +1,200 @@
+package schule.ngb.zm.anim;
+
+import schule.ngb.zm.Color;
+import schule.ngb.zm.Constants;
+import schule.ngb.zm.Vector;
+import schule.ngb.zm.tasks.FrameSynchronizedTask;
+import schule.ngb.zm.tasks.TaskRunner;
+import schule.ngb.zm.util.Log;
+import schule.ngb.zm.util.Validator;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.Future;
+import java.util.function.*;
+
+public class Animations {
+
+ public static final Future animateProperty( String propName, T target, double to, int runtime, DoubleUnaryOperator easing ) {
+ double from;
+ try {
+ from = callGetter(target, propName, double.class);
+ } catch( InvocationTargetException | NoSuchMethodException |
+ IllegalAccessException ex ) {
+ throw new RuntimeException("Can't access property getter for animation.", ex);
+ }
+
+ Method propSetter;
+ try {
+ propSetter = findSetter(target, propName, double.class);
+ } catch( NoSuchMethodException ex ) {
+ throw new RuntimeException("Can't find property setter for animation.", ex);
+ }
+
+ return animateProperty(target, from, to, runtime, easing, ( d ) -> {
+ try {
+ propSetter.invoke(target, d);
+ } catch( IllegalAccessException | InvocationTargetException e ) {
+ throw new RuntimeException("Can't access property setter for animation.", e);
+ }
+ });
+ }
+
+ public static final Future animateProperty( String propName, T target, Color to, int runtime, DoubleUnaryOperator easing ) {
+ Color from;
+ try {
+ from = callGetter(target, propName, Color.class);
+ } catch( InvocationTargetException | NoSuchMethodException |
+ IllegalAccessException ex ) {
+ throw new RuntimeException("Can't access property getter for animation.", ex);
+ }
+
+ Method propSetter;
+ try {
+ propSetter = findSetter(target, propName, Color.class);
+ } catch( NoSuchMethodException ex ) {
+ throw new RuntimeException("Can't find property setter for animation.", ex);
+ }
+
+ return animateProperty(target, from, to, runtime, easing, ( d ) -> {
+ try {
+ propSetter.invoke(target, d);
+ } catch( IllegalAccessException | InvocationTargetException e ) {
+ throw new RuntimeException("Can't access property setter for animation.", e);
+ }
+ });
+ }
+
+ public static final Future animateProperty( String propName, T target, Vector to, int runtime, DoubleUnaryOperator easing ) {
+ Vector from;
+ try {
+ from = callGetter(target, propName, Vector.class);
+ } catch( InvocationTargetException | NoSuchMethodException |
+ IllegalAccessException ex ) {
+ throw new RuntimeException("Can't access property getter for animation.", ex);
+ }
+
+ Method propSetter;
+ try {
+ propSetter = findSetter(target, propName, Vector.class);
+ } catch( NoSuchMethodException ex ) {
+ throw new RuntimeException("Can't find property setter for animation.", ex);
+ }
+
+ return animateProperty(target, from, to, runtime, easing, ( d ) -> {
+ try {
+ propSetter.invoke(target, d);
+ } catch( IllegalAccessException | InvocationTargetException e ) {
+ throw new RuntimeException("Can't access property setter for animation.", e);
+ }
+ });
+ }
+
+ private static final R callGetter( T target, String propName, Class propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ String getterName = makeMethodName("get", propName);
+ Method getter = target.getClass().getMethod(getterName);
+ if( getter != null && getter.getReturnType().equals(propType) ) {
+ return (R) getter.invoke(target);
+ } else {
+ throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName));
+ }
+ }
+
+ private static final Method findSetter( T target, String propName, Class propType ) throws NoSuchMethodException {
+ String setterName = makeMethodName("set", propName);
+ Method setter = target.getClass().getMethod(setterName, propType);
+ if( setter != null && setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) {
+ return setter;
+ } else {
+ throw new NoSuchMethodException(String.format("No setter for property <%s> found.", propName));
+ }
+ }
+
+ private static final String makeMethodName( String prefix, String propName ) {
+ String firstChar = propName.substring(0, 1).toUpperCase();
+ String tail = "";
+ if( propName.length() > 1 ) {
+ tail = propName.substring(1);
+ }
+ return prefix + firstChar + tail;
+ }
+
+ public static final Future animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) {
+ Validator.requireNotNull(target);
+ Validator.requireNotNull(propSetter);
+ return animate(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
+ }
+
+ public static final Future animateProperty( T target, final Color from, final Color to, int runtime, DoubleUnaryOperator easing, Consumer propSetter ) {
+ return animate(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e)));
+ }
+
+
+ public static final Future animateProperty( T target, final Vector from, final Vector to, int runtime, DoubleUnaryOperator easing, Consumer propSetter ) {
+ return animate(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e)));
+ }
+
+ public static final Future animateProperty( T target, R from, R to, int runtime, DoubleUnaryOperator easing, DoubleFunction interpolator, Consumer propSetter ) {
+ return animate(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
+ }
+
+
+ public static final Future animate( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction interpolator, BiConsumer applicator ) {
+ return animate(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e)));
+ }
+
+ public static final Future animate( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
+ /*final long starttime = System.currentTimeMillis();
+ return TaskRunner.run(() -> {
+ double t = 0.0;
+ do {
+ // One animation step for t in [0,1]
+ stepper.accept(easing.applyAsDouble(t));
+ try {
+ Thread.sleep(1000 / Constants.framesPerSecond);
+ } catch( InterruptedException ex ) {
+ }
+ t = (double) (System.currentTimeMillis() - starttime) / (double) runtime;
+ } while( t < 1.0 );
+ stepper.accept(easing.applyAsDouble(1.0));
+ }, target);*/
+ return TaskRunner.run(new FrameSynchronizedTask() {
+ double t = 0.0;
+ final long starttime = System.currentTimeMillis();
+ @Override
+ public void update( double delta ) {
+ // One animation step for t in [0,1]
+ stepper.accept(easing.applyAsDouble(t));
+ t = (double) (System.currentTimeMillis() - starttime) / (double) runtime;
+ running = (t <= 1.0);
+ }
+
+ @Override
+ protected void finish() {
+ stepper.accept(easing.applyAsDouble(1.0));
+ }
+ }, target);
+ }
+
+ public static final Future animate( T target, int runtime, Animator animator ) {
+ return animate(
+ target, runtime,
+ animator::easing,
+ animator::interpolator,
+ animator::applicator
+ );
+ }
+
+ /*public static Future> animate( Animation animation ) {
+ animation.start();
+ return null;
+ }
+
+ public static Future> animate( Animation animation, DoubleUnaryOperator easing ) {
+ animation.start(easing);
+ return null;
+ }*/
+
+ public static final Log LOG = Log.getLogger(Animations.class);
+
+}
diff --git a/src/main/java/schule/ngb/zm/anim/Animator.java b/src/main/java/schule/ngb/zm/anim/Animator.java
new file mode 100644
index 0000000..951279d
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/anim/Animator.java
@@ -0,0 +1,11 @@
+package schule.ngb.zm.anim;
+
+public interface Animator {
+
+ double easing(double t);
+
+ R interpolator(double e);
+
+ void applicator(T target, R value);
+
+}
diff --git a/src/main/java/schule/ngb/zm/anim/Easing.java b/src/main/java/schule/ngb/zm/anim/Easing.java
new file mode 100644
index 0000000..7679e23
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/anim/Easing.java
@@ -0,0 +1,320 @@
+package schule.ngb.zm.anim;
+
+import java.util.function.DoubleUnaryOperator;
+
+/**
+ * @see Cheat Sheet für Easing-Funktionen
+ */
+public class Easing {
+
+ public static final DoubleUnaryOperator DEFAULT_EASING = Easing::smooth;
+
+ public static final DoubleUnaryOperator thereAndBack() {
+ return Easing::thereAndBack;
+ }
+
+ public static final DoubleUnaryOperator thereAndBack( final DoubleUnaryOperator baseEasing ) {
+ return ( t ) -> thereAndBack(t, baseEasing);
+ }
+
+ public static final double thereAndBack( double t ) {
+ return thereAndBack(t, DEFAULT_EASING);
+ }
+
+ public static final double thereAndBack( double t, DoubleUnaryOperator baseEasing ) {
+ if( t < 0.5 ) {
+ return baseEasing.applyAsDouble(2 * t);
+ } else {
+ return baseEasing.applyAsDouble(2 - 2 * t);
+ }
+ }
+
+ public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing ) {
+ return ( t ) -> halfAndHalf(t, firstEasing, secondEasing);
+ }
+
+ public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing, final double split ) {
+ return ( t ) -> halfAndHalf(t, firstEasing, secondEasing, split);
+ }
+
+ public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing ) {
+ return halfAndHalf(t, firstEasing, secondEasing, 0.5);
+ }
+
+ public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing, double split ) {
+ if( t < split ) {
+ return firstEasing.applyAsDouble(2 * t);
+ } else {
+ return secondEasing.applyAsDouble(1 - 2 * t);
+ }
+ }
+
+
+ /*
+ * Functions taken from easings.net
+ */
+
+ public static final DoubleUnaryOperator linear() {
+ return Easing::linear;
+ }
+
+ public static final double linear( double t ) {
+ return t;
+ }
+
+ public static final DoubleUnaryOperator quadIn() {
+ return Easing::quadIn;
+ }
+
+ public static final double quadIn( double t ) {
+ return t * t;
+ }
+
+ public static final DoubleUnaryOperator quadOut() {
+ return Easing::quadOut;
+ }
+
+ public static final double quadOut( double t ) {
+ return 1 - (1 - t) * (1 - t);
+ }
+
+ public static final DoubleUnaryOperator quadInOut() {
+ return Easing::quadInOut;
+ }
+
+ public static final double quadInOut( double t ) {
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
+ }
+
+ public static final DoubleUnaryOperator cubicIn() {
+ return Easing::cubicIn;
+ }
+
+ public static final double cubicIn( double t ) {
+ return t * t * t;
+ }
+
+ public static final DoubleUnaryOperator cubicOut() {
+ return Easing::cubicOut;
+ }
+
+ public static final double cubicOut( double t ) {
+ return 1 - Math.pow(1 - t, 3);
+ }
+
+ public static final DoubleUnaryOperator cubicInOut() {
+ return Easing::cubicInOut;
+ }
+
+ public static final double cubicInOut( double t ) {
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+ }
+
+ public static final DoubleUnaryOperator sineIn() {
+ return Easing::sineIn;
+ }
+
+ public static final double sineIn( double t ) {
+ return 1 - Math.cos((t * Math.PI) / 2);
+ }
+
+ public static final DoubleUnaryOperator sineOut() {
+ return Easing::sineOut;
+ }
+
+ public static final double sineOut( double t ) {
+ return Math.sin((t * Math.PI) / 2);
+ }
+
+ public static final DoubleUnaryOperator sineInOut() {
+ return Easing::sineInOut;
+ }
+
+ public static final double sineInOut( double t ) {
+ return -(Math.cos(Math.PI * t) - 1) / 2;
+ }
+
+ public static final DoubleUnaryOperator elasticIn() {
+ return Easing::elasticIn;
+ }
+
+ public static final double elasticIn( double t ) {
+ double c4 = (2 * Math.PI) / 3;
+
+ return t == 0
+ ? 0
+ : t == 1
+ ? 1
+ : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
+ }
+
+ public static final DoubleUnaryOperator elasticOut() {
+ return Easing::elasticOut;
+ }
+
+ public static final double elasticOut( double t ) {
+ double c4 = (2 * Math.PI) / 3;
+
+ return t == 0
+ ? 0
+ : t == 1
+ ? 1
+ : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
+ }
+
+ public static final DoubleUnaryOperator elasticInOut() {
+ return Easing::elasticInOut;
+ }
+
+ public static final double elasticInOut( double t ) {
+ double c5 = (2 * Math.PI) / 4.5;
+
+ return t == 0
+ ? 0
+ : t == 1
+ ? 1
+ : t < 0.5
+ ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
+ : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
+ }
+
+ public static final DoubleUnaryOperator bounceIn() {
+ return Easing::bounceIn;
+ }
+
+ public static final double bounceIn( double t ) {
+ return 1 - bounceOut(1 - t);
+ }
+
+ public static final DoubleUnaryOperator bounceOut() {
+ return Easing::bounceOut;
+ }
+
+ public static final double bounceOut( double t ) {
+ double n1 = 7.5625;
+ double d1 = 2.75;
+
+ if( t < 1.0 / d1 ) {
+ return n1 * t * t;
+ } else if( t < 2.0 / d1 ) {
+ return n1 * (t -= 1.5 / d1) * t + 0.75;
+ } else if( t < 2.5 / d1 ) {
+ return n1 * (t -= 2.25 / d1) * t + 0.9375;
+ } else {
+ return n1 * (t -= 2.625 / d1) * t + 0.984375;
+ }
+ }
+
+ public static final DoubleUnaryOperator bounceInOut() {
+ return Easing::bounceInOut;
+ }
+
+ public static final double bounceInOut( double t ) {
+ return t < 0.5
+ ? (1 - bounceOut(1 - 2 * t)) / 2
+ : (1 + bounceOut(2 * t - 1)) / 2;
+ }
+
+ public static final DoubleUnaryOperator backIn() {
+ return Easing::backIn;
+ }
+
+ public static final double backIn( double t ) {
+ double c1 = 1.70158;
+ double c3 = c1 + 1;
+
+ return c3 * t * t * t - c1 * t * t;
+ }
+
+ public static final DoubleUnaryOperator backOut() {
+ return Easing::backOut;
+ }
+
+ public static final double backOut( double t ) {
+ double c1 = 1.70158;
+ double c3 = c1 + 1;
+
+ return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
+ }
+
+ public static final DoubleUnaryOperator backInOut() {
+ return Easing::backInOut;
+ }
+
+ public static final double backInOut( double t ) {
+ double c1 = 1.70158;
+ double c2 = c1 * 1.525;
+
+ return t < 0.5
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
+ }
+
+
+ /*
+ * Functions from manim community
+ */
+
+ public static final DoubleUnaryOperator smooth() {
+ return Easing::smooth;
+ }
+
+ public static final double smooth( double t ) {
+ double error = sigmoid(-INFLECTION / 2.0);
+ return Math.min(
+ Math.max(
+ (sigmoid(INFLECTION * (t - 0.5)) - error) / (1 - 2 * error),
+ 0
+ ),
+ 1.0
+ );
+ }
+
+ public static final double rushIn( double t ) {
+ return 2 * smooth(t / 2.0);
+ }
+
+ public static final double rushOut( double t ) {
+ return 2 * smooth(t / 2.0 + 0.5) - 1;
+ }
+
+ public static final double doubleSmooth( double t ) {
+ if( t < 0.5 )
+ return 0.5 * smooth(2 * t);
+ else
+ return 0.5 * (1 + smooth(2 * t - 1));
+ }
+
+ public static final double hobbit( double t ) {
+ double new_t = t < 0.5 ? 2 * t : 2 * (1 - t);
+ return smooth(new_t);
+ }
+
+
+ public static final DoubleUnaryOperator wiggle() {
+ return Easing::wiggle;
+ }
+
+ public static final DoubleUnaryOperator wiggle( final int wiggles ) {
+ return (t) -> Easing.wiggle(t, wiggles);
+ }
+
+ public static final double wiggle( double t ) {
+ return wiggle(t, 2);
+ }
+
+ public static final double wiggle( double t, int wiggles ) {
+ return hobbit(t) * Math.sin(wiggles * Math.PI * t);
+ }
+
+ public static double INFLECTION = 10.0;
+
+ public static final double sigmoid( double x ) {
+ return 1.0 / (1 + Math.exp(-x));
+ }
+
+
+ private Easing() {
+ }
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/DelayedTask.java b/src/main/java/schule/ngb/zm/tasks/DelayedTask.java
new file mode 100644
index 0000000..a39ece6
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/tasks/DelayedTask.java
@@ -0,0 +1,69 @@
+package schule.ngb.zm.tasks;
+
+import schule.ngb.zm.Zeichenmaschine;
+
+import java.util.concurrent.Delayed;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+public abstract class DelayedTask extends Task implements Delayed {
+
+ protected long startTime = System.currentTimeMillis(); // in ms
+
+ /**
+ * Gibt die absolute Verzögerung der Task zurück. Im Gegensatz zu
+ * {@link #getDelay(TimeUnit)} sollte das Ergebnis von {@code getDelay()}
+ * bei mehrmaligem Aufruf konstant bleiben.
+ *
+ * @return Die ursprüngliche Verzögerung in Millisekunden
+ */
+ public abstract int getDelay();
+
+ public long getStartTime() {
+ return startTime + getDelay();
+ }
+
+ /**
+ * Gibt die verbleibende Verzögerung bis zur Ausführung der Task zurück. Im
+ * Gegensatz zu {@link #getDelay()} sollte für mehrere Aufrufe von
+ * {@code getDelay(TimeUnit)} gelten, dass der zeitlich spätere Aufruf einen
+ * kleineren Wert zurückgibt, als der Frühere (abhängig von der gewählten
+ * {@link TimeUnit}).
+ *
+ * @param unit Die Zeiteinheit für die Verzögerung.
+ * @return Die verbleibende Verzögerung in der angegebenen Zeiteinheit.
+ */
+ @Override
+ public long getDelay( TimeUnit unit ) {
+ int diff = (int) (getStartTime() - System.currentTimeMillis());
+ return unit.convert(diff, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public int compareTo( Delayed o ) {
+ return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
+ }
+
+ @Override
+ public void run() {
+ while( getDelay(TimeUnit.MILLISECONDS) > 0 ) {
+ try {
+ System.out.println("Waiting for " + getDelay(TimeUnit.MILLISECONDS) + " ms");
+ Thread.sleep(getDelay(TimeUnit.MILLISECONDS));
+ //wait(getDelay(TimeUnit.MILLISECONDS));
+ } catch( InterruptedException e ) {
+ // Keep waiting
+ }
+ }
+
+ initialize();
+
+ running = true;
+ this.update(0.0);
+ running = false;
+ done = true;
+
+ finish();
+ }
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java b/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java
new file mode 100644
index 0000000..454818a
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java
@@ -0,0 +1,55 @@
+package schule.ngb.zm.tasks;
+
+import schule.ngb.zm.Constants;
+import schule.ngb.zm.Zeichenmaschine;
+
+public abstract class FrameSynchronizedTask extends Task {
+
+ private static Thread mainThread;
+
+ private static final Thread getMainThread() {
+ if( mainThread == null ) {
+ mainThread = Thread.currentThread();
+ if( !mainThread.getName().equals(Constants.APP_NAME) ) {
+ // Need to search for main Zeichenthread ...
+ }
+ }
+ return mainThread;
+ }
+
+ @Override
+ public void run() {
+ initialize();
+
+ running = true;
+ int lastTick = 0;
+ Object lock = Zeichenmaschine.globalSyncLock;
+
+ while( running ) {
+ lastTick = Constants.tick;
+ this.update(lastTick);
+
+ synchronized( lock ) {
+ while( lastTick >= Constants.tick ) {
+ try {
+ lock.wait();
+ } catch( InterruptedException e ) {
+ // We got interrupted ...
+ }
+ }
+ }
+ }
+
+ running = false;
+ done = true;
+
+ finish();
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java b/src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java
new file mode 100644
index 0000000..8fab4f6
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java
@@ -0,0 +1,11 @@
+package schule.ngb.zm.tasks;
+
+import schule.ngb.zm.Constants;
+
+public abstract class FramerateLimitedTask extends RateLimitedTask {
+
+ public int getRate() {
+ return Constants.framesPerSecond;
+ }
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java b/src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java
new file mode 100644
index 0000000..8f1be37
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java
@@ -0,0 +1,57 @@
+package schule.ngb.zm.tasks;
+
+public abstract class RateLimitedTask extends Task {
+
+ public abstract int getRate();
+
+ @Override
+ public final void run() {
+ if( running || done ) {
+ return;
+ }
+
+ initialize();
+
+ // current time in ns
+ long beforeTime = System.nanoTime();
+ // store for deltas
+ long overslept = 0L;
+ // delta in ms
+ double delta = 0;
+
+ running = true;
+ while( running ) {
+ // delta in seconds
+ delta = (System.nanoTime() - beforeTime) / 1000000000.0;
+ beforeTime = System.nanoTime();
+
+ this.update(delta);
+
+ // delta time in ns
+ long afterTime = System.nanoTime();
+ long dt = afterTime - beforeTime;
+ long sleep = 0;
+ if( getRate() > 0 ) {
+ sleep = ((1000000000L / getRate()) - dt) - overslept;
+ }
+
+ if( sleep > 0 ) {
+ try {
+ Thread.sleep(sleep / 1000000L, (int) (sleep % 1000000L));
+ } catch( InterruptedException e ) {
+ // Interrupt not relevant
+ }
+ } else {
+ Thread.yield();
+ }
+ // Did we sleep to long?
+ overslept = (System.nanoTime() - afterTime) - sleep;
+ }
+
+ running = false;
+ done = true;
+
+ finish();
+ }
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/Task.java b/src/main/java/schule/ngb/zm/tasks/Task.java
new file mode 100644
index 0000000..56ac04a
--- /dev/null
+++ b/src/main/java/schule/ngb/zm/tasks/Task.java
@@ -0,0 +1,30 @@
+package schule.ngb.zm.tasks;
+
+import schule.ngb.zm.Updatable;
+
+public abstract class Task implements Runnable, Updatable {
+
+ protected boolean running = false;
+
+ protected boolean done = false;
+
+ @Override
+ public boolean isActive() {
+ return running;
+ }
+
+ public boolean isDone() {
+ return !running & done;
+ }
+
+ public void stop() {
+ running = false;
+ }
+
+ protected void initialize() {
+ }
+
+ protected void finish() {
+ }
+
+}
diff --git a/src/main/java/schule/ngb/zm/tasks/TaskRunner.java b/src/main/java/schule/ngb/zm/tasks/TaskRunner.java
index 5bd20da..0173fca 100644
--- a/src/main/java/schule/ngb/zm/tasks/TaskRunner.java
+++ b/src/main/java/schule/ngb/zm/tasks/TaskRunner.java
@@ -25,6 +25,11 @@ public class TaskRunner {
return runner;
}
+ public static Future> run( Task task ) {
+ TaskRunner r = getTaskRunner();
+ return r.pool.submit(task);
+ }
+
public static Future> run( Runnable task ) {
TaskRunner r = getTaskRunner();
return r.pool.submit(task);
@@ -35,18 +40,13 @@ public class TaskRunner {
return r.pool.submit(task, result);
}
- public static Future> schedule( Runnable task, int ms ) {
- TaskRunner r = getTaskRunner();
- return r.pool.schedule(task, ms, TimeUnit.MILLISECONDS);
- }
-
public static void invokeLater( Runnable task ) {
SwingUtilities.invokeLater(task);
}
public static void shutdown() {
if( runner != null ) {
- runner.pool.shutdown();
+ /*runner.pool.shutdown();
try {
runner.pool.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
@@ -55,15 +55,27 @@ public class TaskRunner {
if( !runner.pool.isTerminated() ) {
runner.pool.shutdownNow();
}
- }
+ }*/
+ runner.pool.shutdownNow();
}
}
- ScheduledExecutorService pool;
+ ExecutorService pool;
private TaskRunner() {
//pool = new ScheduledThreadPoolExecutor(4);
- pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() {
+ /*pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() {
+ private final ThreadFactory threadFactory = Executors.defaultThreadFactory();
+
+ @Override
+ public Thread newThread( Runnable r ) {
+ Thread t = threadFactory.newThread(r);
+ t.setName("TaskRunner-" + t.getName());
+ t.setDaemon(true);
+ return t;
+ }
+ });*/
+ pool = Executors.newCachedThreadPool(new ThreadFactory() {
private final ThreadFactory threadFactory = Executors.defaultThreadFactory();
@Override
diff --git a/src/test/java/schule/ngb/zm/anim/AnimationsTest.java b/src/test/java/schule/ngb/zm/anim/AnimationsTest.java
new file mode 100644
index 0000000..d4f0f89
--- /dev/null
+++ b/src/test/java/schule/ngb/zm/anim/AnimationsTest.java
@@ -0,0 +1,262 @@
+package schule.ngb.zm.anim;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import schule.ngb.zm.Color;
+import schule.ngb.zm.Constants;
+import schule.ngb.zm.Options;
+import schule.ngb.zm.Zeichenmaschine;
+import schule.ngb.zm.shapes.*;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.function.DoubleUnaryOperator;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AnimationsTest {
+
+ private static Zeichenmaschine zm;
+
+ private static ShapesLayer shapes;
+
+ @BeforeAll
+ static void beforeAll() {
+ zm = new Zeichenmaschine(400, 400, "zm-test: Animations", false);
+ shapes = zm.getShapesLayer();
+ assertNotNull(shapes);
+ }
+
+ @AfterAll
+ static void afterAll() {
+ zm.exit();
+ }
+
+ @BeforeEach
+ void setUp() {
+ shapes.removeAll();
+ }
+
+ @Test
+ void animateMove() {
+ Shape s = new Circle(0, 0, 10);
+ shapes.add(s);
+
+ _animateMove(s, 2500, Easing.DEFAULT_EASING);
+ assertEquals(zm.getWidth(), s.getX(), 0.0001);
+ assertEquals(zm.getHeight(), s.getY(), 0.0001);
+
+ _animateMove(s, 2500, Easing.thereAndBack(Easing.linear()));
+ assertEquals(0.0, s.getX(), 0.0001);
+ assertEquals(0.0, s.getY(), 0.0001);
+
+ _animateMove(s, 4000, Easing::bounceInOut);
+ assertEquals(zm.getWidth(), s.getX(), 0.0001);
+ assertEquals(zm.getHeight(), s.getY(), 0.0001);
+ }
+
+ private void _animateMove( Shape s, int runtime, DoubleUnaryOperator easing ) {
+ s.moveTo(0, 0);
+ Future future = Animations.animate(
+ s, runtime,
+ easing,
+ ( e ) -> Constants.interpolate(0, zm.getWidth(), e),
+ ( t, p ) -> {
+ t.moveTo(p, p);
+ }
+ );
+ assertNotNull(future);
+ try {
+ assertEquals(s, future.get());
+ } catch( Exception e ) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void animateCircle() {
+ Shape s = new Circle(0, 0, 10);
+ shapes.add(s);
+
+ _animateCircle(s, 5000, Easing.linear());
+ }
+
+ private void _animateCircle( Shape s, final int runtime, final DoubleUnaryOperator easing ) {
+ final int midX = (int) (zm.getWidth() * .5);
+ final int midY = (int) (zm.getHeight() * .5);
+ final int radius = (int) (zm.getWidth() * .25);
+
+ Animator ani = new Animator() {
+ @Override
+ public double easing( double t ) {
+ return easing.applyAsDouble(t);
+ }
+
+ @Override
+ public Double interpolator( double e ) {
+ return Constants.interpolate(0, 360, e);
+ }
+
+ @Override
+ public void applicator( Shape s, Double angle ) {
+ double rad = Math.toRadians(angle);
+ s.moveTo(midX + radius * Math.cos(rad), midY + radius * Math.sin(rad));
+ }
+ };
+
+ Future future = Animations.animate(s, runtime, ani);
+ assertNotNull(future);
+ try {
+ assertEquals(s, future.get());
+ } catch( Exception e ) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void animateRotate() {
+ Shape s = new Rectangle(0, 0, 129, 80);
+ s.setAnchor(Constants.CENTER);
+ shapes.add(s);
+
+ _animateRotate(s, 3000, Easing::cubicIn);
+ assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001);
+ assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001);
+ assertEquals(0.0, s.getRotation(), 0.0001);
+
+ _animateRotate(s, 500, Easing::elasticInOut);
+ assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001);
+ assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001);
+ assertEquals(0.0, s.getRotation(), 0.0001);
+
+ _animateRotate(s, 1000, Easing::bounceOut);
+ assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001);
+ assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001);
+ assertEquals(0.0, s.getRotation(), 0.0001);
+
+ _animateRotate(s, 6000, Easing::backInOut);
+ assertEquals(zm.getWidth() * 0.5, s.getX(), 0.0001);
+ assertEquals(zm.getHeight() * 0.5, s.getY(), 0.0001);
+ assertEquals(0.0, s.getRotation(), 0.0001);
+ }
+
+ private void _animateRotate( Shape s, int runtime, DoubleUnaryOperator easing ) {
+ s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5);
+ s.rotateTo(0);
+ Future future = Animations.animate(
+ s, runtime,
+ easing,
+ ( e ) -> s.rotateTo(Constants.interpolate(0, 720, e))
+ );
+ assertNotNull(future);
+ try {
+ assertEquals(s, future.get());
+ } catch( Exception e ) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void animateColor() {
+ Shape s = new Ellipse(0, 0, 129, 80);
+ s.setAnchor(Constants.CENTER);
+ shapes.add(s);
+
+ _animateColor(s, Color.RED, 1000, Easing.DEFAULT_EASING);
+ assertEquals(Color.RED, s.getFillColor());
+ _animateColor(s, Color.BLUE, 1500, Easing::backInOut);
+ assertEquals(Color.BLUE, s.getFillColor());
+ _animateColor(s, Color.GREEN, 2000, Easing::bounceOut);
+ assertEquals(Color.GREEN, s.getFillColor());
+ _animateColor(s, Color.YELLOW, 300, Easing::thereAndBack);
+ assertEquals(Color.GREEN, s.getFillColor());
+ }
+
+ private void _animateColor( Shape s, Color to, int runtime, DoubleUnaryOperator easing ) {
+ s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5);
+ final Color from = s.getFillColor();
+ Future future = Animations.animate(
+ s, runtime,
+ easing,
+ ( e ) -> Color.interpolate(from, to, e),
+ ( t, c ) -> t.setFillColor(c)
+ );
+ assertNotNull(future);
+ try {
+ assertEquals(s, future.get());
+ } catch( Exception e ) {
+ fail(e);
+ }
+ }
+
+
+ @Test
+ void animatePropertyColor() {
+ Shape s = new Ellipse(0, 0, 129, 80);
+ s.setAnchor(Constants.CENTER);
+ shapes.add(s);
+
+ _animatePropertyColor(s, Color.RED, 1000, Easing.DEFAULT_EASING);
+ assertEquals(Color.RED, s.getFillColor());
+ _animatePropertyColor(s, Color.BLUE, 1500, Easing::backInOut);
+ assertEquals(Color.BLUE, s.getFillColor());
+ _animatePropertyColor(s, Color.GREEN, 2000, Easing::bounceOut);
+ assertEquals(Color.GREEN, s.getFillColor());
+ _animatePropertyColor(s, Color.YELLOW, 300, Easing::thereAndBack);
+ assertEquals(Color.GREEN, s.getFillColor());
+ }
+
+ private void _animatePropertyColor( Shape s, Color to, int runtime, DoubleUnaryOperator easing ) {
+ s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5);
+ final Color from = s.getFillColor();
+ Future future = Animations.animateProperty(
+ s, from, to, runtime, easing, s::setFillColor
+ );
+ assertNotNull(future);
+ try {
+ assertEquals(s, future.get());
+ } catch( Exception e ) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void animatePropertyReflect() {
+ Shape s = new Ellipse(0, 200, 129, 80);
+ shapes.add(s);
+
+ try {
+ Animations.animateProperty("x", s, 400, 1000, Easing.DEFAULT_EASING);
+ Animations.animateProperty("strokeColor", s, Color.RED, 1000, Easing.DEFAULT_EASING).get();
+ } catch( InterruptedException | ExecutionException e ) {
+ fail(e);
+ }
+ }
+
+ @Test
+ void animateManim() {
+ Shape s = new Circle(0, 0, 10);
+ shapes.add(s);
+ Text t = new Text(0, 0, "Easing");
+ t.setAnchor(Options.Direction.EAST);
+ t.alignTo(Options.Direction.NORTHEAST, -20.0);
+ shapes.add(t);
+
+ t.setText("rushIn");
+ _animateMove(s, 2500, Easing::rushIn);
+ t.setText("rushOut");
+ _animateMove(s, 2500, Easing::rushOut);
+ t.setText("hobbit");
+ _animateMove(s, 2500, Easing::hobbit);
+ t.setText("wiggle(2)");
+ _animateMove(s, 2500, Easing::wiggle);
+ t.setText("wiggle(4)");
+ _animateMove(s, 2500, Easing.wiggle(4));
+ t.setText("doubleSmooth");
+ _animateMove(s, 2500, Easing::doubleSmooth);
+ }
+
+}