31 Commits

Author SHA1 Message Date
J. Neugebauer
3ab9701629 Updated ParticleEmitters and Factories 2024-12-03 16:23:24 +01:00
J. Neugebauer
3196914564 First draft of basic particle system 2024-12-03 10:20:23 +01:00
J. Neugebauer
a7747aea70 Bumped Version to 0.0.35 2024-12-02 18:56:54 +01:00
J. Neugebauer
1d41bf36c5 Updated Animations and Tests 2024-12-02 18:56:36 +01:00
J. Neugebauer
595bdd7556 Fixed spelling 2024-12-02 18:25:55 +01:00
J. Neugebauer
df10d38184 Updated CHANGELOG 2024-12-02 18:25:37 +01:00
fc3039b484 ShapesLayer now handles Updatables separately 2023-06-19 10:26:36 +02:00
6d1d47fed0 Fixed latest release URL 2023-06-19 10:25:44 +02:00
3342db6f79 local.properties ignored 2023-06-19 10:24:05 +02:00
3f07b9ee7e Changelog 2023-01-17 11:28:05 +01:00
4e7adf26f7 Name der quickstart.md angepasst 2023-01-17 11:27:59 +01:00
02085c83fc Validator eingesetzt und Exception handling verbessert 2023-01-17 11:27:08 +01:00
ef248a8580 Rechtschreibung korrigiert 2023-01-17 11:26:20 +01:00
39014fe82e Color.compare eingefügt 2022-12-21 20:09:28 +01:00
e451a2f087 mouseWheelMoved added 2022-12-21 16:18:25 +01:00
936043bf85 changelog 2022-12-14 20:36:30 +01:00
d75f67c0fa build tasks neu strukturiert 2022-12-14 20:36:24 +01:00
adbc29dabe Dokumentation 2022-12-14 20:36:08 +01:00
b687483e6d Versionsnummer 2022-12-14 20:35:55 +01:00
19bacd15e9 Bezir-Kurven im DrawingLayer 2022-12-14 20:35:48 +01:00
2d4abf6f0d Laden von Dock-Icon (macos) angepasst 2022-12-14 20:35:21 +01:00
cefe7c8cfa overview.html verschoben 2022-12-14 20:34:57 +01:00
b04e68c7bd Einführung erweitert 2022-12-14 20:34:32 +01:00
25ce3a35e9 Timing-Problem beim Aufruf des AudioListeners behoben 2022-12-13 10:17:25 +01:00
44d0f79c6c Changelog 2022-12-13 08:19:19 +01:00
43195aa63c Cache Methoden im ImageLoader direkt verwendet 2022-12-13 08:17:18 +01:00
6cc23de620 Dokumentation und umbenannte Methode 2022-12-13 08:14:07 +01:00
836571ca95 Merge branch 'main' into softcache 2022-12-12 21:26:57 +01:00
5232057b15 SoftCache zu Cache generalisiert
Ein Cache kann nun auch mit `WeakReference`n genutzt werden.
2022-12-12 21:26:24 +01:00
f0a3c65552 Gradle Task buildAll hinzugefügt 2022-12-11 21:50:10 +01:00
2c40e1ba31 SoftCache in ImageLoader benutzt 2022-12-11 21:49:51 +01:00
65 changed files with 2691 additions and 569 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

@@ -7,13 +7,20 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased] ## [Unreleased]
## Added ## Added
- Dokumentation `EventDispatcher`. - Dokumentation erweitert.
- Caching-Mechanismen in Klasse `util.Cache` ausgelagert.
- `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.
- Timing-Problem beim Aufruf von `AudioListener.playbackStopped()` in `Sound` behoben.
## Removed ## Removed
- `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar. - `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar.

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'
@@ -33,10 +43,39 @@ dependencies {
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
} }
jar {
manifest {
attributes 'Class-Path': '.'
}
}
tasks.register('jarMP3SPI', Jar) {
group "build"
description "Build jar with MP3SPI included"
archiveClassifier = 'mp3spi'
duplicatesStrategy = 'exclude'
// archivesBaseName = 'zeichenmaschine-mp3spi'
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
}
task buildAll {
group "build"
description "Build all jar packages"
dependsOn 'jar'
dependsOn 'jarMP3SPI'
dependsOn 'sourcesJar'
dependsOn 'javadocJar'
}
javadoc { javadoc {
options { options {
encoding = "UTF-8" encoding = "UTF-8"
overview = "src/main/java/overview.html" overview = "src/resources/java/overview.html"
// title = "Die Zeichenmaschine" // title = "Die Zeichenmaschine"
// options.links 'https://docs.oracle.com/javase/8/docs/api/' // options.links 'https://docs.oracle.com/javase/8/docs/api/'
@@ -46,16 +85,69 @@ javadoc {
options.addStringOption("charset", "UTF-8") options.addStringOption("charset", "UTF-8")
} }
test { task mkdocs(type: Exec) {
useJUnitPlatform() group "documentation"
description "Build MKDocs site"
workingDir "${projectDir}"
commandLine ".venv/bin/python", "-m", "mkdocs", "build"
} }
tasks.register('jarMP3SPI', Jar) { task buildDocs {
archiveClassifier = 'mp3spi' group "documentation"
duplicatesStrategy = 'exclude' description "Run all documentation tasks"
// archivesBaseName = 'zeichenmaschine-mp3spi'
from { dependsOn 'javadoc'
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } dependsOn 'javadocJar'
} dependsOn 'mkdocs'
with jar }
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 {
useJUnitPlatform()
} }

View File

@@ -1,43 +0,0 @@
<figure markdown>
![Zeichenmaschine.xyz](assets/icon_512.png){ width=128 }
</figure>
<h1 class="title">Zeichenmaschine.xyz</h1>
<h2 class="subtitle">Eine kleine Java-Bibliothek für grafische Programmierung im
Informatikunterricht.</h2>
## Projektidee
Die **Zeichenmaschine** ist eine für den Informatikunterricht entwickelte Bibliothek,
die unter anderem an [Processing](https://processing.org/) angelehnt ist. Die
Bibliothek soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen
und für Schülerinnen und Schüler im Unterricht nutzbar machen.
!!! warning
Das Projekt befindet sich noch in der Entwicklungsphase und auch wenn die
aktuelle Version schon funktionsfähig ist und einen Großteil der angestrebten
Funktionen enthält, ist noch keine stabile Version 1.0 erreicht. Vor allem
am Umfang und konsistenten Design der APIs gilt es noch zu arbeiten und es
können sich Änderungen ergeben.
Feedback und Vorschläge zu diesem Prozess (oder auch eine Beteiligung an der
Entwicklung) können sehr gerne über [Github](https://github.com/jneug) oder
[Mastodon](https://bildung.social/@ngb) an mich kommuniziert werden.
(Gleiches gilt für diese Webseite zum Projekt.)
## Dokumentation
* [Schnellstart](quickstart.md)
* [Installation](installation.md)
* {{ javadoc_link() }}
## Über die Zeichenmaschine
!!! info
In der Zeichenmaschine werden bewusst nur englischsprachige Bezeichner für
Klassen, Methoden und Variablen verwendet. Ausnahme sind einzelne Klassen,
die im Zusammnehang mit dem Namen der Bibliothek stehen, wie die
Hauptklasse `Zeichenmaschine`.

71
docs/index.md Normal file
View File

@@ -0,0 +1,71 @@
<figure markdown>
![Zeichenmaschine.xyz](assets/icon_512.png){ width=128 }
</figure>
<h1 class="title">Zeichenmaschine.xyz</h1>
<h2 class="subtitle">Eine kleine Java-Bibliothek für grafische Programmierung im
Informatikunterricht.</h2>
## Projektidee
Die **Zeichenmaschine** ist eine für den Informatikunterricht entwickelte
Bibliothek, die unter anderem an [Processing](https://processing.org/) angelehnt
ist. Die Bibliothek soll einige der üblichen Anfängerschwierigkeiten mit Java
vereinfachen und grafische Ausgaben für Schülerinnen und Schüler im Unterricht
leichter nutzbar machen.
!!! warning
Das Projekt befindet sich noch in der Entwicklungsphase und auch wenn die
aktuelle Version schon funktionsfähig ist und einen Großteil der angestrebten
Funktionen enthält, ist noch keine stabile Version 1.0 erreicht. Vor allem
am Umfang und konsistenten Design der APIs gilt es noch zu arbeiten und es
können sich Änderungen ergeben.
Feedback und Vorschläge zu diesem Prozess (oder auch eine Beteiligung an der
Entwicklung) können sehr gerne über [Github](https://github.com/jneug) oder
[Mastodon](https://bildung.social/@ngb) an mich kommuniziert werden.
(Gleiches gilt für diese Webseite zum Projekt.)
## Dokumentation
* [Schnellstart](schnellstart.md)
* [Installation](installation.md)
* [Javadoc]({{ javadoc() }})
## Über die Zeichenmaschine
Die _Zeichenmaschine_ ist aus dem Wunsch entstanden, nach einer Einführung in
die Grundlagen der Programmiersprache Java mit Processing, einen Übergang zur
objektorientierten Modellierung und Programmierung mit BlueJ zu leichter zu
ermöglichen. Mit Processing kann zwar auch objektorientiert programmiert werden,
aber mit Blick auf die Sekundarstufe II in NRW ist ein Wechsel zu einer
generellen Programmierumgebung wie BlueJ wünschenswert.
Die Motivation von Processing, schnell grafische Ergebnisse der eigenen
Programme zu sehen, sollte aber für den Übergang erhalten bleiben. So ist eine
kleine Bibliothek mit minimalen Abhängigkeiten entstanden, die an Processing
angelehnt einfache Schnittstellen bereitstellte, um Programmen eine grafische
Ausgabe zu ermöglichen, ohne viel "Boilerplate" Code schreiben zu müssen.
Aus diesen Anfängen ist nach und nach eine umfassende Grafikbibliothek
entstanden, die als _Zeichenmaschine_ veröffentlicht wurde.
### Was die Zeichenmaschine nicht ist
Die Bibliothek hat nicht den Anspruch, ein Konkurrent zu Processing oder
anderen, seit Jahren etablierten und ausgereiften, Grafiksystemen zu sein. Vor
allem ist die _Zeichenmaschine_ keine vollwertige Game Engine, die auf die
Ausführung komplexer Spiele spezialisiert ist. Für diese Zwecke gibt es genügend
Alternativen, von deren Nutzung gar nicht abgeraten werden soll.
## Aufbau der Zeichenmaschine
!!! info
In der Zeichenmaschine werden bewusst nur englischsprachige Bezeichner für
Klassen, Methoden und Variablen verwendet. Ausnahme sind einzelne Klassen,
die im Zusammnehang mit dem Namen der Bibliothek stehen, wie die
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 = 32; 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

@@ -191,6 +191,22 @@ public class Vector extends Point2D.Double {
return new Vector(x, y); return new Vector(x, y);
} }
public int getIntX() {
return (int)this.x;
}
public int getRoundedX() {
return (int)Math.round(this.x);
}
public int getIntY() {
return (int)this.y;
}
public int getRoundedY() {
return (int)Math.round(this.y);
}
/** /**
* Setzt die Komponenten dieses Vektors neu. * Setzt die Komponenten dieses Vektors neu.
* *

View File

@@ -2,6 +2,7 @@ package schule.ngb.zm;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
@@ -10,6 +11,7 @@ import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
@@ -26,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() {
@@ -174,12 +176,14 @@ public class Zeichenfenster extends JFrame {
// Das Icon des Fensters ändern // Das Icon des Fensters ändern
try { try {
if( Zeichenmaschine.MACOS ) { if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png"); InputStream iconStream = this.getClass().getResourceAsStream("icon_512.png");
if( iconUrl != null ) { if( iconStream != null ) {
Image icon = ImageIO.read(iconUrl); Image icon = ImageIO.read(iconStream);
// Dock Icon in macOS setzen // Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar(); Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon); taskbar.setIconImage(icon);
} else {
LOG.warn("Could not load dock-icon");
} }
} else { } else {
ArrayList<Image> icons = new ArrayList<>(4); ArrayList<Image> icons = new ArrayList<>(4);
@@ -190,7 +194,11 @@ public class Zeichenfenster extends JFrame {
} }
} }
this.setIconImages(icons); if( icons.isEmpty() ) {
LOG.warn("Could not load dock-icon");
} else {
this.setIconImages(icons);
}
} }
} catch( IllegalArgumentException | IOException e ) { } catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage()); LOG.warn("Could not load image icons: %s", e.getMessage());

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,23 +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();
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.
@@ -1177,6 +1186,9 @@ public class Zeichenmaschine extends Constants {
//saveMousePosition(evt); //saveMousePosition(evt);
mouseMoved(evt); mouseMoved(evt);
break; break;
case MouseEvent.MOUSE_WHEEL:
mouseWheelMoved(evt);
break;
} }
} }
@@ -1220,6 +1232,14 @@ public class Zeichenmaschine extends Constants {
// Intentionally left blank // Intentionally left blank
} }
public void mouseWheelMoved( MouseEvent e ) {
mouseMoved();
}
public void mouseWheelMoved() {
// Intentionally left blank
}
private void saveMousePosition( MouseEvent event ) { private void saveMousePosition( MouseEvent event ) {
if( mouseEvent != null && event.getComponent() == canvas ) { if( mouseEvent != null && event.getComponent() == canvas ) {
pmouseX = mouseX; pmouseX = mouseX;
@@ -1387,7 +1407,7 @@ public class Zeichenmaschine extends Constants {
if( Thread.interrupted() ) { if( Thread.interrupted() ) {
running = false; running = false;
terminateImediately = true; terminateImmediately = true;
break; break;
} }
} }
@@ -1443,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();

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}.
@@ -923,12 +928,43 @@ public class DrawingLayer extends Layer implements Strokeable, Fillable {
} }
} }
public void curveTo( double ctrlX, double ctrlY, double x, double y ) {
if( !pathStarted ) {
path.moveTo(x, y);
pathStarted = true;
} else {
path.quadTo(
ctrlX, ctrlY,
x, y
);
}
}
public void curveTo( double ctrlX1, double ctrlY1, double ctrlX2, double ctrlY2, double x, double y ) {
if( !pathStarted ) {
path.moveTo(x, y);
pathStarted = true;
} else {
path.curveTo(
ctrlX1, ctrlY1,
ctrlX2, ctrlY2,
x, y
);
}
}
/** /**
* Beendet eine zuvor {@link #beginShape() begonnene} Freihand-Form und * Beendet eine zuvor {@link #beginShape() begonnene} Freihand-Form und
* zeichent sie auf die Zeichenebene. * zeichent sie auf die Zeichenebene.
*/ */
public void endShape() { public void endShape() {
path.closePath(); endShape(CLOSED);
}
public void endShape( Options.PathType closingType ) {
if( closingType == Options.PathType.CLOSED ) {
path.closePath();
}
path.trimToSize(); path.trimToSize();
pathStarted = false; pathStarted = false;
@@ -949,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());
} }
/** /**
@@ -966,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);
} }
/** /**
@@ -974,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.
@@ -983,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());
} }
/** /**
@@ -992,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.
@@ -1002,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);
} }
/** /**
@@ -1015,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());
} }
/** /**
@@ -1046,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.
@@ -1055,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;
@@ -1064,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);
} }
/** /**
@@ -1072,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.
@@ -1082,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());
} }
/** /**
@@ -1092,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.
@@ -1103,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);
} }
/** /**
@@ -1112,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.
@@ -1121,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());
} }
/** /**
@@ -1140,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.
@@ -1150,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

@@ -235,8 +235,8 @@ public class Sound implements Audio {
} }
/** /**
* Lädt falls nötig den {@link Clip} für die * Lädt, falls nötig, den {@link Clip} für die
* {@link #audioSource Audioquelle} und startet die Wiedergabe. * {@link #audioSource Audioquelle}.
* *
* @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false} * @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false}
* sonst. * sonst.
@@ -264,10 +264,6 @@ public class Sound implements Audio {
} }
} else if( event.getType() == LineEvent.Type.STOP ) { } else if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped(); playbackStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
} }
} }
}); });
@@ -313,6 +309,10 @@ public class Sound implements Audio {
private void playbackStopped() { private void playbackStopped() {
playing = false; playing = false;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
if( disposeAfterPlay ) { if( disposeAfterPlay ) {
this.dispose(); this.dispose();
disposeAfterPlay = false; disposeAfterPlay = false;

View File

@@ -0,0 +1,77 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Color;
import schule.ngb.zm.Vector;
import java.awt.Graphics2D;
public class BasicParticle extends Particle {
protected Color color, startColor, finalColor;
public BasicParticle() {
super();
}
public BasicParticle( Color startColor ) {
this(startColor, null);
}
public BasicParticle( Color startColor, Color finalColor ) {
super();
this.color = startColor;
this.startColor = startColor;
this.finalColor = finalColor;
}
public Color getColor() {
return color;
}
public void setColor( Color pColor ) {
this.color = pColor;
}
public Color getStartColor() {
return startColor;
}
public void setStartColor( Color pStartColor ) {
this.startColor = pStartColor;
}
public Color getFinalColor() {
return finalColor;
}
public void setFinalColor( Color pFinalColor ) {
this.finalColor = pFinalColor;
}
@Override
public void spawn( int pLifetime, Vector pPosition, Vector pVelocity ) {
super.spawn(pLifetime, pPosition, pVelocity);
this.color = this.startColor;
}
@Override
public void update( double delta ) {
super.update(delta);
if( isActive() && startColor != null && finalColor != null ) {
double t = 1.0 - lifetime / maxLifetime;
this.color = Color.interpolate(startColor, finalColor, t);
}
}
@Override
public void draw( Graphics2D graphics ) {
if( isActive() && this.color != null ) {
graphics.setColor(this.color.getJavaColor());
graphics.fillOval(position.getIntX() - 3, position.getIntY() - 3, 6, 6);
}
}
}

View File

@@ -0,0 +1,43 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Color;
public class BasicParticleFactory implements ParticleFactory {
private final Color startColor;
private final Color finalColor;
private boolean fadeOut = true;
public BasicParticleFactory() {
this(null, null);
}
public BasicParticleFactory( Color startColor ) {
this(startColor, null);
}
public BasicParticleFactory( Color startColor, Color finalColor ) {
this.startColor = startColor;
this.finalColor = finalColor;
}
public void setFadeOut( boolean pFadeOut ) {
this.fadeOut = pFadeOut;
}
@Override
public Particle createParticle() {
Color finalClr = finalColor;
if( fadeOut ) {
if( finalColor != null ) {
finalClr = new Color(finalColor, 0);
} else if( startColor != null ) {
finalClr = new Color(startColor, 0);
}
}
return new BasicParticle(startColor, finalClr);
}
}

View File

@@ -0,0 +1,49 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.util.Log;
import java.util.Arrays;
import java.util.function.Supplier;
public class GenericParticleFactory<T extends Particle> implements ParticleFactory {
private final Class<T> type;
private final Supplier<T> supplier;
public GenericParticleFactory( Class<T> type, Object... params ) {
this.type = type;
// Create paramTypes array once
Class<?>[] paramTypes = new Class<?>[params.length];
for( int i = 0; i < params.length; i++ ) {
paramTypes[i] = params[i].getClass();
}
this.supplier = () -> {
T p = null;
try {
p = GenericParticleFactory.this.type.getDeclaredConstructor(paramTypes).newInstance(params);
} catch( Exception ex ) {
LOG.error( ex,
"Unable to create new Particle of type %s",
GenericParticleFactory.this.type.getCanonicalName()
);
}
return p;
};
}
public GenericParticleFactory( Supplier<T> supplier ) {
this.supplier = supplier;
this.type = (Class<T>)supplier.get().getClass();
}
@Override
public Particle createParticle() {
return this.supplier.get();
}
private static final Log LOG = Log.getLogger(GenericParticleFactory.class);
}

View File

@@ -0,0 +1,60 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.Vector;
public abstract class Particle extends PhysicsObject implements Updatable, Drawable {
protected double maxLifetime = 0, lifetime = 0;
public Particle() {
super();
}
public void spawn( int pLifetime, Vector pPosition, Vector pVelocity ) {
this.maxLifetime = pLifetime;
this.lifetime = pLifetime;
this.position = pPosition.copy();
this.velocity = pVelocity.copy();
this.acceleration = new Vector();
}
@Override
public boolean isActive() {
return lifetime > 0;
}
@Override
public boolean isVisible() {
return isActive();
}
public double getLifetime() {
return lifetime;
}
public void setLifetime( double pLifetime ) {
this.lifetime = pLifetime;
}
public double getMaxLifetime() {
return maxLifetime;
}
public void setMaxLifetime( double pMaxLifetime ) {
this.maxLifetime = pMaxLifetime;
}
@Override
public void update( double delta ) {
super.update(delta);
// lifetime -= delta;
lifetime -= 1;
// TODO: (ngb) calculate delta based on lifetime?
}
}

View File

@@ -0,0 +1,174 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.Vector;
import java.awt.Graphics2D;
public class ParticleEmitter implements Updatable, Drawable {
protected ParticleFactory particleFactory;
private int particlesPerFrame;
private int particleLifetime = 180;
private Particle[] particles;
private boolean active = false;
private Particle nextParticle;
public Vector position;
public Vector direction = new Vector();
public double strength = 100.0;
public int angle = 0;
public double randomness = 0.0;
// private Vortex vortex = null;
public ParticleEmitter( double pX, double pY, int pParticleLifetime, int pParticlesPerFrame, ParticleFactory pFactory ) {
this.position = new Vector(pX, pY);
this.particlesPerFrame = pParticlesPerFrame;
this.particleLifetime = pParticleLifetime;
this.particleFactory = pFactory;
// Create particle pool
this.particles = new Particle[particlesPerFrame * pParticleLifetime];
this.direction = Vector.random(8, 16).normalize();
// vortex = new Vortex(position.copy().add(-10, -10), -.2, 8);
}
@Override
public boolean isActive() {
return active;
}
@Override
public boolean isVisible() {
return active;
}
public void start() {
this.direction.normalize();
// Partikel initialisieren
for( int i = 0; i < particles.length; i++ ) {
particles[i] = particleFactory.createParticle();
}
active = true;
}
public void stop() {
for( int i = 0; i < particles.length; i++ ) {
particles[i].setLifetime(0);
particles[i] = null;
}
active = false;
}
private Particle getNextParticle() {
// TODO: improve by caching next particle
for( Particle p : particles ) {
if( p != null && !p.isActive() ) {
return p;
}
}
return null;
}
public void emitParticle() {
int ppf = particlesPerFrame;
Particle nextParticle = getNextParticle();
while( ppf > 0 && nextParticle != null ) {
int lifetime = (int) random(particleLifetime);
double rotation = (angle / 2.0) - (int) (Math.random() * angle);
Vector velocity = direction.copy().scale(strength).rotate(rotation);
velocity.scale(random());
nextParticle.spawn(lifetime, this.position, velocity);
nextParticle = getNextParticle();
ppf -= 1;
}
}
@Override
public void update( double delta ) {
emitParticle();
boolean _active = false;
for( Particle particle : particles ) {
if( particle != null ) {
if( particle.isActive() ) {
// if( vortex != null ) {
// vortex.attract(particle);
// }
particle.update(delta);
_active = true;
}
}
}
this.active = _active;
}
private double random() {
return 1.0 - (Math.random() * randomness);
}
private double random( double pZahl ) {
return pZahl * random();
}
@Override
public void draw( Graphics2D graphics ) {
java.awt.Color current = graphics.getColor();
for( Particle particle : particles ) {
if( particle != null && particle.isVisible() ) {
particle.draw(graphics);
}
}
// if( vortex != null ) {
// graphics.setColor(java.awt.Color.BLACK);
// double vscale = (4 * vortex.scale);
// graphics.fillOval((int) (vortex.position.x - vscale * .5), (int) (vortex.position.y - vscale * .5), (int) vscale, (int) vscale);
// }
graphics.setColor(current);
}
class Vortex {
Vector position;
double speed = 1.0, scale = 1.0;
public Vortex( Vector pPosition, double pSpeed, double pScale ) {
this.position = pPosition.copy();
this.scale = pScale;
this.speed = pSpeed;
}
public void attract( Particle pPartikel ) {
Vector diff = Vector.sub(pPartikel.position, this.position);
double dx = -diff.y * this.speed;
double dy = diff.x * this.speed;
double f = 1.0 / (1.0 + (dx * dx + dy * dy) / scale);
pPartikel.position.x += (diff.x - pPartikel.velocity.x) * f;
pPartikel.position.y += (diff.y - pPartikel.velocity.y) * f;
}
}
}

View File

@@ -0,0 +1,7 @@
package schule.ngb.zm.particles;
public interface ParticleFactory {
Particle createParticle();
}

View File

@@ -0,0 +1,69 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.Vector;
public abstract class PhysicsObject implements Updatable {
protected Vector position, velocity, acceleration;
protected double mass = 1.0;
public PhysicsObject() {
position = new Vector();
velocity = new Vector();
acceleration = new Vector();
}
public PhysicsObject( Vector pPosition ) {
position = pPosition.copy();
velocity = new Vector();
acceleration = new Vector();
}
public Vector getAcceleration() {
return acceleration;
}
public void setAcceleration( Vector pAcceleration ) {
this.acceleration = pAcceleration;
}
public double getMass() {
return mass;
}
public void setMass( double pMass ) {
this.mass = pMass;
}
public Vector getPosition() {
return position;
}
public void setPosition( Vector pPosition ) {
this.position = pPosition;
}
public Vector getVelocity() {
return velocity;
}
public void setVelocity( Vector pVelocity ) {
this.velocity = pVelocity;
}
public void accelerate( Vector pAcceleration ) {
acceleration.add(Vector.div(pAcceleration, mass));
}
@Override
public void update( double delta ) {
velocity.add(acceleration);
position.add(Vector.scale(velocity, delta));
acceleration.scale(0.0);
}
}

View File

@@ -0,0 +1,35 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.Color;
import java.awt.Graphics2D;
public class StarParticle extends BasicParticle {
public StarParticle() {
super();
this.startColor = Color.PURE_GREEN;
}
public StarParticle( Color startColor ) {
this(startColor, null);
}
public StarParticle( Color startColor, Color finalColor ) {
super();
this.color = startColor;
this.startColor = startColor;
this.finalColor = finalColor;
}
@Override
public void draw( Graphics2D graphics ) {
if( isActive() && this.color != null ) {
graphics.setColor(this.color.getJavaColor());
graphics.drawLine((int) position.x - 3, (int) position.y - 3, (int) position.x + 3, (int) position.y + 3);
graphics.drawLine((int) position.x + 3, (int) position.y - 3, (int) position.x - 3, (int) position.y + 3);
}
}
}

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

@@ -0,0 +1,204 @@
package schule.ngb.zm.util;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Ein Cache ist ein {@link Map} Implementation, die Inhaltsobjekte in einer
* {@link Reference} speichert und als Zwischenspeicher für Objekte dienen kann,
* deren Erstellung aufwendig ist.
* <p>
* Für einen Cache ist nicht garantiert, dass ein eingefügtes Objekt beim
* nächsten Aufruf noch vorhanden ist, da die Referenz inzwischen vom Garbage
* Collector gelöscht worden sein kann.
* <p>
* Als interne Map wird einen {@link ConcurrentHashMap} verwendet.
* <p>
* Ein passender Cache wird mittels der Fabrikmethoden {@link #newSoftCache()}
* und {@link #newWeakCache()} erstellt.
* <pre><code>
* Cache&lt;String, Image&gt; imageCache = Cache.newSoftCache();
* </code></pre>
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
*/
public final class Cache<K, V> implements Map<K, V> {
/**
* Erstellt einen Cache mit {@link SoftReference} Referenzen.
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
* @return Ein Cache.
*/
public static <K, V> Cache<K, V> newSoftCache() {
return new Cache<>(SoftReference::new);
}
/**
* Erstellt einen Cache mit {@link WeakReference} Referenzen.
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
* @return Ein Cache.
*/
public static <K, V> Cache<K, V> newWeakCache() {
return new Cache<>(WeakReference::new);
}
private final Map<K, Reference<V>> cache = new ConcurrentHashMap<>();
private final Reference<V> NOCACHE;
private final Function<V, Reference<V>> refSupplier;
private Cache( Function<V, Reference<V>> refSupplier ) {
this.refSupplier = refSupplier;
NOCACHE = refSupplier.apply(null);
}
@Override
public int size() {
return cache.size();
}
@Override
public boolean isEmpty() {
return cache.isEmpty();
}
@Override
public boolean containsKey( Object key ) {
return cache.containsKey(key) && cache.get(key).get() != null;
}
@Override
public boolean containsValue( Object value ) {
return cache.values().stream()
.anyMatch(( ref ) -> ref.get() != null && ref.get() == value);
}
@Override
public V get( Object key ) {
if( cache.containsKey(key) ) {
return cache.get(key).get();
}
return null;
}
/**
* Deaktiviert das Caching für den angegebenen Schlüssel.
* <p>
* Folgende Aufrufe von {@link #put(Object, Object)} mit demselben Schlüssel
* haben keinen Effekt. Um das Caching wieder zu aktivieren, muss
* {@link #remove(Object)} mit dem Schlüssel aufgerufen werden,
*
* @param key Der Schlüssel.
*/
public void disableCache( K key ) {
cache.put(key, NOCACHE);
}
/**
* Prüft, ob der für den angegebenen Schlüssel zuvor
* {@link #disableCache(Object)} aufgerufen wurde.
*
* @param key Der Schlüssel.
* @return {@code true}, wenn der Schlüssel nicht gespeichert wird.
*/
public boolean isCachingDisabled( K key ) {
return cache.get(key) == NOCACHE;
}
@Override
public V put( K key, V value ) {
if( !isCachingDisabled(key) ) {
V prev = remove(key);
cache.put(key, refSupplier.apply(value));
return prev;
} else {
return null;
}
}
@Override
public V remove( Object key ) {
Reference<V> ref = cache.get(key);
cache.remove(key);
V prev = null;
if( ref != null ) {
prev = ref.get();
ref.clear();
}
return prev;
}
@Override
public void putAll( Map<? extends K, ? extends V> m ) {
for( Entry<? extends K, ? extends V> e : m.entrySet() ) {
put(e.getKey(), e.getValue());
}
}
@Override
public void clear() {
cache.clear();
}
@Override
public Set<K> keySet() {
return cache.keySet();
}
@Override
public Collection<V> values() {
return cache.values().stream()
.filter(( ref ) -> ref.get() != null)
.map(( ref ) -> ref.get())
.collect(Collectors.toList());
}
@Override
public Set<Entry<K, V>> entrySet() {
return cache.entrySet().stream()
.filter(( e ) -> e.getValue() != null && e.getValue().get() != null)
.map(( e ) -> new SoftCacheEntryView(e.getKey()))
.collect(Collectors.toSet());
}
private final class SoftCacheEntryView implements Map.Entry<K, V> {
private K key;
public SoftCacheEntryView( K key ) {
this.key = key;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return Cache.this.get(key);
}
@Override
public V setValue( V value ) {
return Cache.this.put(key, value);
}
}
}

View File

@@ -5,7 +5,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Eine Helferklasse, um Dinge zu zählen. * Eine Hilfsklasse, um Dinge zu zählen.
* <p> * <p>
* Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden, * Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden,
* die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder * die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder

View File

@@ -28,7 +28,7 @@ import static java.lang.Math.log;
import static schule.ngb.zm.Constants.random; import static schule.ngb.zm.Constants.random;
/** /**
* Eine Helferklasse, um zufällige Beispieldaten zu erzeugen. * Eine Hilfsklasse, um zufällige Beispieldaten zu erzeugen.
* <p> * <p>
* Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen. * Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen.
* Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder. * Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder.

View File

@@ -1,141 +0,0 @@
package schule.ngb.zm.util;
import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class SoftCache<K, V> implements Map<K, V> {
private final Map<K, SoftReference<V>> cache = new ConcurrentHashMap<>();
private final SoftReference<V> NOCACHE = new SoftReference<>(null);
public SoftCache() {
}
@Override
public int size() {
return cache.size();
}
@Override
public boolean isEmpty() {
return cache.isEmpty();
}
@Override
public boolean containsKey( Object key ) {
return cache.containsKey(key) && cache.get(key).get() != null;
}
@Override
public boolean containsValue( Object value ) {
return false;
}
private boolean containsRef( Object key ) {
return cache.containsKey(key);
}
@Override
public V get( Object key ) {
if( cache.containsKey(key) ) {
return cache.get(key).get();
}
return null;
}
public void nocache( K key ) {
cache.put(key, NOCACHE);
}
public boolean isNocache( K key ) {
return cache.get(key) == NOCACHE;
}
@Override
public V put( K key, V value ) {
if( !isNocache(key) ) {
V prev = remove(key);
cache.put(key, new SoftReference<>(value));
return prev;
} else {
return null;
}
}
@Override
public V remove( Object key ) {
SoftReference<V> ref = cache.get(key);
cache.remove(key);
V prev = null;
if( ref != null ) {
prev = ref.get();
ref.clear();
}
return prev;
}
@Override
public void putAll( Map<? extends K, ? extends V> m ) {
for( Entry<? extends K, ? extends V> e : m.entrySet() ) {
put(e.getKey(), e.getValue());
}
}
@Override
public void clear() {
cache.clear();
}
@Override
public Set<K> keySet() {
return cache.keySet();
}
@Override
public Collection<V> values() {
return cache.values().stream()
.filter(( ref ) -> ref.get() != null)
.map(( ref ) -> ref.get())
.collect(Collectors.toList());
}
@Override
public Set<Entry<K, V>> entrySet() {
return cache.entrySet().stream()
.filter(( e ) -> e.getValue() != null && e.getValue().get() != null)
.map(( e ) -> new SoftCacheEntryView(e.getKey()))
.collect(Collectors.toSet());
}
private final class SoftCacheEntryView implements Map.Entry<K, V> {
private K key;
public SoftCacheEntryView( K key ) {
this.key = key;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return SoftCache.this.get(key);
}
@Override
public V setValue( V value ) {
return SoftCache.this.put(key, value);
}
}
}

View File

@@ -3,7 +3,7 @@ package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Helferklasse zur Zeitmessung im Nanosekundenbereich. * Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p> * <p>
* Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus * Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus
* gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn * gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn

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,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen, die das Listener-Entwurfsmuster * Dieses Paket enthält Hilfsklassen, die das Listener-Entwurfsmuster
* umsetzen. * umsetzen.
*/ */
package schule.ngb.zm.util.events; package schule.ngb.zm.util.events;

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,9 +13,10 @@ 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;
/** /**
* Helferklasse, um Textdateien in verschiedenen Formaten einzulesen. * Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
*/ */
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public final class FileLoader { public final class FileLoader {
@@ -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

@@ -1,5 +1,6 @@
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
@@ -9,11 +10,12 @@ 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;
/** /**
* Eine Helferklasse mit Klassenmethoden, um Schriftarten zu laden. * Eine Hilfsklasse mit Klassenmethoden, um Schriftarten zu laden.
* <p> * <p>
* Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten, * Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten,
* die aus Dateien geladen wurden, werden in einem internen Cache gespeichert * die aus Dateien geladen wurden, werden in einem internen Cache gespeichert
@@ -22,7 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public class FontLoader { public class FontLoader {
private static final Map<String, Font> fontCache = new ConcurrentHashMap<>(); private static final Cache<String, Font> fontCache = Cache.newSoftCache();
/** /**
* Lädt eine Schrift aus einer Datei. * Lädt eine Schrift aus einer Datei.
@@ -61,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);
@@ -90,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

@@ -1,6 +1,7 @@
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.Cache;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -15,11 +16,10 @@ 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.util.Map; import java.net.MalformedURLException;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Eine Helferklasse mit Klassenmethoden, um Bilder zu laden. * Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
* <p> * <p>
* Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die * Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die
* Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche, * Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche,
@@ -29,9 +29,7 @@ public final class ImageLoader {
public static boolean caching = true; public static boolean caching = true;
private static final Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>(); private static final Cache<String, BufferedImage> imageCache = Cache.newSoftCache();
private static final SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
/** /**
* Lädt ein Bild von der angegebenen Quelle {@code source}. * Lädt ein Bild von der angegebenen Quelle {@code source}.
@@ -70,11 +68,11 @@ 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 && isCached(source) ) { if( caching && imageCache.containsKey(source) ) {
return getCache(source); return imageCache.get(source);
} }
BufferedImage img = null; BufferedImage img = null;
@@ -85,10 +83,12 @@ public final class ImageLoader {
img = ImageIO.read(in); img = ImageIO.read(in);
if( caching && img != null ) { if( caching && img != null ) {
putCache(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;
} }
@@ -113,7 +113,7 @@ public final class ImageLoader {
public static boolean preloadImage( String name, String source ) { public static boolean preloadImage( String name, String source ) {
BufferedImage img = loadImage(source, true); BufferedImage img = loadImage(source, true);
if( img != null ) { if( img != null ) {
putCache(name, img); imageCache.put(name, img);
return true; return true;
} }
return false; return false;
@@ -134,7 +134,7 @@ public final class ImageLoader {
* @param img ZU speicherndes Bild. * @param img ZU speicherndes Bild.
*/ */
public static void preloadImage( String name, BufferedImage img ) { public static void preloadImage( String name, BufferedImage img ) {
putCache(name, img); imageCache.put(name, img);
} }
/** /**
@@ -145,8 +145,7 @@ public final class ImageLoader {
* {@code false}. * {@code false}.
*/ */
public static boolean isCached( String name ) { public static boolean isCached( String name ) {
SoftReference<BufferedImage> imgRef = imageCache.get(name); return imageCache.containsKey(name);
return imgRef != null && imgRef != NOCACHE && imgRef.get() != null;
} }
/** /**
@@ -159,30 +158,6 @@ public final class ImageLoader {
imageCache.remove(name); imageCache.remove(name);
} }
/**
* Speichert ein Bild als {@link SoftReference} im Cache.
*
* @param name Name des Bildes im Zwischenspeicher.
* @param img Das zu speichernde Bild.
*/
private static void putCache( final String name, final BufferedImage img ) {
imageCache.put(name, new SoftReference<>(img));
}
/**
* Holt ein Bild aus dem Cache.
* <p>
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
* werden kann.
*
* @param name
* @return
*/
private static BufferedImage getCache( final String name ) {
return imageCache.get(name).get();
}
/** /**
* Deaktiviert den Cache für die angegebene Quelle. * Deaktiviert den Cache für die angegebene Quelle.
* <p> * <p>
@@ -192,14 +167,14 @@ public final class ImageLoader {
* *
* @param name Die Quelle des Bildes. * @param name Die Quelle des Bildes.
*/ */
public static void preventCache( final String name ) { public static void disableCache( final String name ) {
imageCache.put(name, NOCACHE); imageCache.disableCache(name);
} }
/** /**
* Leer den Cache und löschte alle bisher gespeicherten Bilder. * Leer den Cache und löschte alle bisher gespeicherten Bilder.
* <p> * <p>
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden * Auch vorher mit {@link #disableCache(String)} verhinderte Caches werden
* gelöscht und müssen neu gesetzt werden. * gelöscht und müssen neu gesetzt werden.
*/ */
public static void clearCache() { public static void clearCache() {
@@ -346,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

@@ -13,7 +13,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
/** /**
* Helferklasse, um {@link InputStream}s für Ressourcen zu erhalten. * Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ResourceStreamProvider { public class ResourceStreamProvider {
@@ -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

@@ -1,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen, um Ressourcen aus verschiedenen Quellen * Dieses Paket enthält Hilfsklassen, um Ressourcen aus verschiedenen Quellen
* zu laden. * zu laden.
*/ */
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;

View File

@@ -1,4 +1,4 @@
/** /**
* Dieses Paket enthält Helferklassen für verschiedene Einsatzzwecke. * Dieses Paket enthält Hilfsklassen für verschiedene Einsatzzwecke.
*/ */
package schule.ngb.zm.util; package schule.ngb.zm.util;

View File

@@ -1,5 +1,5 @@
/** /**
* Dieses Paket enthält Helferklassen zur Verarbeitung paralleler Aufgaben * Dieses Paket enthält Hilfsklassen zur Ausführung paralleler Aufgaben
* innerhalb der Zeichenmaschine. * innerhalb der Zeichenmaschine.
*/ */
package schule.ngb.zm.util.tasks; package schule.ngb.zm.util.tasks;

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

@@ -0,0 +1,158 @@
package schule.ngb.zm.particles;
import schule.ngb.zm.*;
import schule.ngb.zm.layers.DrawableLayer;
import schule.ngb.zm.layers.DrawingLayer;
import java.awt.Graphics2D;
public class ParticleExample extends Testmaschine {
public static void main( String[] args ) {
new ParticleExample();
}
public ParticleExample() {
super();
}
ParticleEmitter pe1, pe2, pe3;
Rocket r;
public void setup() {
getLayer(DrawingLayer.class).hide();
background.setColor(0);
drawing.noStroke();
drawing.setFillColor(WHITE);
for( int i = 0; i < 1000; i++ ) {
drawing.point(random(0, canvasWidth), random(0, canvasHeight));
}
pe1 = new ParticleEmitter(
100, 100, 50, 5,
// new BasicParticleFactory(PINK, BLUE)
new GenericParticleFactory<Particle>(() -> {
return new Particle() {
@Override
public void draw( Graphics2D graphics ) {
graphics.setColor(Color.MAGENTA.getJavaColor());
graphics.rotate(Constants.radians(45), (int) position.x, (int) position.y);
graphics.drawRect((int) position.x - 3, (int) position.y - 3, 6, 6);
graphics.rotate(Constants.radians(-45), (int) position.x, (int) position.y);
}
};
})
);
pe1.randomness = .2;
pe1.angle = 45;
pe1.strength = 200;
pe2 = new ParticleEmitter(
300, 300, 50, 10,
new GenericParticleFactory(() -> new StarParticle(RED, new Color(BLUE, 55)))
//new GenericParticleFactory(StarParticle.class, RED, new Color(BLUE, 55))
);
pe2.direction = NORTH.asVector().scale(100);
pe2.randomness = .8;
pe2.angle = 90;
pe2.strength = 200;
pe3 = new ParticleEmitter(
100, 400, 20, 8,
new BasicParticleFactory(YELLOW, RED)
);
pe3.direction = SOUTH.asVector();
pe3.randomness = .33;
pe3.angle = 30;
DrawableLayer drawables = new DrawableLayer();
addLayer(drawables);
drawables.add(pe1, pe2, pe3);
pe1.start();
pe2.start();
pe3.start();
r = new Rocket(200, 400);
drawables.add(r);
r.start();
}
@Override
public void update( double delta ) {
pe1.update(delta);
pe2.update(delta);
pe3.update(delta);
pe3.position.add(NORTH.asVector().scale(Constants.map(runtime, 0, 1000, 0, 10) * delta));
if( r.isActive() ) {
r.update(delta);
}
}
class Rocket extends PhysicsObject implements Drawable {
ParticleEmitter trail;
private boolean starting = false;
private double acc = 4;
public Rocket( double x, double y ) {
super(new Vector(x, y));
trail = new ParticleEmitter(
x, y, 30, 6,
new BasicParticleFactory(YELLOW, RED)
);
trail.direction = SOUTH.asVector();
trail.randomness = .33;
trail.angle = 30;
trail.position = this.position;
}
public void start() {
starting = true;
trail.start();
}
@Override
public boolean isActive() {
return starting;
}
@Override
public boolean isVisible() {
return true;
}
@Override
public void update( double delta ) {
super.update(delta);
if( this.acceleration.lengthSq() < acc * acc ) {
this.accelerate(NORTHWEST.asVector().scale(acc));
}
trail.update(delta);
}
@Override
public void draw( Graphics2D graphics ) {
graphics.rotate(-Constants.radians(velocity.angle() + 180), position.getIntX(), position.getIntY());
trail.draw(graphics);
graphics.setColor(WHITE.getJavaColor());
graphics.fillRect(position.getIntX() - 6, position.getIntY() - 32, 12, 32);
graphics.fillPolygon(
new int[]{position.getIntX() - 6, position.getIntX(), position.getIntX() + 6},
new int[]{position.getIntY() - 32, position.getIntY() - 40, position.getIntY() - 32},
3
);
graphics.rotate(Constants.radians(velocity.angle() + 180), position.getIntX(), position.getIntY());
}
}
}

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();
}
}