From fc7ee363678442ec6ee19d02b406dc110881bed7 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:42:47 +0200 Subject: [PATCH 01/17] Implementierung von Easing-Funktionen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Implementierungen wurden von https://easings.net übernommen. --- src/schule/ngb/zm/anim/Easing.java | 252 +++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 src/schule/ngb/zm/anim/Easing.java diff --git a/src/schule/ngb/zm/anim/Easing.java b/src/schule/ngb/zm/anim/Easing.java new file mode 100644 index 0000000..8dea06b --- /dev/null +++ b/src/schule/ngb/zm/anim/Easing.java @@ -0,0 +1,252 @@ +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::cubicInOut; + + 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); + } + } + + + 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; + } + + private Easing() { + } + +} From 2f59d29d088ec5cc0b7e4e109097087ad9b95772 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:44:36 +0200 Subject: [PATCH 02/17] Animationen als eigener Prozess Die Animations API verwendet Funktionale Aspekte der Java 8 API und erlaubt die Animation beliebiger Objekte, aber ist vor allem auf die `shape.*` Klassen ausgelegt. --- src/schule/ngb/zm/anim/Animations.java | 183 +++++++++++++++++++++++++ src/schule/ngb/zm/anim/Animator.java | 11 ++ 2 files changed, 194 insertions(+) create mode 100644 src/schule/ngb/zm/anim/Animations.java create mode 100644 src/schule/ngb/zm/anim/Animator.java diff --git a/src/schule/ngb/zm/anim/Animations.java b/src/schule/ngb/zm/anim/Animations.java new file mode 100644 index 0000000..c4e7b96 --- /dev/null +++ b/src/schule/ngb/zm/anim/Animations.java @@ -0,0 +1,183 @@ +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.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); + } + + 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/schule/ngb/zm/anim/Animator.java b/src/schule/ngb/zm/anim/Animator.java new file mode 100644 index 0000000..951279d --- /dev/null +++ b/src/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); + +} From 303b667cbffe1cc9d29babc1ee78f491b6c989a4 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 15:45:25 +0200 Subject: [PATCH 03/17] =?UTF-8?q?Tests=20f=C3=BCr=20Animationen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schule/ngb/zm/anim/AnimationsTest.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 test/src/schule/ngb/zm/anim/AnimationsTest.java diff --git a/test/src/schule/ngb/zm/anim/AnimationsTest.java b/test/src/schule/ngb/zm/anim/AnimationsTest.java new file mode 100644 index 0000000..7aa9eb3 --- /dev/null +++ b/test/src/schule/ngb/zm/anim/AnimationsTest.java @@ -0,0 +1,238 @@ +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.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, 400, 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); + } + } + +} From 9ee7c606fe413f9e5329307a76d641c5d5e468f7 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 21:18:45 +0200 Subject: [PATCH 04/17] Implementierung verschiedener Task-Typen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Tasks erfüllen verschiedene Aufgaben und können vom TaskRunner parallel ausgeführt werden. Ob ein so komplexes Task-Management notwendig ist, bleibt offen. --- src/schule/ngb/zm/tasks/DelayedTask.java | 65 +++++++++++++++++++ .../ngb/zm/tasks/FrameSynchronizedTask.java | 51 +++++++++++++++ .../ngb/zm/tasks/FramerateLimitedTask.java | 11 ++++ src/schule/ngb/zm/tasks/RateLimitedTask.java | 53 +++++++++++++++ src/schule/ngb/zm/tasks/Task.java | 24 +++++++ src/schule/ngb/zm/tasks/TaskRunner.java | 30 ++++++--- 6 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 src/schule/ngb/zm/tasks/DelayedTask.java create mode 100644 src/schule/ngb/zm/tasks/FrameSynchronizedTask.java create mode 100644 src/schule/ngb/zm/tasks/FramerateLimitedTask.java create mode 100644 src/schule/ngb/zm/tasks/RateLimitedTask.java create mode 100644 src/schule/ngb/zm/tasks/Task.java diff --git a/src/schule/ngb/zm/tasks/DelayedTask.java b/src/schule/ngb/zm/tasks/DelayedTask.java new file mode 100644 index 0000000..02f3ed5 --- /dev/null +++ b/src/schule/ngb/zm/tasks/DelayedTask.java @@ -0,0 +1,65 @@ +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() { + long delay = getDelay(TimeUnit.MILLISECONDS); + while( delay > 0 ) { + try { + wait(delay); + } catch( InterruptedException e ) { + // Keep waiting + } + delay = getDelay(TimeUnit.MILLISECONDS); + } + + running = true; + this.update(0.0); + running = false; + done = true; + } + +} diff --git a/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java b/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java new file mode 100644 index 0000000..73bda60 --- /dev/null +++ b/src/schule/ngb/zm/tasks/FrameSynchronizedTask.java @@ -0,0 +1,51 @@ +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("Zeichenthread") ) { + // Need to search for main Zeichenthread ... + } + } + return mainThread; + } + + @Override + public void run() { + running = true; + int lastTick = 0; + Thread lock = getMainThread(); + + while( running ) { + lastTick = Constants.tick; + this.update(lastTick); + + synchronized( lock ) { + while( lastTick >= Constants.tick ) { + /*try { + lock.wait(); + } catch( InterruptedException e ) { + // We got interrupted ... + }*/ + Thread.yield(); + } + } + } + + running = false; + done = true; + } + + @Override + public boolean isActive() { + return false; + } + +} diff --git a/src/schule/ngb/zm/tasks/FramerateLimitedTask.java b/src/schule/ngb/zm/tasks/FramerateLimitedTask.java new file mode 100644 index 0000000..8fab4f6 --- /dev/null +++ b/src/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/schule/ngb/zm/tasks/RateLimitedTask.java b/src/schule/ngb/zm/tasks/RateLimitedTask.java new file mode 100644 index 0000000..aadc06d --- /dev/null +++ b/src/schule/ngb/zm/tasks/RateLimitedTask.java @@ -0,0 +1,53 @@ +package schule.ngb.zm.tasks; + +public abstract class RateLimitedTask extends Task { + + public abstract int getRate(); + + @Override + public final void run() { + if( running || done ) { + return; + } + + // 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; + } + +} diff --git a/src/schule/ngb/zm/tasks/Task.java b/src/schule/ngb/zm/tasks/Task.java new file mode 100644 index 0000000..a8ae158 --- /dev/null +++ b/src/schule/ngb/zm/tasks/Task.java @@ -0,0 +1,24 @@ +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; + } + +} diff --git a/src/schule/ngb/zm/tasks/TaskRunner.java b/src/schule/ngb/zm/tasks/TaskRunner.java index 5bd20da..0173fca 100644 --- a/src/schule/ngb/zm/tasks/TaskRunner.java +++ b/src/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 From e4818d4f3e2f95aa1b14bf9379efde2eaa8f9185 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Thu, 7 Jul 2022 21:45:08 +0200 Subject: [PATCH 05/17] =?UTF-8?q?Abstraktion=20f=C3=BCr=20Listener=20API?= =?UTF-8?q?=20erstellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EventGenerator soll als einheitlicher Unterbau zur Umstzung von Listener Patterns dienen. Damit sollen Listener wie AudioListener oder AnimationListener umgesetzt werden. --- src/schule/ngb/zm/events/EventGenerator.java | 50 ++++++++++++++ src/schule/ngb/zm/events/Listener.java | 7 ++ .../ngb/zm/events/EventGeneratorTest.java | 65 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/schule/ngb/zm/events/EventGenerator.java create mode 100644 src/schule/ngb/zm/events/Listener.java create mode 100644 test/src/schule/ngb/zm/events/EventGeneratorTest.java diff --git a/src/schule/ngb/zm/events/EventGenerator.java b/src/schule/ngb/zm/events/EventGenerator.java new file mode 100644 index 0000000..f8834a0 --- /dev/null +++ b/src/schule/ngb/zm/events/EventGenerator.java @@ -0,0 +1,50 @@ +package schule.ngb.zm.events; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.BiConsumer; + +public class EventGenerator> { + + private CopyOnWriteArraySet listeners; + + private ConcurrentMap> eventRegistry; + + public EventGenerator() { + listeners = new CopyOnWriteArraySet<>(); + eventRegistry = new ConcurrentHashMap<>(); + } + + public void registerEventType( String eventKey, BiConsumer 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 ) { + if( eventRegistered(eventKey) ) { + final BiConsumer dispatcher = eventRegistry.get(eventKey); + listeners.stream().forEach(( listener ) -> { + dispatcher.accept(event, listener); + }); + } + } + +} diff --git a/src/schule/ngb/zm/events/Listener.java b/src/schule/ngb/zm/events/Listener.java new file mode 100644 index 0000000..54c6025 --- /dev/null +++ b/src/schule/ngb/zm/events/Listener.java @@ -0,0 +1,7 @@ +package schule.ngb.zm.events; + +public interface Listener { + + + +} diff --git a/test/src/schule/ngb/zm/events/EventGeneratorTest.java b/test/src/schule/ngb/zm/events/EventGeneratorTest.java new file mode 100644 index 0000000..f10979b --- /dev/null +++ b/test/src/schule/ngb/zm/events/EventGeneratorTest.java @@ -0,0 +1,65 @@ +package schule.ngb.zm.events; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EventGeneratorTest { + + class TestEvent { + + private String data; + + private String type; + + public TestEvent( String data, boolean isStart ) { + this.data = data; + this.type = isStart ? "start" : "stop"; + } + + public String getData() { + return data; + } + + public String getType() { + return type; + } + + } + + interface TestListener extends Listener { + + void startEvent( TestEvent t ); + + void stopEvent( TestEvent t ); + + } + + @Test + void eventRegistry() { + EventGenerator gen = new EventGenerator<>(); + + gen.registerEventType("start", ( event, listener ) -> listener.startEvent(event)); + gen.registerEventType("stop", ( event, listener ) -> listener.stopEvent(event)); + + gen.addListener(new TestListener() { + @Override + public void startEvent( TestEvent t ) { + assertEquals("start", t.getType()); + assertTrue(t.getData().startsWith("Start Event")); + } + + @Override + public void stopEvent( TestEvent t ) { + assertEquals("stop", t.getType()); + assertTrue(t.getData().startsWith("Stop Event")); + } + }); + + gen.dispatchEvent("start", new TestEvent("Start Event 1", true)); + gen.dispatchEvent("stop", new TestEvent("Stop Event 1", false)); + gen.dispatchEvent("stop", new TestEvent("Stop Event 2", false)); + gen.dispatchEvent("start", new TestEvent("Start Event 2", true)); + } + +} From d48b167fb389730c2cd34313426c0a90008871ec Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Fri, 8 Jul 2022 07:31:37 +0200 Subject: [PATCH 06/17] Renamed generator to dispatcher --- ...entGenerator.java => EventDispatcher.java} | 14 +++++++++--- src/schule/ngb/zm/media/Audio.java | 2 ++ src/schule/ngb/zm/media/AudioListener.java | 11 ++++++++++ src/schule/ngb/zm/media/Mixer.java | 11 ++++++++++ src/schule/ngb/zm/media/Music.java | 22 +++++++++++++++++++ src/schule/ngb/zm/media/Sound.java | 4 ++++ ...atorTest.java => EventDispatcherTest.java} | 4 ++-- 7 files changed, 63 insertions(+), 5 deletions(-) rename src/schule/ngb/zm/events/{EventGenerator.java => EventDispatcher.java} (78%) create mode 100644 src/schule/ngb/zm/media/AudioListener.java rename test/src/schule/ngb/zm/events/{EventGeneratorTest.java => EventDispatcherTest.java} (93%) diff --git a/src/schule/ngb/zm/events/EventGenerator.java b/src/schule/ngb/zm/events/EventDispatcher.java similarity index 78% rename from src/schule/ngb/zm/events/EventGenerator.java rename to src/schule/ngb/zm/events/EventDispatcher.java index f8834a0..61722c5 100644 --- a/src/schule/ngb/zm/events/EventGenerator.java +++ b/src/schule/ngb/zm/events/EventDispatcher.java @@ -1,22 +1,27 @@ 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 EventGenerator> { +public class EventDispatcher> { private CopyOnWriteArraySet listeners; private ConcurrentMap> eventRegistry; - public EventGenerator() { + 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); } @@ -39,9 +44,12 @@ public class EventGenerator> { } public void dispatchEvent( String eventKey, final E event ) { + Validator.requireNotNull(eventKey); + Validator.requireNotNull(event); + if( eventRegistered(eventKey) ) { final BiConsumer dispatcher = eventRegistry.get(eventKey); - listeners.stream().forEach(( listener ) -> { + listeners.forEach(( listener ) -> { dispatcher.accept(event, listener); }); } diff --git a/src/schule/ngb/zm/media/Audio.java b/src/schule/ngb/zm/media/Audio.java index e2f5137..0292df6 100644 --- a/src/schule/ngb/zm/media/Audio.java +++ b/src/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/schule/ngb/zm/media/AudioListener.java b/src/schule/ngb/zm/media/AudioListener.java new file mode 100644 index 0000000..1600e07 --- /dev/null +++ b/src/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