Merge branch 'main' into games

This commit is contained in:
J. Neugebauer
2024-12-02 18:59:23 +01:00
43 changed files with 1597 additions and 316 deletions

1
.gitignore vendored
View File

@@ -34,6 +34,7 @@ hs_err_pid*
Thumbs.db Thumbs.db
.gradle .gradle
local.properties
**/build/ **/build/
!src/**/build/ !src/**/build/

View File

@@ -10,9 +10,13 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Dokumentation erweitert. - Dokumentation erweitert.
- Caching-Mechanismen in Klasse `util.Cache` ausgelagert. - Caching-Mechanismen in Klasse `util.Cache` ausgelagert.
- `util.io.ImageLoader` und `util.io.FontLoader` verwenden `Cache`. - `util.io.ImageLoader` und `util.io.FontLoader` verwenden `Cache`.
- `mouseWheelMoved` Eventhandler für Mausrad.
- `DrawingLayer.imageRotate(...)` Methoden, um Bilder um ihr Zentrum gedreht zu zeichnen.
## Changed ## Changed
- Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird. - Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird.
- `DrawingLayer.image(...)` mit Größenänderung umbenannt zu `imageScale(...)`.
- Klassen in `schule.ngb.zm.util.io` werfen nun nur eine Warnung ohne Stack-Trace, wenn die Ressource nicht gefunden werden konnte.
## Fixed ## Fixed
- `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt. - `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt.

View File

@@ -1,10 +1,11 @@
plugins { plugins {
id 'idea' id 'idea'
id 'java-library' id 'java-library'
id 'org.hidetake.ssh' version '2.10.1'
} }
group 'schule.ngb' group 'schule.ngb'
version '0.0.34-SNAPSHOT' version '0.0.35-SNAPSHOT'
java { java {
withSourcesJar() withSourcesJar()
@@ -19,6 +20,15 @@ repositories {
mavenCentral() mavenCentral()
} }
remotes {
uberspace {
host = 'westphal.uberspace.de'
user = 'ngb'
identity = file("${System.properties['user.home']}/.ssh/uberspace_rsa")
knownHosts = allowAnyHosts
}
}
dependencies { dependencies {
runtimeOnly 'com.googlecode.soundlibs:jlayer:1.0.1.4' runtimeOnly 'com.googlecode.soundlibs:jlayer:1.0.1.4'
runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4' runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4'
@@ -92,6 +102,52 @@ task buildDocs {
dependsOn 'mkdocs' dependsOn 'mkdocs'
} }
task zipSite(type: Zip) {
group "documentation"
description "Create zip archives for documentations"
dependsOn 'mkdocs'
from fileTree("${buildDir}/docs/site")
exclude '*.py'
exclude '__pycache__'
archiveName 'site.zip'
destinationDir(file("${buildDir}/docs"))
}
task zipJavadoc(type: Zip) {
group "documentation"
description "Create zip archives for javadoc"
dependsOn 'javadoc'
from fileTree("${buildDir}/docs/javadoc")
archiveName 'javadoc.zip'
destinationDir(file("${buildDir}/docs"))
}
task uploadDocs {
group "documentation"
description "Run all documentation tasks and upload artifacts to zeichenmaschine.xyz"
dependsOn 'zipSite'
dependsOn 'zipJavadoc'
doLast {
ssh.run {
session(remotes.uberspace) {
execute 'rm -rf /var/www/virtual/ngb/zeichenmaschine.xyz/*', ignoreError: true
put from: "${buildDir}/docs/site.zip", into: '/var/www/virtual/ngb/zeichenmaschine.xyz', ignoreError: true
execute 'unzip -o -q /var/www/virtual/ngb/zeichenmaschine.xyz/site.zip -d /var/www/virtual/ngb/zeichenmaschine.xyz'
put from: "${buildDir}/docs/javadoc.zip", into: '/var/www/virtual/ngb/zeichenmaschine.xyz', ignoreError: true
execute 'unzip -o -q /var/www/virtual/ngb/zeichenmaschine.xyz/javadoc.zip -d /var/www/virtual/ngb/zeichenmaschine.xyz/docs'
}
}
}
}
test { test {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@@ -30,9 +30,9 @@ leichter nutzbar machen.
## Dokumentation ## Dokumentation
* [Schnellstart](quickstart.md) * [Schnellstart](schnellstart.md)
* [Installation](installation.md) * [Installation](installation.md)
* {{ javadoc_link() }} * [Javadoc]({{ javadoc() }})
## Über die Zeichenmaschine ## Über die Zeichenmaschine
@@ -68,3 +68,4 @@ Alternativen, von deren Nutzung gar nicht abgeraten werden soll.
Klassen, Methoden und Variablen verwendet. Ausnahme sind einzelne Klassen, Klassen, Methoden und Variablen verwendet. Ausnahme sind einzelne Klassen,
die im Zusammnehang mit dem Namen der Bibliothek stehen, wie die die im Zusammnehang mit dem Namen der Bibliothek stehen, wie die
Hauptklasse `Zeichenmaschine`. Hauptklasse `Zeichenmaschine`.

View File

@@ -2,10 +2,9 @@
Um ein einfaches Projekt mit der **Zeichenmaschine** aufzusetzen ist nicht mehr Um ein einfaches Projekt mit der **Zeichenmaschine** aufzusetzen ist nicht mehr
nötig, als nötig, als
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest) die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/releases/latest)
herunterzuladen und dem *Classpath* des Projekts hinzuzufügen. Beschreibungen herunterzuladen und dem *Classpath* des Projekts hinzuzufügen. Beschreibungen
für für verschiedene Entwicklungsumgebungen sind hier aufgelistet.
verschiedene Entwicklungsumgebungen sind hier aufgelistet.
## Integration in Entwicklungsumgebungen ## Integration in Entwicklungsumgebungen

View File

@@ -57,7 +57,7 @@ erstellt und in einem Fenster mit dem Titel „Shapes“ angezeigt.
### Formen zeichnen ### Formen zeichnen
Eine Zeichenmaschine hat verschiedene Möglichkeiten, Inhalte in das Eine Zeichenmaschine hat verschiedene Möglichkeiten Inhalte in das
Zeichenfenster zu zeichnen. Um ein einfaches statisches Bild zu erzeugen, Zeichenfenster zu zeichnen. Um ein einfaches statisches Bild zu erzeugen,
überschreiben wir die {{ jdl("schule.ngb.zm.Zeichenmaschine", "draw()", überschreiben wir die {{ jdl("schule.ngb.zm.Zeichenmaschine", "draw()",
c=False) }} Methode. c=False) }} Methode.
@@ -146,7 +146,7 @@ Im Beispiel setzen wir nun die Grundeinstellungen in der `setup()` Methode. In
## Interaktionen mit der Maus: Whack-a-mole ## Interaktionen mit der Maus: Whack-a-mole
Mit der Zeichenmaschine lassen sich Interaktionen mit der Maus leicht umsetzen. Mit der Zeichenmaschine lassen sich Interaktionen mit der Maus leicht umsetzen.
Wor wollen das Beispielprogramm zu einem Wir wollen das Beispielprogramm zu einem
[Whac-A-Mole](https://de.wikipedia.org/wiki/Whac-A-Mole) Spiel erweitern. [Whac-A-Mole](https://de.wikipedia.org/wiki/Whac-A-Mole) Spiel erweitern.
Auf der Zeichenfläche wird nur noch ein gelber Kreis an einer zufälligen Stelle Auf der Zeichenfläche wird nur noch ein gelber Kreis an einer zufälligen Stelle
@@ -219,7 +219,7 @@ aber auch abnehmen und stellt eine Methode dafür bereit
Um auf einen Mausklick zu reagieren, ergänzen wir die Um auf einen Mausklick zu reagieren, ergänzen wir die
{{ jdm("Zeichenmaschine", "mouseClicked()") }} Methode: {{ jdm("Zeichenmaschine", "mouseClicked()") }} Methode:
``` ```java
@Override @Override
public void mouseClicked() { public void mouseClicked() {
if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) { if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) {
@@ -290,8 +290,8 @@ angeklickt wird.
## Ein paar Details zur Zeichenmaschine ## Ein paar Details zur Zeichenmaschine
Die _Zeichenmaschine_ wurde stark von der kreativen Programmierumgebung Die _Zeichenmaschine_ wurde stark von der kreativen Programmierumgebung
[Processing](https://processing.org) inspiriert. Wenn Du Processing schon [Processing](https://processing.org) inspiriert. Wenn du Processing schon
kennst, dann werden Dir einige der Konzepte der _Zeichenmaschine_ schon bekannt kennst, dann werden dir einige der Konzepte der _Zeichenmaschine_ schon bekannt
vorkommen. vorkommen.
### Farben ### Farben
@@ -392,14 +392,14 @@ Sekunde aufgerufen wird. Normalerweise passiert dies genau 60-mal pro Sekunde.
### Lebenszeit eines Kreises ### Lebenszeit eines Kreises
Jeder Kreis soll drei Sekunden zu sehen sein. Daher fügen wir eine neue Jeder Kreis soll drei Sekunden zu sehen sein. Daher fügen wir eine neue
Objektvariable namens `moleTime` ein, die zunächst auf drei steht. Da wir auch Objektvariable namens `moleTime` ein, die zunächst auf Drei steht. Da wir auch
Bruchteile von Skeunden abziehen wollen, wählen wir als Datentyp `double`: Bruchteile von Sekunden abziehen wollen, wählen wir als Datentyp `double`:
```Java ```Java
private double moleTime=3.0; private double moleTime=3.0;
``` ```
Der Parameter `delta`, der `update()` Methode ist der Zeitraum in Sekunden, seit Der Parameter `delta` der `update()` Methode ist der Zeitraum in Sekunden seit
dem letzten Frame. Subtrahieren wir diesen Wert bei jedem Frame von `moleTime`, dem letzten Frame. Subtrahieren wir diesen Wert bei jedem Frame von `moleTime`,
wird der Wert immer kleiner. Dann müssen wir nur noch prüfen, ob er kleiner Null wird der Wert immer kleiner. Dann müssen wir nur noch prüfen, ob er kleiner Null
ist und in dem Fall den Kreis auf eine neue Position springen lassen. ist und in dem Fall den Kreis auf eine neue Position springen lassen.
@@ -489,7 +489,7 @@ drawing.circle(moleX,moleY,moleRadius*(moleTime/3.0));
### Punktezähler ### Punktezähler
Zum Schluss wollen wir noch bei jedem Treffer mit der Maus die Punkte Zählen und Zum Schluss wollen wir noch bei jedem Treffer mit der Maus die Punkte zählen und
als Text auf die Zeichenfläche schreiben. als Text auf die Zeichenfläche schreiben.
Dazu ergänzen wir eine weitere Objektvariable `score`, die in `mouseClicked()` Dazu ergänzen wir eine weitere Objektvariable `score`, die in `mouseClicked()`
@@ -589,18 +589,18 @@ drawing.setFillColor(BLACK);
## Wie es weitergehen kann ## Wie es weitergehen kann
In diesem Schnellstart-Tutorial hast Du die Grundlagen der _Zeichenmaschine_ In diesem Schnellstart-Tutorial hast du die Grundlagen der _Zeichenmaschine_
gelernt. Um weiterzumachen, kannst Du versuchen, das Whack-a-mole Spiel um diese gelernt. Um weiterzumachen, kannst du versuchen, das Whack-a-mole Spiel um diese
Funktionen zu erweitern: Funktionen zu erweitern:
- Mehrere "Maulwürfe" gleichzeitig. - Mehrere "Maulwürfe" gleichzeitig.
- Unterschiedliche Zeiten pro Maulwurf. - Unterschiedliche Zeiten pro Maulwurf.
Wenn Du mehr über die Möglichkeiten lernen möchtest, die Dir die Wenn du mehr über die Möglichkeiten lernen möchtest, die dir die
_Zeichenmaschine_ bereitstellt, kannst Du Dir die weiteren Tutorials in dieser _Zeichenmaschine_ bereitstellt, kannst du dir die weiteren Tutorials in dieser
Dokumentation ansehen. Ein guter Startpunkt ist das Dokumentation ansehen. Ein guter Startpunkt ist das
[Aquarium](tutorials/aquarium/aquarium1.md). [Aquarium](tutorials/aquarium/aquarium1.md).
Viele verschiedene Beispiele, ohne detailliertes Tutorial, findest Du in der Viele verschiedene Beispiele, ohne detailliertes Tutorial, findest du in der
Kategorie Beispiele und auf GitHub im Repository Kategorie Beispiele und auf GitHub im Repository
[jneug/zeichenmaschine-examples](https://github.com/jneug/zeichenmaschine-examples). [jneug/zeichenmaschine-examples](https://github.com/jneug/zeichenmaschine-examples).

View File

@@ -8,7 +8,7 @@ site_dir: build/docs/site
theme: theme:
name: material name: material
custom_dir: docs/home_override/ # custom_dir: docs/home_override/
language: de language: de
logo: assets/icon_64.png logo: assets/icon_64.png
favicon: assets/icon_32.png favicon: assets/icon_32.png
@@ -37,7 +37,7 @@ extra_css:
- assets/zmstyles.css - assets/zmstyles.css
nav: nav:
- Einführung: einfuehrung.md - Einführung: index.md
- Schnellstart: schnellstart.md - Schnellstart: schnellstart.md
- Installation: installation.md - Installation: installation.md
- Tutorials: - Tutorials:

View File

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

View File

@@ -576,6 +576,18 @@ public class Color implements Paint {
} }
} }
public double compare( Color color ) {
double maxDist = 764.8333151739665;
// see: https://www.compuphase.com/cmetric.htm
long rmean = (getRed() + color.getRed()) / 2;
long r = getRed() - color.getRed();
long g = getGreen() - color.getGreen();
long b = getBlue() - color.getBlue();
return 1.0 - (Math.sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8)) / maxDist);
}
/** /**
* Prüft, ob ein anderes Objekt zu diesem gleich ist. * Prüft, ob ein anderes Objekt zu diesem gleich ist.
* <p> * <p>

View File

@@ -11,6 +11,7 @@ import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
@@ -67,7 +68,7 @@ public class Constants {
/** /**
* Patchversion der Zeichenmaschine. * 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. * Version der Zeichenmaschine als Text-String.
@@ -170,6 +171,21 @@ public class Constants {
*/ */
public static final Options.StrokeType DOTTED = Options.StrokeType.DOTTED; 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. * Option für Pfeile mit Strichen als Kopf.
*/ */
@@ -1268,7 +1284,7 @@ public class Constants {
* *
* @return Die {@code Random}-Instanz. * @return Die {@code Random}-Instanz.
*/ */
private static Random getRandom() { public static Random getRandom() {
if( random == null ) { if( random == null ) {
random = new Random(); random = new Random();
} }
@@ -1389,26 +1405,6 @@ public class Constants {
return getRandom().nextGaussian(); 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. * Wählt ein zufälliges Element aus dem Array aus.
* *
@@ -1571,6 +1567,18 @@ public class Constants {
return valueList.toArray(values); 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. * Geteilte {@code Noise}-Instanz zur Erzeugung von Perlin-Noise.
*/ */

View File

@@ -1,5 +1,6 @@
package schule.ngb.zm; package schule.ngb.zm;
import java.awt.BasicStroke;
import java.awt.geom.Arc2D; import java.awt.geom.Arc2D;
/** /**
@@ -31,6 +32,36 @@ public final class Options {
DOTTED 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. * Stile für Pfeilspitzen.
*/ */

View File

@@ -174,7 +174,7 @@ public interface Strokeable extends Drawable {
* @param weight Die Dicke der Konturlinie. * @param weight Die Dicke der Konturlinie.
*/ */
default void setStrokeWeight( double weight ) { 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 * @see Options.StrokeType
*/ */
default void setStrokeType( Options.StrokeType type ) { 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 * @param strokeWeight
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften. * @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 ) { switch( strokeType ) {
case DOTTED: case DOTTED:
return new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, strokeJoin.awt_type,
10.0f, new float[]{1.0f, 5.0f}, 0.0f); 10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED: case DASHED:
return new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, strokeJoin.awt_type,
10.0f, new float[]{5.0f}, 0.0f); 10.0f, new float[]{5.0f}, 0.0f);
case SOLID: case SOLID:
default: default:
return new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, 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. * Setzt das Look and Feel auf den Standard des Systems.
* <p> * <p>
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen * Sollte einmalig vor Erstellen des ersten Programmfensters aufgerufen
* werden. * werden.
*/ */
public static final void setLookAndFeel() { public static final void setLookAndFeel() {

View File

@@ -94,7 +94,7 @@ public class Zeichenmaschine extends Constants {
*/ */
private boolean running; private boolean running;
private boolean terminateImediately = false; private boolean terminateImmediately = false;
/** /**
* Ob die ZM nach dem nächsten Frame pausiert werden soll. * Ob die ZM nach dem nächsten Frame pausiert werden soll.
@@ -545,7 +545,7 @@ public class Zeichenmaschine extends Constants {
if( running ) { if( running ) {
running = false; running = false;
terminateImediately = true; terminateImmediately = true;
quitAfterShutdown = true; quitAfterShutdown = true;
mainThread.interrupt(); mainThread.interrupt();
} else { } else {
@@ -769,6 +769,23 @@ public class Zeichenmaschine extends Constants {
framesPerSecond = framesPerSecondInternal; 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 * Speichert den aktuellen Inhalt der {@link Zeichenleinwand} in einer
* Bilddatei auf der Festplatte. Zur Auswahl der Zieldatei wird dem Nutzer * 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. * Bilddatei im angegebenen Dateipfad auf der Festplatte.
*/ */
public final void saveImage( String filepath ) { 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 { try {
ImageLoader.saveImage(img, new File(filepath), true); ImageLoader.saveImage(getImage(), new File(filepath), true);
} catch( IOException ex ) { } catch( IOException ex ) {
ex.printStackTrace(); ex.printStackTrace();
} }
} }
/** /**
* Erstellt eine Momentanaufnahme des aktuellen Inhalts der * Erstellt eine Momentaufnahme des aktuellen Inhalts der
* {@link Zeichenleinwand} und erstellt daraus eine * {@link Zeichenleinwand} und erstellt daraus eine
* {@link ImageLayer Bildebene}. Die Ebene wird automatisch der * {@link ImageLayer Bildebene}. Die Ebene wird automatisch der
* {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt. * {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt.
@@ -1225,7 +1233,7 @@ public class Zeichenmaschine extends Constants {
} }
public void mouseWheelMoved( MouseEvent e ) { public void mouseWheelMoved( MouseEvent e ) {
mouseWheelMoved(); mouseMoved();
} }
public void mouseWheelMoved() { public void mouseWheelMoved() {
@@ -1399,7 +1407,7 @@ public class Zeichenmaschine extends Constants {
if( Thread.interrupted() ) { if( Thread.interrupted() ) {
running = false; running = false;
terminateImediately = true; terminateImmediately = true;
break; break;
} }
} }
@@ -1455,7 +1463,7 @@ public class Zeichenmaschine extends Constants {
} }
state = Options.AppState.STOPPED; state = Options.AppState.STOPPED;
// Shutdown the updateThread // Shutdown the updateThread
while( !terminateImediately && updateThreadExecutor.isRunning() ) { while( !terminateImmediately && updateThreadExecutor.isRunning() ) {
Thread.yield(); Thread.yield();
} }
updateThreadExecutor.shutdownNow(); updateThreadExecutor.shutdownNow();
@@ -1562,7 +1570,7 @@ public class Zeichenmaschine extends Constants {
@Override @Override
public void mouseWheelMoved( MouseWheelEvent e ) { public void mouseWheelMoved( MouseWheelEvent e ) {
enqueueEvent(e); // enqueueEvent(e);
} }
} }

View File

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

View File

@@ -4,9 +4,15 @@ import schule.ngb.zm.util.Validator;
import java.util.function.DoubleUnaryOperator; 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> { 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 ) { public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing); super(runtime, easing);

View File

@@ -1,22 +1,28 @@
package schule.ngb.zm.anim; package schule.ngb.zm.anim;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
// TODO: (ngb) Maybe use AnimationFacade to override runtime?
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> { 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 overrideRuntime = -1;
private int lag = 0; private final int lag;
private int active = 0; private int active = 0;
public AnimationGroup( Animation<T>... anims ) {
this(0, -1, null, Arrays.asList(anims));
}
public AnimationGroup( Collection<Animation<T>> anims ) { public AnimationGroup( Collection<Animation<T>> anims ) {
this(0, -1, null, anims); this(0, -1, null, anims);
} }
@@ -42,6 +48,8 @@ public class AnimationGroup<T> extends Animation<T> {
if( easing != null ) { if( easing != null ) {
this.easing = easing; this.easing = easing;
overrideEasing = true; overrideEasing = true;
} else {
overrideEasing = false;
} }
if( runtime > 0 ) { if( runtime > 0 ) {
@@ -64,52 +72,110 @@ public class AnimationGroup<T> extends Animation<T> {
return anim.getAnimationTarget(); 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 @Override
public void update( double delta ) { public DoubleUnaryOperator getEasing() {
elapsedTime += (int) (delta * 1000); for( Animation<T> anim : anims ) {
// Animation is done. Stop all Animations. if( anim.isActive() ) {
if( elapsedTime > runtime ) { return anim.getEasing();
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();
}
} }
running = false;
this.stop();
} }
if( this.finished ) {
while( active < anims.size() && elapsedTime >= active * lag ) { return anims.get(anims.size() - 1).getEasing();
anims.get(active).start(); } else {
active += 1; return anims.get(0).getEasing();
} }
}
for( int i = 0; i < active; i++ ) { // @Override
double t = 0.0; // public void update( double delta ) {
if( overrideRuntime > 0 ) { // elapsedTime += (int) (delta * 1000);
t = (double) (elapsedTime - i*lag) / (double) overrideRuntime; //
} else { // // Animation is done. Stop all Animations.
t = (double) (elapsedTime - i*lag) / (double) anims.get(i).getRuntime(); // 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 @Override
public void animate( double e ) { 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; 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> { 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); super(runtime, easing);
object = target; this.target = target;
centerx = cx; this.centerX = cx;
centery = cy; this.centerY = cy;
Vector vec = new Vector(target.getX(), target.getY()).sub(cx, cy); this.rotateTo = Constants.radians(Constants.limit(rotateTo, 0, 360));
startangle = vec.heading(); this.rotateRight = rotateRight;
radius = vec.length(); }
@Override
public void initialize() {
Vector vec = new Vector(target.getX(), target.getY()).sub(centerX, centerY);
startAngle = vec.heading();
rotationRadius = vec.length();
} }
@Override @Override
public Shape getAnimationTarget() { public Shape getAnimationTarget() {
return object; return target;
} }
@Override @Override
public void animate( double e ) { public void animate( double e ) {
double angle = startangle + Constants.radians(Constants.interpolate(0, 360, e)); double angle = startAngle;
double x = centerx + radius * Constants.cos(angle); if( rotateRight ) {
double y = centery + radius * Constants.sin(angle); angle += Constants.interpolate(0, rotateTo, e);
object.moveTo(x, y); } 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; private int lag = 0;
/** /**
* Speichert eine Approximation der aktuellen Steigung der Easing-Funktion, * Speichert eine Approximation der aktuellen Steigung der Easing-Funktion, um im Fall
* um im Fall {@code easeInOnly == true} nach dem ersten Durchlauf die * {@code easeInOnly == true} nach dem ersten Durchlauf die passende Geschwindigkeit
* passende Geschwindigkeit beizubehalten. * beizubehalten.
*/ */
private double m = 1.0, lastEase = 0.0; 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 ) { private ContinousAnimation( Animation<T> baseAnimation, int lag, boolean easeInOnly ) {
super(baseAnimation.getRuntime(), baseAnimation.getEasing()); super(baseAnimation.getRuntime() + lag, baseAnimation.getEasing());
this.baseAnimation = baseAnimation; this.baseAnimation = baseAnimation;
this.lag = lag; this.lag = lag;
this.easeInOnly = easeInOnly; this.easeInOnly = easeInOnly;
@@ -40,35 +40,80 @@ public class ContinousAnimation<T> extends Animation<T> {
return baseAnimation.getAnimationTarget(); 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 @Override
public void update( double delta ) { public void update( double delta ) {
elapsedTime += (int) (delta * 1000); int currentRuntime = elapsedTime + (int) (delta * 1000);
if( elapsedTime >= runtime + lag ) { if( currentRuntime >= runtime + lag ) {
elapsedTime %= (runtime + lag); elapsedTime = currentRuntime % (runtime + lag);
if( easeInOnly && easing != null ) { if( easeInOnly && easing != null ) {
easing = null; easing = Easing.linear();
// runtime = (int)((1.0/m)*(runtime + lag)); // runtime = (int)((1.0/m)*(runtime + lag));
} }
} }
double t = (double) elapsedTime / (double) runtime; super.update(delta);
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 @Override
public void animate( double e ) { public void animate( double e ) {
// double t = (double) elapsedTime / (double) runtime;
// if( t >= 1.0 ) {
// t = 1.0;
// }
baseAnimation.elapsedTime = elapsedTime;
baseAnimation.animate(e); 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; public static final int FADE_OUT = 0;
private Shape object; private final Shape target;
private final int targetAlpha;
private Color fill, stroke; 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); super(runtime, easing);
this.object = object; this.target = target;
fill = object.getFillColor(); this.targetAlpha = targetAlpha;
}
@Override
public void initialize() {
fill = target.getFillColor();
fillAlpha = fill.getAlpha(); fillAlpha = fill.getAlpha();
stroke = object.getStrokeColor(); stroke = target.getStrokeColor();
strokeAlpha = stroke.getAlpha(); strokeAlpha = stroke.getAlpha();
tAlpha = alpha;
} }
@Override @Override
public Shape getAnimationTarget() { public Shape getAnimationTarget() {
return object; return target;
} }
@Override @Override
public void animate( double e ) { public void animate( double e ) {
object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e))); target.setFillColor(fill, (int) Constants.interpolate(fillAlpha, targetAlpha, e));
object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e))); target.setStrokeColor(stroke, (int) Constants.interpolate(strokeAlpha, targetAlpha, e));
} }
} }

View File

@@ -1,23 +1,28 @@
package schule.ngb.zm.anim; package schule.ngb.zm.anim;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Shape; import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
public class FillAnimation extends Animation<Shape> { 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); super(runtime, easing);
this.object = object; this.object = target;
oFill = object.getFillColor(); targetFill = newFill;
tFill = newFill; }
@Override
public void initialize() {
originFill = object.getFillColor();
} }
@Override @Override
@@ -27,7 +32,7 @@ public class FillAnimation extends Animation<Shape> {
@Override @Override
public void animate( double e ) { 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; package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants; 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 schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
public class MoveAnimation extends Animation<Shape> { 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); super(runtime, easing);
this.object = object; this.object = target;
oX = object.getX(); this.targetX = targetX;
oY = object.getY(); this.targetY = targetY;
tX = x; }
tY = y;
@Override
public void initialize() {
originX = object.getX();
originY = object.getY();
} }
@Override @Override
@@ -32,8 +35,8 @@ public class MoveAnimation extends Animation<Shape> {
@Override @Override
public void animate( double e ) { public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e)); object.setX(Constants.interpolate(originX, targetX, e));
object.setY(Constants.interpolate(oY, tY, 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(); return shapeDelegate.getStrokeType();
} }
@Override
public Options.StrokeJoin getStrokeJoin() {
return shapeDelegate.getStrokeJoin();
}
/** /**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED}, * Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}. * {@link #DOTTED} und {@link #SOLID}.
@@ -980,7 +985,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @see ImageLoader#loadImage(String) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y ) { 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) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y, Options.Direction anchor ) { 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 * Koordinaten auf die Zeichenebene. Das Bild wird um den angegebenen Faktor
* skaliert. * skaliert.
* <p> * <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)} * Siehe
* für mehr Details. * {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
* *
* @param imageSource Die Bildquelle. * @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes. * @param x x-Koordinate des Ankerpunktes.
@@ -1014,8 +1020,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param scale Der Skalierungsfaktor des Bildes. * @param scale Der Skalierungsfaktor des Bildes.
* @see ImageLoader#loadImage(String) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y, double scale ) { public void imageScale( String imageSource, double x, double y, double scale ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, shapeDelegate.getAnchor()); 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 * Koordinaten auf die Zeichenebene. Das Bild wird um den angegebenen Faktor
* skaliert und der angegebene Ankerpunkt verwendet. * skaliert und der angegebene Ankerpunkt verwendet.
* <p> * <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)} * Siehe
* für mehr Details. * {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
* *
* @param imageSource Die Bildquelle. * @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes. * @param x x-Koordinate des Ankerpunktes.
@@ -1033,8 +1040,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param anchor Der Ankerpunkt. * @param anchor Der Ankerpunkt.
* @see ImageLoader#loadImage(String) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y, double scale, Options.Direction anchor ) { public void imageScale( String imageSource, double x, double y, double scale, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, 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. * @param y y-Koordinate des Ankerpunktes.
*/ */
public void image( Image image, double x, double y ) { 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 * Zeichnet das angegebene Bild an den angegebenen Koordinaten auf die
* Zeichenebene. Das Bild wird um den angegebenen Faktor skaliert. * Zeichenebene. Das Bild wird um den angegebenen Faktor skaliert.
* <p> * <p>
* Siehe {@link #image(Image, double, double, double, Options.Direction)} * Siehe
* für mehr Details. * {@link #imageScale(Image, double, double, double, Options.Direction)} für
* mehr Details.
* *
* @param image Das vorher geladene Bild. * @param image Das vorher geladene Bild.
* @param x x-Koordinate des Ankerpunktes. * @param x x-Koordinate des Ankerpunktes.
* @param y y-Koordinate des Ankerpunktes. * @param y y-Koordinate des Ankerpunktes.
* @param scale Der Skalierungsfaktor des Bildes. * @param scale Der Skalierungsfaktor des Bildes.
*/ */
public void image( Image image, double x, double y, double scale ) { public void imageScale( Image image, double x, double y, double scale ) {
image(image, x, y, scale, shapeDelegate.getAnchor()); 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. * Das Seitenverhältnis wird immer beibehalten.
* <p> * <p>
* Soll das Bild innerhalb eines vorgegebenen Rechtecks liegen, sollte * Soll das Bild innerhalb eines vorgegebenen Rechtecks liegen, sollte
* {@link #image(Image, double, double, double, double, Options.Direction)} * {@link #imageScale(Image, double, double, double, double,
* verwendet werden. * Options.Direction)} verwendet werden.
* *
* @param image Das vorher geladene Bild. * @param image Das vorher geladene Bild.
* @param x x-Koordinate des Ankerpunktes. * @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 scale Der Skalierungsfaktor des Bildes.
* @param anchor Der Ankerpunkt. * @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 ) { /*if( image != null ) {
double neww = image.getWidth(null) * scale; double neww = image.getWidth(null) * scale;
double newh = image.getHeight(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 neww = image.getWidth(null) * scale;
double newh = image.getHeight(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. * Koordinaten in der angegebenen Größe auf die Zeichenebene.
* <p> * <p>
* Siehe * Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)} * {@link #imageScale(Image, double, double, double, double,
* für mehr Details. * Options.Direction)} für mehr Details.
* *
* @param imageSource Die Bildquelle. * @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes. * @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. * @param height Höhe des Bildes auf der Zeichenebene oder 0.
* @see ImageLoader#loadImage(String) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y, double width, double height ) { public void imageScale( String imageSource, double x, double y, double width, double height ) {
image(ImageLoader.loadImage(imageSource), x, y, width, height, shapeDelegate.getAnchor()); 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. * angegebene Ankerpunkt verwendet.
* <p> * <p>
* Siehe * Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)} * {@link #imageScale(Image, double, double, double, double,
* für mehr Details. * Options.Direction)} für mehr Details.
* *
* @param imageSource Die Bildquelle. * @param imageSource Die Bildquelle.
* @param x x-Koordinate des Ankerpunktes. * @param x x-Koordinate des Ankerpunktes.
@@ -1134,8 +1142,8 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* @param anchor Der Ankerpunkt. * @param anchor Der Ankerpunkt.
* @see ImageLoader#loadImage(String) * @see ImageLoader#loadImage(String)
*/ */
public void image( String imageSource, double x, double y, double width, double height, Options.Direction anchor ) { public void imageScale( String imageSource, double x, double y, double width, double height, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, width, height, 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. * angegebenen Größe auf die Zeichenebene.
* <p> * <p>
* Siehe * Siehe
* {@link #image(Image, double, double, double, double, Options.Direction)} * {@link #imageScale(Image, double, double, double, double,
* für mehr Details. * Options.Direction)} für mehr Details.
* *
* @param image Ein Bild-Objekt. * @param image Ein Bild-Objekt.
* @param x x-Koordinate des Ankerpunktes. * @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 width Breite des Bildes auf der Zeichenebene oder 0.
* @param height Höhe 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 ) { public void imageScale( Image image, double x, double y, double width, double height ) {
image(image, x, y, width, height, shapeDelegate.getAnchor()); imageScale(image, x, y, width, height, shapeDelegate.getAnchor());
} }
/** /**
@@ -1171,7 +1179,7 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
* <p> * <p>
* Soll die Bildgröße unter Beachtung der Abmessungen um einen Faktor * Soll die Bildgröße unter Beachtung der Abmessungen um einen Faktor
* verändert werden, sollte * verändert werden, sollte
* {@link #image(Image, double, double, double, Options.Direction)} * {@link #imageScale(Image, double, double, double, Options.Direction)}
* verwendet werden. * verwendet werden.
* *
* @param image Ein Bild-Objekt. * @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 height Höhe des Bildes auf der Zeichenebene oder 0.
* @param anchor Der Ankerpunkt. * @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? // TODO: Use Validator or at least LOG a message if image == null?
if( image != null ) { if( image != null ) {
AffineTransform orig = drawing.getTransform();
int imgWidth = image.getWidth(null);
int imgHeight = image.getHeight(null);
if( width == 0 ) { if( width == 0 ) {
width = (height / image.getHeight(null)) * image.getWidth(null); width = (height / imgHeight) * imgWidth;
} else if( height == 0 ) { } 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); 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.drawImage(image, (int) anchorPoint.x, (int) anchorPoint.y, (int) width, (int) height, null);
drawing.setTransform(orig);
} }
} }

View File

@@ -1,6 +1,7 @@
package schule.ngb.zm.layers; package schule.ngb.zm.layers;
import schule.ngb.zm.Layer; import schule.ngb.zm.Layer;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.anim.Animation; import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.anim.AnimationFacade; import schule.ngb.zm.anim.AnimationFacade;
import schule.ngb.zm.anim.Easing; import schule.ngb.zm.anim.Easing;
@@ -24,20 +25,26 @@ public class ShapesLayer extends Layer {
*/ */
protected boolean clearBeforeDraw = true; protected boolean clearBeforeDraw = true;
private final List<Shape> shapes; protected boolean updateShapes = true;
protected final List<Shape> shapes;
private final List<Animation<? extends Shape>> animations; private final List<Animation<? extends Shape>> animations;
private final List<Updatable> updatables;
public ShapesLayer() { public ShapesLayer() {
super(); super();
shapes = new LinkedList<>(); shapes = new LinkedList<>();
animations = new LinkedList<>(); animations = new LinkedList<>();
updatables = new LinkedList<>();
} }
public ShapesLayer( int width, int height ) { public ShapesLayer( int width, int height ) {
super(width, height); super(width, height);
shapes = new LinkedList<>(); shapes = new LinkedList<>();
animations = new LinkedList<>(); animations = new LinkedList<>();
updatables = new LinkedList<>();
} }
public Shape getShape( int index ) { public Shape getShape( int index ) {
@@ -70,12 +77,24 @@ public class ShapesLayer extends Layer {
public void add( Shape... shapes ) { public void add( Shape... shapes ) {
synchronized( this.shapes ) { synchronized( this.shapes ) {
Collections.addAll(this.shapes, shapes); Collections.addAll(this.shapes, shapes);
for( Shape s : shapes ) {
if( Updatable.class.isInstance(s) ) {
updatables.add((Updatable) s);
}
}
} }
} }
public void add( Collection<Shape> shapes ) { public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) { synchronized( this.shapes ) {
this.shapes.addAll(shapes); this.shapes.addAll(shapes);
for( Shape s : shapes ) {
if( Updatable.class.isInstance(s) ) {
updatables.add((Updatable) s);
}
}
} }
} }
@@ -139,6 +158,27 @@ public class ShapesLayer extends Layer {
@Override @Override
public void update( double delta ) { 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();
if( u.isActive() ) {
u.update(delta);
}
}
*/
}
Iterator<Animation<? extends Shape>> it = animations.iterator(); Iterator<Animation<? extends Shape>> it = animations.iterator();
while( it.hasNext() ) { while( it.hasNext() ) {
Animation<? extends Shape> anim = it.next(); Animation<? extends Shape> anim = it.next();

View File

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

View File

@@ -1,5 +1,9 @@
package schule.ngb.zm.shapes; 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.CubicCurve2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D; import java.awt.geom.QuadCurve2D;
@@ -170,6 +174,30 @@ public class Curve extends Shape {
move(dx, dy); 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 @Override
public boolean equals( Object o ) { public boolean equals( Object o ) {
if( this == o ) return true; if( this == o ) return true;

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ public class Validator {
public static final <T> T requireNotNull( T obj, Supplier<String> msg ) { public static final <T> T requireNotNull( T obj, Supplier<String> msg ) {
if( obj == null ) { if( obj == null ) {
throw new NullPointerException(msg == null ? "Parameter may nut be null." : msg.get()); throw new NullPointerException(msg == null ? "Parameter may not be null." : msg.get());
} }
return obj; return obj;
} }

View File

@@ -1,9 +1,11 @@
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
@@ -11,6 +13,7 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level;
/** /**
* Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen. * Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
@@ -63,6 +66,9 @@ public final class FileLoader {
* @return Eine Liste mit den Zeilen der Textdatei. * @return Eine Liste mit den Zeilen der Textdatei.
*/ */
public static List<String> loadLines( String source, Charset charset ) { public static List<String> loadLines( String source, Charset charset ) {
Validator.requireNotNull(source, "source");
Validator.requireNotNull(charset, "charset");
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) { try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
@@ -72,8 +78,11 @@ public final class FileLoader {
} }
return result; return result;
} catch( MalformedURLException muex ) {
LOG.warn("Could not find resource for <%s>", source);
return Collections.emptyList();
} catch( IOException ex ) { } catch( IOException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source); LOG.warn(ex, "Error while loading lines from source <%s>", source);
return Collections.emptyList(); return Collections.emptyList();
} }
} }
@@ -101,6 +110,9 @@ public final class FileLoader {
* @return Der Inhalt der Textdatei. * @return Der Inhalt der Textdatei.
*/ */
public static String loadText( String source, Charset charset ) { public static String loadText( String source, Charset charset ) {
Validator.requireNotNull(source, "source");
Validator.requireNotNull(charset, "charset");
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) { try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
StringBuilder result = new StringBuilder(); StringBuilder result = new StringBuilder();
@@ -110,8 +122,11 @@ public final class FileLoader {
} }
return result.toString(); return result.toString();
} catch( MalformedURLException muex ) {
LOG.warn("Could not find resource for <%s>", source);
return "";
} catch( IOException ex ) { } catch( IOException ex ) {
LOG.error(ex, "Error while loading string from source <%s>", source); LOG.warn(ex, "Error while loading string from source <%s>", source);
return ""; return "";
} }
} }
@@ -166,34 +181,39 @@ public final class FileLoader {
).toArray(String[][]::new); ).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); 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> * <p>
* Die gelesenen Strings werden mit {@link Double#parseDouble(String)} in * Die Zeilen der Eingabedatei werden anhand der Zeichenkette {@code separator}
* {@code double} umgeformt. Es leigt in der Verantwortung des Nutzers * in einzelne Teile aufgetrennt. {@code separator} wird als regulärer Ausdruck
* sicherzustellen, dass die CSV-Datei auch nur Zahlen enthält, die korrekt * interpretiert (siehe {@link String#split(String)}).
* in {@code double} umgewandelt werden können. Zellen für die die * <p>
* Umwandlung fehlschlägt werden mit 0.0 befüllt. * 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> * <p>
* Die Methode unterliegt denselben Einschränkungen wie * Die Methode unterliegt denselben Einschränkungen wie
* {@link #loadCsv(String, char, boolean, Charset)}. * {@link #loadCsv(String, char, boolean, Charset)}.
* *
* @param source Die Quelle der CSV-Daten. * @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 skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung. * @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s. * @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; int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset); List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map( return lines.stream().skip(n).map(
( line ) -> Arrays ( line ) -> Arrays
.stream(line.split(Character.toString(separator))) //.stream(line.split(Character.toString(separator)))
.stream(line.split(separator))
.mapToDouble( .mapToDouble(
( value ) -> { ( value ) -> {
try { try {

View File

@@ -10,6 +10,7 @@ import java.awt.Image;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -62,8 +63,8 @@ public class FontLoader {
} }
public static Font loadFont( String name, String source ) { public static Font loadFont( String name, String source ) {
Validator.requireNotNull(source, "Font source may not be null"); Validator.requireNotNull(source, "source");
Validator.requireNotEmpty(source, "Font source may not be empty."); Validator.requireNotEmpty(source, "source");
if( fontCache.containsKey(name) ) { if( fontCache.containsKey(name) ) {
LOG.trace("Retrieved font <%s> from font cache.", name); LOG.trace("Retrieved font <%s> from font cache.", name);
@@ -91,10 +92,12 @@ public class FontLoader {
//ge.registerFont(font); //ge.registerFont(font);
} }
LOG.debug("Loaded custom font from source <%s>.", source); LOG.debug("Loaded custom font from source <%s>.", source);
} catch( MalformedURLException muex ) {
LOG.warn("Could not find font resource for <%s>", source);
} catch( IOException ioex ) { } catch( IOException ioex ) {
LOG.error(ioex, "Error loading custom font file from source <%s>.", source); LOG.warn(ioex, "Error loading custom font file from source <%s>.", source);
} catch( FontFormatException ffex ) { } catch( FontFormatException ffex ) {
LOG.error(ffex, "Error creating custom font from source <%s>.", source); LOG.warn(ffex, "Error creating custom font from source <%s>.", source);
} }
return font; return font;

View File

@@ -16,6 +16,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
/** /**
* Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden. * Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
@@ -67,8 +68,8 @@ public final class ImageLoader {
* @return * @return
*/ */
public static BufferedImage loadImage( String source, boolean caching ) { public static BufferedImage loadImage( String source, boolean caching ) {
Validator.requireNotNull(source, "Image source may not be null"); Validator.requireNotNull(source, "source");
Validator.requireNotEmpty(source, "Image source may not be empty."); Validator.requireNotEmpty(source, "source");
if( caching && imageCache.containsKey(source) ) { if( caching && imageCache.containsKey(source) ) {
return imageCache.get(source); return imageCache.get(source);
@@ -84,8 +85,10 @@ public final class ImageLoader {
if( caching && img != null ) { if( caching && img != null ) {
imageCache.put(source, img); imageCache.put(source, img);
} }
} catch( MalformedURLException muex ) {
LOG.warn("Could not find image resource for <%s>", source);
} catch( IOException ioex ) { } catch( IOException ioex ) {
LOG.error(ioex, "Error loading image file from source <%s>.", source); LOG.warn(ioex, "Error loading image file from source <%s>.", source);
} }
return img; return img;
} }
@@ -318,12 +321,8 @@ public final class ImageLoader {
* @throws IOException Falls es einen Fehler beim Speichern gab. * @throws IOException Falls es einen Fehler beim Speichern gab.
*/ */
public static void saveImage( BufferedImage image, File file, boolean overwriteIfExists ) throws IOException { public static void saveImage( BufferedImage image, File file, boolean overwriteIfExists ) throws IOException {
if( image == null ) { Validator.requireNotNull(image, "image");
throw new NullPointerException("Image may not be <null>."); Validator.requireNotNull(file, "file");
}
if( file == null ) {
throw new NullPointerException("File may not be <null>.");
}
if( file.isFile() ) { if( file.isFile() ) {
// Datei existiert schon // Datei existiert schon

View File

@@ -47,8 +47,8 @@ public class ResourceStreamProvider {
* einer bestehenden Ressource. * einer bestehenden Ressource.
*/ */
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException { public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null"); Validator.requireNotNull(source, "source");
Validator.requireNotEmpty(source, "Resource source may not be empty."); Validator.requireNotEmpty(source, "source");
// Ist source ein valider Dateipfad? // Ist source ein valider Dateipfad?
File file = new File(source); File file = new File(source);

View File

@@ -252,4 +252,18 @@ class ColorTest {
void darker() { void darker() {
} }
@Test
void compare() {
assertEquals(1.0, Color.RED.compare(Color.RED), 0.0001);
assertEquals(1.0, Color.BLUE.compare(Color.BLUE), 0.0001);
assertEquals(1.0, Color.WHITE.compare(Color.WHITE), 0.0001);
assertEquals(1.0, Color.BLACK.compare(Color.BLACK), 0.0001);
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}, {2.1,2.2,2.3},
{3.1,3.2,3.3} {3.1,3.2,3.3}
}; };
csv = FileLoader.loadValues("data_comma.csv", ',', true); csv = FileLoader.loadValues("data_comma.csv", ",", true);
assertArrayEquals(data, csv); 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); 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();
}
}