From 7031aa40cc45c2a685ee761bb8595d09003c6556 Mon Sep 17 00:00:00 2001 From: "J. Neugebauer" Date: Sat, 16 Jul 2022 17:01:39 +0200 Subject: [PATCH] Animationssystem erweitert --- src/main/java/schule/ngb/zm/Constants.java | 6 + .../java/schule/ngb/zm/Zeichenmaschine.java | 4 + .../java/schule/ngb/zm/anim/Animation.java | 125 +++++++++++++++++- .../schule/ngb/zm/anim/AnimationFacade.java | 36 +++++ .../schule/ngb/zm/anim/AnimationGroup.java | 76 +++++++++++ .../java/schule/ngb/zm/anim/Animations.java | 38 ++++-- .../schule/ngb/zm/anim/FillAnimation.java | 33 +++++ .../schule/ngb/zm/anim/MorphAnimation.java | 53 ++++++++ .../schule/ngb/zm/anim/MoveAnimation.java | 39 ++++++ .../schule/ngb/zm/anim/RotateAnimation.java | 32 +++++ .../schule/ngb/zm/anim/StrokeAnimation.java | 32 +++++ .../schule/ngb/zm/shapes/ShapesLayer.java | 43 +++++- .../ngb/zm/tasks/FrameSynchronizedTask.java | 29 ++-- 13 files changed, 524 insertions(+), 22 deletions(-) create mode 100644 src/main/java/schule/ngb/zm/anim/AnimationFacade.java create mode 100644 src/main/java/schule/ngb/zm/anim/AnimationGroup.java create mode 100644 src/main/java/schule/ngb/zm/anim/FillAnimation.java create mode 100644 src/main/java/schule/ngb/zm/anim/MorphAnimation.java create mode 100644 src/main/java/schule/ngb/zm/anim/MoveAnimation.java create mode 100644 src/main/java/schule/ngb/zm/anim/RotateAnimation.java create mode 100644 src/main/java/schule/ngb/zm/anim/StrokeAnimation.java diff --git a/src/main/java/schule/ngb/zm/Constants.java b/src/main/java/schule/ngb/zm/Constants.java index b9436f0..97a6025 100644 --- a/src/main/java/schule/ngb/zm/Constants.java +++ b/src/main/java/schule/ngb/zm/Constants.java @@ -1,5 +1,6 @@ package schule.ngb.zm; +import schule.ngb.zm.anim.Easing; import schule.ngb.zm.util.ImageLoader; import schule.ngb.zm.util.Noise; @@ -8,6 +9,7 @@ import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.Random; +import java.util.function.DoubleUnaryOperator; /** * Basisklasse für die meisten Objekte der Zeichemaschine, die von Nutzern @@ -110,6 +112,10 @@ public class Constants { */ public static final int STD_BUFFER = 10; + public static int DEFAULT_ANIM_RUNTIME = 1000; + + public static DoubleUnaryOperator DEFAULT_EASING = Easing.DEFAULT_EASING; + /** * Option für durchgezogene Konturen und Linien. */ diff --git a/src/main/java/schule/ngb/zm/Zeichenmaschine.java b/src/main/java/schule/ngb/zm/Zeichenmaschine.java index 8cd16cf..f2236de 100644 --- a/src/main/java/schule/ngb/zm/Zeichenmaschine.java +++ b/src/main/java/schule/ngb/zm/Zeichenmaschine.java @@ -1408,6 +1408,10 @@ public class Zeichenmaschine extends Constants { if( state == Options.AppState.RUNNING ) { state = Options.AppState.UPDATING; update(delta); + + for( Layer l: canvas.getLayers() ) { + l.update(delta); + } state = Options.AppState.RUNNING; } } diff --git a/src/main/java/schule/ngb/zm/anim/Animation.java b/src/main/java/schule/ngb/zm/anim/Animation.java index 793f352..709fbaf 100644 --- a/src/main/java/schule/ngb/zm/anim/Animation.java +++ b/src/main/java/schule/ngb/zm/anim/Animation.java @@ -1,8 +1,131 @@ package schule.ngb.zm.anim; +import schule.ngb.zm.Constants; +import schule.ngb.zm.Updatable; import schule.ngb.zm.events.EventDispatcher; +import schule.ngb.zm.tasks.FrameSynchronizedTask; +import schule.ngb.zm.tasks.TaskRunner; -public class Animation { +import java.util.function.DoubleUnaryOperator; + +public abstract class Animation implements Updatable { + + protected int runtime; + + protected int elapsed_time = 0; + + protected boolean running = false, finished = false; + + protected DoubleUnaryOperator easing; + + public Animation() { + this.runtime = Constants.DEFAULT_ANIM_RUNTIME; + this.easing = Constants.DEFAULT_EASING; + } + + public Animation( DoubleUnaryOperator easing ) { + this.runtime = Constants.DEFAULT_ANIM_RUNTIME; + this.easing = easing; + } + + public Animation( int runtime ) { + this.runtime = runtime; + this.easing = Constants.DEFAULT_EASING; + } + + public Animation( int runtime, DoubleUnaryOperator easing ) { + this.runtime = runtime; + this.easing = easing; + } + + public int getRuntime() { + return runtime; + } + + public void setRuntime( int pRuntime ) { + this.runtime = pRuntime; + } + + public DoubleUnaryOperator getEasing() { + return easing; + } + + public void setEasing( DoubleUnaryOperator pEasing ) { + this.easing = pEasing; + } + + public abstract T getAnimationTarget(); + + public final void start() { + this.initialize(); + elapsed_time = 0; + running = true; + finished = false; + interpolate(easing.applyAsDouble(0.0)); + initializeEventDispatcher().dispatchEvent("start", this); + } + + public final void stop() { + running = false; + // Make sure the last animation frame was interpolated correctly + interpolate(easing.applyAsDouble((double) elapsed_time / (double) runtime)); + this.finish(); + finished = true; + initializeEventDispatcher().dispatchEvent("stop", this); + } + + public void initialize() { + // Intentionally left blank + } + + public void finish() { + // Intentionally left blank + } + + public final void await() { + while( !finished ) { + try { + Thread.sleep(1); + } catch( InterruptedException ex ) { + // Keep waiting + } + } + } + + @Override + public boolean isActive() { + return running; + } + + @Override + public void update( double delta ) { + elapsed_time += (int) (delta * 1000); + if( elapsed_time > runtime ) + elapsed_time = runtime; + + double t = (double) elapsed_time / (double) runtime; + if( t >= 1.0 ) { + running = false; + stop(); + } else { + interpolate(easing.applyAsDouble(t)); + } + } + + /** + * Setzt den Fortschritt der Animation auf den angegebenen Wert. + *

+ * {@code e} liegt in der Regel zwischen 0 und 1. Je nach verwendeten + * {@link Easing} Funktion kann der Wert aber in Ausnahmefällen unter 0 oder + * über 1 liegen. Die {@code step()} Methode muss dem nicht Rechnung tragen + * und kann wenn sinnvoll den {@code e} Wert auf [0, 1] limitieren: + *


+	 * e = Constants.limit(e, 0, 1);
+	 * 
+ * + * @param e + */ + public abstract void interpolate( double e ); EventDispatcher eventDispatcher; diff --git a/src/main/java/schule/ngb/zm/anim/AnimationFacade.java b/src/main/java/schule/ngb/zm/anim/AnimationFacade.java new file mode 100644 index 0000000..192b540 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/AnimationFacade.java @@ -0,0 +1,36 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.util.Validator; + +import java.util.function.DoubleUnaryOperator; + +public class AnimationFacade extends Animation { + + private Animation anim; + + public AnimationFacade( Animation anim, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + this.anim = Validator.requireNotNull(anim); + } + + @Override + public S getAnimationTarget() { + return anim.getAnimationTarget(); + } + + @Override + public void interpolate( double e ) { + anim.interpolate(e); + } + + @Override + public void initialize() { + anim.initialize(); + } + + @Override + public void finish() { + anim.finish(); + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/AnimationGroup.java b/src/main/java/schule/ngb/zm/anim/AnimationGroup.java new file mode 100644 index 0000000..ea2e244 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/AnimationGroup.java @@ -0,0 +1,76 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.shapes.Shape; + +import java.util.Arrays; +import java.util.function.DoubleUnaryOperator; + +public class AnimationGroup extends Animation { + + Animation[] anims; + + private boolean overrideRuntime = false; + + + public AnimationGroup( DoubleUnaryOperator easing, Animation... anims ) { + super(easing); + this.anims = anims; + + int maxRuntime = Arrays.stream(this.anims).mapToInt((a) -> a.getRuntime()).reduce(0, Integer::max); + setRuntime(maxRuntime); + } + + public AnimationGroup( int runtime, DoubleUnaryOperator easing, Animation... anims ) { + super(runtime, easing); + this.anims = anims; + overrideRuntime = true; + } + + @Override + public Shape getAnimationTarget() { + return null; + } + + @Override + public void update( double delta ) { + if( overrideRuntime ) { + synchronized( anims ) { + for( Animation anim: anims ) { + if( anim.isActive() ) { + anim.update(delta); + } + } + } + } else { + super.update(delta); + } + } + + @Override + public void interpolate( double e ) { + synchronized( anims ) { + for( Animation anim: anims ) { + anim.interpolate(e); + } + } + } + + @Override + public void initialize() { + synchronized( anims ) { + for( Animation anim: anims ) { + anim.initialize(); + } + } + } + + @Override + public void finish() { + synchronized( anims ) { + for( Animation anim: anims ) { + anim.finish(); + } + } + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/Animations.java b/src/main/java/schule/ngb/zm/anim/Animations.java index d9f63a2..c80c008 100644 --- a/src/main/java/schule/ngb/zm/anim/Animations.java +++ b/src/main/java/schule/ngb/zm/anim/Animations.java @@ -4,6 +4,7 @@ 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.FramerateLimitedTask; import schule.ngb.zm.tasks.TaskRunner; import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Validator; @@ -158,7 +159,7 @@ public class Animations { } while( t < 1.0 ); stepper.accept(easing.applyAsDouble(1.0)); }, target);*/ - return TaskRunner.run(new FrameSynchronizedTask() { + return TaskRunner.run(new FramerateLimitedTask() { double t = 0.0; final long starttime = System.currentTimeMillis(); @Override @@ -185,15 +186,36 @@ public class Animations { ); } - /*public static Future animate( Animation animation ) { - animation.start(); - return null; + public static Future animate( Animation animation ) { + return TaskRunner.run(new FramerateLimitedTask() { + @Override + protected void initialize() { + animation.start(); + } + + @Override + public void update( double delta ) { + animation.update(delta); + running = animation.isActive(); + } + }, animation); } - public static Future animate( Animation animation, DoubleUnaryOperator easing ) { - animation.start(easing); - return null; - }*/ + public static Future> animate( Animation animation, DoubleUnaryOperator easing ) { + final AnimationFacade facade = new AnimationFacade<>(animation, animation.getRuntime(), easing); + return TaskRunner.run(new FramerateLimitedTask() { + @Override + protected void initialize() { + facade.start(); + } + + @Override + public void update( double delta ) { + facade.update(delta); + running = facade.isActive(); + } + }, animation); + } public static final Log LOG = Log.getLogger(Animations.class); diff --git a/src/main/java/schule/ngb/zm/anim/FillAnimation.java b/src/main/java/schule/ngb/zm/anim/FillAnimation.java new file mode 100644 index 0000000..f10c375 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/FillAnimation.java @@ -0,0 +1,33 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Color; +import schule.ngb.zm.Constants; +import schule.ngb.zm.shapes.Shape; + +import java.util.function.DoubleUnaryOperator; + +public class FillAnimation extends Animation { + + private Shape object; + + private Color oFill, tFill; + + public FillAnimation( Shape object, Color newFill, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + + this.object = object; + oFill = object.getFillColor(); + tFill = newFill; + } + + @Override + public Shape getAnimationTarget() { + return object; + } + + @Override + public void interpolate( double e ) { + object.setFillColor(Color.interpolate(oFill, tFill, e)); + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/MorphAnimation.java b/src/main/java/schule/ngb/zm/anim/MorphAnimation.java new file mode 100644 index 0000000..2e04e40 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/MorphAnimation.java @@ -0,0 +1,53 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Color; +import schule.ngb.zm.Constants; +import schule.ngb.zm.shapes.Circle; +import schule.ngb.zm.shapes.Ellipse; +import schule.ngb.zm.shapes.Rectangle; +import schule.ngb.zm.shapes.Shape; + +import java.util.function.DoubleUnaryOperator; + +public class MorphAnimation extends Animation { + + private Shape object, original, target; + + public MorphAnimation( Shape object, Shape target, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + + this.original = object.copy(); + this.object = object; + this.target = target; + } + + @Override + public Shape getAnimationTarget() { + return object; + } + + @Override + public void interpolate( double e ) { + object.setX(Constants.interpolate(original.getX(), target.getX(), e)); + object.setY(Constants.interpolate(original.getY(), target.getY(), e)); + object.setFillColor(Color.interpolate(original.getFillColor(), target.getFillColor(), e)); + object.setStrokeColor(Color.interpolate(original.getStrokeColor(), target.getStrokeColor(), e)); + object.rotateTo(Constants.interpolate(original.getRotation(), target.getRotation(), e)); + object.scale(Constants.interpolate(original.getScale(), target.getScale(), e)); + object.setStrokeWeight(Constants.interpolate(original.getStrokeWeight(), target.getStrokeWeight(), e)); + + if( object instanceof Rectangle ) { + Rectangle r = (Rectangle)object; + r.setWidth(Constants.interpolate(original.getWidth(), target.getWidth(), e)); + r.setHeight(Constants.interpolate(original.getHeight(), target.getHeight(), e)); + } else if( object instanceof Circle ) { + Circle r = (Circle)object; + r.setRadius(Constants.interpolate(original.getWidth()*.5, target.getWidth()*.5, e)); + } else if( object instanceof Ellipse ) { + Ellipse r = (Ellipse)object; + r.setWidth(Constants.interpolate(original.getWidth(), target.getWidth(), e)); + r.setHeight(Constants.interpolate(original.getHeight(), target.getHeight(), e)); + } + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/MoveAnimation.java b/src/main/java/schule/ngb/zm/anim/MoveAnimation.java new file mode 100644 index 0000000..a068a0f --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/MoveAnimation.java @@ -0,0 +1,39 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Color; +import schule.ngb.zm.Constants; +import schule.ngb.zm.shapes.Circle; +import schule.ngb.zm.shapes.Ellipse; +import schule.ngb.zm.shapes.Rectangle; +import schule.ngb.zm.shapes.Shape; + +import java.util.function.DoubleUnaryOperator; + +public class MoveAnimation extends Animation { + + private Shape object; + + private double oX, oY, tX, tY; + + public MoveAnimation( Shape object, double x, double y, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + + this.object = object; + oX = object.getX(); + oY = object.getY(); + tX = x; + tY = y; + } + + @Override + public Shape getAnimationTarget() { + return object; + } + + @Override + public void interpolate( double e ) { + object.setX(Constants.interpolate(oX, tX, e)); + object.setY(Constants.interpolate(oY, tY, e)); + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/RotateAnimation.java b/src/main/java/schule/ngb/zm/anim/RotateAnimation.java new file mode 100644 index 0000000..70d1c93 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/RotateAnimation.java @@ -0,0 +1,32 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Constants; +import schule.ngb.zm.shapes.Shape; + +import java.util.function.DoubleUnaryOperator; + +public class RotateAnimation extends Animation { + + private Shape object; + + private double oA, tA; + + public RotateAnimation( Shape object, double angle, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + + this.object = object; + oA = object.getRotation(); + tA = angle; + } + + @Override + public Shape getAnimationTarget() { + return object; + } + + @Override + public void interpolate( double e ) { + object.rotateTo(Constants.interpolate(oA, tA, e)); + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/StrokeAnimation.java b/src/main/java/schule/ngb/zm/anim/StrokeAnimation.java new file mode 100644 index 0000000..ea46455 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/StrokeAnimation.java @@ -0,0 +1,32 @@ +package schule.ngb.zm.anim; + +import schule.ngb.zm.Color; +import schule.ngb.zm.shapes.Shape; + +import java.util.function.DoubleUnaryOperator; + +public class StrokeAnimation extends Animation { + + private Shape object; + + private Color oFill, tFill; + + public StrokeAnimation( Shape object, Color newStroke, int runtime, DoubleUnaryOperator easing ) { + super(runtime, easing); + + this.object = object; + oFill = object.getFillColor(); + tFill = newStroke; + } + + @Override + public Shape getAnimationTarget() { + return object; + } + + @Override + public void interpolate( double e ) { + object.setStrokeColor(Color.interpolate(oFill, tFill, e)); + } + +} diff --git a/src/main/java/schule/ngb/zm/shapes/ShapesLayer.java b/src/main/java/schule/ngb/zm/shapes/ShapesLayer.java index 8626d12..bc4e4c7 100644 --- a/src/main/java/schule/ngb/zm/shapes/ShapesLayer.java +++ b/src/main/java/schule/ngb/zm/shapes/ShapesLayer.java @@ -1,25 +1,35 @@ package schule.ngb.zm.shapes; import schule.ngb.zm.Layer; +import schule.ngb.zm.anim.Animation; +import schule.ngb.zm.anim.AnimationFacade; +import schule.ngb.zm.anim.Easing; import java.awt.Graphics2D; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedList; +import java.util.List; +import java.util.function.DoubleUnaryOperator; public class ShapesLayer extends Layer { protected boolean clearBeforeDraw = true; - private LinkedList shapes; + private List shapes; + + private List> animations; public ShapesLayer() { super(); - shapes = new LinkedList(); + shapes = new LinkedList<>(); + animations = new LinkedList<>(); } public ShapesLayer( int width, int height ) { super(width, height); - shapes = new LinkedList(); + shapes = new LinkedList<>(); + animations = new LinkedList<>(); } public Shape getShape( int index ) { @@ -103,6 +113,33 @@ public class ShapesLayer extends Layer { } } + public void play( Animation anim ) { + this.animations.add(anim); + anim.start(); + } + + public void play( Animation anim, int runtime ) { + play(anim, runtime, Easing.DEFAULT_EASING); + } + + public void play( Animation anim, int runtime, DoubleUnaryOperator easing ) { + AnimationFacade facade = new AnimationFacade<>(anim, runtime, easing); + play(facade); + } + + @Override + public void update( double delta ) { + Iterator> it = animations.iterator(); + while( it.hasNext() ) { + Animation anim = it.next(); + anim.update(delta); + + if( !anim.isActive() ) { + animations.remove(anim); + } + } + } + @Override public void draw( Graphics2D pGraphics ) { if( clearBeforeDraw ) { diff --git a/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java b/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java index 454818a..4cf7ab1 100644 --- a/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java +++ b/src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java @@ -20,17 +20,31 @@ public abstract class FrameSynchronizedTask extends Task { @Override public void run() { initialize(); - running = true; - int lastTick = 0; + Object lock = Zeichenmaschine.globalSyncLock; + // start of thread in ms + final long start = System.currentTimeMillis(); + // current time in ns + long beforeTime = System.nanoTime(); + // store for deltas + long overslept = 0L; + // internal counters for tick and runtime + int _tick = 0; + + double delta = 0.0; + while( running ) { - lastTick = Constants.tick; - this.update(lastTick); + // delta in seconds + delta = (System.nanoTime() - beforeTime) / 1000000000.0; + beforeTime = System.nanoTime(); + + _tick = Constants.tick; + this.update(delta); synchronized( lock ) { - while( lastTick >= Constants.tick ) { + while( _tick >= Constants.tick ) { try { lock.wait(); } catch( InterruptedException e ) { @@ -46,10 +60,5 @@ public abstract class FrameSynchronizedTask extends Task { finish(); } - @Override - public boolean isActive() { - return false; - } - }