Updated Animations and Tests

This commit is contained in:
J. Neugebauer
2024-12-02 18:56:36 +01:00
parent 595bdd7556
commit 1d41bf36c5
31 changed files with 1350 additions and 200 deletions

View File

@@ -34,6 +34,11 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
*/
protected Options.StrokeType strokeType = SOLID;
/**
* Die Art der Kantenverbindungen von Linien.
*/
protected Options.StrokeJoin strokeJoin = MITER;
/**
* Cache für den aktuellen {@code Stroke} der Kontur. Wird nach Änderung
* einer der Kontureigenschaften auf {@code null} gesetzt und beim nächsten
@@ -53,6 +58,7 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
*/
protected MultipleGradientPaint fill = null;
// TODO: Add TexturePaint fill (https://docs.oracle.com/javase/8/docs//api/java/awt/TexturePaint.html)
// Implementierung Drawable Interface
@@ -154,7 +160,7 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight);
stroke = Strokeable.createStroke(strokeType, strokeWeight, strokeJoin);
}
return stroke;
}
@@ -191,4 +197,15 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
this.stroke = null;
}
@Override
public Options.StrokeJoin getStrokeJoin() {
return strokeJoin;
}
@Override
public void setStrokeJoin( Options.StrokeJoin join ) {
strokeJoin = join;
this.stroke = null;
}
}

View File

@@ -11,6 +11,7 @@ import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.function.DoubleUnaryOperator;
@@ -67,7 +68,7 @@ public class Constants {
/**
* Patchversion der Zeichenmaschine.
*/
public static final int APP_VERSION_REV = 34;
public static final int APP_VERSION_REV = 35;
/**
* Version der Zeichenmaschine als Text-String.
@@ -170,6 +171,21 @@ public class Constants {
*/
public static final Options.StrokeType DOTTED = Options.StrokeType.DOTTED;
/**
* Option für abgerundete Kantenverbindungen von Konturen und Linien.
*/
public static final Options.StrokeJoin ROUND = Options.StrokeJoin.ROUND;
/**
* Option für abgeschnittene Kantenverbindungen von Konturen und Linien.
*/
public static final Options.StrokeJoin BEVEL = Options.StrokeJoin.BEVEL;
/**
* Option für eckige Kantenverbindungen von Konturen und Linien.
*/
public static final Options.StrokeJoin MITER = Options.StrokeJoin.MITER;
/**
* Option für Pfeile mit Strichen als Kopf.
*/
@@ -1268,7 +1284,7 @@ public class Constants {
*
* @return Die {@code Random}-Instanz.
*/
private static Random getRandom() {
public static Random getRandom() {
if( random == null ) {
random = new Random();
}
@@ -1389,26 +1405,6 @@ public class Constants {
return getRandom().nextGaussian();
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
public static final int choice( int... values ) {
return values[random(0, values.length - 1)];
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
public static final double choice( double... values ) {
return values[random(0, values.length - 1)];
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
@@ -1571,6 +1567,18 @@ public class Constants {
return valueList.toArray(values);
}
/**
* Bringt die Werte im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Werte, die gemischt werden sollen.
* @param <T> Datentyp der Elemente.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final <T> List<T> shuffle( List<T> values ) {
Collections.shuffle(values, random);
return values;
}
/**
* Geteilte {@code Noise}-Instanz zur Erzeugung von Perlin-Noise.
*/

View File

@@ -1,5 +1,6 @@
package schule.ngb.zm;
import java.awt.BasicStroke;
import java.awt.geom.Arc2D;
/**
@@ -31,6 +32,36 @@ public final class Options {
DOTTED
}
/**
* Linienstile für Konturlinien.
*/
public enum StrokeJoin {
/**
* Abgerundete Verbindungen.
*/
ROUND(BasicStroke.JOIN_ROUND),
/**
* Abgeschnittene Verbindungen.
*/
BEVEL(BasicStroke.JOIN_BEVEL),
/**
* Eckige Verbindungen.
*/
MITER(BasicStroke.JOIN_MITER);
/**
* Der entsprechende Wert der Konstanten in {@link java.awt}
*/
public final int awt_type;
StrokeJoin( int type ) {
awt_type = type;
}
}
/**
* Stile für Pfeilspitzen.
*/

View File

@@ -174,7 +174,7 @@ public interface Strokeable extends Drawable {
* @param weight Die Dicke der Konturlinie.
*/
default void setStrokeWeight( double weight ) {
setStroke(createStroke(getStrokeType(), weight));
setStroke(createStroke(getStrokeType(), weight, getStrokeJoin()));
}
/**
@@ -193,7 +193,26 @@ public interface Strokeable extends Drawable {
* @see Options.StrokeType
*/
default void setStrokeType( Options.StrokeType type ) {
setStroke(createStroke(type, getStrokeWeight()));
setStroke(createStroke(type, getStrokeWeight(), getStrokeJoin()));
}
/**
* Gibt die Art der Konturverbindungen zurück.
*
* @return Die aktuelle Art der Konturverbindungen.
* @see Options.StrokeJoin
*/
Options.StrokeJoin getStrokeJoin();
/**
* Setzt den Typ der Konturverbindungen. Erlaubte Werte sind {@link Constants#ROUND},
* {@link Constants#MITER} und {@link Constants#BEVEL}.
*
* @param join Eine der möglichen Konturverbindungen.
* @see Options.StrokeJoin
*/
default void setStrokeJoin( Options.StrokeJoin join ) {
setStroke(createStroke(getStrokeType(), getStrokeWeight(), join));
}
/**
@@ -205,26 +224,26 @@ public interface Strokeable extends Drawable {
* @param strokeWeight
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight ) {
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight, Options.StrokeJoin strokeJoin ) {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
strokeJoin.awt_type,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
strokeJoin.awt_type,
10.0f, new float[]{5.0f}, 0.0f);
case SOLID:
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
strokeJoin.awt_type);
}
}

View File

@@ -28,7 +28,7 @@ public class Zeichenfenster extends JFrame {
/**
* Setzt das Look and Feel auf den Standard des Systems.
* <p>
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen
* Sollte einmalig vor Erstellen des ersten Programmfensters aufgerufen
* werden.
*/
public static final void setLookAndFeel() {

View File

@@ -94,7 +94,7 @@ public class Zeichenmaschine extends Constants {
*/
private boolean running;
private boolean terminateImediately = false;
private boolean terminateImmediately = false;
/**
* Ob die ZM nach dem nächsten Frame pausiert werden soll.
@@ -545,7 +545,7 @@ public class Zeichenmaschine extends Constants {
if( running ) {
running = false;
terminateImediately = true;
terminateImmediately = true;
quitAfterShutdown = true;
mainThread.interrupt();
} else {
@@ -769,6 +769,23 @@ public class Zeichenmaschine extends Constants {
framesPerSecond = framesPerSecondInternal;
}
/**
* Erstellt aus dem aktuellen Inhalt der {@link Zeichenleinwand} ein neues
* {@link BufferedImage}.
*/
public final BufferedImage getImage() {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics();
// TODO: Transparente Hintergründe beim Speichern von png erlauben
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g);
g.dispose();
return img;
}
/**
* Speichert den aktuellen Inhalt der {@link Zeichenleinwand} in einer
* Bilddatei auf der Festplatte. Zur Auswahl der Zieldatei wird dem Nutzer
@@ -794,24 +811,15 @@ public class Zeichenmaschine extends Constants {
* Bilddatei im angegebenen Dateipfad auf der Festplatte.
*/
public final void saveImage( String filepath ) {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics();
// TODO: Transparente Hintergründe beim Speichern von png erlauben
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g);
g.dispose();
try {
ImageLoader.saveImage(img, new File(filepath), true);
ImageLoader.saveImage(getImage(), new File(filepath), true);
} catch( IOException ex ) {
ex.printStackTrace();
}
}
/**
* Erstellt eine Momentanaufnahme des aktuellen Inhalts der
* Erstellt eine Momentaufnahme des aktuellen Inhalts der
* {@link Zeichenleinwand} und erstellt daraus eine
* {@link ImageLayer Bildebene}. Die Ebene wird automatisch der
* {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt.
@@ -1399,7 +1407,7 @@ public class Zeichenmaschine extends Constants {
if( Thread.interrupted() ) {
running = false;
terminateImediately = true;
terminateImmediately = true;
break;
}
}
@@ -1455,7 +1463,7 @@ public class Zeichenmaschine extends Constants {
}
state = Options.AppState.STOPPED;
// Shutdown the updateThread
while( !terminateImediately && updateThreadExecutor.isRunning() ) {
while( !terminateImmediately && updateThreadExecutor.isRunning() ) {
Thread.yield();
}
updateThreadExecutor.shutdownNow();

View File

@@ -50,7 +50,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
}
public void setEasing( DoubleUnaryOperator pEasing ) {
this.easing = pEasing;
this.easing = Validator.requireNotNull(pEasing, "easing");
}
public abstract T getAnimationTarget();
@@ -61,7 +61,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
running = true;
finished = false;
animate(easing.applyAsDouble(0.0));
initializeEventDispatcher().dispatchEvent("start", this);
dispatchEvent("start");
}
public final void stop() {
@@ -70,7 +70,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
animate(easing.applyAsDouble((double) elapsedTime / (double) runtime));
this.finish();
finished = true;
initializeEventDispatcher().dispatchEvent("stop", this);
dispatchEvent("stop");
}
public void initialize() {
@@ -100,10 +100,9 @@ public abstract class Animation<T> extends Constants implements Updatable {
double t = (double) elapsedTime / (double) runtime;
if( t >= 1.0 ) {
running = false;
stop();
} else {
animate(easing.applyAsDouble(t));
animate(getEasing().applyAsDouble(t));
}
}
@@ -118,7 +117,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
* e = Constants.limit(e, 0, 1);
* </code></pre>
*
* @param e Fortschritt der Animation nachdem die Easingfunktion angewandt
* @param e Fortschritt der Animation, nachdem die Easing-Funktion angewandt
* wurde.
*/
public abstract void animate( double e );
@@ -134,6 +133,12 @@ public abstract class Animation<T> extends Constants implements Updatable {
return eventDispatcher;
}
private void dispatchEvent( String type ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent(type, this);
}
}
public void addListener( AnimationListener listener ) {
initializeEventDispatcher().addListener(listener);
}

View File

@@ -4,9 +4,15 @@ import schule.ngb.zm.util.Validator;
import java.util.function.DoubleUnaryOperator;
/**
* Eine Wrapper Animation, um die Werte einer anderen Animation (Laufzeit, Easing) zu überschrieben,
* ohne die Werte der Originalanimation zu verändern.
*
* @param <S> Art des Animierten Objektes.
*/
public class AnimationFacade<S> extends Animation<S> {
private Animation<S> anim;
private final Animation<S> anim;
public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);

View File

@@ -1,22 +1,28 @@
package schule.ngb.zm.anim;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
// TODO: (ngb) Maybe use AnimationFacade to override runtime?
@SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> {
List<Animation<T>> anims;
private final List<Animation<T>> anims;
private boolean overrideEasing = false;
private final boolean overrideEasing;
private int overrideRuntime = -1;
private int lag = 0;
private final int lag;
private int active = 0;
public AnimationGroup( Animation<T>... anims ) {
this(0, -1, null, Arrays.asList(anims));
}
public AnimationGroup( Collection<Animation<T>> anims ) {
this(0, -1, null, anims);
}
@@ -42,6 +48,8 @@ public class AnimationGroup<T> extends Animation<T> {
if( easing != null ) {
this.easing = easing;
overrideEasing = true;
} else {
overrideEasing = false;
}
if( runtime > 0 ) {
@@ -64,52 +72,110 @@ public class AnimationGroup<T> extends Animation<T> {
return anim.getAnimationTarget();
}
}
return anims.get(anims.size() - 1).getAnimationTarget();
if( this.finished ) {
return anims.get(anims.size() - 1).getAnimationTarget();
} else {
return anims.get(0).getAnimationTarget();
}
}
@Override
public void update( double delta ) {
elapsedTime += (int) (delta * 1000);
// Animation is done. Stop all Animations.
if( elapsedTime > runtime ) {
for( int i = 0; i < anims.size(); i++ ) {
if( anims.get(i).isActive() ) {
anims.get(i).elapsedTime = anims.get(i).runtime;
anims.get(i).stop();
}
public DoubleUnaryOperator getEasing() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getEasing();
}
running = false;
this.stop();
}
while( active < anims.size() && elapsedTime >= active * lag ) {
anims.get(active).start();
active += 1;
if( this.finished ) {
return anims.get(anims.size() - 1).getEasing();
} else {
return anims.get(0).getEasing();
}
}
for( int i = 0; i < active; i++ ) {
double t = 0.0;
if( overrideRuntime > 0 ) {
t = (double) (elapsedTime - i*lag) / (double) overrideRuntime;
} else {
t = (double) (elapsedTime - i*lag) / (double) anims.get(i).getRuntime();
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
//
// // Animation is done. Stop all Animations.
// if( elapsedTime > runtime ) {
// for( int i = 0; i < anims.size(); i++ ) {
// if( anims.get(i).isActive() ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// }
// }
// elapsedTime = runtime;
// running = false;
// this.stop();
// }
//
// while( active < anims.size() && elapsedTime >= active * lag ) {
// anims.get(active).start();
// active += 1;
// }
//
// for( int i = 0; i < active; i++ ) {
// double t = 0.0;
// if( overrideRuntime > 0 ) {
// t = (double) (elapsedTime - i*lag) / (double) overrideRuntime;
// } else {
// t = (double) (elapsedTime - i*lag) / (double) anims.get(i).getRuntime();
// }
//
// if( t >= 1.0 ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// } else {
// double e = overrideEasing ?
// easing.applyAsDouble(t) :
// anims.get(i).easing.applyAsDouble(t);
//
// anims.get(i).animate(e);
// }
// }
// }
if( t >= 1.0 ) {
anims.get(i).elapsedTime = anims.get(i).runtime;
anims.get(i).stop();
} else {
double e = overrideEasing ?
easing.applyAsDouble(t) :
anims.get(i).easing.applyAsDouble(t);
anims.get(i).animate(e);
@Override
public void finish() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
anim.elapsedTime = anim.runtime;
anim.stop();
}
}
}
@Override
public void animate( double e ) {
while( active < anims.size() && elapsedTime >= active * lag ) {
anims.get(active).start();
active += 1;
}
for( int i = 0; i < active; i++ ) {
Animation<T> curAnim = anims.get(i);
double curRuntime = curAnim.getRuntime();
if( overrideRuntime > 0 ) {
curRuntime = overrideRuntime;
}
double t = (double) (elapsedTime - i * lag) / (double) curRuntime;
if( t >= 1.0 ) {
curAnim.elapsedTime = curAnim.getRuntime();
curAnim.stop();
} else {
e = overrideEasing ?
easing.applyAsDouble(t) :
curAnim.easing.applyAsDouble(t);
curAnim.elapsedTime = (elapsedTime - i * lag);
curAnim.animate(e);
}
}
}
}

View File

@@ -0,0 +1,144 @@
package schule.ngb.zm.anim;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
/**
* Führt eine Liste von Animationen nacheinander aus. Jede Animation startet direkt nachdem die
* davor geendet ist. Optional kann zwischen dem Ende einer und dem Start der nächsten Animation
* ein
* <var>lag</var> eingefügt werden.
*
* @param <T> Die Art des animierten Objektes.
*/
@SuppressWarnings( "unused" )
public class AnimationSequence<T> extends Animation<T> {
private final List<Animation<T>> anims;
private final int lag;
private int currentAnimationIndex = -1, currentStart = -1, nextStart = -1;
@SafeVarargs
public AnimationSequence( Animation<T>... anims ) {
this(0, Arrays.asList(anims));
}
public AnimationSequence( Collection<Animation<T>> anims ) {
this(0, anims);
}
public AnimationSequence( int lag, Collection<Animation<T>> anims ) {
super(Easing::linear);
this.anims = List.copyOf(anims);
this.lag = lag;
this.runtime = (anims.size() - 1) * lag + anims.stream().mapToInt(Animation::getRuntime).sum();
}
@Override
public T getAnimationTarget() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getAnimationTarget();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getAnimationTarget();
} else {
return anims.get(0).getAnimationTarget();
}
}
@Override
public DoubleUnaryOperator getEasing() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getEasing();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getEasing();
} else {
return anims.get(0).getEasing();
}
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
//
// // Animation is done. Stop all Animations.
// if( elapsedTime > runtime ) {
// for( int i = 0; i < anims.size(); i++ ) {
// if( anims.get(i).isActive() ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// }
// }
// elapsedTime = runtime;
// running = false;
// this.stop();
// }
//
// Animation<T> curAnim = null;
// if( elapsedTime > nextStart ) {
// currentAnimation += 1;
// curAnim = anims.get(currentAnimation);
// currentStart = nextStart;
// nextStart += lag + curAnim.getRuntime();
// curAnim.start();
// } else {
// curAnim = anims.get(currentAnimation);
// }
//
// // Calculate delta for current animation
// double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
// if( t >= 1.0 ) {
// curAnim.elapsedTime = curAnim.runtime;
// curAnim.stop();
// } else {
// curAnim.animate(curAnim.easing.applyAsDouble(t));
// }
// }
@Override
public void finish() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
anim.elapsedTime = anim.runtime;
anim.stop();
}
}
}
@Override
public void animate( double e ) {
Animation<T> curAnim = null;
if( running && elapsedTime > nextStart ) {
currentAnimationIndex += 1;
curAnim = anims.get(currentAnimationIndex);
currentStart = nextStart;
nextStart += lag + curAnim.getRuntime();
curAnim.start();
} else {
curAnim = anims.get(currentAnimationIndex);
}
// Calculate delta for current animation
double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
if( t >= 1.0 ) {
curAnim.elapsedTime = curAnim.runtime;
curAnim.stop();
} else {
curAnim.elapsedTime = (elapsedTime - currentStart);
curAnim.animate(curAnim.easing.applyAsDouble(t));
}
}
}

View File

@@ -7,33 +7,87 @@ import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
/**
* Animates the {@code target} in a circular motion centered at (<var>cx</var>, <var>cy</var>).
*/
public class CircleAnimation extends Animation<Shape> {
private Shape object;
private final Shape target;
private double centerx, centery, radius, startangle;
private final double centerX, centerY, rotateTo;
public CircleAnimation( Shape target, double cx, double cy, int runtime, DoubleUnaryOperator easing ) {
private double rotationRadius, startAngle;
private final boolean rotateRight;
public CircleAnimation( Shape target, double cx, double cy ) {
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo ) {
this(target, cx, cy, rotateTo, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight ) {
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight ) {
this(target, cx, cy, rotateTo, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, int runtime ) {
this(target, cx, cy, 360, true, runtime, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, int runtime ) {
this(target, cx, cy, 360, rotateRight, runtime, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, DoubleUnaryOperator easing ) {
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, easing);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, DoubleUnaryOperator easing ) {
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, easing);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, int runtime, DoubleUnaryOperator easing ) {
this(target, cx, cy, rotateTo, true, runtime, easing);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
object = target;
centerx = cx;
centery = cy;
Vector vec = new Vector(target.getX(), target.getY()).sub(cx, cy);
startangle = vec.heading();
radius = vec.length();
this.target = target;
this.centerX = cx;
this.centerY = cy;
this.rotateTo = Constants.radians(Constants.limit(rotateTo, 0, 360));
this.rotateRight = rotateRight;
}
@Override
public void initialize() {
Vector vec = new Vector(target.getX(), target.getY()).sub(centerX, centerY);
startAngle = vec.heading();
rotationRadius = vec.length();
}
@Override
public Shape getAnimationTarget() {
return object;
return target;
}
@Override
public void animate( double e ) {
double angle = startangle + Constants.radians(Constants.interpolate(0, 360, e));
double x = centerx + radius * Constants.cos(angle);
double y = centery + radius * Constants.sin(angle);
object.moveTo(x, y);
double angle = startAngle;
if( rotateRight ) {
angle += Constants.interpolate(0, rotateTo, e);
} else {
angle -= Constants.interpolate(0, rotateTo, e);
}
double x = centerX + rotationRadius * Constants.cos(angle);
double y = centerY + rotationRadius * Constants.sin(angle);
target.moveTo(x, y);
}
}

View File

@@ -8,9 +8,9 @@ public class ContinousAnimation<T> extends Animation<T> {
private int lag = 0;
/**
* Speichert eine Approximation der aktuellen Steigung der Easing-Funktion,
* um im Fall {@code easeInOnly == true} nach dem ersten Durchlauf die
* passende Geschwindigkeit beizubehalten.
* Speichert eine Approximation der aktuellen Steigung der Easing-Funktion, um im Fall
* {@code easeInOnly == true} nach dem ersten Durchlauf die passende Geschwindigkeit
* beizubehalten.
*/
private double m = 1.0, lastEase = 0.0;
@@ -29,7 +29,7 @@ public class ContinousAnimation<T> extends Animation<T> {
}
private ContinousAnimation( Animation<T> baseAnimation, int lag, boolean easeInOnly ) {
super(baseAnimation.getRuntime(), baseAnimation.getEasing());
super(baseAnimation.getRuntime() + lag, baseAnimation.getEasing());
this.baseAnimation = baseAnimation;
this.lag = lag;
this.easeInOnly = easeInOnly;
@@ -40,35 +40,80 @@ public class ContinousAnimation<T> extends Animation<T> {
return baseAnimation.getAnimationTarget();
}
@Override
public int getRuntime() {
return Integer.MAX_VALUE;
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
// if( elapsedTime >= runtime + lag ) {
// elapsedTime %= (runtime + lag);
//
// if( easeInOnly && easing != null ) {
// easing = null;
// // runtime = (int)((1.0/m)*(runtime + lag));
// }
// }
//
// double t = (double) elapsedTime / (double) runtime;
// if( t >= 1.0 ) {
// t = 1.0;
// }
// if( easing != null ) {
// double e = easing.applyAsDouble(t);
// animate(e);
// m = (e-lastEase)/(delta*1000/(asDouble(runtime)));
// lastEase = e;
// } else {
// animate(t);
// }
// }
@Override
public void finish() {
baseAnimation.elapsedTime = baseAnimation.getRuntime();
baseAnimation.stop();
}
@Override
public void initialize() {
baseAnimation.start();
}
@Override
public void setRuntime( int pRuntime ) {
baseAnimation.setRuntime(pRuntime);
runtime = pRuntime + lag;
}
@Override
public void update( double delta ) {
elapsedTime += (int) (delta * 1000);
if( elapsedTime >= runtime + lag ) {
elapsedTime %= (runtime + lag);
int currentRuntime = elapsedTime + (int) (delta * 1000);
if( currentRuntime >= runtime + lag ) {
elapsedTime = currentRuntime % (runtime + lag);
if( easeInOnly && easing != null ) {
easing = null;
easing = Easing.linear();
// runtime = (int)((1.0/m)*(runtime + lag));
}
}
double t = (double) elapsedTime / (double) runtime;
if( t >= 1.0 ) {
t = 1.0;
}
if( easing != null ) {
double e = easing.applyAsDouble(t);
animate(e);
m = (e-lastEase)/(delta*1000/(asDouble(runtime)));
lastEase = e;
} else {
animate(t);
}
super.update(delta);
}
@Override
public void animate( double e ) {
// double t = (double) elapsedTime / (double) runtime;
// if( t >= 1.0 ) {
// t = 1.0;
// }
baseAnimation.elapsedTime = elapsedTime;
baseAnimation.animate(e);
m = (e - lastEase) / (delta * 1000 / (asDouble(runtime)));
lastEase = e;
}
}

View File

@@ -13,32 +13,51 @@ public class FadeAnimation extends Animation<Shape> {
public static final int FADE_OUT = 0;
private Shape object;
private final Shape target;
private final int targetAlpha;
private Color fill, stroke;
private int fillAlpha, strokeAlpha, tAlpha;
private int fillAlpha, strokeAlpha;
public FadeAnimation( Shape object, int alpha, int runtime, DoubleUnaryOperator easing ) {
public FadeAnimation( Shape target, int targetAlpha ) {
this(target, targetAlpha, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public FadeAnimation( Shape target, int targetAlpha, int runtime ) {
this(target, targetAlpha, runtime, DEFAULT_EASING);
}
public FadeAnimation( Shape target, int runtime, DoubleUnaryOperator easing ) {
this(target, 0, runtime, easing);
}
public FadeAnimation( Shape target, int targetAlpha, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
fill = object.getFillColor();
this.target = target;
this.targetAlpha = targetAlpha;
}
@Override
public void initialize() {
fill = target.getFillColor();
fillAlpha = fill.getAlpha();
stroke = object.getStrokeColor();
stroke = target.getStrokeColor();
strokeAlpha = stroke.getAlpha();
tAlpha = alpha;
}
@Override
public Shape getAnimationTarget() {
return object;
return target;
}
@Override
public void animate( double e ) {
object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e)));
object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e)));
target.setFillColor(fill, (int) Constants.interpolate(fillAlpha, targetAlpha, e));
target.setStrokeColor(stroke, (int) Constants.interpolate(strokeAlpha, targetAlpha, e));
}
}

View File

@@ -1,23 +1,28 @@
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 final Shape object;
private Color oFill, tFill;
private Color originFill;
public FillAnimation( Shape object, Color newFill, int runtime, DoubleUnaryOperator easing ) {
private final Color targetFill;
public FillAnimation( Shape target, Color newFill, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oFill = object.getFillColor();
tFill = newFill;
this.object = target;
targetFill = newFill;
}
@Override
public void initialize() {
originFill = object.getFillColor();
}
@Override
@@ -27,7 +32,7 @@ public class FillAnimation extends Animation<Shape> {
@Override
public void animate( double e ) {
object.setFillColor(Color.interpolate(oFill, tFill, e));
object.setFillColor(Color.interpolate(originFill, targetFill, e));
}
}

View File

@@ -1,28 +1,31 @@
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 final Shape object;
private double oX, oY, tX, tY;
private final double targetX, targetY;
public MoveAnimation( Shape object, double x, double y, int runtime, DoubleUnaryOperator easing ) {
private double originX, originY;
public MoveAnimation( Shape target, double targetX, double targetY, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oX = object.getX();
oY = object.getY();
tX = x;
tY = y;
this.object = target;
this.targetX = targetX;
this.targetY = targetY;
}
@Override
public void initialize() {
originX = object.getX();
originY = object.getY();
}
@Override
@@ -32,8 +35,8 @@ public class MoveAnimation extends Animation<Shape> {
@Override
public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e));
object.setY(Constants.interpolate(oY, tY, e));
object.setX(Constants.interpolate(originX, targetX, e));
object.setY(Constants.interpolate(originY, targetY, e));
}
}

View File

@@ -404,6 +404,11 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
return shapeDelegate.getStrokeType();
}
@Override
public Options.StrokeJoin getStrokeJoin() {
return shapeDelegate.getStrokeJoin();
}
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
@@ -980,7 +985,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y ) {
image(ImageLoader.loadImage(imageSource), x, y, 1.0, shapeDelegate.getAnchor());
imageScale(ImageLoader.loadImage(imageSource), x, y, 1.0, shapeDelegate.getAnchor());
}
/**
@@ -997,7 +1002,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, 1.0, anchor);
imageScale(ImageLoader.loadImage(imageSource), x, y, 1.0, anchor);
}
/**
@@ -1005,8 +1010,9 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* Koordinaten auf die Zeichenebene. Das Bild wird um den angegebenen Faktor
* skaliert.
* <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)}
* für mehr Details.
* Siehe
* {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
@@ -1014,8 +1020,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param scale Der Skalierungsfaktor des Bildes.
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y, double scale ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, shapeDelegate.getAnchor());
public void imageScale( String imageSource, double x, double y, double scale ) {
imageScale(ImageLoader.loadImage(imageSource), x, y, scale, shapeDelegate.getAnchor());
}
/**
@@ -1023,8 +1029,9 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* Koordinaten auf die Zeichenebene. Das Bild wird um den angegebenen Faktor
* skaliert und der angegebene Ankerpunkt verwendet.
* <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)}
* für mehr Details.
* Siehe
* {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
@@ -1033,8 +1040,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param anchor Der Ankerpunkt.
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y, double scale, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, anchor);
public void imageScale( String imageSource, double x, double y, double scale, Options.Direction anchor ) {
imageScale(ImageLoader.loadImage(imageSource), x, y, scale, anchor);
}
/**
@@ -1046,23 +1053,24 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param y y-Koordinate des Ankerpunktes.
*/
public void image( Image image, double x, double y ) {
image(image, x, y, 1.0, shapeDelegate.getAnchor());
imageScale(image, x, y, 1.0, shapeDelegate.getAnchor());
}
/**
* Zeichnet das angegebene Bild an den angegebenen Koordinaten auf die
* Zeichenebene. Das Bild wird um den angegebenen Faktor skaliert.
* <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)}
* für mehr Details.
* Siehe
* {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
*
* @param image Das vorher geladene Bild.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param scale Der Skalierungsfaktor des Bildes.
*/
public void image( Image image, double x, double y, double scale ) {
image(image, x, y, scale, shapeDelegate.getAnchor());
public void imageScale( Image image, double x, double y, double scale ) {
imageScale(image, x, y, scale, shapeDelegate.getAnchor());
}
/**
@@ -1077,8 +1085,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* Das Seitenverhältnis wird immer beibehalten.
* <p>
* Soll das Bild innerhalb eines vorgegebenen Rechtecks liegen, sollte
* {@link #image(Image, double, double, double, double, Options.Direction)}
* verwendet werden.
* {@link #imageScale(Image, double, double, double, double,
* Options.Direction)} verwendet werden.
*
* @param image Das vorher geladene Bild.
* @param x x-Koordinate des Ankerpunktes.
@@ -1086,7 +1094,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param scale Der Skalierungsfaktor des Bildes.
* @param anchor Der Ankerpunkt.
*/
public void image( Image image, double x, double y, double scale, Options.Direction anchor ) {
public void imageScale( Image image, double x, double y, double scale, Options.Direction anchor ) {
/*if( image != null ) {
double neww = image.getWidth(null) * scale;
double newh = image.getHeight(null) * scale;
@@ -1095,7 +1103,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
}*/
double neww = image.getWidth(null) * scale;
double newh = image.getHeight(null) * scale;
image(image, x, y, neww, newh, anchor);
imageScale(image, x, y, neww, newh, anchor);
}
/**
@@ -1103,8 +1111,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* Koordinaten in der angegebenen Größe auf die Zeichenebene.
* <p>
* Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)}
* für mehr Details.
* {@link #imageScale(Image, double, double, double, double,
* Options.Direction)} für mehr Details.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
@@ -1113,8 +1121,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y, double width, double height ) {
image(ImageLoader.loadImage(imageSource), x, y, width, height, shapeDelegate.getAnchor());
public void imageScale( String imageSource, double x, double y, double width, double height ) {
imageScale(ImageLoader.loadImage(imageSource), x, y, width, height, shapeDelegate.getAnchor());
}
/**
@@ -1123,8 +1131,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* angegebene Ankerpunkt verwendet.
* <p>
* Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)}
* für mehr Details.
* {@link #imageScale(Image, double, double, double, double,
* Options.Direction)} für mehr Details.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
@@ -1134,8 +1142,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param anchor Der Ankerpunkt.
* @see ImageLoader#loadImage(String)
*/
public void image( String imageSource, double x, double y, double width, double height, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, width, height, anchor);
public void imageScale( String imageSource, double x, double y, double width, double height, Options.Direction anchor ) {
imageScale(ImageLoader.loadImage(imageSource), x, y, width, height, anchor);
}
/**
@@ -1143,8 +1151,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* angegebenen Größe auf die Zeichenebene.
* <p>
* Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)}
* für mehr Details.
* {@link #imageScale(Image, double, double, double, double,
* Options.Direction)} für mehr Details.
*
* @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes.
@@ -1152,8 +1160,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
*/
public void image( Image image, double x, double y, double width, double height ) {
image(image, x, y, width, height, shapeDelegate.getAnchor());
public void imageScale( Image image, double x, double y, double width, double height ) {
imageScale(image, x, y, width, height, shapeDelegate.getAnchor());
}
/**
@@ -1171,7 +1179,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* <p>
* Soll die Bildgröße unter Beachtung der Abmessungen um einen Faktor
* verändert werden, sollte
* {@link #image(Image, double, double, double, Options.Direction)}
* {@link #imageScale(Image, double, double, double, Options.Direction)}
* verwendet werden.
*
* @param image Ein Bild-Objekt.
@@ -1181,17 +1189,163 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @param anchor Der Ankerpunkt.
*/
public void image( Image image, double x, double y, double width, double height, Options.Direction anchor ) {
public void imageScale( Image image, double x, double y, double width, double height, Options.Direction anchor ) {
imageRotateAndScale(image, x, y, 0, width, height, anchor);
}
/**
* Zeichnet das Bild von der angegebenen Bildquelle an den angegebenen
* Koordinaten mit der angegebenen Drehung auf die Zeichenebene.
* <p>
* Das Bild wird um seinen Mittelpunkt als Rotationszentrum gedreht.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
*/
public void imageRotate( String imageSource, double x, double y, double angle ) {
imageRotate(ImageLoader.loadImage(imageSource), x, y, angle, shapeDelegate.getAnchor());
}
/**
* Zeichnet das Bild von der angegebenen Bildquelle an den angegebenen
* Koordinaten mit der angegebenen Drehung auf die Zeichenebene. Der
* angegebene Ankerpunkt wird verwendet.
* <p>
* Das Bild wird um seinen Mittelpunkt als Rotationszentrum gedreht.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param anchor Der Ankerpunkt.
*/
public void imageRotate( String imageSource, double x, double y, double angle, Options.Direction anchor ) {
imageRotate(ImageLoader.loadImage(imageSource), x, y, angle, anchor);
}
/**
* Zeichnet das angegebene Bild an den angegebenen Koordinaten mit der
* angegebenen Drehung auf die Zeichenebene.
* <p>
* Das Bild wird um seinen Mittelpunkt als Rotationszentrum gedreht.
*
* @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
*/
public void imageRotate( Image image, double x, double y, double angle ) {
imageRotateAndScale(image, x, y, angle, image.getWidth(null), image.getHeight(null), shapeDelegate.getAnchor());
}
/**
* Zeichnet das angegebene Bild an den angegebenen Koordinaten mit der
* angegebenen Drehung auf die Zeichenebene. Der angegebene Ankerpunkt wird
* verwendet.
* <p>
* Das Bild wird um seinen Mittelpunkt als Rotationszentrum gedreht.
*
* @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param anchor Der Ankerpunkt.
*/
public void imageRotate( Image image, double x, double y, double angle, Options.Direction anchor ) {
imageRotateAndScale(image, x, y, angle, image.getWidth(null), image.getHeight(null), anchor);
}
/**
* Zeichnet das Bild von der angegebenen Bildquelle an den angegebenen
* Koordinaten mit der angegebenen Drehung in der angegebenen Größe auf die
* Zeichenebene.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @see #imageRotate(String, double, double, double)
* @see #imageScale(Image, double, double, double)
*/
public void imageRotateAndScale( String imageSource, double x, double y, double angle, double width, double height ) {
imageRotateAndScale(ImageLoader.loadImage(imageSource), x, y, angle, width, height, shapeDelegate.getAnchor());
}
/**
* Zeichnet das Bild von der angegebenen Bildquelle an den angegebenen
* Koordinaten mit der angegebenen Drehung in der angegebenen Größe auf die
* Zeichenebene. Der angegebene Ankerpunkt wird verwendet.
*
* @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @param anchor Der Ankerpunkt.
* @see #imageRotate(String, double, double, double)
* @see #imageScale(Image, double, double, double)
*/
public void imageRotateAndScale( String imageSource, double x, double y, double angle, double width, double height, Options.Direction anchor ) {
imageRotateAndScale(ImageLoader.loadImage(imageSource), x, y, angle, width, height, anchor);
}
/**
* Zeichnet das angegebene Bild an den angegebenen Koordinaten mit der
* angegebenen Drehung in der angegebenen Größe auf die Zeichenebene. Der
* angegebene Ankerpunkt wird verwendet.
*
* @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @see #imageRotate(String, double, double, double)
* @see #imageScale(Image, double, double, double)
*/
public void imageRotateAndScale( Image image, double x, double y, double angle, double width, double height ) {
imageRotateAndScale(image, x, y, angle, width, height, shapeDelegate.getAnchor());
}
/**
* Zeichnet das angegebene Bild an den angegebenen Koordinaten mit der
* angegebenen Drehung in der angegebenen Größe auf die Zeichenebene. Der
* angegebene Ankerpunkt wird verwendet.
*
* @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes.
* @param angle Winkel in Grad.
* @param width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @param anchor Der Ankerpunkt.
* @see #imageRotate(String, double, double, double)
* @see #imageScale(Image, double, double, double)
*/
public void imageRotateAndScale( Image image, double x, double y, double angle, double width, double height, Options.Direction anchor ) {
// TODO: Use Validator or at least LOG a message if image == null?
if( image != null ) {
AffineTransform orig = drawing.getTransform();
int imgWidth = image.getWidth(null);
int imgHeight = image.getHeight(null);
if( width == 0 ) {
width = (height / image.getHeight(null)) * image.getWidth(null);
width = (height / imgHeight) * imgWidth;
} else if( height == 0 ) {
height = (width / image.getWidth(null)) * image.getHeight(null);
height = (width / imgWidth) * imgHeight;
}
Point2D.Double anchorPoint = getOriginPoint(x, y, width, height, anchor);
drawing.rotate(Math.toRadians(angle), anchorPoint.x + width / 2, anchorPoint.y + height / 2);
drawing.drawImage(image, (int) anchorPoint.x, (int) anchorPoint.y, (int) width, (int) height, null);
drawing.setTransform(orig);
}
}

View File

@@ -159,6 +159,16 @@ public class ShapesLayer extends Layer {
@Override
public void update( double delta ) {
if( updateShapes ) {
synchronized( shapes ) {
List<Updatable> uit = List.copyOf(updatables);
for( Updatable u : uit ) {
if( u.isActive() ) {
u.update(delta);
}
}
}
/*
Iterator<Updatable> uit = updatables.iterator();
while( uit.hasNext() ) {
Updatable u = uit.next();
@@ -166,6 +176,7 @@ public class ShapesLayer extends Layer {
u.update(delta);
}
}
*/
}
Iterator<Animation<? extends Shape>> it = animations.iterator();

View File

@@ -230,6 +230,11 @@ public class TurtleLayer extends Layer implements Strokeable, Fillable {
return mainTurtle.getStrokeType();
}
@Override
public Options.StrokeJoin getStrokeJoin() {
return mainTurtle.getStrokeJoin();
}
@Override
public void setStrokeType( Options.StrokeType type ) {
mainTurtle.setStrokeType(type);

View File

@@ -1,5 +1,9 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Options;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
@@ -170,6 +174,30 @@ public class Curve extends Shape {
move(dx, dy);
}
@Override
public void draw( Graphics2D graphics, AffineTransform transform ) {
if( !visible ) {
return;
}
AffineTransform orig = graphics.getTransform();
if( transform != null ) {
//graphics.transform(transform);
}
graphics.translate(x, y);
graphics.rotate(Math.toRadians(rotation));
java.awt.Shape shape = getShape();
java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
graphics.setTransform(orig);
}
@Override
public boolean equals( Object o ) {
if( this == o ) return true;

View File

@@ -11,6 +11,7 @@ public class CustomShape extends Shape {
public CustomShape( double x, double y ) {
super(x, y);
path = new Path2D.Double();
path.moveTo(x, y);
}
public CustomShape( CustomShape custom ) {
@@ -36,7 +37,7 @@ public class CustomShape extends Shape {
}
public void lineTo( double x, double y ) {
path.lineTo(x - x, y - y);
path.lineTo(x - this.x, y - this.y);
calculateBounds();
}

View File

@@ -404,6 +404,7 @@ public abstract class Shape extends BasicDrawable {
setStrokeColor(shape.getStrokeColor());
setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType());
setStrokeJoin(shape.getStrokeJoin());
visible = shape.isVisible();
rotation = shape.getRotation();
scale(shape.getScale());

View File

@@ -181,34 +181,39 @@ public final class FileLoader {
).toArray(String[][]::new);
}
public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
public static double[][] loadValues( String source, String separator, boolean skipFirst ) {
return loadValues(source, separator, skipFirst, UTF8);
}
/**
* Lädt Double-Werte aus einer CSV Datei in ein zweidimensionales Array.
* Lädt Double-Werte aus einer Text-Datei in ein zweidimensionales Array.
* <p>
* Die gelesenen Strings werden mit {@link Double#parseDouble(String)} in
* {@code double} umgeformt. Es leigt in der Verantwortung des Nutzers
* sicherzustellen, dass die CSV-Datei auch nur Zahlen enthält, die korrekt
* in {@code double} umgewandelt werden können. Zellen für die die
* Umwandlung fehlschlägt werden mit 0.0 befüllt.
* Die Zeilen der Eingabedatei werden anhand der Zeichenkette {@code separator}
* in einzelne Teile aufgetrennt. {@code separator} wird als regulärer Ausdruck
* interpretiert (siehe {@link String#split(String)}).
* <p>
* Jeder Teilstring wird mit {@link Double#parseDouble(String)} in
* {@code double} umgeformt. Es liegt in der Verantwortung des Nutzers,
* sicherzustellen, dass die Eingabedatei nur Zahlen enthält, die korrekt
* in {@code double} umgewandelt werden können. Zellen, für die die
* Umwandlung fehlschlägt, werden mit 0.0 befüllt.
* <p>
* Die Methode unterliegt denselben Einschränkungen wie
* {@link #loadCsv(String, char, boolean, Charset)}.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param separator Ein Trennzeichen oder ein regulärer Ausdruck.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static double[][] loadValues( String source, char separator, boolean skipFirst, Charset charset ) {
public static double[][] loadValues( String source, String separator, boolean skipFirst, Charset charset ) {
int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
//.stream(line.split(Character.toString(separator)))
.stream(line.split(separator))
.mapToDouble(
( value ) -> {
try {

View File

@@ -262,6 +262,8 @@ class ColorTest {
assertEquals(0.0, Color.BLACK.compare(Color.WHITE), 0.0001);
assertEquals(0.0, Color.WHITE.compare(Color.BLACK), 0.0001);
assertEquals(0.5, Color.GRAY.compare(Color.BLACK), 0.01);
}
}

View File

@@ -0,0 +1,51 @@
package schule.ngb.zm;
import schule.ngb.zm.layers.DrawingLayer;
import schule.ngb.zm.util.Log;
public class Testmaschine extends Zeichenmaschine {
static {
Log.enableGlobalDebugging();
}
private DrawingLayer gridLayer;
public Testmaschine() {
this(400, 400);
}
public Testmaschine( int width, int height ) {
super(width, height, "Testmaschine", false);
}
@Override
public void settings() {
gridLayer = new DrawingLayer(getWidth(), getHeight());
this.getCanvas().addLayer(1, gridLayer);
setGrid(50, 10);
}
public void setGrid( int majorGrid, int minorGrid ) {
gridLayer.clear();
gridLayer.clear(LIGHTGRAY);
gridLayer.setStrokeColor(LIGHTGRAY.darker(20));
for( int i = 0; i < getWidth(); i += minorGrid ) {
gridLayer.line(i, 0, i, gridLayer.getHeight());
}
for( int i = 0; i < getHeight(); i += minorGrid ) {
gridLayer.line(0, i, gridLayer.getWidth(), i);
}
gridLayer.setStrokeColor(LIGHTGRAY.darker(50));
for( int i = 0; i < getWidth(); i += majorGrid ) {
gridLayer.line(i, 0, i, gridLayer.getHeight());
}
for( int i = 0; i < getHeight(); i += majorGrid ) {
gridLayer.line(0, i, gridLayer.getWidth(), i);
}
}
}

View File

@@ -0,0 +1,71 @@
package schule.ngb.zm;
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.layers.DrawingLayer;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static schule.ngb.zm.util.test.ImageAssertions.assertEquals;
import static schule.ngb.zm.util.test.ImageAssertions.setSaveDiffImageOnFail;
public class TestmaschineTest {
private static Testmaschine tm;
private static DrawingLayer drawing;
@BeforeAll
static void beforeAll() {
setSaveDiffImageOnFail(true);
tm = new Testmaschine();
drawing = tm.getDrawingLayer();
assertNotNull(drawing);
}
@AfterAll
static void afterAll() {
tm.exit();
}
@BeforeEach
void setUp() {
drawing.clear();
}
@Test
void testSaveDiffImage() {
drawing.noStroke();
drawing.setAnchor(Constants.NORTHWEST);
drawing.setFillColor(Constants.BLUE);
drawing.rect(0, 0, 400, 400);
drawing.setFillColor(Constants.RED);
drawing.rect(100, 100, 200, 200);
BufferedImage img1 = ImageLoader.createImage(400, 400);
Graphics2D graphics = img1.createGraphics();
graphics.setColor(Constants.BLUE.getJavaColor());
graphics.fillRect(0, 0, 400, 400);
graphics.setColor(Constants.RED.getJavaColor());
graphics.fillRect(100, 100, 200, 200);
assertEquals(drawing.buffer, drawing.buffer);
assertEquals(ImageLoader.copyImage(drawing.buffer), drawing.buffer);
assertEquals(img1, drawing.buffer);
assertEquals(img1, tm.getImage());
}
@Test
void testGrid() {
// tm.setGrid(50, 10);
tm.delay(2000);
}
}

View File

@@ -0,0 +1,96 @@
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.Testmaschine;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.shapes.Circle;
import schule.ngb.zm.shapes.Shape;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class AnimationGroupsTest {
private static Testmaschine zm;
private static ShapesLayer shapes;
@BeforeAll
static void beforeAll() {
zm = new Testmaschine();
shapes = zm.getShapesLayer();
assertNotNull(shapes);
}
@AfterAll
static void afterAll() {
zm.exit();
}
@BeforeEach
void setUp() {
shapes.removeAll();
}
@Test
void animationGroup() {
Shape s = new Circle(0, 0, 10);
shapes.add(s);
Animation<Shape> anims = new AnimationGroup<>(
500,
Arrays.asList(
new MoveAnimation(s, 200, 200, 2000, Easing.DEFAULT_EASING),
new FillAnimation(s, Color.GREEN, 1000, Easing.sineIn())
)
);
Animations.playAndWait(anims);
assertEquals(200, s.getX());
assertEquals(200, s.getY());
assertEquals(Color.GREEN, s.getFillColor());
}
@Test
void animationSequence() {
Shape s = new Circle(0, 0, 10);
shapes.add(s);
Animation<Shape> anims = new AnimationSequence<>(
Arrays.asList(
new CircleAnimation(s, 200, 0, 90, false, 1000, Easing::rushIn),
new CircleAnimation(s, 200, 400, 90, 1000, Easing::rushOut),
new CircleAnimation(s, 200, 400, 90, false, 1000, Easing::rushIn),
new CircleAnimation(s, 200, 0, 90, 1000, Easing::rushOut)
)
);
Animations.playAndWait(anims);
assertEquals(0, s.getX());
assertEquals(0, s.getY());
}
@Test
void animationSequenceContinous() {
Shape s = new Circle(0, 0, 10);
shapes.add(s);
Animation<Shape> anims = new ContinousAnimation<>(new AnimationSequence<>(
Arrays.asList(
new CircleAnimation(s, 200, 0, 90, false, 1000, Easing::rushIn),
new CircleAnimation(s, 200, 400, 90, 1000, Easing::rushOut),
new CircleAnimation(s, 200, 400, 90, false, 1000, Easing::rushIn),
new CircleAnimation(s, 200, 0, 90, 1000, Easing::rushOut)
)
), false);
Animations.playAndWait(anims);
zm.delay(8000);
anims.stop();
}
}

View File

@@ -0,0 +1,75 @@
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 org.junit.jupiter.api.extension.ExtendWith;
import schule.ngb.zm.Testmaschine;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.shapes.Circle;
import schule.ngb.zm.shapes.Shape;
import schule.ngb.zm.util.test.TestEnv;
import static org.junit.jupiter.api.Assertions.*;
public class AnimationTest {
private static Testmaschine zm;
private static ShapesLayer shapes;
@BeforeAll
static void beforeAll() {
zm = new Testmaschine();
shapes = zm.getShapesLayer();
assertNotNull(shapes);
}
@AfterAll
static void afterAll() {
zm.exit();
}
@BeforeEach
void setUp() {
shapes.removeAll();
}
@Test
void circleAnimation() {
Shape s = new Circle(zm.getWidth()/4.0, zm.getHeight()/2.0, 10);
shapes.add(s);
CircleAnimation anim = new CircleAnimation(s, zm.getWidth()/2.0, zm.getHeight()/2.0, 360, true, 3000, Easing::linear);
Animations.playAndWait(anim);
assertEquals(zm.getWidth()/4.0, s.getX());
assertEquals(zm.getHeight()/2.0, s.getY());
}
@Test
void fadeAnimation() {
Shape s = new Circle(zm.getWidth()/4.0, zm.getHeight()/2.0, 10);
s.setFillColor(s.getFillColor(), 0);
s.setStrokeColor(s.getStrokeColor(), 0);
shapes.add(s);
Animation<Shape> anim = new FadeAnimation(s, 255, 1000);
Animations.playAndWait(anim);
assertEquals(s.getFillColor().getAlpha(), 255);
}
@Test
void continousAnimation() {
Shape s = new Circle(zm.getWidth()/4, zm.getHeight()/2, 10);
shapes.add(s);
ContinousAnimation anim = new ContinousAnimation(
new CircleAnimation(s, zm.getWidth()/2, zm.getHeight()/2, 360, true, 1000, Easing::linear)
);
Animations.play(anim);
zm.delay(3000);
anim.stop();
}
}

View File

@@ -0,0 +1,30 @@
package schule.ngb.zm.layers;
import org.junit.jupiter.api.Test;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;
import static org.junit.jupiter.api.Assertions.*;
class DrawingLayerTest {
@Test
void imageRotateAndScale() {
Zeichenmaschine zm = new Zeichenmaschine();
zm.getDrawingLayer().imageRotateAndScale(
"WitchCraftIcons_122_t.PNG",
50, 100,
90,
300, 200,
Constants.NORTHWEST
);
zm.redraw();
try {
Thread.sleep(4000);
} catch( InterruptedException e ) {
throw new RuntimeException(e);
}
}
}

View File

@@ -72,10 +72,10 @@ class FileLoaderTest {
{2.1,2.2,2.3},
{3.1,3.2,3.3}
};
csv = FileLoader.loadValues("data_comma.csv", ',', true);
csv = FileLoader.loadValues("data_comma.csv", ",", true);
assertArrayEquals(data, csv);
csv = FileLoader.loadValues("data_semicolon_latin.csv", ';', true, FileLoader.ISO_8859_1);
csv = FileLoader.loadValues("data_semicolon_latin.csv", ";", true, FileLoader.ISO_8859_1);
assertArrayEquals(data, csv);
}

View File

@@ -0,0 +1,165 @@
package schule.ngb.zm.util.test;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.Supplier;
public final class ImageAssertions {
private static boolean SAVE_DIFF_IMAGE_ON_FAIL = false;
private static File DIFF_IMAGE_PATH = new File("build/test-results/diff");
private static AssertionFailedError ASSERTION_FAILED_ERROR = null;
public static boolean isSaveDiffImageOnFail() {
return SAVE_DIFF_IMAGE_ON_FAIL;
}
public static final void setSaveDiffImageOnFail( boolean saveOnFail ) {
SAVE_DIFF_IMAGE_ON_FAIL = saveOnFail;
}
public static File getDiffImagePath() {
return DIFF_IMAGE_PATH;
}
public static void assertEquals( BufferedImage expected, BufferedImage actual ) {
assertEquals(expected, actual, () -> "Actual image differs from expected buffer.");
}
public static void assertEquals( BufferedImage expected, BufferedImage actual, String message ) {
assertEquals(expected, actual, () -> message);
}
public static void assertEquals( BufferedImage expected, BufferedImage actual, Supplier<String> messageSupplier ) {
// Compare image dimensions
int expectedHeight = expected.getHeight(), expectedWidth = expected.getWidth();
int actualHeight = actual.getHeight(), actualWidth = actual.getWidth();
try {
Assertions.assertEquals(expectedHeight, actualHeight);
Assertions.assertEquals(expectedWidth, actualWidth);
} catch( AssertionFailedError afe ) {
ASSERTION_FAILED_ERROR = afe;
fail(expected, actual, messageSupplier);
}
// TODO: Fix comparison of transparent pixels
for( int x = 0; x < actualWidth; x++ ) {
for( int y = 0; y < actualHeight; y++ ) {
try {
Assertions.assertTrue(comparePixels(expected.getRGB(x, y), actual.getRGB(x, y)));
} catch( AssertionFailedError afe ) {
ASSERTION_FAILED_ERROR = afe;
fail(expected, actual, messageSupplier);
}
}
}
}
public static void assertNotEquals( BufferedImage expected, BufferedImage actual ) {
assertNotEquals(expected, actual, () -> "Actual image is the same as expected buffer.");
}
public static void assertNotEquals( BufferedImage expected, BufferedImage actual, String message ) {
assertNotEquals(expected, actual, () -> message);
}
public static void assertNotEquals( BufferedImage expected, BufferedImage actual, Supplier<String> messageSupplier ) {
// Compare image dimensions
int expectedHeight = expected.getHeight(), expectedWidth = expected.getWidth();
int actualHeight = actual.getHeight(), actualWidth = actual.getWidth();
if( expectedHeight != actualHeight || expectedWidth != actualWidth ) {
// Image dimensions differ, assertion is true
return;
}
for( int x = 0; x < actualWidth; x++ ) {
for( int y = 0; y < actualHeight; y++ ) {
if( !comparePixels(expected.getRGB(x, y), actual.getRGB(x, y)) ) {
// Found different pixels, assertion is true
return;
}
}
}
// Images are the same, fail without diff
fail(expected, actual, messageSupplier, false);
}
private static void fail( BufferedImage expected, BufferedImage actual, Supplier<String> messageSupplier ) {
fail(expected, actual, messageSupplier, SAVE_DIFF_IMAGE_ON_FAIL);
}
private static void fail( BufferedImage expected, BufferedImage actual, Supplier<String> messageSupplier, boolean saveDiffImage ) {
if( saveDiffImage ) {
saveDiffImage(expected, actual);
}
throw new AssertionFailedError(
messageSupplier != null ? messageSupplier.get() : null,
ASSERTION_FAILED_ERROR
);
}
private static boolean comparePixels( int a, int b ) {
// TODO: Fix comparison of transparent pixels
return a == b || ((0xFF000000 & a) == 0 && (0xFF000000 & b) == 0);
}
public static BufferedImage createDiffImage( BufferedImage expected, BufferedImage actual ) {
// Error color (white)
int errorColor = 0xFF00FF;
int expectedHeight = expected.getHeight(), expectedWidth = expected.getWidth();
int actualHeight = actual.getHeight(), actualWidth = actual.getWidth();
int maxHeight = Math.max(expectedHeight, actualHeight), maxWidth = Math.max(expectedWidth, actualWidth);
BufferedImage diff = ImageLoader.createImage(maxWidth, maxHeight);
for( int x = 0; x < maxWidth; x++ ) {
for( int y = 0; y < maxHeight; y++ ) {
diff.setRGB(x, y, 0);
if( x > actualWidth || y > actualHeight || x > expectedWidth || y > expectedHeight ) {
// Set overflow pixels to error color
diff.setRGB(x, y, errorColor);
} else if( !comparePixels(actual.getRGB(x, y), expected.getRGB(x, y)) ) {
// Set differences to error color
// If both pixels are transparent, the color dows not matter ...
// TODO: saturate error color based on how different the colors are?
diff.setRGB(x, y, errorColor);
}
}
}
return diff;
}
public static boolean saveDiffImage( BufferedImage expected, BufferedImage actual ) {
BufferedImage diff = createDiffImage(expected, actual);
try {
File diffFile = new File(DIFF_IMAGE_PATH, makeDiffName());
if( !diffFile.getParentFile().exists() ) {
diffFile.mkdirs();
}
ImageLoader.saveImage(diff, diffFile);
} catch( IOException ioe ) {
// We fail anyways at this point
// TODO: Log something?
return false;
}
return true;
}
private static String makeDiffName() {
return System.currentTimeMillis() + ".png";
}
private ImageAssertions() {
}
}

View File

@@ -0,0 +1,25 @@
package schule.ngb.zm.util.test;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import schule.ngb.zm.Testmaschine;
import schule.ngb.zm.Zeichenmaschine;
public class TestEnv implements ParameterResolver {
@Override
public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext ) throws ParameterResolutionException {
return (
parameterContext.getParameter().getType() == Zeichenmaschine.class ||
parameterContext.getParameter().getType() == Testmaschine.class
);
}
@Override
public Object resolveParameter( ParameterContext parameterContext, ExtensionContext extensionContext ) throws ParameterResolutionException {
return new Testmaschine();
}
}