Animationssystem erweitert

This commit is contained in:
ngb
2022-07-16 17:01:39 +02:00
parent c295821d85
commit 7031aa40cc
13 changed files with 524 additions and 22 deletions

View File

@@ -1,5 +1,6 @@
package schule.ngb.zm; package schule.ngb.zm;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.util.ImageLoader; import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.util.Noise; import schule.ngb.zm.util.Noise;
@@ -8,6 +9,7 @@ import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Random; import java.util.Random;
import java.util.function.DoubleUnaryOperator;
/** /**
* Basisklasse für die meisten Objekte der Zeichemaschine, die von Nutzern * 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 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. * Option für durchgezogene Konturen und Linien.
*/ */

View File

@@ -1408,6 +1408,10 @@ public class Zeichenmaschine extends Constants {
if( state == Options.AppState.RUNNING ) { if( state == Options.AppState.RUNNING ) {
state = Options.AppState.UPDATING; state = Options.AppState.UPDATING;
update(delta); update(delta);
for( Layer l: canvas.getLayers() ) {
l.update(delta);
}
state = Options.AppState.RUNNING; state = Options.AppState.RUNNING;
} }
} }

View File

@@ -1,8 +1,131 @@
package schule.ngb.zm.anim; 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.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<T> 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.
* <p>
* {@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:
* <pre><code>
* e = Constants.limit(e, 0, 1);
* </code></pre>
*
* @param e
*/
public abstract void interpolate( double e );
EventDispatcher<Animation, AnimationListener> eventDispatcher; EventDispatcher<Animation, AnimationListener> eventDispatcher;

View File

@@ -0,0 +1,36 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.util.Validator;
import java.util.function.DoubleUnaryOperator;
public class AnimationFacade<S> extends Animation<S> {
private Animation<S> anim;
public AnimationFacade( Animation<S> 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();
}
}

View File

@@ -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<Shape> {
Animation<? extends Shape>[] anims;
private boolean overrideRuntime = false;
public AnimationGroup( DoubleUnaryOperator easing, Animation<? extends Shape>... 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<? extends Shape>... 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<? extends Shape> anim: anims ) {
if( anim.isActive() ) {
anim.update(delta);
}
}
}
} else {
super.update(delta);
}
}
@Override
public void interpolate( double e ) {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.interpolate(e);
}
}
}
@Override
public void initialize() {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.initialize();
}
}
}
@Override
public void finish() {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.finish();
}
}
}
}

View File

@@ -4,6 +4,7 @@ import schule.ngb.zm.Color;
import schule.ngb.zm.Constants; import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector; import schule.ngb.zm.Vector;
import schule.ngb.zm.tasks.FrameSynchronizedTask; import schule.ngb.zm.tasks.FrameSynchronizedTask;
import schule.ngb.zm.tasks.FramerateLimitedTask;
import schule.ngb.zm.tasks.TaskRunner; import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
@@ -158,7 +159,7 @@ public class Animations {
} while( t < 1.0 ); } while( t < 1.0 );
stepper.accept(easing.applyAsDouble(1.0)); stepper.accept(easing.applyAsDouble(1.0));
}, target);*/ }, target);*/
return TaskRunner.run(new FrameSynchronizedTask() { return TaskRunner.run(new FramerateLimitedTask() {
double t = 0.0; double t = 0.0;
final long starttime = System.currentTimeMillis(); final long starttime = System.currentTimeMillis();
@Override @Override
@@ -185,15 +186,36 @@ public class Animations {
); );
} }
/*public static <T> Future<?> animate( Animation<T> animation ) { public static <T> Future<?> animate( Animation<T> animation ) {
animation.start(); return TaskRunner.run(new FramerateLimitedTask() {
return null; @Override
protected void initialize() {
animation.start();
}
@Override
public void update( double delta ) {
animation.update(delta);
running = animation.isActive();
}
}, animation);
} }
public static <T> Future<?> animate( Animation<T> animation, DoubleUnaryOperator easing ) { public static <T> Future<Animation<T>> animate( Animation<T> animation, DoubleUnaryOperator easing ) {
animation.start(easing); final AnimationFacade<T> facade = new AnimationFacade<>(animation, animation.getRuntime(), easing);
return null; 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); public static final Log LOG = Log.getLogger(Animations.class);

View File

@@ -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<Shape> {
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));
}
}

View File

@@ -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<Shape> {
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));
}
}
}

View File

@@ -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<Shape> {
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));
}
}

View File

@@ -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<Shape> {
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));
}
}

View File

@@ -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<Shape> {
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));
}
}

View File

@@ -1,25 +1,35 @@
package schule.ngb.zm.shapes; package schule.ngb.zm.shapes;
import schule.ngb.zm.Layer; 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.awt.Graphics2D;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
public class ShapesLayer extends Layer { public class ShapesLayer extends Layer {
protected boolean clearBeforeDraw = true; protected boolean clearBeforeDraw = true;
private LinkedList<Shape> shapes; private List<Shape> shapes;
private List<Animation<? extends Shape>> animations;
public ShapesLayer() { public ShapesLayer() {
super(); super();
shapes = new LinkedList<Shape>(); shapes = new LinkedList<>();
animations = new LinkedList<>();
} }
public ShapesLayer( int width, int height ) { public ShapesLayer( int width, int height ) {
super(width, height); super(width, height);
shapes = new LinkedList<Shape>(); shapes = new LinkedList<>();
animations = new LinkedList<>();
} }
public Shape getShape( int index ) { public Shape getShape( int index ) {
@@ -103,6 +113,33 @@ public class ShapesLayer extends Layer {
} }
} }
public <S extends Shape> void play( Animation<S> anim ) {
this.animations.add(anim);
anim.start();
}
public <S extends Shape> void play( Animation<S> anim, int runtime ) {
play(anim, runtime, Easing.DEFAULT_EASING);
}
public <S extends Shape> void play( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
AnimationFacade<S> facade = new AnimationFacade<>(anim, runtime, easing);
play(facade);
}
@Override
public void update( double delta ) {
Iterator<Animation<? extends Shape>> it = animations.iterator();
while( it.hasNext() ) {
Animation<? extends Shape> anim = it.next();
anim.update(delta);
if( !anim.isActive() ) {
animations.remove(anim);
}
}
}
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D pGraphics ) {
if( clearBeforeDraw ) { if( clearBeforeDraw ) {

View File

@@ -20,17 +20,31 @@ public abstract class FrameSynchronizedTask extends Task {
@Override @Override
public void run() { public void run() {
initialize(); initialize();
running = true; running = true;
int lastTick = 0;
Object lock = Zeichenmaschine.globalSyncLock; 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 ) { while( running ) {
lastTick = Constants.tick; // delta in seconds
this.update(lastTick); delta = (System.nanoTime() - beforeTime) / 1000000000.0;
beforeTime = System.nanoTime();
_tick = Constants.tick;
this.update(delta);
synchronized( lock ) { synchronized( lock ) {
while( lastTick >= Constants.tick ) { while( _tick >= Constants.tick ) {
try { try {
lock.wait(); lock.wait();
} catch( InterruptedException e ) { } catch( InterruptedException e ) {
@@ -46,10 +60,5 @@ public abstract class FrameSynchronizedTask extends Task {
finish(); finish();
} }
@Override
public boolean isActive() {
return false;
}
} }