diff --git a/src/main/java/schule/ngb/zm/BasicDrawable.java b/src/main/java/schule/ngb/zm/BasicDrawable.java index 120bfc4..3d403cd 100644 --- a/src/main/java/schule/ngb/zm/BasicDrawable.java +++ b/src/main/java/schule/ngb/zm/BasicDrawable.java @@ -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; + } + } diff --git a/src/main/java/schule/ngb/zm/Constants.java b/src/main/java/schule/ngb/zm/Constants.java index 8a9cc54..6aef3c4 100644 --- a/src/main/java/schule/ngb/zm/Constants.java +++ b/src/main/java/schule/ngb/zm/Constants.java @@ -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 Datentyp der Elemente. + * @return Das Array in zufälliger Reihenfolge. + */ + public static final List shuffle( List values ) { + Collections.shuffle(values, random); + return values; + } + /** * Geteilte {@code Noise}-Instanz zur Erzeugung von Perlin-Noise. */ diff --git a/src/main/java/schule/ngb/zm/Options.java b/src/main/java/schule/ngb/zm/Options.java index d170a87..ac719d8 100644 --- a/src/main/java/schule/ngb/zm/Options.java +++ b/src/main/java/schule/ngb/zm/Options.java @@ -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. */ diff --git a/src/main/java/schule/ngb/zm/Strokeable.java b/src/main/java/schule/ngb/zm/Strokeable.java index 22b4921..f9b1cc8 100644 --- a/src/main/java/schule/ngb/zm/Strokeable.java +++ b/src/main/java/schule/ngb/zm/Strokeable.java @@ -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); } } diff --git a/src/main/java/schule/ngb/zm/Zeichenfenster.java b/src/main/java/schule/ngb/zm/Zeichenfenster.java index 74109e2..c12f681 100644 --- a/src/main/java/schule/ngb/zm/Zeichenfenster.java +++ b/src/main/java/schule/ngb/zm/Zeichenfenster.java @@ -28,7 +28,7 @@ public class Zeichenfenster extends JFrame { /** * Setzt das Look and Feel auf den Standard des Systems. *

- * Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen + * Sollte einmalig vor Erstellen des ersten Programmfensters aufgerufen * werden. */ public static final void setLookAndFeel() { diff --git a/src/main/java/schule/ngb/zm/Zeichenmaschine.java b/src/main/java/schule/ngb/zm/Zeichenmaschine.java index eeabe96..e550697 100644 --- a/src/main/java/schule/ngb/zm/Zeichenmaschine.java +++ b/src/main/java/schule/ngb/zm/Zeichenmaschine.java @@ -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(); diff --git a/src/main/java/schule/ngb/zm/anim/Animation.java b/src/main/java/schule/ngb/zm/anim/Animation.java index 8a01557..5b80915 100644 --- a/src/main/java/schule/ngb/zm/anim/Animation.java +++ b/src/main/java/schule/ngb/zm/anim/Animation.java @@ -50,7 +50,7 @@ public abstract class Animation 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 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 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 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 extends Constants implements Updatable { * e = Constants.limit(e, 0, 1); * * - * @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 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); } diff --git a/src/main/java/schule/ngb/zm/anim/AnimationFacade.java b/src/main/java/schule/ngb/zm/anim/AnimationFacade.java index 3bc8aa7..4b499a9 100644 --- a/src/main/java/schule/ngb/zm/anim/AnimationFacade.java +++ b/src/main/java/schule/ngb/zm/anim/AnimationFacade.java @@ -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 Art des Animierten Objektes. + */ public class AnimationFacade extends Animation { - private Animation anim; + private final Animation anim; public AnimationFacade( Animation anim, int runtime, DoubleUnaryOperator easing ) { super(runtime, easing); diff --git a/src/main/java/schule/ngb/zm/anim/AnimationGroup.java b/src/main/java/schule/ngb/zm/anim/AnimationGroup.java index c1de074..41beb2e 100644 --- a/src/main/java/schule/ngb/zm/anim/AnimationGroup.java +++ b/src/main/java/schule/ngb/zm/anim/AnimationGroup.java @@ -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 extends Animation { - List> anims; + private final List> 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... anims ) { + this(0, -1, null, Arrays.asList(anims)); + } + public AnimationGroup( Collection> anims ) { this(0, -1, null, anims); } @@ -42,6 +48,8 @@ public class AnimationGroup extends Animation { if( easing != null ) { this.easing = easing; overrideEasing = true; + } else { + overrideEasing = false; } if( runtime > 0 ) { @@ -64,52 +72,110 @@ public class AnimationGroup extends Animation { 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 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 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 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); + } + } + } } diff --git a/src/main/java/schule/ngb/zm/anim/AnimationSequence.java b/src/main/java/schule/ngb/zm/anim/AnimationSequence.java new file mode 100644 index 0000000..66949d8 --- /dev/null +++ b/src/main/java/schule/ngb/zm/anim/AnimationSequence.java @@ -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 + * lag eingefügt werden. + * + * @param Die Art des animierten Objektes. + */ +@SuppressWarnings( "unused" ) +public class AnimationSequence extends Animation { + + private final List> anims; + + private final int lag; + + private int currentAnimationIndex = -1, currentStart = -1, nextStart = -1; + + @SafeVarargs + public AnimationSequence( Animation... anims ) { + this(0, Arrays.asList(anims)); + } + + public AnimationSequence( Collection> anims ) { + this(0, anims); + } + + public AnimationSequence( int lag, Collection> 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 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 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 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 anim : anims ) { + if( anim.isActive() ) { + anim.elapsedTime = anim.runtime; + anim.stop(); + } + } + } + + @Override + public void animate( double e ) { + Animation 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)); + } + } + +} diff --git a/src/main/java/schule/ngb/zm/anim/CircleAnimation.java b/src/main/java/schule/ngb/zm/anim/CircleAnimation.java index 4c0cdb8..320a0ed 100644 --- a/src/main/java/schule/ngb/zm/anim/CircleAnimation.java +++ b/src/main/java/schule/ngb/zm/anim/CircleAnimation.java @@ -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 (cx, cy). + */ public class CircleAnimation extends Animation { - 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); } } diff --git a/src/main/java/schule/ngb/zm/anim/ContinousAnimation.java b/src/main/java/schule/ngb/zm/anim/ContinousAnimation.java index 924adbe..b96b430 100644 --- a/src/main/java/schule/ngb/zm/anim/ContinousAnimation.java +++ b/src/main/java/schule/ngb/zm/anim/ContinousAnimation.java @@ -8,9 +8,9 @@ public class ContinousAnimation extends Animation { 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 extends Animation { } private ContinousAnimation( Animation 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 extends Animation { 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; } } diff --git a/src/main/java/schule/ngb/zm/anim/FadeAnimation.java b/src/main/java/schule/ngb/zm/anim/FadeAnimation.java index 4895eda..31a6132 100644 --- a/src/main/java/schule/ngb/zm/anim/FadeAnimation.java +++ b/src/main/java/schule/ngb/zm/anim/FadeAnimation.java @@ -13,32 +13,51 @@ public class FadeAnimation extends Animation { 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)); } } diff --git a/src/main/java/schule/ngb/zm/anim/FillAnimation.java b/src/main/java/schule/ngb/zm/anim/FillAnimation.java index 92c51d5..c87b125 100644 --- a/src/main/java/schule/ngb/zm/anim/FillAnimation.java +++ b/src/main/java/schule/ngb/zm/anim/FillAnimation.java @@ -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 { - 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 { @Override public void animate( double e ) { - object.setFillColor(Color.interpolate(oFill, tFill, e)); + object.setFillColor(Color.interpolate(originFill, targetFill, e)); } } diff --git a/src/main/java/schule/ngb/zm/anim/MoveAnimation.java b/src/main/java/schule/ngb/zm/anim/MoveAnimation.java index 3a826ce..bc90c5c 100644 --- a/src/main/java/schule/ngb/zm/anim/MoveAnimation.java +++ b/src/main/java/schule/ngb/zm/anim/MoveAnimation.java @@ -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 { - 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 { @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)); } } diff --git a/src/main/java/schule/ngb/zm/layers/DrawingLayer.java b/src/main/java/schule/ngb/zm/layers/DrawingLayer.java index 9c0f4fe..085d8f7 100644 --- a/src/main/java/schule/ngb/zm/layers/DrawingLayer.java +++ b/src/main/java/schule/ngb/zm/layers/DrawingLayer.java @@ -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. *

- * 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. *

- * 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. *

- * 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. *

* 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. *

* 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. *

* 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. *

* 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 { *

* 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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); } } diff --git a/src/main/java/schule/ngb/zm/layers/ShapesLayer.java b/src/main/java/schule/ngb/zm/layers/ShapesLayer.java index 30426d2..78d170d 100644 --- a/src/main/java/schule/ngb/zm/layers/ShapesLayer.java +++ b/src/main/java/schule/ngb/zm/layers/ShapesLayer.java @@ -159,6 +159,16 @@ public class ShapesLayer extends Layer { @Override public void update( double delta ) { if( updateShapes ) { + synchronized( shapes ) { + List uit = List.copyOf(updatables); + for( Updatable u : uit ) { + if( u.isActive() ) { + u.update(delta); + } + } + } + + /* Iterator uit = updatables.iterator(); while( uit.hasNext() ) { Updatable u = uit.next(); @@ -166,6 +176,7 @@ public class ShapesLayer extends Layer { u.update(delta); } } + */ } Iterator> it = animations.iterator(); diff --git a/src/main/java/schule/ngb/zm/layers/TurtleLayer.java b/src/main/java/schule/ngb/zm/layers/TurtleLayer.java index 21cc366..cdb4851 100644 --- a/src/main/java/schule/ngb/zm/layers/TurtleLayer.java +++ b/src/main/java/schule/ngb/zm/layers/TurtleLayer.java @@ -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); diff --git a/src/main/java/schule/ngb/zm/shapes/Curve.java b/src/main/java/schule/ngb/zm/shapes/Curve.java index b7357f4..54d3762 100644 --- a/src/main/java/schule/ngb/zm/shapes/Curve.java +++ b/src/main/java/schule/ngb/zm/shapes/Curve.java @@ -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; diff --git a/src/main/java/schule/ngb/zm/shapes/CustomShape.java b/src/main/java/schule/ngb/zm/shapes/CustomShape.java index 4758d67..2b189d5 100644 --- a/src/main/java/schule/ngb/zm/shapes/CustomShape.java +++ b/src/main/java/schule/ngb/zm/shapes/CustomShape.java @@ -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(); } diff --git a/src/main/java/schule/ngb/zm/shapes/Shape.java b/src/main/java/schule/ngb/zm/shapes/Shape.java index 33a8be2..6cdf583 100644 --- a/src/main/java/schule/ngb/zm/shapes/Shape.java +++ b/src/main/java/schule/ngb/zm/shapes/Shape.java @@ -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()); diff --git a/src/main/java/schule/ngb/zm/util/io/FileLoader.java b/src/main/java/schule/ngb/zm/util/io/FileLoader.java index b15df5b..8ecb6f3 100644 --- a/src/main/java/schule/ngb/zm/util/io/FileLoader.java +++ b/src/main/java/schule/ngb/zm/util/io/FileLoader.java @@ -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. *

- * 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)}). + *

+ * 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. *

* 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 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 { diff --git a/src/test/java/schule/ngb/zm/ColorTest.java b/src/test/java/schule/ngb/zm/ColorTest.java index b5d88fd..ca68a30 100644 --- a/src/test/java/schule/ngb/zm/ColorTest.java +++ b/src/test/java/schule/ngb/zm/ColorTest.java @@ -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); } } diff --git a/src/test/java/schule/ngb/zm/Testmaschine.java b/src/test/java/schule/ngb/zm/Testmaschine.java new file mode 100644 index 0000000..dd22370 --- /dev/null +++ b/src/test/java/schule/ngb/zm/Testmaschine.java @@ -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); + } + } + +} diff --git a/src/test/java/schule/ngb/zm/TestmaschineTest.java b/src/test/java/schule/ngb/zm/TestmaschineTest.java new file mode 100644 index 0000000..4065676 --- /dev/null +++ b/src/test/java/schule/ngb/zm/TestmaschineTest.java @@ -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); + } + +} diff --git a/src/test/java/schule/ngb/zm/anim/AnimationGroupsTest.java b/src/test/java/schule/ngb/zm/anim/AnimationGroupsTest.java new file mode 100644 index 0000000..3d54b4d --- /dev/null +++ b/src/test/java/schule/ngb/zm/anim/AnimationGroupsTest.java @@ -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 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 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 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(); + } + +} diff --git a/src/test/java/schule/ngb/zm/anim/AnimationTest.java b/src/test/java/schule/ngb/zm/anim/AnimationTest.java new file mode 100644 index 0000000..98b8e75 --- /dev/null +++ b/src/test/java/schule/ngb/zm/anim/AnimationTest.java @@ -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 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(); + } + +} diff --git a/src/test/java/schule/ngb/zm/layers/DrawingLayerTest.java b/src/test/java/schule/ngb/zm/layers/DrawingLayerTest.java new file mode 100644 index 0000000..9aebe58 --- /dev/null +++ b/src/test/java/schule/ngb/zm/layers/DrawingLayerTest.java @@ -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); + } + } + +} diff --git a/src/test/java/schule/ngb/zm/util/FileLoaderTest.java b/src/test/java/schule/ngb/zm/util/FileLoaderTest.java index f1b45c1..f622a05 100644 --- a/src/test/java/schule/ngb/zm/util/FileLoaderTest.java +++ b/src/test/java/schule/ngb/zm/util/FileLoaderTest.java @@ -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); } diff --git a/src/test/java/schule/ngb/zm/util/test/ImageAssertions.java b/src/test/java/schule/ngb/zm/util/test/ImageAssertions.java new file mode 100644 index 0000000..27e1d53 --- /dev/null +++ b/src/test/java/schule/ngb/zm/util/test/ImageAssertions.java @@ -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 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 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 messageSupplier ) { + fail(expected, actual, messageSupplier, SAVE_DIFF_IMAGE_ON_FAIL); + } + + private static void fail( BufferedImage expected, BufferedImage actual, Supplier 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() { + } + +} diff --git a/src/test/java/schule/ngb/zm/util/test/TestEnv.java b/src/test/java/schule/ngb/zm/util/test/TestEnv.java new file mode 100644 index 0000000..252a342 --- /dev/null +++ b/src/test/java/schule/ngb/zm/util/test/TestEnv.java @@ -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(); + } + +}