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 0979b6a..8cd16cf 100644 --- a/src/main/java/schule/ngb/zm/Zeichenmaschine.java +++ b/src/main/java/schule/ngb/zm/Zeichenmaschine.java @@ -1281,10 +1281,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); } @@ -1333,6 +1360,9 @@ public class Zeichenmaschine extends Constants { } runTasks(); + synchronized( globalSyncLock ) { + globalSyncLock.notifyAll(); + } // delta time in ns long afterTime = System.nanoTime(); @@ -1418,7 +1448,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/Animation.java b/src/main/java/schule/ngb/zm/anim/Animation.java new file mode 100644 index 0000000..793f352 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/Animation.java @@ -0,0 +1,26 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.events.EventDispatcher; + +public class Animation { + + EventDispatcher eventDispatcher; + + private EventDispatcher initializeEventDispatcher() { + if( eventDispatcher == null ) { + eventDispatcher = new EventDispatcher<>(); + eventDispatcher.registerEventType("start", ( a, l ) -> l.animationStarted(a)); + eventDispatcher.registerEventType("stop", ( a, l ) -> l.animationStopped(a)); + } + return eventDispatcher; + } + + public void addListener( AnimationListener listener ) { + initializeEventDispatcher().addListener(listener); + } + + public void removeListener( AnimationListener listener ) { + initializeEventDispatcher().removeListener(listener); + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/AnimationListener.java b/src/main/java/schule/ngb/zm/anim/AnimationListener.java new file mode 100644 index 0000000..13e62da --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/AnimationListener.java @@ -0,0 +1,11 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.events.Listener; + +public interface AnimationListener extends Listener { + + void animationStarted( Animation anim ); + + void animationStopped( Animation anim ); + +} 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/events/EventDispatcher.java b/src/main/java/schule/ngb/zm/events/EventDispatcher.java new file mode 100644 index 0000000..61722c5 --- /dev/null +++ b/src/main/java/schule/ngb/zm/events/EventDispatcher.java @@ -0,0 +1,58 @@ +package schule.ngb.zm.events; + +import schule.ngb.zm.util.Validator; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.BiConsumer; + +public class EventDispatcher> { + + private CopyOnWriteArraySet listeners; + + private ConcurrentMap> eventRegistry; + + public EventDispatcher() { + listeners = new CopyOnWriteArraySet<>(); + eventRegistry = new ConcurrentHashMap<>(); + } + + public void registerEventType( String eventKey, BiConsumer dispatcher ) { + Validator.requireNotNull(eventKey); + Validator.requireNotNull(dispatcher); + + if( !eventRegistered(eventKey) ) { + eventRegistry.put(eventKey, dispatcher); + } + } + + public void addListener( L listener ) { + listeners.add(listener); + } + + public void removeListener( L listener ) { + listeners.remove(listener); + } + + public boolean hasListeners() { + return !listeners.isEmpty(); + } + + public boolean eventRegistered( String eventKey ) { + return eventRegistry.containsKey(eventKey); + } + + public void dispatchEvent( String eventKey, final E event ) { + Validator.requireNotNull(eventKey); + Validator.requireNotNull(event); + + if( eventRegistered(eventKey) ) { + final BiConsumer dispatcher = eventRegistry.get(eventKey); + listeners.forEach(( listener ) -> { + dispatcher.accept(event, listener); + }); + } + } + +} diff --git a/src/main/java/schule/ngb/zm/events/Listener.java b/src/main/java/schule/ngb/zm/events/Listener.java new file mode 100644 index 0000000..54c6025 --- /dev/null +++ b/src/main/java/schule/ngb/zm/events/Listener.java @@ -0,0 +1,7 @@ +package schule.ngb.zm.events; + +public interface Listener { + + + +} diff --git a/src/main/java/schule/ngb/zm/media/Audio.java b/src/main/java/schule/ngb/zm/media/Audio.java index fa64162..6e8640b 100644 --- a/src/main/java/schule/ngb/zm/media/Audio.java +++ b/src/main/java/schule/ngb/zm/media/Audio.java @@ -5,6 +5,8 @@ package schule.ngb.zm.media; */ public interface Audio { + String getSource(); + /** * Prüft, ob das Medium gerade abgespielt wird. * diff --git a/src/main/java/schule/ngb/zm/media/AudioListener.java b/src/main/java/schule/ngb/zm/media/AudioListener.java new file mode 100644 index 0000000..1600e07 --- /dev/null +++ b/src/main/java/schule/ngb/zm/media/AudioListener.java @@ -0,0 +1,11 @@ +package schule.ngb.zm.media; + +import schule.ngb.zm.events.Listener; + +public interface AudioListener extends Listener