97 Commits

Author SHA1 Message Date
c7e1eb11ed maxInt Test eingefügt 2022-12-08 21:16:35 +01:00
9fc58b05b6 Dokumentation 2022-12-08 21:16:01 +01:00
8de3c41b9b Dokumentation 2022-12-08 16:09:13 +01:00
15e47ceaa8 Reichenfolge Parameter in addValue an PieChart angepasst 2022-12-08 16:08:59 +01:00
9f28786ab6 AudiListener interface angepasst
Die Listener Methoden haben nun sprechendere Namen.
Sound und Mixer akzeptieren nun auch AudioListener.
2022-12-08 16:08:35 +01:00
18b5c50016 Dokumentation 2022-12-08 12:47:59 +01:00
03945e029a Schnellstart Tutorial fertig 2022-12-08 10:16:22 +01:00
90e043e5f8 Gradle Tasks für source und javadoc jars 2022-12-08 10:16:07 +01:00
559459aef6 Dokumentation 2022-12-08 10:15:35 +01:00
8d0bd2bc99 Race condition beim Beenden behoben
Das Beenden der Zeichenmaschine und vor allem das Schließen des Zeichenfesnters wird im Swing Thread ausgeführt. Es konnte passieren, dass der Zeichenthread noch einen draw-Aufruf verarbeitete, während die Zeichenleinwand schon disposed wurde. Dann konnte eine NullPointerException auftreten.

Der Zeichenthread hat nun 500 ms Zeit, von alleine zu beenden, bevor die ZM vollständig beendet wird.
2022-12-08 10:14:00 +01:00
b76d533739 Erste Seiten mit mkdocs 2022-11-29 10:55:54 +01:00
807a13b725 buildfile cleanup 2022-11-29 10:55:22 +01:00
d4c5dbbb53 Gradle 7.4 -> 7.5 2022-11-29 10:55:10 +01:00
080db1f431 Einige Bugfixes und Verbesserungen und ganz viel Doku 2022-11-29 10:12:14 +01:00
47827683e8 Farbnamen werden nun in Colo-Objekte geparsed
`Color.parseString(String)` liest nun eine Datei mit Farbnamen und Hexcode Kombinationen ein. Wird der String in der Liste der Farbnamen gefunden, wird aus dem entsprechenden Hexcode ein `Color`-Objekt erzeugt.
2022-11-29 10:11:43 +01:00
ec30afd441 Fixed icon loading on windows 2022-11-28 09:26:45 +01:00
d3bdbdbffb Konstanten für Schriften und kleinere fixes 2022-11-28 09:11:37 +01:00
8cc7167d7e Formatierung und Doku 2022-11-28 09:11:13 +01:00
9e4271c304 Laden alternativer Schriften möglich 2022-11-28 09:10:59 +01:00
4f13f5177d Mausposition merken wenn pausiert 2022-11-28 09:10:42 +01:00
6321a7d421 render Method added 2022-11-28 09:10:31 +01:00
135af10729 Python files added 2022-11-28 09:09:58 +01:00
912f68c58f Javadoc 2022-08-01 20:50:23 +02:00
7f1d9012e9 Unter macOS auf Cmd+Q reagieren 2022-08-01 20:50:16 +02:00
60ed045986 Javadoc 2022-08-01 20:49:49 +02:00
dc16608333 Javadoc 2022-08-01 14:48:17 +02:00
7b6398fe52 Einfache Faker-Klasse, um Zufallsdaten zu erzeugen 2022-08-01 14:42:20 +02:00
8f98ddc56d Changelog 2022-08-01 10:08:15 +02:00
782ce33540 GradientPaint durch MultipleGradientPaint ersetzt 2022-08-01 10:08:11 +02:00
537527e525 Versuch den Interrupt von dispose() zu verhindern 2022-08-01 10:07:53 +02:00
8e93866b5e Interfaces verschoben 2022-07-31 10:03:28 +02:00
fcb536ff96 copyFrom angepasst 2022-07-31 10:02:11 +02:00
70c607f2e8 java.io -> java.nio 2022-07-31 10:00:22 +02:00
6126ed3c15 Vereinheitlichung der APIs für Füllungen und Konturen 2022-07-31 09:59:36 +02:00
b0353c53a0 Refactorings 2022-07-28 12:25:56 +02:00
c93a203ab9 DrawingLayer delegiert nun zu einer Shape
Macht Weniger doppelte Implementierungen nötig
2022-07-28 12:25:35 +02:00
f1d32685b4 KeyListener wieder zur Canvas bewegt 2022-07-28 12:24:48 +02:00
91842b511f Refactorings und Javadoc 2022-07-28 12:24:30 +02:00
52b480b46b Refactorings 2022-07-27 20:37:13 +02:00
4d2ade899d Refactorings und Javadoc 2022-07-27 20:37:01 +02:00
dcdca893b7 Refactorings und Javadoc 2022-07-27 20:36:34 +02:00
ebf0135486 Versionsnummer erhöht 2022-07-27 13:57:06 +02:00
fea1083926 Javadoc 2022-07-27 13:56:58 +02:00
250d9d17d3 Neuer Zustand QUITING 2022-07-27 13:56:09 +02:00
2a71243fc6 SuppressWarnings eingefügt 2022-07-27 13:55:28 +02:00
03d37222bf Neue choice() Methoden 2022-07-27 13:55:11 +02:00
687d7d35b7 Verbesserter Vollbildmodus und Trennung GUI / Controller 2022-07-27 13:54:55 +02:00
e2e6f8c291 Bug: Synchronized Methoden verschoben 2022-07-26 18:15:23 +02:00
916a581768 Refactorings 2022-07-26 18:14:59 +02:00
5bb2f75193 Bug: getShapes in ShapeGroup war immer leer 2022-07-26 18:14:50 +02:00
f0e4cd6c80 Refactoring des Beendens der ZM 2022-07-26 18:14:23 +02:00
a228b21c84 Verantwortlichkeiten für Layout und Aufgaben klarer getrennt 2022-07-26 08:59:30 +02:00
68c88ec9ca Merge branch 'main' into zeichenfenster
# Conflicts:
#	src/main/java/schule/ngb/zm/media/Sound.java
2022-07-25 19:07:51 +02:00
0d1dd771dd Logger eingefügt 2022-07-25 19:06:01 +02:00
e995bfc4fe Bug: Spielemaschine blockt nicht mehr nebenläufige Threads 2022-07-25 19:05:54 +02:00
97ff03990a Shape caching entfernt
In Tests konnten keine Geschwindigkeitsvorteile festgestellt werden.
2022-07-25 19:05:28 +02:00
bd2364a8df Laden von Schriftarten mit eigenem Namen möglich 2022-07-25 19:05:04 +02:00
617b915874 Refactorings zur Nebenläufigkeit 2022-07-25 17:45:39 +02:00
7772793e8d Kommentar 2022-07-25 17:44:31 +02:00
bd8c0e37a7 Audio-Methoden synchronisiert 2022-07-25 17:44:22 +02:00
ecbe2b4f6b interpolate zu animate umbenannt
Animation erbt zur Vereinfachung nun auch von Constants und dort gibt es schon eine interpolate Methode.
2022-07-25 17:42:06 +02:00
20fe700756 Rechtschreibung und standard Log-Format 2022-07-25 17:41:18 +02:00
0100a3f574 Methode um mehrere Animationen im ShapesLayer zu starten 2022-07-25 17:41:01 +02:00
aceb79c44f Animate Methode zu play umbenannt 2022-07-25 17:40:42 +02:00
a4e29ccdba Loader KLassen in io Paket verschoben 2022-07-25 17:38:53 +02:00
55014c8eec Klasse Zeichenfenster ausgelagert 2022-07-25 17:35:46 +02:00
4f958cd57c ImageLoaders in io Paket verschoben 2022-07-21 22:01:54 +02:00
04506f6e9c JFrame in eine eigene Klasse ausgelagert 2022-07-21 22:01:38 +02:00
5a27e18634 Javadoc und kleine Refactorings 2022-07-21 21:02:50 +02:00
8b23c658e8 Animator Interface entfernt 2022-07-21 21:02:30 +02:00
1ca13c977a Javadoc 2022-07-21 21:02:10 +02:00
78c93666d0 Javadoc 2022-07-21 21:01:46 +02:00
917eb805c6 Bug: Threadsafety 2022-07-21 21:01:33 +02:00
fddd8d621b flush() nach jeder Log-Nachricht
Der Logger sendet nun nach jedem Log die Nachricht zum OutputStream.
2022-07-21 21:00:55 +02:00
4bf0068051 Icons werden nun in allen Größen geladen
Alle vorhandenen Icons werden geladen und mit Jframe.setIconImages() dem Fenster hinzugefügt. Unter macOS wird nur die Größe 512 geladen und als Dock-Icon gesetzt.
2022-07-21 20:59:28 +02:00
371a962432 Syncronisation des Zeichenthreads mit update/draw über eigenen Zustand
delay() setzt den Zustand auf DELAYED und der Zeichenthread läuft weiter, wenn der update/draw Thread in diesen Zustand wechselt (also delay() aufgerufen wurde). Es wird nicht mehr Thread.getState() geprüft, dies zu unzuverlässi gwar.
2022-07-21 10:54:08 +02:00
99848e47f8 colt abhängigkeit nur für’s kompilieren 2022-07-21 10:52:47 +02:00
f75aaf4b7e Predict-Methode für eine Eingabe 2022-07-21 10:52:19 +02:00
e5c6fa634a Anpassung der Package-Struktur 2022-07-20 17:15:29 +02:00
ccc83414c7 Merge branch 'optional-ml' 2022-07-20 17:09:24 +02:00
16477463d4 java doc und refactorings 2022-07-20 17:09:09 +02:00
d3997561fc Streams durch Schleifen ersetzt
Der Overhead durch die parallelen Streams war zu hoch. Jedenfalls bei den relativ kleinen Matrizen im Test. Bei größeren Matrizen könnte die Parallelität einen Vorteil bringen. Ggf. sollte dies getesett werden und abhängig von der Größe die bestte Methode gewählt werden.
2022-07-19 22:53:46 +02:00
b6b4ffe6a5 Weitere Tests eingefügt und verbessert 2022-07-19 22:52:23 +02:00
bf261b5e9b Colt als optionale Abhängigkeit
DAs Anlernen des NN geht um den Faktor 20 schneller, wenn Colt benutzt wird.
2022-07-19 20:05:37 +02:00
b79f26f51e Matric interface umbenannt 2022-07-19 09:14:00 +02:00
538a8215e6 Userinput wird nach Stopp der ZM weiterverarbeitet 2022-07-19 08:56:00 +02:00
cbda5c3077 Bug: Linearer Farbverlauf wurde nicht korrekt berechnet 2022-07-19 08:55:06 +02:00
2caa528a5e Listeniterationen Threadsafe gemacht 2022-07-18 22:48:28 +02:00
bb50abb7bd Javadoc 2022-07-18 22:48:08 +02:00
38d5f22fb6 Bug: UpdateThreadExecutor blockt nun korrekt den Zeichenthread 2022-07-18 22:47:37 +02:00
d34c60505e Bug: mousePressed wurde nicht ausgelöst 2022-07-18 22:46:48 +02:00
4c8e5c8939 USing Colt library as optional dependency 2022-07-18 11:06:08 +02:00
9a9a714050 Javadoc 2022-07-17 16:38:42 +02:00
f0b064a3d5 Changelog 2022-07-17 15:57:34 +02:00
c922357ab7 Bug behoben: Flackern bei Farbverläufen 2022-07-17 15:57:24 +02:00
17c31a1a03 Bug behoben: delay() funktioniert nun auch nach Stopp der ZM 2022-07-17 15:57:02 +02:00
6551bb75c9 Farbverläufe für Formen und neue Konstantennamen 2022-07-17 15:45:05 +02:00
138 changed files with 15013 additions and 2686 deletions

8
.gitignore vendored
View File

@@ -48,3 +48,11 @@ gradle-app.setting
# Cache of project # Cache of project
.gradletasknamecache .gradletasknamecache
# Python mkdocs
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

View File

@@ -6,13 +6,24 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased] ## [Unreleased]
### Changed
- `FilledShape` und `StrokedShape` durch `Fillable` und `Strokeable` Interfaces ersetzt.
- `Shape` erweitert nun `BasisDrawable` als abstrakte Grundlage.
- `io` Klassen nutzen nun mehr der `java.nio` Funktionen.
- Package-Struktur angepasst.
## Version 0.0.23
### Added ### Added
- System für EventListener erstellt - System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle. - `AudioListener` und `AnimationListener` als erste Anwendungsfälle.
- Pakete für Animationen und Maschinelles-Lernen hinzugefügt - Pakete für Animationen und Maschinelles-Lernen.
- Farbverläufe als Füllung.
### Changed ### Changed
- `update(double)` und `draw()` werden nun in einem eigenen Thread aufgerufen. - `update(double)` und `draw()` werden nun in einem eigenen Thread aufgerufen.
- Die Standardwerte in `Constants` wurden mit dem Prefix `DEFAULT_` benannt (vorher `STD_`).
- Die Standardwerte sind nun nicht mehr `final` und können vom Nutzer manuell gesetzt werden.
## Version 0.0.22 ## Version 0.0.22
@@ -24,7 +35,7 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Changed ### Changed
- Neue Package-Struktur: - Neue Package-Struktur:
- `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video). - `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video).
- `schule.ngb.zm.tasks` für alles Rund um Parallelität. - `schule.ngb.zm.util.tasks` für alles Rund um Parallelität.
- `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging. - `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging.
### Removed ### Removed

View File

@@ -3,17 +3,13 @@ plugins {
id 'java-library' id 'java-library'
} }
/*properties {
zmVersion {
major = 0;
minor = 0;
rev = 21;
}
}*/
group 'schule.ngb' group 'schule.ngb'
version '0.0.22-SNAPSHOT' version '0.0.32-SNAPSHOT'
//version '{$zmVersion.major}.{$zmVersion.minor}.{$zmVersion.rev}-SNAPSHOT'
java {
withSourcesJar()
withJavadocJar()
}
compileJava { compileJava {
options.release = 11 options.release = 11
@@ -28,8 +24,13 @@ dependencies {
runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4' runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4'
runtimeOnly 'com.googlecode.soundlibs:mp3spi:1.9.5.4' runtimeOnly 'com.googlecode.soundlibs:mp3spi:1.9.5.4'
compileOnlyApi 'colt:colt:1.2.0'
//api 'colt:colt:1.2.0'
//api 'net.sourceforge.parallelcolt:parallelcolt:0.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
} }
test { test {

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,634 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"type": "rectangle",
"version": 152,
"versionNonce": 1288225375,
"isDeleted": false,
"id": "fxk8rHocjpTteICJMa6n8",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 619.9963325816416,
"y": 186.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#fab005",
"width": 150,
"height": 46,
"seed": 1339263918,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [
{
"type": "text",
"id": "JmEBjNProPAgJQZUvMGGa"
},
{
"id": "eryKwAzIMcBMQP0Ybl1Mm",
"type": "arrow"
}
],
"updated": 1670307372550,
"link": null,
"locked": false
},
{
"type": "rectangle",
"version": 264,
"versionNonce": 1267158257,
"isDeleted": false,
"id": "wZLPkORf755_2Vp0J6I0V",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 566.9963325816416,
"y": 289.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#fab005",
"width": 248,
"height": 50,
"seed": 359801906,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [
{
"type": "text",
"id": "9TujIdwDvtinylO3z50y6"
},
{
"id": "eryKwAzIMcBMQP0Ybl1Mm",
"type": "arrow"
},
{
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
"type": "arrow"
}
],
"updated": 1670307379131,
"link": null,
"locked": false
},
{
"type": "rectangle",
"version": 376,
"versionNonce": 1807861489,
"isDeleted": false,
"id": "290mWFx31fA5smc5FqPUr",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 607.9963325816416,
"y": 392.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"width": 143,
"height": 50,
"seed": 1948766318,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [
{
"type": "text",
"id": "_ISR-LCZm2Hu2G57R_uxN"
},
{
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
"type": "arrow"
},
{
"id": "hUUqTCXva-vZjnBeM-PR3",
"type": "arrow"
}
],
"updated": 1670307888001,
"link": null,
"locked": false
},
{
"type": "rectangle",
"version": 518,
"versionNonce": 2136415391,
"isDeleted": false,
"id": "kbEG_cCZadugfCPxYedhf",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 614.9963325816416,
"y": 589.9758066195944,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"width": 131,
"height": 50,
"seed": 97599794,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [
{
"type": "text",
"id": "6KjVrNy_dxGXJtntdqrjY"
},
{
"id": "hUUqTCXva-vZjnBeM-PR3",
"type": "arrow"
},
{
"id": "bplSSGA4kyy-Av7nKNK1B",
"type": "arrow"
},
{
"id": "qukSk_W6enSdwERPEPkhZ",
"type": "arrow"
}
],
"updated": 1670307888001,
"link": null,
"locked": false
},
{
"type": "text",
"version": 110,
"versionNonce": 1350901746,
"isDeleted": false,
"id": "JmEBjNProPAgJQZUvMGGa",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 643.9963325816416,
"y": 199.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 102,
"height": 20,
"seed": 1889147762,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1670164406970,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "new Shapes()",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "fxk8rHocjpTteICJMa6n8",
"originalText": "new Shapes()"
},
{
"type": "text",
"version": 245,
"versionNonce": 1536101230,
"isDeleted": false,
"id": "9TujIdwDvtinylO3z50y6",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 584.9963325816416,
"y": 304.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 212,
"height": 20,
"seed": 1297543150,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1670164433291,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "super(800, 800, \"Shapes\")",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "wZLPkORf755_2Vp0J6I0V",
"originalText": "super(800, 800, \"Shapes\")"
},
{
"type": "text",
"version": 362,
"versionNonce": 2069822514,
"isDeleted": false,
"id": "_ISR-LCZm2Hu2G57R_uxN",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 650.4963325816416,
"y": 407.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 58,
"height": 20,
"seed": 525219186,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1670164509204,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "setup()",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "290mWFx31fA5smc5FqPUr",
"originalText": "setup()"
},
{
"type": "text",
"version": 502,
"versionNonce": 747611665,
"isDeleted": false,
"id": "6KjVrNy_dxGXJtntdqrjY",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 655.4963325816416,
"y": 604.9758066195944,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 50,
"height": 20,
"seed": 1808016110,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1670307245844,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "draw()",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "kbEG_cCZadugfCPxYedhf",
"originalText": "draw()"
},
{
"type": "arrow",
"version": 31,
"versionNonce": 101778865,
"isDeleted": false,
"id": "eryKwAzIMcBMQP0Ybl1Mm",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 694.9963325816416,
"y": 242.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#12b886",
"width": 0,
"height": 38,
"seed": 1130444978,
"groupIds": [],
"strokeSharpness": "round",
"boundElements": [],
"updated": 1670307364549,
"link": null,
"locked": false,
"startBinding": {
"elementId": "fxk8rHocjpTteICJMa6n8",
"focus": 0,
"gap": 10
},
"endBinding": {
"elementId": "wZLPkORf755_2Vp0J6I0V",
"focus": 0.03225806451612903,
"gap": 9
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
0,
38
]
]
},
{
"type": "arrow",
"version": 160,
"versionNonce": 1672489439,
"isDeleted": false,
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 686.7253597961584,
"y": 347.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#12b886",
"width": 0.3256260532492661,
"height": 36,
"seed": 1138446962,
"groupIds": [],
"strokeSharpness": "round",
"boundElements": [],
"updated": 1670307364549,
"link": null,
"locked": false,
"startBinding": {
"elementId": "wZLPkORf755_2Vp0J6I0V",
"gap": 8,
"focus": 0.03595554587056003
},
"endBinding": {
"elementId": "290mWFx31fA5smc5FqPUr",
"gap": 9,
"focus": 0.09195903246894747
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
-0.3256260532492661,
36
]
]
},
{
"type": "arrow",
"version": 616,
"versionNonce": 447594705,
"isDeleted": false,
"id": "hUUqTCXva-vZjnBeM-PR3",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 684.6524492354905,
"y": 452.97580661959455,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"width": 1.5815106831711319,
"height": 29.000000000000114,
"seed": 1169930546,
"groupIds": [],
"strokeSharpness": "round",
"boundElements": [],
"updated": 1670307888001,
"link": null,
"locked": false,
"startBinding": {
"elementId": "290mWFx31fA5smc5FqPUr",
"focus": -0.044553538542618336,
"gap": 10
},
"endBinding": {
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
"focus": 0.08947713014192164,
"gap": 6.999999999999915
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"points": [
[
0,
0
],
[
1.5815106831711319,
29.000000000000114
]
]
},
{
"type": "rectangle",
"version": 560,
"versionNonce": 11022527,
"isDeleted": false,
"id": "5cG3FjQlYuIGPZMsIMgJU",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 616.4963325816416,
"y": 488.9758066195945,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"width": 131,
"height": 50,
"seed": 609895569,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [
{
"type": "text",
"id": "-Fl205ZvyGaxHAHt7C3r3"
},
{
"id": "hUUqTCXva-vZjnBeM-PR3",
"type": "arrow"
},
{
"id": "bplSSGA4kyy-Av7nKNK1B",
"type": "arrow"
},
{
"id": "qukSk_W6enSdwERPEPkhZ",
"type": "arrow"
}
],
"updated": 1670307888001,
"link": null,
"locked": false
},
{
"type": "text",
"version": 551,
"versionNonce": 685087263,
"isDeleted": false,
"id": "-Fl205ZvyGaxHAHt7C3r3",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 647.4963325816416,
"y": 503.97580661959455,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"width": 69,
"height": 20,
"seed": 415680767,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": null,
"updated": 1670307260196,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "update()",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "5cG3FjQlYuIGPZMsIMgJU",
"originalText": "update()"
},
{
"id": "bplSSGA4kyy-Av7nKNK1B",
"type": "arrow",
"x": 683.9963325816416,
"y": 546.9758066195944,
"width": 1,
"height": 34,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 1937189809,
"version": 33,
"versionNonce": 1639939761,
"isDeleted": false,
"boundElements": null,
"updated": 1670307888001,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-1,
34
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
"focus": -0.044849023090586096,
"gap": 7.999999999999858
},
"endBinding": {
"elementId": "kbEG_cCZadugfCPxYedhf",
"focus": 0.022646536412078155,
"gap": 9
},
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "qukSk_W6enSdwERPEPkhZ",
"type": "arrow",
"x": 758.9963325816416,
"y": 615.9758066195944,
"width": 87,
"height": 107.99999999999994,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "transparent",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 677436529,
"version": 170,
"versionNonce": 377207327,
"isDeleted": false,
"boundElements": [],
"updated": 1670307315516,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
86,
0
],
[
83,
-106.99999999999994
],
[
-1,
-107.99999999999994
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "kbEG_cCZadugfCPxYedhf",
"focus": 0.04,
"gap": 13
},
"endBinding": {
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
"focus": -0.26783652736088875,
"gap": 10.5
},
"startArrowhead": null,
"endArrowhead": "arrow"
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

View File

@@ -0,0 +1,375 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "Mhp-wQ2wZxCI4BYWvTvV2",
"type": "ellipse",
"x": 420,
"y": 233,
"width": 385.99999999999994,
"height": 385.99999999999994,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#ffdf22",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 474299186,
"version": 149,
"versionNonce": 2072559726,
"isDeleted": false,
"boundElements": null,
"updated": 1670158627399,
"link": null,
"locked": false
},
{
"id": "SqZRA75ACKHo0lB799wpS",
"type": "line",
"x": 605,
"y": 426,
"width": 173.02018127597637,
"height": 99.89324823492274,
"angle": 0,
"strokeColor": "#087f5b",
"backgroundColor": "#ffdf22",
"fillStyle": "cross-hatch",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 484805038,
"version": 286,
"versionNonce": 1603007154,
"isDeleted": false,
"boundElements": null,
"updated": 1670159044301,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
173.02018127597637,
-99.89324823492274
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "7XVC5Wqiy62pN9Vc92bgl",
"type": "text",
"x": 645.7358370304399,
"y": 356.14958623820286,
"width": 86,
"height": 20,
"angle": 5.766085793504818,
"strokeColor": "#087f5b",
"backgroundColor": "#ffdf22",
"fillStyle": "cross-hatch",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 1883808110,
"version": 228,
"versionNonce": 627525998,
"isDeleted": false,
"boundElements": null,
"updated": 1670159044301,
"link": null,
"locked": false,
"text": "moleRadius",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "moleRadius"
},
{
"id": "zp9JMFpfOA6ZEWCCiqwW0",
"type": "line",
"x": 606,
"y": 427,
"width": 128.012747010704,
"height": 208.02230726873202,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#fa5252",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 224372978,
"version": 81,
"versionNonce": 563971630,
"isDeleted": false,
"boundElements": null,
"updated": 1670158772553,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-128.012747010704,
-208.02230726873202
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "LIDFlKBUhkbZdzTUFT1qp",
"type": "line",
"x": 604.8240237554426,
"y": 425.10629955596494,
"width": 127.71248741178238,
"height": 4.628286654564533,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#fa5252",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 34525358,
"version": 124,
"versionNonce": 1131380590,
"isDeleted": false,
"boundElements": null,
"updated": 1670158787752,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-127.71248741178238,
-4.628286654564533
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "-Of3RfHAvJDq0qHIwjm2U",
"type": "ellipse",
"x": 471.96813247323996,
"y": 413.60866976111646,
"width": 12,
"height": 12,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#4c6ef5",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 1050702514,
"version": 384,
"versionNonce": 1957432558,
"isDeleted": false,
"boundElements": null,
"updated": 1670158970885,
"link": null,
"locked": false
},
{
"id": "NB4WGe-lT7XqLrsKX8jN8",
"type": "ellipse",
"x": 472,
"y": 212,
"width": 12,
"height": 12,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#fa5252",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 2059751730,
"version": 277,
"versionNonce": 756441010,
"isDeleted": false,
"boundElements": null,
"updated": 1670158769939,
"link": null,
"locked": false
},
{
"id": "T1vEWEHV-1FM4A7Q7dTzy",
"type": "ellipse",
"x": 601.6117120882632,
"y": 422.28007605476614,
"width": 6.436664554034337,
"height": 6.436664554034337,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#000000",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 1983845742,
"version": 376,
"versionNonce": 1202556466,
"isDeleted": false,
"boundElements": null,
"updated": 1670158984089,
"link": null,
"locked": false
},
{
"id": "eEMIGJ2Ag5HqWZLPmjXMM",
"type": "text",
"x": 551.4695946462369,
"y": 432.8553279772536,
"width": 108,
"height": 20,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#000000",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 817794290,
"version": 97,
"versionNonce": 1438641266,
"isDeleted": false,
"boundElements": null,
"updated": 1670158980419,
"link": null,
"locked": false,
"text": "(moleX, moleY)",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "(moleX, moleY)"
},
{
"id": "6cMmrU3lcJbub8sEKnVWe",
"type": "text",
"x": 409.80838527586974,
"y": 186.48331271464983,
"width": 135,
"height": 20,
"angle": 0,
"strokeColor": "#c92a2a",
"backgroundColor": "#000000",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 399694958,
"version": 166,
"versionNonce": 1571185842,
"isDeleted": false,
"boundElements": null,
"updated": 1670158939920,
"link": null,
"locked": false,
"text": "(mouseX, mouseY)",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "(mouseX, mouseY)"
},
{
"id": "0idmv_zLtEYyctHB4lSWV",
"type": "text",
"x": 406.7568976895149,
"y": 390.3911282833501,
"width": 135,
"height": 20,
"angle": 0,
"strokeColor": "#364fc7",
"backgroundColor": "#000000",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 1008210350,
"version": 224,
"versionNonce": 968376110,
"isDeleted": false,
"boundElements": null,
"updated": 1670158975919,
"link": null,
"locked": false,
"text": "(mouseX, mouseY)",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "(mouseX, mouseY)"
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 KiB

View File

@@ -0,0 +1,375 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "HPGP8tbAPy0aGI3_MCE3D",
"type": "diamond",
"x": 491.42779164474496,
"y": 479.56091158550765,
"width": 432.03244941244884,
"height": 55.325188818463005,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#868e96",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1103578030,
"version": 138,
"versionNonce": 1150893294,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false
},
{
"id": "vhIFJN3-91oJXV-RfFsBf",
"type": "diamond",
"x": 491.42779164474496,
"y": 435.8942449188411,
"width": 432.03244941244884,
"height": 55.325188818463005,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#fab005",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 618886834,
"version": 198,
"versionNonce": 1515704562,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false
},
{
"id": "B2pM-4m_3Dk2w_6dY-hVJ",
"type": "diamond",
"x": 491.42779164474496,
"y": 392.2275782521744,
"width": 432.03244941244884,
"height": 55.325188818463005,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#228be6",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 844345582,
"version": 231,
"versionNonce": 1954508590,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false
},
{
"id": "Ge28XC9PqD26hknNaFXKP",
"type": "diamond",
"x": 491.42779164474496,
"y": 348.5609115855077,
"width": 432.03244941244884,
"height": 55.325188818463005,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1326657902,
"version": 249,
"versionNonce": 685040306,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false
},
{
"id": "PLU80s2TkyMEAOVgolwJx",
"type": "line",
"x": 506.9963325816415,
"y": 507.9758066195945,
"width": 0,
"height": 132.00000000000006,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1246804466,
"version": 39,
"versionNonce": 627399022,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
-132.00000000000006
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "xDdJ5FnbMLIbN95FE3Iyc",
"type": "line",
"x": 910.9963325816416,
"y": 506.9758066195945,
"width": 0,
"height": 132,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1034568050,
"version": 77,
"versionNonce": 1363786866,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
-132
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "clYu5Q9GzKbAfohBEsGwn",
"type": "line",
"x": 709.9963325816416,
"y": 532.9758066195944,
"width": 0,
"height": 129.99999999999994,
"angle": 0,
"strokeColor": "#000000",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1798035246,
"version": 42,
"versionNonce": 244579246,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
-129.99999999999994
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null
},
{
"id": "v2nD7_bvWJYHqs82XDWO2",
"type": "text",
"x": 935.9963325816416,
"y": 497.2235059947391,
"width": 159,
"height": 20,
"angle": 0,
"strokeColor": "#343a40",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1323260978,
"version": 111,
"versionNonce": 372528622,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"text": "Ebene 0: ColorLayer",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "Ebene 0: ColorLayer"
},
{
"id": "c6T2j8hozL3RZcNYGMgRl",
"type": "text",
"x": 935.9963325816416,
"y": 453.55683932807256,
"width": 172,
"height": 20,
"angle": 0,
"strokeColor": "#e67700",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 1949746930,
"version": 111,
"versionNonce": 1448527858,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"text": "Ebene 1: DrawingLayer",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "Ebene 1: DrawingLayer"
},
{
"id": "bAmGMg2i4Hvv7t_obXSKn",
"type": "text",
"x": 935.9963325816416,
"y": 409.8901726614059,
"width": 174,
"height": 20,
"angle": 0,
"strokeColor": "#1864ab",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 706529710,
"version": 144,
"versionNonce": 1968138286,
"isDeleted": false,
"boundElements": null,
"updated": 1670163691142,
"link": null,
"locked": false,
"text": "Ebene 2: ShapesLayer",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "Ebene 2: ShapesLayer"
},
{
"id": "9-QieQt-nVqGCe5D4r15j",
"type": "text",
"x": 935.9963325816416,
"y": 366.2235059947392,
"width": 65,
"height": 20,
"angle": 0,
"strokeColor": "#2b8a3e",
"backgroundColor": "#40c057",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "dotted",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "round",
"seed": 2100742126,
"version": 179,
"versionNonce": 289074606,
"isDeleted": false,
"boundElements": null,
"updated": 1670163697684,
"link": null,
"locked": false,
"text": "Ebene 3",
"fontSize": 16,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"baseline": 14,
"containerId": null,
"originalText": "Ebene 3"
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

12
docs/assets/zmstyles.css Normal file
View File

@@ -0,0 +1,12 @@
h1.title {
text-align: center;
color: #363636;
margin-bottom: .25rem;
}
h2.subtitle {
text-align: center;
font-size: 1rem;
color: #4a4a4a;
margin-top: -.25rem;
margin-bottom: -1.25rem;
}

View File

@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block tabs %}
{{ super() }}
{% endblock %}
{% block content %}{% endblock %}
{% block footer %}{% endblock %}

43
docs/index.md Normal file
View File

@@ -0,0 +1,43 @@
<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](install.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`.

44
docs/install.md Normal file
View File

@@ -0,0 +1,44 @@
# Installation
Um ein einfaches Projekt mit der **Zeichenmaschine** aufzusetzen ist nicht mehr
nötig, als
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
herunterzuladen und dem *Classpath* des Projekts hinzuzufügen. Beschreibungen
für
verschiedene Entwicklungsumgebungen sind hier aufgelistet.
## Integration in Entwicklungsumgebungen
### BlueJ
[BlueJ](https://bluej.org) sucht an drei Orten nach Bibliotheken, die für ein
Projekt in den Classpath aufgenommen werden:
- Für ein einzelnes Projekt im Projektordner im Unterordner `+libs`.
- Im Reiter "Bibliotheken" der BlueJ-Einstellungen.
Hier können Programmbibliotheken hinzugefügt werden, die dann allen Projekten
zur Verfügung stehen.
- Für alle Projekte und alle Nutzer dieser BlueJ-Version im
Unterordner `userlib` des Programmordners.
Auf Windows-Systemen ist dieser im Order `lib` des Installationsordners von BlueJ zu finden.
Auf macos-Systemen muss via Rechtsklick auf die Programmdatei `BlueJ.app` über den Menüpunkt "Paketinhalt zeigen" in den Ordner `Contents/Resources/Java/` navigiert werden.
### VSCode / VSCodium
> Coming soon
### IntelliJ
> Coming soon
### Eclipse
> Coming soon
### NetBeans
> Coming soon

129
docs/macros.py Normal file
View File

@@ -0,0 +1,129 @@
import re
from typing import List
def define_env(env):
@env.macro
def javadoc(clazz: str = None, target: str = None) -> str:
if not "javadoc_url" in env.variables:
return clazz
if not clazz:
return f"{env.variables['javadoc_url'].rstrip('/')}/index.html"
else:
if "javadoc_default_package" in env.variables and not clazz.startswith(env.variables['javadoc_default_package']):
clazz = f"{env.variables['javadoc_default_package'].rstrip('.')}.{clazz}"
javadoc_url = env.variables["javadoc_url"].rstrip("/")
path = list()
name = list()
for p in clazz.split('.'):
if p[0].islower():
path.append(p)
else:
name.append(p)
path = '/'.join(path) + '/' + '.'.join(name) + ".html"
if target:
path = f"{path}#{target}"
return f"{javadoc_url}/{path}"
@env.macro
def jd(cl: str = None, t: str = None) -> str:
return javadoc(cl, t)
@env.macro
def javadoc_link(
clazz: str = None,
target: str = None,
strip_package: bool = True,
strip_clazz: bool = False,
strip_params: bool = True,
title: str = None
) -> str:
name = clazz or "Javadoc"
if strip_package:
if clazz and clazz.rfind(".") > -1:
name = clazz[clazz.rfind(".") + 1 :]
if target:
# _target = re.sub(r"([^(][^,]*?\.)*?([^)]+)", lambda m: m.group(2), target)
_target = target
print(f'{_target.strip()=}')
if m := re.match(r'^(.+?)\((.*)\)$', _target):
if strip_params and m.group(2):
params = m.group(2).split(',')
for i, param in enumerate(params):
dot = param.rfind('.')
if dot >= 0:
params[i] = param[dot+1:].strip()
params = ", ".join(params)
_target = f'{m.group(1)}({params})'
if strip_clazz:
name = _target
else:
name = f"{name}.{_target}"
if title:
name = title
return f"[`{name}`]({javadoc(clazz, target)})"
@env.macro
def jdl(
cl: str = None,
t: str = None,
p: bool = False,
c: bool = True,
title: str = None
) -> str:
return javadoc_link(cl, t, strip_package=not p, strip_clazz=not c, strip_params=True, title=title)
@env.macro
def jdc(
cl: str,
p: bool = False
) -> str:
return javadoc_link(cl, strip_package=not p)
@env.macro
def jdm(
cl: str,
t: str,
p: bool = False,
c: bool = False
) -> str:
return javadoc_link(cl, t, strip_package=not p, strip_clazz=not c)
@env.macro
def javadoc_signature(
clazz: str = None,
member: str = None,
package: str = None,
params: List[str] = list(),
) -> str:
sig = clazz or ""
if clazz and package:
sig = f"{package}.{sig}"
if member:
sig = f"{sig}#{member}"
pparams = ",".join(params)
sig = f"{sig}({pparams})"
return sig
@env.macro
def jds(
cl: str = None,
m: str = None,
pkg: str = None,
params: List[str] = list(),
) -> str:
javadoc_signature(cl, m, pkg, params)
# schule/ngb/zm/Zeichenmaschine.html#setCursor(java.awt.Image,int,int)
# schule/ngb/zm/Zeichenmaschine.html#getLayer(java.lang.Class)
# schule/ngb/zm/DrawableLayer.html#add(schule.ngb.zm.Drawable...)

606
docs/quickstart.md Normal file
View File

@@ -0,0 +1,606 @@
# Schnellstart mit der Zeichenmaschine
Um die **Zeichenmaschine** in einem Projekt zu nutzen ist nicht mehr nötig, als
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
herunterzuladen und
dem [Classpath](https://www.delftstack.com/de/howto/java/java-classpath-/)
hinzuzufügen. Eine Beschreibung für verschiedene Entwicklungsumgebungen findet
sich im Abschnitt [Installation](install.md).
## Die Basisklasse
Eine _Zeichenmaschine_ wird immer als Unterklasse von {{ javadoc_link("
schule.ngb.zm.Zeichenmaschine") }} erstellt.
```java
public class Shapes extends Zeichenmaschine {
}
```
Die gezeigte Klasse ist schon eine lauffähige Zeichenmaschine und kann gestartet
werden.
!!! note "main Methode"
Bei einigen Entwicklungsumgebungen muss noch eine `main` Methode erstellt
werden, um die Zeichenmaschine zu starten:
```java
public static void main(String[] args) {
new Shapes();
}
```
Es öffnet sich ein Zeichenfenster in einer vordefinierten Größe. Um die
Abmessungen und den Titel des Fensters zu ändern, legen wir einen Konstruktor
an.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
}
```
Starten wir das Projekt, wird eine Zeichenfläche in der Größe 800-mal-800 Pixel
erstellt und in einem Fenster mit dem Titel „Shapes“ angezeigt.
<figure markdown>
![Shapes 2](assets/quickstart/shapes_2.png){ width=400 }
</figure>
### Formen zeichnen
Eine Zeichenmaschine hat verschiedene Möglichkeiten, Inhalte in das
Zeichenfenster zu zeichnen. Um ein einfaches statisches Bild zu erzeugen,
überschreiben wir die {{ jdl("schule.ngb.zm.Zeichenmaschine", "draw()",
c=False) }} Methode.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void draw() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
drawing.circle(400, 400, 100);
}
}
```
Wir sehen einen gelben Kreis (ohne Konturlinie) auf einem blauen Hintergrund.
<figure markdown>
![Shapes 3](assets/quickstart/shapes_3.png){ width=400 }
</figure>
### Vorbereitung der Zeichenfläche
Im Beispiel oben setzen wir die Hintergrundfarbe auf Blau, die Füllfarbe auf
Gelb und deaktivieren die Konturlinie. Wenn diese Einstellungen für alle
Zeichenobjekte gleich bleiben, können wir sie statt in `draw()` auch in die {{
jdl('Zeichenmaschine', 'setup()', c=False) }} Methode schreiben. Diese bereitet
die Zeichenfläche vor dem ersten Zeichnen vor.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
}
@Override
public void draw() {
for( int i = 0; i < 10; i++ ) {
drawing.circle(
random(0, canvasWidth),
random(0, canvasHeight),
random(50, 200)
);
}
}
}
```
Im Beispiel setzen wir nun die Grundeinstellungen in der `setup()` Methode. In
`draw()` werden zehn gelbe Kreise an Zufallskoordinaten gezeichnet.
<figure markdown>
![Shapes 4](assets/quickstart/shapes_4.1.png){ width=400 }
</figure>
!!! tip ""
Mit {{ jdm("Constants", "canvasWidth") }} und
{{ jdm("Constants", "canvasHeight") }} kannst du in der Zeichenmaschine
auf die aktuelle Größe der Zeichenfläche zugreifen.
{{ jdm("Constants", "random(int,int)") }} erzeugt eine Zufallszahl
innerhalb der angegebenen Grenzen.
## Interaktionen mit der Maus: Whack-a-mole
Mit der Zeichenmaschine lassen sich Interaktionen mit der Maus leicht umsetzen.
Wor wollen das Beispielprogramm zu einem
[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
angezeigt. Sobald die Spieler:in auf den Kreis klickt, soll dieser an eine neue
Position springen.
Damit wir den Kreis an eine neue Position springen lassen können, müssen wir
zufällige `x`- und `y`-Koordinaten generieren. Dazu erstellen wir zunächst zwei
_Objektvariablen_ für die Koordinaten, die in der `setup()` Methode mit
zufälligen Werte initialisiert werden. Diese benutzen wir, um die `draw
()` Methode anzupassen.
??? example "Quelltext"
```Java
public class Shapes extends Zeichenmaschine {
private int moleRadius = 20;
private int moleX;
private int moleY;
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
moleX = random(50, canvasWidth - 50);
moleY = random(50, canvasHeight - 50);
}
@Override
public void draw() {
drawing.clear();
drawing.circle(moleX, moleY, moleRadius);
}
}
```
<figure markdown>
![Shapes 5](assets/quickstart/shapes_5.1.png){ width=600 }
</figure>
Als Nächstes prüfen wir bei jedem Mausklick, ob die Mauskoordinaten innerhalb
des gelben Kreises (des Maulwurfs) liegen. Die Mauskoordinaten sind jederzeit
über die Variablen `mouseX` und `mouseY` abrufbar. Um zu prüfen, ob diese
Koordinaten innerhalb des Kreises liegen, vergleichen wir den Abstand zwischen
Kreismittelpunkt `(moleX, moleY)` und den Mauskoordinaten
`(mouseX, mouseY)` mit dem Radius des Kreises (im Bild grün). Ist die Entfernung
kleiner als der Radius (blauer Kreis), wurde innerhalb des Kreises geklickt.
Sonst außerhalb (roter Kreis).
<figure markdown>
![Kollision Maus mit Kreis](assets/quickstart/CircleMouseCollision.png){ width=400 }
</figure>
Den Abstand vom Mittelpunkt zur Maus lässt sich mithilfe des Satzes des
Pythagoras leicht selber berechnen. Die Zeichenmaschine kann uns diese Arbeit
aber auch abnehmen und stellt eine Methode dafür bereit
({{ jdm("Constants", "distance(double,double,double,double)") }}).
Um auf einen Mausklick zu reagieren, ergänzen wir die
{{ jdm("Zeichenmaschine", "mouseClicked()") }} Methode:
```
@Override
public void mouseClicked() {
if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) {
moleX = random(50, canvasWidth - 50);
moleY = random(50, canvasHeight - 50);
redraw();
}
}
```
??? example "Quelltext"
```Java
public class Shapes extends Zeichenmaschine {
private int moleRadius = 20;
private int moleX;
private int moleY;
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
moleX = random(50, canvasWidth - 50);
moleY = random(50, canvasHeight - 50);
}
@Override
public void draw() {
drawing.clear();
drawing.circle(moleX, moleY, moleRadius);
}
@Override
public void mouseClicked() {
if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) {
moleX = random(50, canvasWidth - 50);
moleY = random(50, canvasHeight - 50);
redraw();
}
}
}
```
!!! warning ""
Der Aufruf von {{ jdm("Zeichenmaschine", "redraw()") }} zeichnet
die Zeichenfläche neu, indem die `draw()` Methode erneut aufgerufen wird.
Du solltest `draw()` niemals direkt aufrufen.
Nun springt der Kreis an eine andere Stelle, wenn er direkt mit der Maus
angeklickt wird.
<figure markdown>
![Whack-a-mole](assets/quickstart/shapes_5.3.gif){ width=400 }
</figure>
## Ein paar Details zur Zeichenmaschine
Die _Zeichenmaschine_ wurde stark von der kreativen Programmierumgebung
[Processing](https://processing.org) inspiriert. Wenn Du Processing schon
kennst, dann werden Dir einige der Konzepte der _Zeichenmaschine_ schon bekannt
vorkommen.
### Farben
Farben können auf verschiedene Weisen angegeben werden. Unser Beispiel nutzt
bisher zwei Arten:
1. Die einfachste Möglichkeit sind die _Farbkonstanten_
wie {{ jdm('Constants', 'BLUE') }} oder {{ jdm('Constants', 'RED') }}. Im
Beispiel setzen wir den Hintergrund auf die Farbe `BLUE`.
2. Farben werden häufig im RGB-Farbraum definiert. Dazu wird jeweils der Rot-,
Grün- und Blauanteil der Farbe als Wert zwischen 0 und 255 angegeben. Im
Beispiel setzen wir die Farbe der Kreise auf `255, 223, 34`, also viel Rot
und Grün und nur ein wenig Blau.
### Ebenen
Die Zeichenfläche besteht aus einzelnen {{ jdl("Layer", title="Ebenen") }}, die
auf übereinander liegen. Bis auf die unterste Ebene sind die Ebenen zunächst
durchsichtig, wodurch die Zeichnungen unterer Ebenen durchscheinen.
<figure markdown>
![Whack-a-mole](assets/quickstart/Layers.png){ width=600 }
</figure>
Eine _Zeichenmaschine_ besitzt zu Beginn drei Ebenen:
1. Die unterste Ebene ist ein {{ jdc("layers.ColorLayer") }}, die nur aus einer
Farbe (oder einem Farbverlauf) besteht und keine durchsichtigen Bereiche
besitzt. Im Beispiel setzen wir diese Ebene auf die Farbe `BLUE`.
2. Die nächste Ebene ist ein {{ jdc("layers.DrawingLayer") }}, auf die wir
unsere Formen zeichnen können. Die Ebene ist zunächst komplett durchsichtig.
3. Die oberste Ebene ist ein {{ jdc("layers.ShapesLayer") }}, die zur
Darstellung von Form-Objekten der Klasse {{ jdc("shapes.Shape") }} genutzt
werden kann.
Du kannst einer Zeichenfläche aber auch beliebige neue oder selbst programmierte
Ebenen hinzufügen.
### Ablauf
Die _Zeichenmaschine_ ruft nach dem Start die Methoden in einem festen Ablauf
auf.
<figure markdown>
![Whack-a-mole](assets/quickstart/AblaufMoleStatic.png){ width=500 }
</figure>
Erstellst Du eine _Zeichenmaschine_ (beziehungsweise ein Objekt einer
Unterklasse), dann wird zuerst die {{ jdm('Zeichenmaschine', 'setup()') }}
Methode ausgeführt. Danach folgt einmalig die
{{ jdm('Zeichenmaschine', 'draw()') }} Methode und dann endet das Hauptprogramm.
Die Eingaben der Maus werden in einem parallelen Ablauf (einem _Thread_)
abgefangen und daraufhin die {{ jdm('Zeichenmaschine', 'mouseClicked()') }}
Methode aufgerufen. In unserem Programm prüfen wir, ob mit der Maus
innerhalb des Kreises geklickt wurde und rufen dann
{{ jdm('Zeichenmaschine', 'redraw()') }} auf, woraufhin ein weiteres Mal
`draw()` ausgeführt wird.
In _Processing_ wird dies der "statische" Modus genannt, weil das Programm nur
einmal abläuft und dann stoppt. "Statisch" trifft es nicht ganz, da das Programm
ja zum Beispiel durch Mauseingaben auch verändert werden kann. Wichtig ist aber,
dass mit `redraw()` die Zeichenfläche manuell neu gezeichnet werden muss, damit
sich der Inhalt ändert.
## Dynamische Programme
Wir wollen unser kleines Spiel dynamischer machen, indem die Kreise nur drei
Sekunden angezeigt werden und dann von selbst an einen neuen Ort springen.
Schafft man es, den Kreis in dieser Zeit anzuklicken, bekommt man einen Punkt.
Als zusätzliche Herausforderung lassen wir jeden Kreis in den drei Sekunden
immer kleiner werden.
### Die Update-Draw-Schleifen
![Whack-a-mole](assets/quickstart/AblaufMoleActive.png){ width=200 align=right }
Bisher hat die _Zeichenmaschine_ einmalig `draw()` aufgerufen und dann nur noch
auf Benutzereingaben mit der Maus reagiert. Nun wollen wir das gezeichnete Bild
aber laufend anpassen. Der Kreis soll schrumpfen und nach 3 Sekunden
verschwinden.
Dazu ergänzen wir ein {{ jdl('Zeichenmaschine', 'update(double)', c=False) }}
Methode in unserem Programm. Nun schaltet die _Zeichenmaschine_ in einen
dynamischen Modus und startet die _Update-Draw-Schleife_. Das beduetet, nach
Aufruf von `setup()` wird fortlaufend immer wieder zuerst `update()` und dann
`draw()` aufgerufen.
Jeder Aufruf der `draw()` Methode zeichnet nun die Zeichenfläche neu. Jedes
Bild, das gezeichnet wird (auch, wenn es genauso aussieht, wie das davor), nennt
man ein _Frame_. Von Videospielen kennst Du vielleicht schon den Begriff "
_Frames per second_" (Fps, dt. Bilder pro Sekunde). Er bedeutet, wie oft das
Spiel neue Frames zeichnet, oder in der _Zeichenmaschine_, wie oft `draw()` pro
Sekunde aufgerufen wird. Normalerweise passiert dies genau 60-mal pro Sekunde.
### Lebenszeit eines Kreises
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
Bruchteile von Skeunden abziehen wollen, wählen wir als Datentyp `double`:
```Java
private double moleTime=3.0;
```
Der Parameter `delta`, der `update()` Methode ist der Zeitraum in Sekunden, seit
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
ist und in dem Fall den Kreis auf eine neue Position springen lassen.
Die Größe des Kreises passen wir so an, dass der Anteil der vergangenen Zeit die
Größe des Kreises bestimmt:
```Java
drawing.circle(moleX,moleY,moleRadius*(moleTime/3.0));
```
??? example "Quelltext"
```Java
import schule.ngb.zm.Zeichenmaschine;
public class Shapes extends Zeichenmaschine {
private int moleRadius = 20;
private int moleX;
private int moleY;
private double moleTime;
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
randomizeMole();
}
private void randomizeMole() {
moleX = random(moleRadius*2, canvasWidth - moleRadius*2);
moleY = random(moleRadius*2, canvasHeight - moleRadius*2);
moleTime = 3.0;
}
@Override
public void update( double delta ) {
moleTime -= delta;
if( moleTime <= 0 ) {
randomizeMole();
}
}
@Override
public void draw() {
drawing.clear();
drawing.circle(moleX, moleY, moleRadius * (moleTime / 3.0));
}
@Override
public void mouseClicked() {
if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) {
randomizeMole();
redraw();
}
}
public static void main( String[] args ) {
new Shapes();
}
}
```
<figure markdown>
![Whack-a-mole](assets/quickstart/shapes_6.1.gif){ width=400 }
</figure>
!!! tip ""
Der Maulwurf muss mittlerweile an drei verschiedenen Stellen im Programm
auf eine zufällige Position gesetzt werden (am Anfang, wenn er angeklickt
wurde und wenn die drei Sekunden abgelaufen sind). Daher wurde das Versetzen
in eine eigene Methode `randomizeMole()` ausgelagert.
### Punktezähler
Zum Schluss wollen wir noch bei jedem Treffer mit der Maus die Punkte Zählen und
als Text auf die Zeichenfläche schreiben.
Dazu ergänzen wir eine weitere Objektvariable `score`, die in `mouseClicked()`
erhöht wird, falls der Kreis getroffen wurde.
Um den Punktestand anzuzeigen ergänzen wir in `draw()` einen Aufruf von
{{ jdl('layers.DrawingLayer', 'text(java.lang.String,double,double,schule.ngb.zm.Options.Direction)') }}
mit dem Inhalt von `score` und den Koordinaten, an denen der Text gezeigt
werden soll.
Die Zeichenebene zeichnet im Moment alle Formen und Text ausgehend vom Zentrum
der Form. Damit der Text 10 Pixel vom Rand entfernt links oben angezeigt wird,
können wir der Text Methode (und allen anderen, die etwas zeichnen) eine {{
jdl('Options.Direction', title='Richtung') }} übergeben, die festlegt, von
welchem Ausgangspunkt (oder _Ankerpunkt_) die Form gezeichnet werden soll.
```Java
drawing.setFillColor(BLACK);
drawing.text("Punkte: "+score,10,10,NORTHWEST);
```
<figure markdown>
![Whack-a-mole](assets/quickstart/shapes_6.2.png){ width=400 }
</figure>
??? example "Quelltext"
```Java
import schule.ngb.zm.Zeichenmaschine;
public class Shapes extends Zeichenmaschine {
private int moleRadius = 20;
private int moleX;
private int moleY;
private double moleTime;
private int score = 0;
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.noStroke();
drawing.setFontSize(24);
randomizeMole();
}
private void randomizeMole() {
moleX = random(moleRadius*2, canvasWidth - moleRadius*2);
moleY = random(moleRadius*2, canvasHeight - moleRadius*2);
moleTime = 3.0;
}
@Override
public void update( double delta ) {
moleTime -= delta;
if( moleTime <= 0 ) {
randomizeMole();
}
}
@Override
public void draw() {
drawing.clear();
drawing.setFillColor(255, 223, 34);
drawing.circle(moleX, moleY, moleRadius * (moleTime / 3.0));
drawing.setFillColor(BLACK);
drawing.text("Punkte: " + score, 10, 10, NORTHWEST);
}
@Override
public void mouseClicked() {
if( distance(moleX, moleY, mouseX, mouseY) < moleRadius ) {
score += 1;
randomizeMole();
redraw();
}
}
public static void main( String[] args ) {
new Shapes();
}
}
```
## Wie es weitergehen kann
In diesem Schnellstart-Tutorial hast Du die Grundlagen der _Zeichenmaschine_
gelernt. Um weiterzumachen, kannst Du versuchen, das Whack-a-mole Spiel um diese
Funktionen zu erweitern:
- Mehrere "Maulwürfe" gleichzeitig.
- Unterschiedliche Zeiten pro Maulwurf.
Wenn Du mehr über die Möglichkeiten lernen möchtest, die Dir die
_Zeichenmaschine_ bereitstellt, kannst Du Dir die weiteren Tutorials in dieser
Dokumentation ansehen. Ein guter Startpunkt ist das
[Aquarium](tutorials/aquarium/aquarium1.md).
Viele verschiedene Beispiele, ohne detailliertes Tutorial, findest Du in der
Kategorie Beispiele und auf GitHub im Repository
[jneug/zeichenmaschine-examples](https://github.com/jneug/zeichenmaschine-examples).

View File

@@ -0,0 +1,19 @@
# Tutorial: Aquarium
In diesem Tutorial wollen wir mithilfe der _Zeichenmaschine_ ein (bonbonbuntes)
interaktives Aquarium entwickeln. Dabei werden wir in verschiedenen Ausbaustufen
zunächst das System Modellieren und dann implementieren.
!!! info "Mein bonbonbuntes Aquarium"
Das Projekt [Mein bonbonbuntes Aquarium](http://blog.schockwellenreiter.de/2021/02/2021021201.html)
stammt ursprünglich aus dem Blog [Schockwellenreiter](http://blog.schockwellenreiter.de)
von [Jörg Kantel](http://cognitiones.kantel-chaos-team.de/cv.html).
Das Endprodukt soll folgendes umfassen:
- Darstellung eines hübschen Aquariums mit Fischen, die hin und her schwimmen.
- Zur Darstellung wollen wir wie im Original die Sprites aus dem [Fish Pack von Kenny.nl](https://www.kenney.nl/assets/fish-pack) nutzen.
- Das Aquarium soll durch passende Geräusche untermalt werden.
- Bei einem Klick in das Aquarium soll ein zufälliger Fisch erscheinen.
- Bei einem Druck auf die Leertaste soll ein Hai durch das Aquarium schwimmen und alle Fische auf seinem Weg auffressen.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

83
mkdocs.yml Normal file
View File

@@ -0,0 +1,83 @@
site_name: Zeichenmaschine.xyz
site_description: Eine kleine Java-Bibliothek für grafische Programmierung im Informatikunterricht.
site_author: J. Neugebauer
repo_url: https://github.com/jneug/zeichenmaschine
repo_name: jneug/zeichenmaschine
site_dir: build/docs/site
theme:
name: material
custom_dir: docs/home_override/
language: de
logo: assets/icon_64.png
favicon: assets/icon_32.png
features:
- content.code.annotate
- navigation.top
- navigation.tracking
- search.suggest
font: false
palette:
- media: "(prefers-color-scheme: light)"
scheme: default
primary: blue
accent: deep orange
toggle:
icon: material/weather-sunny
name: Dunkles Design aktivieren
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: blue
accent: deep orange
toggle:
icon: material/weather-night
name: Helles Design aktivieren
extra_css:
- assets/zmstyles.css
nav:
- Einführung: index.md
- Schnellstart: quickstart.md
- Installation: install.md
- Tutorials:
- Aquarium: tutorials/aquarium/aquarium1.md
- Beispiele:
- Mondrian: beispiele/mondrian.md
markdown_extensions:
- admonition
- attr_list
- def_list
- footnotes
- md_in_html
- toc:
permalink: true
- pymdownx.magiclink
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.smartsymbols
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
plugins:
- search:
lang: de
separator: '[\s\-\.]'
- macros:
module_name: docs/macros
extra:
javadoc_url: https://zeichenmaschine.xyz/docs/
javadoc_default_package: schule.ngb.zm

25
requirements.txt Normal file
View File

@@ -0,0 +1,25 @@
certifi==2022.9.24
charset-normalizer==2.1.1
click==8.1.3
ghp-import==2.1.0
idna==3.4
Jinja2==3.1.2
Markdown==3.3.7
MarkupSafe==2.1.1
mergedeep==1.3.4
mkdocs==1.4.1
mkdocs-macros-plugin==0.7.0
mkdocs-material==8.5.6
mkdocs-material-extensions==1.0.3
packaging==21.3
Pygments==2.13.0
pymdown-extensions==9.6
pyparsing==3.0.9
python-dateutil==2.8.2
PyYAML==6.0
pyyaml_env_tag==0.1
requests==2.28.1
six==1.16.0
termcolor==2.0.1
urllib3==1.26.12
watchdog==2.1.9

View File

@@ -0,0 +1,236 @@
package schule.ngb.zm;
import java.awt.*;
/**
* Basisimplementierung der {@link Strokeable} und {@link Fillable} APIs.
*
* Die Klasse bietet eine Grundlage zur Implementierung eigener Zeichenobjekte,
* die eine Füllung und eine Konturlinie haben können.
*/
public abstract class BasicDrawable extends Constants implements Strokeable, Fillable {
/**
* Ob das Objekt gezeichnet werden soll.
*/
protected boolean visible = true;
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn das Objekt ohne
* Kontur dargestellt werden soll.
*/
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
/**
* Die Art der Konturlinie.
*/
protected Options.StrokeType strokeType = SOLID;
/**
* Cache für den aktuellen {@code Stroke} der Kontur. Wird nach Änderung
* einer der Kontureigenschaften auf {@code null} gesetzt und beim nächsten
* Zeichnen neu erstellt.
*/
protected Stroke stroke = null;
/**
* Die aktuelle Füllfarbe der Form oder {@code null}, wenn das Objekt nicht
* gefüllt werden soll.
*/
protected Color fillColor = DEFAULT_FILLCOLOR;
/**
* Der aktuelle Farbverlauf des Objektes oder {@code null}, wenn es keinen
* Farbverlauf besitzt.
*/
protected MultipleGradientPaint fill = null;
// Implementierung Drawable Interface
/**
* Ob das Objekt angezeigt bzw. gezeichnet werden soll.
*
* @return {@code true}, wenn das Objekt angezeigt werden soll,
* {@code false} sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt das Objekt.
*/
public void hide() {
visible = false;
}
/**
* Zeigt das Objekt an.
*/
public void show() {
visible = true;
}
/**
* Versteckt da Objekt, wenn es derzeit angezeigt wird und zeigt es
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
public abstract void draw( Graphics2D graphics );
// Implementierung Fillable Interface
/**
* {@inheritDoc}
*/
@Override
public void setFill( Paint fill ) {
if( fill == null ) {
this.fill = null;
} else if( fill instanceof Color ) {
this.fillColor = ((Color) fill);
this.fill = null;
} else if( fill instanceof MultipleGradientPaint ) {
this.fillColor = null;
this.fill = (MultipleGradientPaint) fill;
}
}
/**
* {@inheritDoc}
*/
@Override
public Paint getFill() {
if( fill != null ) {
return fill;
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
return fillColor;
} else {
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFillColor() {
return fillColor != null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasGradient() {
return fill != null;
}
/**
* {@inheritDoc}
*/
@Override
public Color getFillColor() {
return fillColor;
}
/**
* {@inheritDoc}
*/
@Override
public void setFillColor( Color color ) {
fillColor = color;
fill = null;
}
/**
* {@inheritDoc}
*/
@Override
public MultipleGradientPaint getGradient() {
return fill;
}
// Implementierung Strokeable Interface
/**
* {@inheritDoc}
*/
@Override
public void setStroke( Stroke stroke ) {
this.stroke = stroke;
}
/**
* {@inheritDoc}
*/
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight);
}
return stroke;
}
/**
* {@inheritDoc}
*/
@Override
public Color getStrokeColor() {
return strokeColor;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeColor( Color color ) {
strokeColor = color;
}
/**
* {@inheritDoc}
*/
@Override
public double getStrokeWeight() {
return strokeWeight;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeWeight( double weight ) {
strokeWeight = weight;
this.stroke = null;
}
/**
* {@inheritDoc}
*/
@Override
public Options.StrokeType getStrokeType() {
return strokeType;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeType( Options.StrokeType type ) {
strokeType = type;
this.stroke = null;
}
}

View File

@@ -1,5 +1,14 @@
package schule.ngb.zm; package schule.ngb.zm;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/** /**
* Repräsentiert eine Farbe in der Zeichenmaschine. * Repräsentiert eine Farbe in der Zeichenmaschine.
* <p> * <p>
@@ -9,7 +18,8 @@ package schule.ngb.zm;
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255 * Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend). * (deckend).
*/ */
public class Color { @SuppressWarnings( "unused" )
public class Color implements Paint {
//@formatter:off //@formatter:off
@@ -42,38 +52,67 @@ public class Color {
* Die Farbe Zeichenmaschinen-Rot. * Die Farbe Zeichenmaschinen-Rot.
*/ */
public static final Color RED = new Color(240, 80, 37); public static final Color RED = new Color(240, 80, 37);
/**
* Die Farbe Rot.
*/
public static final Color PURE_RED = new Color(255, 0, 0);
/** /**
* Die Farbe Zeichenmaschinen-Grün. * Die Farbe Zeichenmaschinen-Grün.
*/ */
public static final Color GREEN = new Color(98, 199, 119); public static final Color GREEN = new Color(98, 199, 119);
/**
* Die Farbe Grün.
*/
public static final Color PURE_GREEN = new Color(0, 255, 0);
/** /**
* Die Farbe Zeichenmaschinen-Blau. * Die Farbe Zeichenmaschinen-Blau.
*/ */
public static final Color BLUE = new Color(49, 197, 244); public static final Color BLUE = new Color(43, 128, 243); // 49, 197, 244
/**
* Die Farbe Blau.
*/
public static final Color PURE_BLUE = new Color(0, 0, 255);
/** /**
* Die Farbe Zeichenmaschinen-Gelb. * Die Farbe Zeichenmaschinen-Gelb.
*/ */
public static final Color YELLOW = new Color(248, 239, 34); public static final Color YELLOW = new Color(248, 239, 34);
/**
* Die Farbe Gelb.
*/
public static final Color PURE_YELLOW = new Color(255, 255, 0);
/** /**
* Die Farbe Zeichenmaschinen-Orange. * Die Farbe Zeichenmaschinen-Orange.
*/ */
public static final Color ORANGE = new Color(248, 158, 80); public static final Color ORANGE = new Color(248, 158, 80);
/** /**
* Die Farbe Zeichenmaschinen-Türkis. * Die Farbe Zeichenmaschinen-Türkis.
*/ */
public static final Color CYAN = new Color(java.awt.Color.CYAN); public static final Color CYAN = new Color(java.awt.Color.CYAN);
/** /**
* Die Farbe Zeichenmaschinen-Magenta. * Die Farbe Zeichenmaschinen-Magenta.
*/ */
public static final Color MAGENTA = new Color(java.awt.Color.MAGENTA); public static final Color MAGENTA = new Color(java.awt.Color.MAGENTA);
/** /**
* Die Farbe Zeichenmaschinen-Rosa. * Die Farbe Zeichenmaschinen-Rosa.
*/ */
public static final Color PINK = new Color(240, 99, 164); public static final Color PINK = new Color(240, 99, 164);
/** /**
* Die Farbe Zeichenmaschinen-Lila. * Die Farbe Zeichenmaschinen-Lila.
*/ */
public static final Color PURPLE = new Color(101, 0, 191); public static final Color PURPLE = new Color(101, 0, 191);
/** /**
* Die Farbe Zeichenmaschinen-Braun. * Die Farbe Zeichenmaschinen-Braun.
*/ */
@@ -83,6 +122,7 @@ public class Color {
* Die Farbe Helmholtz-Grün. * Die Farbe Helmholtz-Grün.
*/ */
public static final Color HGGREEN = new Color(0, 165, 81); public static final Color HGGREEN = new Color(0, 165, 81);
/** /**
* Die Farbe Helmholtz-Rot. * Die Farbe Helmholtz-Rot.
*/ */
@@ -102,7 +142,7 @@ public class Color {
} }
/** /**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var>. * Erstellt eine graue Farbe entsprechend dem Grauwert {@code gray}.
* *
* @param gray Ein Grauwert zwischen 0 und 255. * @param gray Ein Grauwert zwischen 0 und 255.
*/ */
@@ -111,8 +151,8 @@ public class Color {
} }
/** /**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var> und * Erstellt eine graue Farbe entsprechend dem Grauwert {@code gray} und dem
* des Transparentwertes <var>alpha</var>. * Transparenzwert {@code alpha}.
* *
* @param gray Ein Grauwert zwischen 0 und 255. * @param gray Ein Grauwert zwischen 0 und 255.
*/ */
@@ -121,9 +161,9 @@ public class Color {
} }
/** /**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und * Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die * {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* Werte liegen zwischen 0 und 255. * liegen zwischen 0 und 255.
* *
* @param red Rotwert zwischen 0 und 255. * @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255. * @param green Grünwert zwischen 0 und 255.
@@ -134,11 +174,11 @@ public class Color {
} }
/** /**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und * Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die * {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* Werte liegen zwischen 0 und 255. * liegen zwischen 0 und 255. {@code alpha} gibt den den Transparentwert an
* <var>alpha</var> gibt den den Transparentwert an (auch zwischen * (auch zwischen 0 und 255), wobei 0 komplett durchsichtig ist und 255
* 0 und 255), wobei 0 komplett durchsichtig ist und 255 komplett deckend. * komplett deckend.
* *
* @param red Rotwert zwischen 0 und 255. * @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255. * @param green Grünwert zwischen 0 und 255.
@@ -146,24 +186,24 @@ public class Color {
* @param alpha Transparentwert zwischen 0 und 255. * @param alpha Transparentwert zwischen 0 und 255.
*/ */
public Color( int red, int green, int blue, int alpha ) { public Color( int red, int green, int blue, int alpha ) {
rgba = ((alpha&0xFF) << 24) | ((red&0xFF) << 16) | ((green&0xFF) << 8) | ((blue&0xFF) << 0); rgba = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF);
} }
/** /**
* Erstellt eine Farbe als Kopie von <var>color</var>. * Erstellt eine Farbe als Kopie von {@code color}.
* *
* @param color * @param color Eine Farbe.
*/ */
public Color( Color color ) { public Color( Color color ) {
this(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()); this(color.getRGBA(), true);
} }
/** /**
* Erstellt eine Farbe als Kopie von <var>color</var> und ersetzt den * Erstellt eine Farbe als Kopie von {@code color} und ersetzt den
* Transparentwert durch <var>alpha</var>. * Transparentwert durch {@code alpha}.
* *
* @param color * @param color Eine Farbe.
* @param alpha * @param alpha Der neue Transparenzwert.
*/ */
public Color( Color color, int alpha ) { public Color( Color color, int alpha ) {
this(color.getRed(), color.getGreen(), color.getBlue(), alpha); this(color.getRed(), color.getGreen(), color.getBlue(), alpha);
@@ -198,46 +238,97 @@ public class Color {
/** /**
* Erzeugt eine Farbe aus einem kodierten RGBA Integer-Wert. * Erzeugt eine Farbe aus einem kodierten RGBA Integer-Wert.
* <p>
* Der 32-bit Integer enthält (von rechts) in Bit 1 bis 8 den Rotwert, in
* Bit 9 bis 16 Grünwert, in Bit 17 bis 24 den Blauwert und in Bit 25 bis 32
* den Transparenzwert der Farbe.
* *
* @param rgba * @param rgba Eine RGBA-Farbe.
* @return * @return Ein Farbobjekt.
*/ */
public static Color getRGBColor( int rgba ) { public static Color getRGBColor( int rgba ) {
Color c = new Color(rgba, true); return new Color(rgba, true);
return c;
} }
/**
* Erzeugt eine Farbe aus Werten im
* <a href="https://de.wikipedia.org/wiki/HSV-Farbraum">HSB-Farbraum</a>.
* <p>
* {code h} beschreibt den Farbwert (engl. <em>hue</em>), {@code s} die
* Sättigung (engl. <em>saturation</em>) und {@code b} die absolute
* Helligkeit (engl. <em>brightness</em>) der Farbe. Alle Werte werden
* zwischen 0.0 und 1.0 angegeben.
*
* @param h Der Farbwert.
* @param s Die Sättigung.
* @param b Die absolute Helligkeit.
* @return Ein Farbobjekt.
* @see java.awt.Color#getHSBColor(float, float, float)
*/
public static Color getHSBColor( double h, double s, double b ) { public static Color getHSBColor( double h, double s, double b ) {
return new Color(java.awt.Color.getHSBColor((float) h, (float) s, (float) b)); return new Color(java.awt.Color.getHSBColor((float) h, (float) s, (float) b));
} }
/**
* Erzeugt eine Farbe aus Werten im
* <a href="https://de.wikipedia.org/wiki/HSV-Farbraum">HSL-Farbraum</a>.
* <p>
* {code h} beschreibt den Farbwert (engl. <em>hue</em>), {@code s} die
* Sättigung (engl. <em>saturation</em>) und {@code l} die relative
* Helligkeit (engl. <em>lightness</em>) der Farbe. Alle Werte werden
* zwischen 0.0 und 1.0 angegeben.
*
* @param h Der Farbwert.
* @param s Die Sättigung.
* @param l Die relative Helligkeit.
* @return Ein Farbobjekt.
*/
public static Color getHSLColor( double h, double s, double l ) { public static Color getHSLColor( double h, double s, double l ) {
int rgb = Color.HSLtoRGB(new float[]{(float) h, (float) s, (float) l}); int rgb = Color.HSLtoRGB(new float[]{(float) h, (float) s, (float) l});
return Color.getRGBColor(rgb); return Color.getRGBColor(rgb);
} }
public static Color parseString( String pColor ) { /**
pColor = pColor.toLowerCase().strip(); * Erstellt aus einem Farbnamen ein Farbobjekt.
if( pColor.contains("red") || pColor.contains("rot") ) { * <p>
return Color.RED.copy(); * Die gültigen Farbnamen können unter <a
} else if( pColor.contains("blue") || pColor.contains("blau") ) { * href="https://htmlcolors.com/color-names">https://htmlcolors.com/color-names</a>
return Color.BLUE.copy(); * nachgeschlagen werden.
} else if( pColor.contains("green") || pColor.contains("grün") || pColor.contains("gruen") ) { *
return Color.GREEN.copy(); * @param color Der Name einer Farbe.
} else if( pColor.contains("yellow") || pColor.contains("gelb") ) { * @return Ein Farbobjekt.
return Color.YELLOW.copy(); */
} else { public static Color parseString( String color ) {
return new Color(); color = color.toLowerCase().strip();
// Parse colornames file and return first match
try( InputStream in = Color.class.getResourceAsStream("colornames.csv"); BufferedReader reader = new BufferedReader(new InputStreamReader(in)) ) {
String line;
while( (line = reader.readLine()) != null ) {
String[] parts = line.split(",");
if( parts.length == 2 ) {
if( parts[0].equals(color) ) {
return Color.parseHexcode(parts[1]);
}
}
}
} catch( IOException ex ) {
// LOG?
} }
return new Color();
} }
/** /**
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann sechs- * Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann drei-,
* oder achtstellig sein (wenn ein Transparentwert vorhanden ist). Dem Code * sechs- oder achtstellig sein (wenn ein Transparentwert vorhanden ist).
* kann ein {@code #} Zeichen vorangestellt sein. * Dem Code kann ein {@code #} Zeichen vorangestellt sein, muss es aber
* nicht.
* <p>
* Bei einem dreistelligen Code wird jedes zeichen doppelt interpretiert.
* Das beduetet {@code #ABC} ist gleichbedeutend mit {@code #AABBCC}.
* *
* @param hexcode * @param hexcode Eine Farbe als Hexcode.
* @return * @return Ein Farbobjekt.
*/ */
public static Color parseHexcode( String hexcode ) { public static Color parseHexcode( String hexcode ) {
if( hexcode.startsWith("#") ) { if( hexcode.startsWith("#") ) {
@@ -254,8 +345,6 @@ public class Color {
} else if( hexcode.length() == 8 ) { } else if( hexcode.length() == 8 ) {
alpha = Integer.valueOf(hexcode.substring(6, 8), 16); alpha = Integer.valueOf(hexcode.substring(6, 8), 16);
hexcode = hexcode.substring(0, 6); hexcode = hexcode.substring(0, 6);
} else {
hexcode = hexcode;
} }
return Color.getRGBColor((alpha << 24) | Integer.valueOf(hexcode, 16)); return Color.getRGBColor((alpha << 24) | Integer.valueOf(hexcode, 16));
@@ -276,7 +365,7 @@ public class Color {
if( color1 == null && color2 == null ) { if( color1 == null && color2 == null ) {
throw new IllegalArgumentException("Color.interpolate() needs at least one color to be not null."); throw new IllegalArgumentException("Color.interpolate() needs at least one color to be not null.");
} }
if( t < 0.0 || color2 == null ) { if( (color1 != null && t < 0.0) || color2 == null ) {
return color1.copy(); return color1.copy();
} }
if( t > 1.0 || color1 == null ) { if( t > 1.0 || color1 == null ) {
@@ -302,12 +391,12 @@ public class Color {
if( c == 0 ) { if( c == 0 ) {
h_ = 0; h_ = 0;
} else if( max == r ) { } else if( max == r ) {
h_ = (float) (g - b) / c; h_ = (g - b) / c;
if( h_ < 0 ) h_ += 6.f; if( h_ < 0 ) h_ += 6.f;
} else if( max == g ) { } else if( max == g ) {
h_ = (float) (b - r) / c + 2.f; h_ = (b - r) / c + 2.f;
} else if( max == b ) { } else if( max == b ) {
h_ = (float) (r - g) / c + 4.f; h_ = (r - g) / c + 4.f;
} }
float h = 60.f * h_; float h = 60.f * h_;
@@ -326,6 +415,14 @@ public class Color {
return hsl; return hsl;
} }
/**
* Konvertiert die Komponenten einer Farbe aus dem HSL-Farbraum in den
* RGB-Farbraum.
*
* @param hsl Die HSL-Komponenten als float-Array.
* @return Der RGBA-Farbwert.
* @see #HSLtoRGB(float[], int)
*/
public static int HSLtoRGB( float[] hsl ) { public static int HSLtoRGB( float[] hsl ) {
return HSLtoRGB(hsl, 255); return HSLtoRGB(hsl, 255);
} }
@@ -334,9 +431,9 @@ public class Color {
* Konvertiert eine Farbe mit Komponenten im HSL-Farbraum in den * Konvertiert eine Farbe mit Komponenten im HSL-Farbraum in den
* RGB-Farbraum. * RGB-Farbraum.
* <p> * <p>
* Die Farbkomponenten werden als Float-Array übergeben. Im Index 0 steht * Die Farbkomponenten werden als float-Array übergeben. Im Index 0 steht
* der h-Wert im Bereich 0 bis 360, Index 1 und 2 enthalten den s- und * der H-Wert im Bereich 0 bis 360, Index 1 und 2 enthalten den S- und
* l-Wert im Bereich von 0 bis 1. * L-Wert im Bereich von 0 bis 1.
* *
* @param hsl Die Farbkomponenten im HSL-Farbraum. * @param hsl Die Farbkomponenten im HSL-Farbraum.
* @param alpha Ein Transparenzwert im Bereich 0 bis 255. * @param alpha Ein Transparenzwert im Bereich 0 bis 255.
@@ -463,17 +560,37 @@ public class Color {
} }
@Override @Override
public PaintContext createContext( ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints ) {
return getJavaColor().createContext(cm, deviceBounds, userBounds, xform, hints);
}
@Override
public int getTransparency() {
int alpha = getAlpha();
if( alpha == 0xff ) {
return Transparency.OPAQUE;
} else if( alpha == 0 ) {
return Transparency.BITMASK;
} else {
return Transparency.TRANSLUCENT;
}
}
/** /**
* Prüft, ob ein anderes Objekt zu diesem gleich ist. * Prüft, ob ein anderes Objekt zu diesem gleich ist.
* <p>
* Die Methode gibt genau dann {@code true} zurück, wenn das andere Objekt
* nicht {@code null} ist, vom Typ {@code Color} ist und es dieselben Rot-,
* Grün-, Blau- und Transparenzwerte hat.
* *
* Die Methode gibt genau dann {@code true} zurück, wenn das andere
* Objekt nicht {@code null} ist, vom Typ {@code Color} ist und es
* dieselben Rot-, Grün-, Blau- und Transparenzwerte hat.
* @param obj Das zu vergleichende Objekt. * @param obj Das zu vergleichende Objekt.
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}. * @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/ */
@Override
public boolean equals( Object obj ) { public boolean equals( Object obj ) {
if( obj == null ) { return false; } if( obj == null ) {
return false;
}
if( obj instanceof Color ) { if( obj instanceof Color ) {
return ((Color) obj).getRGBA() == this.rgba; return ((Color) obj).getRGBA() == this.rgba;
} else if( obj instanceof java.awt.Color ) { } else if( obj instanceof java.awt.Color ) {

View File

@@ -1,55 +0,0 @@
package schule.ngb.zm;
public class ColorLayer extends Layer {
private Color background;
public ColorLayer( Color color ) {
this.background = color;
clear();
}
public ColorLayer( int width, int height, Color color ) {
super(width, height);
this.background = color;
clear();
}
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
clear();
}
public Color getColor() {
return background;
}
public void setColor( Color color ) {
background = color;
clear();
}
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
@Override
public void clear() {
drawing.setColor(background.getJavaColor());
drawing.fillRect(0, 0, getWidth(), getHeight());
}
}

View File

@@ -1,23 +1,27 @@
package schule.ngb.zm; package schule.ngb.zm;
import schule.ngb.zm.anim.Easing; import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.util.ImageLoader; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Noise; import schule.ngb.zm.util.Noise;
import java.awt.Cursor; import java.awt.Cursor;
import java.awt.Font;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random; import java.util.Random;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
/** /**
* Basisklasse für die meisten Objekte der Zeichemaschine, die von Nutzern * Basisklasse für die meisten Objekte der Zeichenmaschine, die von Nutzern
* erweitert werden können. * erweitert werden können.
* <p> * <p>
* Die Konstanten stellen viele Funktionen zur einfachen Programmierung bereit * Die Konstanten stellen viele Funktionen zur einfachen Programmierung bereit
* und enthält auch einige dynamische Werte, die von der Zeichenmaschine laufend * und enthält auch einige dynamische Werte, die von der Zeichenmaschine laufend
* aktuell gehalten werden (beispielsweise {@link #runtime}. * aktuell gehalten werden (beispielsweise {@link #runtime}).
* <p> * <p>
* Für die Implementierung eigener Klassen ist es oft hilfreich von * Für die Implementierung eigener Klassen ist es oft hilfreich von
* {@code Constants} zu erben, um die Methoden und Konstanten einfach im * {@code Constants} zu erben, um die Methoden und Konstanten einfach im
@@ -64,53 +68,89 @@ public class Constants {
/** /**
* Patchversion der Zeichenmaschine. * Patchversion der Zeichenmaschine.
*/ */
public static final int APP_VERSION_REV = 22; public static final int APP_VERSION_REV = 32;
/** /**
* Version der Zeichenmaschine als Text-String. * Version der Zeichenmaschine als Text-String.
*/ */
public static final String APP_VERSION = APP_VERSION_MAJ + "." + APP_VERSION_MIN + "." + APP_VERSION_REV; public static final String APP_VERSION = APP_VERSION_MAJ + "." + APP_VERSION_MIN + "." + APP_VERSION_REV;
/**
* Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde.
*/
public static final boolean MACOS;
/**
* Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde.
*/
public static final boolean WINDOWS;
/**
* Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde.
*/
public static final boolean LINUX;
static {
final String name = System.getProperty("os.name");
if( name.contains("Mac") ) {
MACOS = true;
WINDOWS = false;
LINUX = false;
} else if( name.contains("Windows") ) {
MACOS = false;
WINDOWS = true;
LINUX = false;
} else if( name.equals("Linux") ) { // true for the ibm vm
MACOS = false;
WINDOWS = false;
LINUX = true;
} else {
MACOS = false;
WINDOWS = false;
LINUX = false;
}
}
/** /**
* Standardbreite eines Zeichenfensters. * Standardbreite eines Zeichenfensters.
*/ */
public static final int STD_WIDTH = 400; public static final int DEFAULT_WIDTH = 400;
/** /**
* Standardhöhe eines Zeichenfensters. * Standardhöhe eines Zeichenfensters.
*/ */
public static final int STD_HEIGHT = 400; public static final int DEFAULT_HEIGHT = 400;
/** /**
* Standardwert für die Frames pro Sekunde einer Zeichenmaschine. * Standardwert für die Frames pro Sekunde einer Zeichenmaschine.
*/ */
public static final int STD_FPS = 60; public static final int DEFAULT_FPS = 60;
/** /**
* Standardfarbe der Füllungen. * Standardfarbe der Füllungen.
*/ */
public static final Color STD_FILLCOLOR = Color.WHITE; public static Color DEFAULT_FILLCOLOR = Color.WHITE;
/** /**
* Standardfarbe der Konturen. * Standardfarbe der Konturen.
*/ */
public static final Color STD_STROKECOLOR = Color.BLACK; public static Color DEFAULT_STROKECOLOR = Color.BLACK;
/** /**
* Standardwert für die Dicke der Konturen. * Standardwert für die Dicke der Konturen.
*/ */
public static final double STD_STROKEWEIGHT = 1.0; public static double DEFAULT_STROKEWEIGHT = 1.0;
/** /**
* Standardwert für die Schriftgröße. * Standardwert für die Schriftgröße.
*/ */
public static final int STD_FONTSIZE = 14; public static int DEFAULT_FONTSIZE = 14;
/** /**
* Standardwert für den Abstand von Formen. * Standardwert für den Abstand von Formen.
*/ */
public static final int STD_BUFFER = 10; public static int DEFAULT_BUFFER = 10;
public static int DEFAULT_ANIM_RUNTIME = 1000; public static int DEFAULT_ANIM_RUNTIME = 1000;
@@ -405,16 +445,32 @@ public class Constants {
*/ */
public static final double TWO_PI = Math.PI * 2.0; public static final double TWO_PI = Math.PI * 2.0;
/**
* Konstante für fette Schrift.
*/
public static final int BOLD = Font.BOLD;
/**
* Konstante für kursive Schrift.
*/
public static final int ITALIC = Font.ITALIC;
/**
* Konstante für normale Schrift.
*/
public static final int PLAIN = Font.PLAIN;
/* /*
* Globale Variablen, die von allen Klassen genutzt werden dürfen. Änderungen * Globale Variablen, die von allen Klassen genutzt werden dürfen. Änderungen
* wirken sich auf die aktuelle Zeichenmaschine aus und sollten nur von der * wirken sich auf die aktuelle Zeichenmaschine aus und sollten nur von der
* Zeichenmaschine selbst vorgenommen werden. * Zeichenmaschine selbst vorgenommen werden.
*/ */
// TODO: (ngb) volatile ?
/** /**
* Aktuell dargestellte Bilder pro Sekunde. * Aktuell dargestellte Bilder pro Sekunde.
*/ */
public static int framesPerSecond = STD_FPS; public static int framesPerSecond = DEFAULT_FPS;
/** /**
* Anzahl der Ticks (Frames), die das Programm bisher läuft. * Anzahl der Ticks (Frames), die das Programm bisher läuft.
@@ -730,6 +786,30 @@ public class Constants {
// Mathematische Funktionen // Mathematische Funktionen
/**
* Berechnet das Minimum aller angegebenen Werte.
*
* <pre><code>
* int minimum = min(1, 5, 3); // 1
* </code></pre>
*
* @param numbers Die Werte, aus denen das Minimum ermittelt werden soll.
* @return Das Minimum der Werte.
* @throws IllegalArgumentException Wenn die Eingabe {@code null} oder leer
* ist.
*/
public static final int min( int... numbers ) {
if( numbers == null || numbers.length == 0 ) {
throw new IllegalArgumentException("Array may not be <null> or empty.");
}
int min = numbers[0];
for( int i = 1; i < numbers.length; i++ ) {
min = Math.min(min, numbers[i]);
}
return min;
}
/** /**
* Berechnet das Minimum aller angegebenen Werte. * Berechnet das Minimum aller angegebenen Werte.
* *
@@ -754,6 +834,30 @@ public class Constants {
return min; return min;
} }
/**
* Berechnet das Maximum aller angegebenen Werte.
*
* <pre><code>
* double maximum = max(1, 5, 3); // 5
* </code></pre>
*
* @param numbers Die Werte, aus denen das Maximum ermittelt werden soll.
* @return Das Maximum der Werte.
* @throws IllegalArgumentException Wenn die Eingabe {@code null} oder leer
* ist.
*/
public static final int max( int... numbers ) {
if( numbers == null || numbers.length == 0 ) {
throw new IllegalArgumentException("Array may not be <null> or empty.");
}
int max = numbers[0];
for( int i = 1; i < numbers.length; i++ ) {
max = Math.max(max, numbers[i]);
}
return max;
}
/** /**
* Berechnet das Maximum aller angegebenen Werte. * Berechnet das Maximum aller angegebenen Werte.
* *
@@ -999,7 +1103,7 @@ public class Constants {
} }
/** /**
* Ermittelt den Arkuskosinus der angegebenen Zahl. * Ermittelt den Arcuskosinus der angegebenen Zahl.
* *
* @param x Eine Zahl. * @param x Eine Zahl.
* @return {@code acos(x)}. * @return {@code acos(x)}.
@@ -1009,7 +1113,7 @@ public class Constants {
} }
/** /**
* Ermittelt den Arkusktangens der angegebenen Zahl. * Ermittelt den Arcusktangens der angegebenen Zahl.
* *
* @param x Eine Zahl. * @param x Eine Zahl.
* @return {@code atan(x)}. * @return {@code atan(x)}.
@@ -1149,6 +1253,12 @@ public class Constants {
return interpolate(toMin, toMax, (value - fromMin) / (fromMax - fromMin)); return interpolate(toMin, toMax, (value - fromMin) / (fromMax - fromMin));
} }
public static final double distance( double fromX, double fromY, double toX, double toY ) {
double diffX = toX - fromX;
double diffY = toY - fromY;
return sqrt(diffX*diffX + diffY*diffY);
}
/** /**
* Geteilte {@code Random}-Instanz für einheitliche Zufallszahlen. * Geteilte {@code Random}-Instanz für einheitliche Zufallszahlen.
*/ */
@@ -1269,7 +1379,8 @@ public class Constants {
} }
/** /**
* Erzeugt eine Pseudozufallszahl nach einer Gaussverteilung. * Erzeugt eine Pseudozufallszahl zwischen -1 und 1 nach einer
* Normalverteilung mit Mittelwert 0 und Standardabweichung 1.
* *
* @return Eine Zufallszahl. * @return Eine Zufallszahl.
* @see Random#nextGaussian() * @see Random#nextGaussian()
@@ -1278,6 +1389,26 @@ 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.
* *
@@ -1285,7 +1416,7 @@ public class Constants {
* @param <T> Datentyp des Elements. * @param <T> Datentyp des Elements.
* @return Ein zufälliges Element aus dem Array. * @return Ein zufälliges Element aus dem Array.
*/ */
public static final <T> T choice( T[] values ) { public static final <T> T choice( T... values ) {
return values[random(0, values.length - 1)]; return values[random(0, values.length - 1)];
} }
@@ -1294,15 +1425,149 @@ public class Constants {
* *
* @param values Ein Array mit Werten, die zur Auswahl stehen. * @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente. * @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/
public static final int[] choice( int[] values, int n, boolean unique ) {
if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
int[] result = new int[n];
int[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) {
int j = random(0, valuesCopy.length - 1);
int l = valuesCopy.length - 1;
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[l];
valuesCopy[l] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, l);
}
return result;
}
/**
* Wählt die angegebene Anzahl Elemente aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/
public static final double[] choice( double[] values, int n, boolean unique ) {
if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
double[] result = new double[n];
double[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) {
int j = random(0, valuesCopy.length - 1);
int l = valuesCopy.length - 1;
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[l];
valuesCopy[l] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, l);
}
return result;
}
/**
* Wählt die angegebene Anzahl Elemente aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @param <T> Datentyp der Elemente. * @param <T> Datentyp der Elemente.
* @return Ein zufälliges Element aus dem Array. * @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/ */
public static final <T> T[] choice( T[] values, int n ) { public static final <T> T[] choice( T[] values, int n, boolean unique ) {
Object[] result = new Object[n]; if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
T[] result = Arrays.copyOf(values, n);
T[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) { for( int i = 0; i < n; i++ ) {
result[i] = choice(values); int last = valuesCopy.length - 1;
int j = random(0, last);
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[last];
valuesCopy[last] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, last);
} }
return (T[]) result; return result;
}
/**
* Bringt die Zahlen im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Zahlen, die gemischt werden sollen.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final int[] shuffle( int[] values ) {
for( int i = 0; i < values.length - 1; i++ ) {
int j = random(i, values.length - 1);
int tmp = values[i];
values[i] = values[j];
values[j] = tmp;
}
return values;
}
/**
* Bringt die Zahlen im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Zahlen, die gemischt werden sollen.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final double[] shuffle( double[] values ) {
for( int i = 0; i < values.length - 1; i++ ) {
int j = random(i, values.length - 1);
double tmp = values[i];
values[i] = values[j];
values[j] = tmp;
}
return 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> T[] shuffle( T[] values ) {
java.util.List<T> valueList = Arrays.asList(values);
Collections.shuffle(valueList, random);
return valueList.toArray(values);
} }
/** /**
@@ -1471,38 +1736,93 @@ public class Constants {
} }
} }
/**
* Konvertiert einen char-Wert in einen double-Wert.
*
* @param value Der char-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( char value ) { public static final double asDouble( char value ) {
return (double) value; return value;
} }
/**
* Konvertiert einen byte-Wert in einen double-Wert.
*
* @param value Der byte-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( byte value ) { public static final double asDouble( byte value ) {
return value; return value;
} }
/**
* Konvertiert einen short-Wert in einen double-Wert.
*
* @param value Der short-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( short value ) { public static final double asDouble( short value ) {
return value; return value;
} }
/**
* Konvertiert einen long-Wert in einen double-Wert.
*
* @param value Der long-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( long value ) { public static final double asDouble( long value ) {
return (double) value; return (double) value;
} }
/**
* Konvertiert einen double-Wert in einen double-Wert.
*
* @param value Der double-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( double value ) { public static final double asDouble( double value ) {
return value; return value;
} }
/**
* Konvertiert einen float-Wert in einen double-Wert.
*
* @param value Der float-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( float value ) { public static final double asDouble( float value ) {
return value; return value;
} }
/**
* Konvertiert einen int-Wert in einen double-Wert.
*
* @param value Der int-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( int value ) { public static final double asDouble( int value ) {
return value; return value;
} }
/**
* Konvertiert einen boolean-Wert in einen double-Wert.
*
* @param value Der boolean-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( boolean value ) { public static final double asDouble( boolean value ) {
return value ? 1.0 : 0.0; return value ? 1.0 : 0.0;
} }
/**
* Konvertiert einen String in einen double-Wert.
*
* @param value Der String.
* @return Ein double-Wert.
* @see Double#parseDouble(String)
*/
public static final double asDouble( String value ) { public static final double asDouble( String value ) {
try { try {
return Double.parseDouble(value); return Double.parseDouble(value);
@@ -1511,38 +1831,93 @@ public class Constants {
} }
} }
/**
* Konvertiert einen char-Wert in einen boolean-Wert.
*
* @param value Der char-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( char value ) { public static final boolean asBool( char value ) {
return value != 0; return value != 0;
} }
/**
* Konvertiert einen byte-Wert in einen boolean-Wert.
*
* @param value Der byte-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( byte value ) { public static final boolean asBool( byte value ) {
return value != 0; return value != 0;
} }
/**
* Konvertiert einen short-Wert in einen boolean-Wert.
*
* @param value Der short-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( short value ) { public static final boolean asBool( short value ) {
return value != 0; return value != 0;
} }
/**
* Konvertiert einen int-Wert in einen boolean-Wert.
*
* @param value Der int-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( int value ) { public static final boolean asBool( int value ) {
return value != 0; return value != 0;
} }
/**
* Konvertiert einen long-Wert in einen boolean-Wert.
*
* @param value Der long-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( long value ) { public static final boolean asBool( long value ) {
return value != 0L; return value != 0L;
} }
/**
* Konvertiert einen double-Wert in einen boolean-Wert.
*
* @param value Der double-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( double value ) { public static final boolean asBool( double value ) {
return value != 0.0; return value != 0.0;
} }
/**
* Konvertiert einen float-Wert in einen boolean-Wert.
*
* @param value Der float-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( float value ) { public static final boolean asBool( float value ) {
return value != 0.0f; return value != 0.0f;
} }
/**
* Konvertiert einen boolean-Wert in einen boolean-Wert.
*
* @param value Der boolean-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( boolean value ) { public static final boolean asBool( boolean value ) {
return value; return value;
} }
/**
* Konvertiert einen String in einen boolean-Wert.
*
* @param value Der String.
* @return Ein boolean-Wert.
* @see Boolean#parseBoolean(String)
*/
public static final boolean asBool( String value ) { public static final boolean asBool( String value ) {
return Boolean.parseBoolean(value); return Boolean.parseBoolean(value);
} }
@@ -1603,7 +1978,7 @@ public class Constants {
return Integer.valueOf(binary, 16); return Integer.valueOf(binary, 16);
} }
// Konstants für Key events (Copied from KeyEvent) // Konstanten für Key events (Copied from KeyEvent)
/** /**
* Constant for the ENTER virtual key. * Constant for the ENTER virtual key.

View File

@@ -3,14 +3,29 @@ package schule.ngb.zm;
import java.awt.*; import java.awt.*;
/** /**
* Zeichenbare Objekte können auf eine Zeichenfläche gezeichnet werden. * {@code Drawable} Objekte können auf eine Zeichenfläche gezeichnet werden. In
* In der Regel werden sie einmal pro Frame gezeichnet. * der Regel werden sie einmal pro Frame gezeichnet.
*/ */
public interface Drawable { public interface Drawable {
/** /**
* Gibt an, ob das Objekt derzeit sichtbar ist (also gezeichnet werden * Gibt an, ob das Objekt derzeit sichtbar ist (also gezeichnet werden
* muss). * muss).
* <p>
* Wie mit dieser Information umgegangen wird, ist nicht weiter festgelegt.
* In der Regel sollte eine aufrufende Instanz zunächst prüfen, ob das
* Objekt aktiv ist, und nur dann{@link #draw(Graphics2D)} aufrufen. Für
* implementierende Klassen ist es aber gegebenenfalls auch sinnvoll, bei
* Inaktivität den Aufruf von {@code draw(Graphics2D)} schnell abzubrechen:
* <pre><code>
* void draw( Graphics2D graphics ) {
* if( !isVisible() ) {
* return;
* }
*
* // Objekt zeichnen..
* }
* </code></pre>
* *
* @return {@code true}, wenn das Objekt sichtbar ist. * @return {@code true}, wenn das Objekt sichtbar ist.
*/ */
@@ -18,7 +33,7 @@ public interface Drawable {
/** /**
* Wird aufgerufen, um das Objekt auf die Zeichenfläche <var>graphics</var> * Wird aufgerufen, um das Objekt auf die Zeichenfläche <var>graphics</var>
* zu draw. * zu zeichnen.
* <p> * <p>
* Das Objekt muss dafür Sorge tragen, dass der Zustand der Zeichenfläche * Das Objekt muss dafür Sorge tragen, dass der Zustand der Zeichenfläche
* (Transformationsmatrix, Farbe, ...) erhalten bleibt. Das Objekt sollte * (Transformationsmatrix, Farbe, ...) erhalten bleibt. Das Objekt sollte

View File

@@ -1,56 +0,0 @@
package schule.ngb.zm;
import java.awt.*;
import java.util.LinkedList;
public class DrawableLayer extends Layer {
protected LinkedList<Drawable> drawables = new LinkedList<>();
protected boolean clearBeforeDraw = true;
public DrawableLayer() {
}
public DrawableLayer( int width, int height ) {
super(width, height);
}
public void add( Drawable... drawables ) {
synchronized( drawables ) {
for( Drawable d : drawables ) {
this.drawables.add(d);
}
}
}
public java.util.List<Drawable> getDrawables() {
return drawables;
}
public boolean isClearBeforeDraw() {
return clearBeforeDraw;
}
public void setClearBeforeDraw( boolean pClearBeforeDraw ) {
this.clearBeforeDraw = pClearBeforeDraw;
}
@Override
public void draw( Graphics2D pGraphics ) {
if( clearBeforeDraw ) {
clear();
}
synchronized( drawables ) {
for( Drawable d : drawables ) {
if( d.isVisible() ) {
d.draw(drawing);
}
}
}
super.draw(pGraphics);
}
}

View File

@@ -1,560 +0,0 @@
package schule.ngb.zm;
import schule.ngb.zm.util.ImageLoader;
import java.awt.*;
import java.awt.geom.*;
import java.util.Stack;
public class DrawingLayer extends Layer {
protected Color fillColor = STD_FILLCOLOR;
protected Color strokeColor = STD_STROKECOLOR;
protected double strokeWeight = STD_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
private Options.Direction default_anchor = CENTER;
protected Line2D.Double line = new Line2D.Double();
protected Ellipse2D.Double ellipse = new Ellipse2D.Double();
protected Rectangle2D.Double rect = new Rectangle2D.Double();
protected Arc2D.Double arc = new Arc2D.Double();
protected Path2D.Double path = new Path2D.Double();
private boolean pathStarted = false;
private Stack<AffineTransform> transformStack = new Stack<>();
private FontMetrics fontMetrics = null;
public DrawingLayer() {
super();
transformStack.push(new AffineTransform());
fontMetrics = drawing.getFontMetrics();
}
public DrawingLayer( int width, int height ) {
super(width, height);
transformStack.push(new AffineTransform());
fontMetrics = drawing.getFontMetrics();
}
public Color getColor() {
return fillColor;
}
public void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
public void setFillColor( Color color ) {
fillColor = color;
drawing.setColor(color.getJavaColor());
}
public void noFill() {
fillColor = null;
}
public void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
public void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
public Color getStrokeColor() {
return strokeColor;
}
public void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
public void setStrokeColor( Color color ) {
strokeColor = color;
drawing.setColor(color.getJavaColor());
}
public void noStroke() {
strokeColor = null;
}
public void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
public void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
public void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
public void setStrokeWeight( double pWeight ) {
strokeWeight = pWeight;
drawing.setStroke(createStroke());
}
protected Stroke createStroke() {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
}
}
public Options.StrokeType getStrokeType() {
return strokeType;
}
public void setStrokeType( Options.StrokeType type ) {
switch( type ) {
case DASHED:
this.strokeType = DASHED;
break;
case DOTTED:
this.strokeType = DOTTED;
break;
default:
this.strokeType = SOLID;
break;
}
}
public void resetStroke() {
setStrokeColor(STD_STROKECOLOR);
setStrokeWeight(STD_STROKEWEIGHT);
setStrokeType(SOLID);
}
public void setAnchor( Options.Direction anchor ) {
default_anchor = anchor;
}
public void clear( int gray ) {
clear(gray, gray, gray, 255);
}
public void clear( int gray, int alpha ) {
clear(gray, gray, gray, alpha);
}
public void clear( int red, int green, int blue ) {
clear(red, green, blue, 255);
}
public void clear( int red, int green, int blue, int alpha ) {
clear(new Color(red, green, blue, alpha));
}
public void clear( Color pColor ) {
/*graphics.setBackground(pColor);
graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());*/
java.awt.Color currentColor = drawing.getColor();
pushMatrix();
resetMatrix();
drawing.setColor(pColor.getJavaColor());
drawing.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
drawing.setColor(currentColor);
popMatrix();
}
public void line( double x1, double y1, double x2, double y2 ) {
//Shape line = new Line2D.Double(x1, y1, x2, y2);
line.setLine(x1, y1, x2, y2);
drawShape(line);
}
public void pixel( double x, double y ) {
// square(x, y, 1);
buffer.setRGB((int)x, (int)y, fillColor.getRGBA());
}
public void square( double x, double y, double w ) {
rect(x, y, w, w);
}
public void square( double x, double y, double w, Options.Direction anchor ) {
rect(x, y, w, w, anchor);
}
public void rect( double x, double y, double w, double h ) {
rect(x, y, w, h, default_anchor);
}
public void rect( double x, double y, double w, double h, Options.Direction anchor ) {
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, anchor);
// Shape rect = new Rectangle2D.Double(anchorPoint.getX(), anchorPoint.getY(), w, h);
rect.setRect(anchorPoint.getX(), anchorPoint.getY(), w, h);
fillShape(rect);
drawShape(rect);
}
public void point( double x, double y ) {
circle(x - 1, y - 1, 2);
}
public void circle( double x, double y, double r ) {
ellipse(x, y, r+r, r+r, default_anchor);
}
public void circle( double x, double y, double r, Options.Direction anchor ) {
ellipse(x, y, r+r, r+r, anchor);
}
public void ellipse( double x, double y, double w, double h ) {
ellipse(x, y, w, h, default_anchor);
}
public void ellipse( double x, double y, double w, double h, Options.Direction anchor ) {
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, anchor);
// Shape ellipse = new Ellipse2D.Double(anchorPoint.x, anchorPoint.y, w, h);
ellipse.setFrame(anchorPoint.x, anchorPoint.y, w, h);
fillShape(ellipse);
drawShape(ellipse);
}
public void arc( double x, double y, double r, double angle1, double angle2 ) {
arc(x, y, r+r, r+r, angle1, angle2);
}
public void arc( double x, double y, double w, double h, double angle1, double angle2 ) {
while( angle2 < angle1 ) angle2 += 360.0;
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, CENTER);
/*Shape arc = new Arc2D.Double(
anchorPoint.x,
anchorPoint.y,
d, d,
angle1, angle2 - angle1,
Arc2D.OPEN
);*/
arc.setArc(
anchorPoint.x, anchorPoint.y,
w, h,
//Math.toRadians(angle1), Math.toRadians(angle2 - angle1),
angle1, angle2-angle1,
Arc2D.OPEN
);
drawShape(arc);
}
public void pie( double x, double y, double r, double angle1, double angle2 ) {
while( angle2 < angle1 ) angle2 += 360.0;
double d = r+r;
Point2D.Double anchorPoint = getAnchorPoint(x, y, d, d, CENTER);
/*Shape arc = new Arc2D.Double(
anchorPoint.x,
anchorPoint.y,
d, d,
angle1, angle2 - angle1,
Arc2D.PIE
);*/
arc.setArc(
anchorPoint.x, anchorPoint.y,
d, d,
angle1, angle2 - angle1,
Arc2D.PIE
);
fillShape(arc);
drawShape(arc);
}
public void curve( double x1, double y1, double x2, double y2, double x3, double y3 ) {
QuadCurve2D curve = new QuadCurve2D.Double(x1, y1, x2, y2, x3, y3);
drawShape(curve);
}
public void curve( double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4 ) {
CubicCurve2D curve = new CubicCurve2D.Double(x1, y1, x2, y2, x3, y3, x4, y4);
drawShape(curve);
}
public void triangle( double x1, double y1, double x2, double y2, double x3, double y3 ) {
Path2D path = new Path2D.Double();
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.lineTo(x1, y1);
fillShape(path);
drawShape(path);
}
public void rhombus( double x, double y, double width, double height ) {
rhombus(x, y, width, height, default_anchor);
}
public void rhombus( double x, double y, double width, double height, Options.Direction anchor ) {
double whalf = width / 2.0, hhalf = height / 2.0;
Point2D.Double anchorPoint = getAnchorPoint(x, y, width, height, anchor);
polygon(anchorPoint.x + whalf, anchorPoint.y, anchorPoint.x + width, anchorPoint.y + hhalf, anchorPoint.x + whalf, anchorPoint.y + height, anchorPoint.x, anchorPoint.y + hhalf);
}
public void polygon( double... coordinates ) {
if( coordinates.length < 4 ) {
return;
}
Path2D path = new Path2D.Double();
path.moveTo(coordinates[0], coordinates[1]);
for( int i = 2; i < coordinates.length; i += 2 ) {
if( i + 1 < coordinates.length ) {
path.lineTo(coordinates[i], coordinates[i + 1]);
}
}
int len = coordinates.length;
if( coordinates[len - 2] != coordinates[0] || coordinates[len - 1] != coordinates[1] ) {
path.lineTo(coordinates[0], coordinates[1]);
}
fillShape(path);
drawShape(path);
}
public void polygon( Point2D... points ) {
if( points.length < 2 ) {
return;
}
Path2D path = new Path2D.Double();
path.moveTo(points[0].getX(), points[0].getY());
for( int i = 1; i < points.length; i += 1 ) {
path.lineTo(points[i].getX(), points[i].getY());
}
int len = points.length;
if( points[len - 1].equals(points[0]) ) {
path.moveTo(points[0].getX(), points[0].getY());
}
fillShape(path);
drawShape(path);
}
// TODO: General shape drawing
/*
public void beginShape() {
path.reset();
pathStarted = false;
}
public void lineTo( double x, double y ) {
if( !pathStarted ) {
path.moveTo(x, y);
pathStarted = true;
} else {
path.lineTo(x, y);
}
}
public void endShape() {
path.closePath();
path.trimToSize();
pathStarted = false;
fillShape(path);
drawShape(path);
}
*/
public void image( String imageSource, double x, double y ) {
image(ImageLoader.loadImage(imageSource), x, y, 1.0, default_anchor);
}
public void image( String imageSource, double x, double y, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, 1.0, anchor);
}
public void image( String imageSource, double x, double y, double scale ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, default_anchor);
}
public void image( String imageSource, double x, double y, double scale, Options.Direction anchor ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, anchor);
}
public void image( Image image, double x, double y ) {
image(image, x, y, 1.0, default_anchor);
}
public void image( Image image, double x, double y, double scale ) {
image(image, x, y, scale, default_anchor);
}
public void image( Image image, double x, double y, double scale, Options.Direction anchor ) {
if( image != null ) {
double neww = image.getWidth(null) * scale;
double newh = image.getHeight(null) * scale;
Point2D.Double anchorPoint = getAnchorPoint(x, y, neww, newh, anchor);
drawing.drawImage(image, (int) anchorPoint.x, (int) anchorPoint.y, (int) neww, (int) newh, null);
}
}
public void text( String text, double x, double y ) {
text(text, x, y, default_anchor);
}
public void text( String text, double x, double y, Options.Direction anchor ) {
FontMetrics fm = drawing.getFontMetrics();
Point2D.Double anchorPoint = getAnchorPoint(x, y + fm.getAscent(), fm.stringWidth(text), fm.getHeight(), anchor);
drawing.drawString(text, (float) anchorPoint.x, (float) anchorPoint.y);
}
public void setFontSize( int size ) {
setFont(drawing.getFont().deriveFont((float)size));
}
public void setFont( String fontName ) {
Font font = new Font(fontName, drawing.getFont().getStyle(), drawing.getFont().getSize());
setFont(font);
}
public void setFont( String fontName, int size ) {
Font font = new Font(fontName, drawing.getFont().getStyle(), size);
setFont(font);
}
public void setFont( String fontName, int size, int style ) {
Font font = new Font(fontName, style, size);
setFont(font);
}
public void setFont( Font font ) {
drawing.setFont(font);
fontMetrics = drawing.getFontMetrics();
}
private Point2D.Double transformToCanvas( double x, double y ) {
return transformToCanvas(new Point2D.Double(x,y));
}
private Point2D.Double transformToCanvas( Point2D.Double pPoint ) {
AffineTransform matrix = getMatrix();
matrix.transform(pPoint, pPoint);
return pPoint;
}
private Point2D.Double transformToUser( double x, double y ) {
return transformToUser(new Point2D.Double(x,y));
}
private Point2D.Double transformToUser( Point2D.Double pPoint ) {
AffineTransform matrix = getMatrix();
try {
matrix.inverseTransform(pPoint, pPoint);
} catch( NoninvertibleTransformException e ) {
e.printStackTrace();
}
return pPoint;
}
private Point2D.Double getAnchorPoint( double x, double y, double w, double h, Options.Direction anchor ) {
double whalf = w * .5, hhalf = h * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(x - whalf, y - hhalf);
if( NORTH.in(anchor) ) {
anchorPoint.y += hhalf;
}
if( SOUTH.in(anchor) ) {
anchorPoint.y -= hhalf;
}
if( WEST.in(anchor) ) {
anchorPoint.x += whalf;
}
if( EAST.in(anchor) ) {
anchorPoint.x -= whalf;
}
return anchorPoint;
}
private void fillShape( Shape pShape ) {
if( fillColor != null && fillColor.getAlpha() > 0.0 ) {
drawing.setColor(fillColor.getJavaColor());
drawing.fill(pShape);
}
}
private void drawShape( Shape pShape ) {
if( strokeColor != null && strokeColor.getAlpha() > 0.0
&& strokeWeight > 0.0 ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.draw(pShape);
}
}
public void translate( double dx, double dy ) {
drawing.translate(dx, dy);
}
public void scale( double factor ) {
drawing.scale(factor, factor);
}
public void rotate( double pAngle ) {
drawing.rotate(Math.toRadians(pAngle));
}
public void shear( double dx, double dy ) {
drawing.shear(dx, dy);
}
public AffineTransform getMatrix() {
return drawing.getTransform();
}
public void pushMatrix() {
transformStack.push(drawing.getTransform());
}
public void popMatrix() {
if( transformStack.isEmpty() ) {
resetMatrix();
} else {
drawing.setTransform(transformStack.pop());
}
}
public void resetMatrix() {
drawing.setTransform(new AffineTransform());
}
}

View File

@@ -0,0 +1,279 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.Shape;
import java.awt.*;
/**
* {@link Drawable} Klassen, die mit einer Füllung versehen werden können.
* <p>
* Das {@code Fillable} Interface dient hauptsächlich zur Vereinheitlichung der
* API für Füllungen. Durch Implementation wird sichergestellt, dass alle
* Objekte, die eine Füllung haben können, dieselben Methoden zur Verfügung
* stellen. Wenn eine {@link Shape} eine
* {@link Fillable#setFillColor(Color, int)} Methode hat, dann sollte auch eine
* {@link schule.ngb.zm.layers.TurtleLayer.Turtle} dieselbe Methode anbieten. Im
* Einzelfall kann es sinnvoll sein, weitere Methoden für Füllungen zur
* Verfügung zu stellen. Allerdings sollte davon nach Möglichkeit zugunsten
* einer einheitlichen API abgesehen werden.
* <p>
* Das Äquivalent für Konturlinien stellt {@link Strokeable} dar.
* <p>
* Im einfachsten Fall reicht es {@link #setFill(Paint)} und {@link #getFill()}
* zu implementieren. Die anderen Methoden besitzen Standardimplementierungen,
* die sich auf die beiden Methoden beziehen. Allerdings ist es in vielen Fällen
* sinnvoll, einige der Methoden gezielt zu überschreiben, um sie an spezifische
* Situationen anzupassen.
*/
public interface Fillable extends Drawable {
/**
* Setzt die Füllung direkt auf das angegebene {@code Paint}-Objekt.
*
* @param fill Die neue Füllung.
*/
void setFill( Paint fill );
/**
* Gibt die aktuell gesetzte Füllung zurück.
* <p>
* Die Art der Füllung kann anhand der Abfragen {@link #hasFillColor()} und
* {@link #hasGradient()} ermittelt werden.
*
* @return Die aktuelle Füllung.
*/
Paint getFill();
/**
* Gibt an, ob aktuell eine sichtbare Füllung konfiguriert ist.
* <p>
* Eine Füllung gilt als sichtbar, wenn eine nciht transparente Füllfarbe
* oder ein Farbverlauf eingestellt ist.
*
* @return {@code true}, wenn die Füllung sichtbar ist, {@code false} sonst.
*/
default boolean hasFill() {
return (hasFillColor() && getFillColor().getAlpha() > 0) || hasGradient();
}
/**
* Gibt an, ob eine Füllfarbe konfiguriert ist.
* <p>
* Im Gegensatz zu {@link #hasFill()} prüft die Methode <em>nicht</em>, ob
* die Füllfarbe transparent ist.
*
* @return {@code true}, wenn eine Füllfarbe gesetzt ist.
*/
default boolean hasFillColor() {
Paint fill = getFill();
return fill instanceof Color;
}
/**
* Gibt an, ob ein Farbverlauf konfiguriert ist.
*
* @return {@code true}, wenn ein Farbverlauf gesetzt ist.
*/
default boolean hasGradient() {
Paint fill = getFill();
return fill instanceof MultipleGradientPaint;
}
/**
* Gibt die aktuelle Füllfarbe der Form zurück.
*
* @return Die aktuelle Füllfarbe oder {@code null}.
*/
default Color getFillColor() {
if( hasFillColor() ) {
return (Color) getFill();
} else {
return null;
}
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @see Color
*/
default void setFillColor( Color color ) {
setFill(color);
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe und setzt die Transparenz
* auf den angegebenen Wert. 0 is komplett durchsichtig und 255 komplett
* deckend.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
default void setFillColor( Color color, int alpha ) {
setFillColor(new Color(color, alpha));
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität. 0
* entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
default void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität und
* dem angegebenen Transparenzwert. Der Grauwert 0 entspricht schwarz, 255
* entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
default void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
default void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
default void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Füllung der Form.
*/
default void noFill() {
setFillColor(null);
noGradient();
}
/**
* Setzt die Füllfarbe auf den Standardwert zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_FILLCOLOR
*/
default void resetFill() {
setFillColor(Constants.DEFAULT_FILLCOLOR);
noGradient();
}
/**
* Gibt den aktuellen Farbverlauf der Form zurück.
*
* @return Der aktuelle Farbverlauf oder {@code null}.
*/
default MultipleGradientPaint getGradient() {
if( hasGradient() ) {
return (MultipleGradientPaint) getFill();
} else {
return null;
}
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
default void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
setFill(new LinearGradientPaint(
(float) fromX, (float) fromY,
(float) toX, (float) toY,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()}
));
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der in die angegebene
* Richtung verläuft.
*
* @param from Farbe am Startpunkt.
* @param to Farbe am Endpunkt.
* @param dir Richtung des Farbverlaufs.
*/
default void setGradient( Color from, Color to, Options.Direction dir ) {
int whalf = (Constants.canvasWidth / 2);
int hhalf = (Constants.canvasHeight / 2);
setGradient(whalf - dir.x * whalf, hhalf - dir.y * hhalf, from, whalf + dir.x * whalf, hhalf + dir.y * hhalf, to);
}
/**
* Setzt die Füllung auf einen kreisförmigen (radialen) Farbverlauf, mit dem
* Zentrum im Punkt ({@code centerX}, {@code centerY}) und dem angegebenen
* Radius. Der Verlauf starte im Zentrum mit der Farbe {@code from} und
* endet am Rand des durch den Radius beschriebenen Kreises mit der Farbe
* {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
default void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
setFill(new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()}));
}
/**
* Setzt die Füllung auf einen kreisförmigen (radialen) Farbverlauf, der im
* Zentrum beginnt.
*
* @param from Farbe im Zentrum.
* @param to Farbe am Rand.
*/
default void setGradient( Color from, Color to ) {
int whalf = (Constants.canvasWidth / 2);
int hhalf = (Constants.canvasHeight / 2);
setGradient(whalf, hhalf, Math.min(whalf, hhalf), from, to);
}
/**
* Entfernt den Farbverlauf von der Form.
*/
default void noGradient() {
setFill(null);
}
}

View File

@@ -1,11 +0,0 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
public class GraphicsLayer extends Layer {
public Graphics2D getGraphics() {
return drawing;
}
}

View File

@@ -1,69 +0,0 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
import java.awt.Image;
import schule.ngb.zm.util.ImageLoader;
public class ImageLayer extends Layer {
protected Image image;
protected double x = 0;
protected double y = 0;
protected boolean redraw = true;
public ImageLayer(String source) {
image = ImageLoader.loadImage(source);
}
public ImageLayer(Image image) {
this.image = image;
}
public ImageLayer(int width, int height, Image image) {
super(width, height);
this.image = image;
}
public void setImage(Image image) {
this.image = image;
redraw = true;
}
public double getX() {
return x;
}
public void setX(double pX) {
this.x = pX;
redraw = true;
}
public double getY() {
return y;
}
public void setY(double pY) {
this.y = pY;
redraw = true;
}
@Override
public void clear() {
super.clear();
redraw = true;
}
@Override
public void draw(Graphics2D graphics) {
if (redraw && visible) {
drawing.drawImage(image, (int) x, (int) y, null);
redraw = false;
}
super.draw(graphics);
}
}

View File

@@ -6,33 +6,80 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
/**
* Basisklasse für Ebenen der {@link Zeichenleinwand}.
* <p>
* Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
* <p>
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer
* {@link Zeichenmaschine}. Sie erben von {@code Constants}, da neue Ebenentypen
* von Nutzern implementiert werden können.
*/
public abstract class Layer extends Constants implements Drawable, Updatable { public abstract class Layer extends Constants implements Drawable, Updatable {
/**
* Interner Puffer für die Ebene, der einmal pro Frame auf die
* Zeichenleinwand übertragen wird.
*/
protected BufferedImage buffer; protected BufferedImage buffer;
/**
* Der Grafikkontext der Ebene, der zum Zeichnen der Inhalte verwendet
* wird.
*/
protected Graphics2D drawing; protected Graphics2D drawing;
/**
* Ob die Ebene derzeit sichtbar ist.
*/
protected boolean visible = true; protected boolean visible = true;
/**
* Ob die Ebene aktiv ist, also {@link #update(double) Updates} empfangen
* soll.
*/
protected boolean active = true; protected boolean active = true;
/**
* Erstellt eine neue Ebene mit den Standardmaßen.
*/
public Layer() { public Layer() {
this(STD_WIDTH, STD_HEIGHT); this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
} }
/**
* Erstellt eine neue Ebene mit den angegebenen Maßen.
*
* @param width Die Breite der Ebene.
* @param height Die Höhe der Ebene.
*/
public Layer( int width, int height ) { public Layer( int width, int height ) {
createCanvas(width, height); createCanvas(width, height);
} }
/**
* @return Die Breite der Ebene.
*/
public int getWidth() { public int getWidth() {
return buffer.getWidth(); return buffer.getWidth();
} }
/**
* @return Die Höhe der Ebene.
*/
public int getHeight() { public int getHeight() {
return buffer.getHeight(); return buffer.getHeight();
} }
/**
* Ändert die Größe der Ebene auf die angegebenen Maße.
*
* @param width Die neue Breite.
* @param height Die neue Höhe.
*/
public void setSize( int width, int height ) { public void setSize( int width, int height ) {
if( buffer != null ) { if( buffer != null ) {
if( buffer.getWidth() != width || buffer.getHeight() != height ) { if( buffer.getWidth() != width || buffer.getHeight() != height ) {
@@ -44,14 +91,19 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
} }
/**
* Gibt die Ressourcen der Ebene frei.
*/
public void dispose() { public void dispose() {
drawing.dispose(); drawing.dispose();
drawing = null;
buffer = null;
} }
/** /**
* Erstellt einen neuen Puffer für die Ebene und konfiguriert diesen. * Erstellt einen neuen Puffer für die Ebene und konfiguriert diesen.
* *
* @param width Width des neuen Puffers. * @param width Breite des neuen Puffers.
* @param height Höhe des neuen Puffers. * @param height Höhe des neuen Puffers.
*/ */
private void createCanvas( int width, int height ) { private void createCanvas( int width, int height ) {
@@ -78,7 +130,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe und * Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe und
* kopiert den Inhalt des alten Puffers in den Neuen. * kopiert den Inhalt des alten Puffers in den Neuen.
* *
* @param width Width des neuen Puffers. * @param width Breite des neuen Puffers.
* @param height Höhe des neuen Puffers. * @param height Höhe des neuen Puffers.
*/ */
private void recreateCanvas( int width, int height ) { private void recreateCanvas( int width, int height ) {
@@ -102,41 +154,80 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
} }
/** /**
* Zeichnet den Puffer auf die Grafik-Instanz. * Zeichnet den Puffer auf den Grafikkontext.
* *
* @param pGraphics * @param graphics Der Grafikkontext, auf den gezeichnet wird.
*/ */
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D graphics ) {
if( visible ) { if( visible ) {
pGraphics.drawImage(buffer, 0, 0, null); graphics.drawImage(buffer, 0, 0, null);
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isVisible() { public boolean isVisible() {
return visible; return visible;
} }
/**
* Versteckt die Ebene.
*/
public void hide() { public void hide() {
visible = false; visible = false;
} }
public void view() { /**
* Zeigt die Ebene an, falls sie versteckt war.
*/
@SuppressWarnings( "unused" )
public void show() {
visible = true; visible = true;
} }
/**
* Versteckt oder zeigt die Ebene, je nachdem, welchen Zustand sie derzeit
* hat.
*/
@SuppressWarnings( "unused" )
public void toggle() { public void toggle() {
visible = !visible; visible = !visible;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void update( double delta ) { public void update( double delta ) {
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isActive() { public boolean isActive() {
return active; return active;
} }
/**
* Prüft, ob die angegebenen Koordinaten innerhalb der Ebene liegen, oder
* nicht.
* <p>
* Eine Koordinate liegt in der Ebene, wenn die {@code x}- und
* {@code y}-Koordinaten größer oder gleich Null und kleiner als die Breite
* bzw. Höhe der Ebene sind.
*
* @param x Die x-Koordinate.
* @param y Die y-Koordinate.
* @return {@code true}, wenn die Koordinaten innerhalb der Ebene liegen,
* {@code false}, wenn sie außerhalb liegen.
*/
@SuppressWarnings( "unused" )
public boolean isInBounds( int x, int y ) {
return (x >= 0 && y >= 0 && x < getWidth() && y < getHeight());
}
} }

View File

@@ -11,17 +11,71 @@ public final class Options {
private Options() { private Options() {
} }
/**
* Linienstile für Konturlinien.
*/
public enum StrokeType { public enum StrokeType {
SOLID, DASHED, DOTTED /**
* Durchgezogene Linien.
*/
SOLID,
/**
* Getrichelte Linien.
*/
DASHED,
/**
* Gepunktete Linien.
*/
DOTTED
} }
/**
* Stile für Pfeilspitzen.
*/
public enum ArrowHead { public enum ArrowHead {
LINES, FILLED /**
* Einfache Pfeilspitze aus zwei Linien.
*/
LINES,
/**
* Gefülltes Dreieck.
*/
FILLED
} }
/**
* Arten von Bögen.
* <p>
* Die Werte legen fest, wie Bögen geschlossen werden sollen, wenn sie
* beispielsweise gefüllt werden.
* <p>
* Wrapper für die AWT-Konstanten in {@link Arc2D}.
*/
public enum PathType { public enum PathType {
OPEN(Arc2D.OPEN), CLOSED(Arc2D.CHORD), PIE(Arc2D.PIE); /**
* Offener Pfad, bei dem die Pfadenden direkt miteinander verbunden
* werden ohne eine Linie zu ziehen.
*/
OPEN(Arc2D.OPEN),
/**
* Geschlossener Pfad, bei dem die Pfadenden direkt miteinander
* verbunden werden, indem eine Linie gezogen wird.
*/
CLOSED(Arc2D.CHORD),
/**
* Geschlossener Pfad, bei dem Linien von den Pfadenden zum Mittelpunkt
* des Kreises, der den Kreisbogen festlegt, gezogen werden.
*/
PIE(Arc2D.PIE);
/**
* Der entsprechende Wert der Konstanten in {@link Arc2D}
*/
public final int awt_type; public final int awt_type;
PathType( int type ) { PathType( int type ) {
@@ -33,22 +87,100 @@ public final class Options {
* Zustände in denen sich die Zeichenmaschine befinden kann. * Zustände in denen sich die Zeichenmaschine befinden kann.
*/ */
public enum AppState { public enum AppState {
/**
* Die Zeichenmaschine befindet sich in der Initialisierung. Die
* Laufzeitumgebung wird konfiguriert und alle nötigen Komonenten
* ({@link Zeichenfenster}, {@link Zeichenleinwand}, ...) werden
* erstellt.
*/
INITIALIZING, INITIALIZING,
/**
* Die Initialisierung der Zeichenmaschine ist beendet, aber der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde noch nicht gestartet.
*/
INITIALIZED, INITIALIZED,
/**
* Die Zeichenmaschine wurde gestartet und der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* arbeitet.
*/
RUNNING, RUNNING,
UPDATING,
DRAWING, /**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde pausiert.
*/
PAUSED, PAUSED,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde gestoppt, die Zeichenmaschine ist aber noch nicht vollständig
* heruntergefahren und hat noch nicht alle Ressourcen freigegeben.
*/
STOPPED, STOPPED,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* ist beendet.
*/
TERMINATED, TERMINATED,
IDLE
/**
* Die Zeichenmaschine ist dabei, vollständig herunterzufahren und alle
* Ressourcen freizugeben.
*/
QUITING,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wartet gerade auf den nächsten Frame.
*/
IDLE,
/**
* Die Zeichenmaschine führt gerade
* {@link Zeichenmaschine#update(double)} aus.
*/
UPDATING,
/**
* Die Zeichenmaschine führt gerade {@link Zeichenmaschine#draw()} aus.
*/
DRAWING,
/**
* Die Ausführung der Zeichenmaschine wurde mit
* {@link Zeichenmaschine#delay(int)} verzögert und wartet auf
* Fortsetzung.
*/
DELAYED,
/**
* Die Zeichenmaschine sendet gereade gesammelte Events und führt Tasks
* aus.
*/
DISPATCHING
} }
/** /**
* Richtungen für die Ausrichtung von Formen. Richtungen sind durch * Richtungen für die Ausrichtung von Formen. Richtungen sind durch
* Einheitsvektoren bzw. deren Kombination repräsentiert, wodurch mit ihnen * Einheitsvektoren bzw. deren Kombination im Koordinatensystem der
* gerechnet werden kann. Jede Richtung ist zusätzlich als Himmelsrichtung * Zeichenfläche repräsentiert, wodurch mit ihnen gerechnet werden kann. Die
* definiert. * Richtung {@link #DOWN} ist beispielsweise gleich {@code (1, 0)}.
* <p>
* Jede Richtung ist zusätzlich als Himmelsrichtung definiert. {@link #EAST}
* ist äquivalent zu {@link #RIGHT} als {@code (0, 1)} definiert. Auch wenn
* beide Werte dieselbe Richtung beschreiben sind sie nicht "gleich"
* ({@code EAST != RIGHT}). Um verschiedene Richtungen zuverlässig zu
* vergleichen, sollte daher {@link #equals(Direction)} verwendet werden.
* <p>
* Für zusammengesetzten Richtungen wie {@link #NORTHEAST} bzw
* {@link #UPRIGHT} lassen sich mit {@link #in(Direction)} und
* {@link #contains(Direction)} Beziehungen zu den anderen Richtungen
* prüfen. Beispielsweise ist {@code NORTHEAST.contains(NORTH)} wahr.
*/ */
public enum Direction { public enum Direction {
CENTER(0, 0), CENTER(0, 0),
@@ -86,30 +218,116 @@ public final class Options {
this.y = original.y; this.y = original.y;
} }
/**
* Prüft, ob die angegebene Richtung gleich dieser ist. Dabei werden die
* Komponenten des Richtungsvektors geprüft. Daher sind für die Methode
* beispielsweise {@link #NORTH} und {@link #UP} gleich.
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn die Richtungen dieselben Komponenten
* haben, {@code false} sonst.
*/
public boolean equals( Direction dir ) {
return this.x == dir.x && this.y == dir.y;
}
/**
* Prüft, ob diese Richtung Tile der angegebenen Richtung ist.
* <p>
* Beispielsweise ist {@link #NORTH} Teil von {@link #NORTHWEST}, aber
* nicht von {@link #SOUTHWEST}. Dabei wird doe Art der Richtung nicht
* beachtet. {@link #UP} ist daher auch Teil von {@link #NORTHWEST}.
*
* <pre>
* NORTH.in(NORTHWEST) // true
* NORTH.in(SOUTHWEST) // false
* UP.in(NORTHWEST) // true
* </pre>
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn diese Richtungen Teil der anderen ist,
* {@code false} sonst.
*/
public boolean in( Direction dir ) { public boolean in( Direction dir ) {
return (this.x == dir.x && this.y == 0) || (this.y == dir.y && this.x == 0) || (this.x == dir.x && this.y == dir.y); return (this.x == dir.x && this.y == 0) || (this.y == dir.y && this.x == 0) || (this.x == dir.x && this.y == dir.y);
} }
/**
* Prüft, ob die angegebene Richtung Teil dieser Richtung ist.
* <p>
* Beispielsweise ist {@link #NORTH} Teil von {@link #NORTHWEST}, aber
* nicht von {@link #SOUTHWEST}. Dabei wird die Art der Richtung nicht
* beachtet. {@link #UP} ist daher auch Teil von {@link #NORTHWEST}.
*
* <pre>
* NORTHWEST.contains(NORTH) // true
* SOUTHWEST.in(NORTH) // false
* NORTHWEST.in(UP) // true
* </pre>
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn diese Richtungen Teil der anderen ist,
* {@code false} sonst.
*/
public boolean contains( Direction dir ) { public boolean contains( Direction dir ) {
return dir.in(this); return dir.in(this);
} }
/**
* @return Diese Richtung als Vektor-Objekt.
*/
public Vector asVector() { public Vector asVector() {
return new Vector(x, y); return new Vector(x, y);
} }
/** /**
* Gibt die entgegengesetzte Richtung zu dieser zurück. * Liefert die entgegengesetzte Richtung zu dieser.
* <p>
* Es wird die Art der Richtung berücksichtigt. Das bedeutet, das
* Inverse von {@link #UP} ist {@link #DOWN}, während das Inverse von
* {@link #NORTH} zu {@link #SOUTH} wird.
* *
* @return * @return Die entgegengesetzte Richtung zu dieser.
*/ */
public Direction inverse() { public Direction inverse() {
for( Direction dir : Direction.values() ) { switch( this ) {
if( dir.x == -this.x && dir.y == -this.y ) { case UP:
return dir; return DOWN;
} case DOWN:
return UP;
case LEFT:
return RIGHT;
case RIGHT:
return LEFT;
case UPLEFT:
return DOWNRIGHT;
case UPRIGHT:
return DOWNLEFT;
case DOWNLEFT:
return UPRIGHT;
case DOWNRIGHT:
return UPLEFT;
case MIDDLE:
return MIDDLE;
case NORTH:
return SOUTH;
case SOUTH:
return NORTH;
case EAST:
return WEST;
case WEST:
return EAST;
case SOUTHWEST:
return NORTHEAST;
case SOUTHEAST:
return NORTHWEST;
case NORTHEAST:
return SOUTHWEST;
case NORTHWEST:
return SOUTHEAST;
default:
return CENTER;
} }
return CENTER;
} }
} }

View File

@@ -1,18 +1,24 @@
package schule.ngb.zm; package schule.ngb.zm;
import java.awt.Graphics; import schule.ngb.zm.layers.DrawableLayer;
import java.util.LinkedList;
import java.awt.Graphics2D;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@SuppressWarnings( "unused" )
public class Spielemaschine extends Zeichenmaschine { public class Spielemaschine extends Zeichenmaschine {
private LinkedList<Drawable> drawables; private LinkedList<Drawable> drawables;
private LinkedList<Updatable> updatables; private LinkedList<Updatable> updatables;
private GraphicsLayer mainLayer; private GameLayer mainLayer;
public Spielemaschine( String title ) { public Spielemaschine( String title ) {
this(STD_WIDTH, STD_HEIGHT, title); this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title);
} }
@@ -22,7 +28,7 @@ public class Spielemaschine extends Zeichenmaschine {
drawables = new LinkedList<>(); drawables = new LinkedList<>();
updatables = new LinkedList<>(); updatables = new LinkedList<>();
mainLayer = new GraphicsLayer(); mainLayer = new GameLayer();
canvas.addLayer(mainLayer); canvas.addLayer(mainLayer);
} }
@@ -79,9 +85,10 @@ public class Spielemaschine extends Zeichenmaschine {
@Override @Override
public final void update( double delta ) { public final void update( double delta ) {
synchronized( updatables ) { synchronized( updatables ) {
for( Updatable updatable : updatables ) { List<Updatable> it = List.copyOf(updatables);
if( updatable.isActive() ) { for( Updatable u: it ) {
updatable.update(delta); if( u.isActive() ) {
u.update(delta);
} }
} }
} }
@@ -91,14 +98,27 @@ public class Spielemaschine extends Zeichenmaschine {
@Override @Override
public final void draw() { public final void draw() {
mainLayer.clear();
synchronized( drawables ) { }
for( Drawable drawable : drawables ) {
if( drawable.isVisible() ) { private class GameLayer extends Layer {
drawable.draw(mainLayer.getGraphics());
public Graphics2D getGraphics() {
return drawing;
}
@Override
public void draw( Graphics2D graphics ) {
clear();
List<Drawable> it = List.copyOf(drawables);
for( Drawable d: it ) {
if( d.isVisible() ) {
d.draw(drawing);
} }
} }
super.draw(graphics);
} }
} }
} }

View File

@@ -0,0 +1,229 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.Shape;
import java.awt.BasicStroke;
import java.awt.Stroke;
/**
* {@link Drawable} Klassen, die mit einer Konturlinie versehen werden können.
* <p>
* Das {@code Strokeable} Interface dient hauptsächlich zur Vereinheitlichung
* der API für Konturlinien. Durch Implementation wird sichergestellt, dass alle
* Objekte, die eine Konturlinie haben können, dieselben Methoden zur Verfügung
* stellen. Wenn eine {@link Shape} eine
* {@link Strokeable#setStrokeColor(Color, int)} Methode hat, dann sollte auch
* eine {@link schule.ngb.zm.layers.TurtleLayer.Turtle} dieselbe Methode
* anbieten. Im Einzelfall kann es sinnvoll sein, weitere Methoden für
* Konturlinien zur verfügung zu stellen. Allerdings sollte davon nach
* Möglichkeit zugunsten einer einheitlichen API abgesehen werden.
* <p>
* Das Äquivalent für Füllungen stellt {@link Fillable} dar.
*/
public interface Strokeable extends Drawable {
/**
* Setzt den {@code Stroke} für die Konturlinie direkt.
*
* @param stroke Ein {@code Stroke}-Objekt.
*/
void setStroke( Stroke stroke );
/**
* Gibt ein {@code Stroke}-Objekt mit den aktuell gesetzten Eigenschaften
* zurück.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
Stroke getStroke();
/**
* Gibt an, ob die aktuell gesetzten Eigenschaften eine sichtbare
* Konturlinie erzeugen.
* <p>
* Die Konturlinie gilt als sichtbar, wenn sie eine nicht transparente Farbe
* und eine Dicke größer 0 besitzt.
* <p>
* Das bedeutet, falls die Methode {@code false} zurückgibt, dann kann
* {@link #getStroke()} trotzdem ein gültiges {@link Stroke}-Objekt
* zurückgeben, beispielsweise wenn keine Farbe gesetzt wurde.
*
* @return {@code true}, wenn die Konturlinie sichtbar ist, {@code false}
* sonst.
*/
default boolean hasStroke() {
Color strokeColor = getStrokeColor();
double strokeWeight = getStrokeWeight();
return strokeColor != null && strokeColor.getAlpha() > 0 && strokeWeight > 0;
}
/**
* Gibt die aktuelle Farbe der Konturlinie zurück.
*
* @return Die Konturfarbe oder {@code null}.
*/
Color getStrokeColor();
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe.
*
* @param color Die neue Farbe der Konturlinie.
* @see Color
*/
void setStrokeColor( Color color );
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die
* Transparenz auf den angegebenen Wert. 0 is komplett durchsichtig und 255
* komplett deckend.
*
* @param color Die neue Farbe der Konturlinie oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
default void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha));
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
default void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
default void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
default void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
default void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Kontur der Form.
*/
default void noStroke() {
setStrokeColor(null);
}
/**
* Setzt die Farbe der Konturlinie auf die Standardwerte zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_STROKECOLOR
* @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT
* @see schule.ngb.zm.Constants#SOLID
*/
default void resetStroke() {
setStrokeColor(Constants.DEFAULT_STROKECOLOR);
setStrokeWeight(Constants.DEFAULT_STROKEWEIGHT);
setStrokeType(Constants.SOLID);
}
/**
* Gibt die Dicke der Konturlinie zurück.
*
* @return Die aktuelle Dicke der Linie.
*/
double getStrokeWeight();
/**
* Setzt die Dicke der Konturlinie. Die Dicke muss größer 0 sein. Wird 0
* übergeben, dann wird keine Kontur mehr angezeigt.
*
* @param weight Die Dicke der Konturlinie.
*/
default void setStrokeWeight( double weight ) {
setStroke(createStroke(getStrokeType(), weight));
}
/**
* Gibt die Art der Konturlinie zurück.
*
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
Options.StrokeType getStrokeType();
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link Constants#DASHED},
* {@link Constants#DOTTED} und {@link Constants#SOLID}.
*
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
default void setStrokeType( Options.StrokeType type ) {
setStroke(createStroke(type, getStrokeWeight()));
}
/**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
* zwischengespeichert.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight ) {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
case SOLID:
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
}
}
}

View File

@@ -1,23 +1,44 @@
package schule.ngb.zm; package schule.ngb.zm;
/** /**
* Aktualisierbare Objekte können in regelmäßigen Intervallen (meist einmal * {@code Updatable} Objekte können in regelmäßigen Intervallen (meist einmal
* pro Frame) ihren Zustand update. Diese Änderung kann abhängig vom * pro Frame) ihren Zustand aktualisieren. Diese Änderung kann abhängig vom
* Zeitintervall (in Sekunden) zum letzten Aufruf passieren. * Zeitintervall (in Sekunden) zum letzten Aufruf passieren.
*/ */
public interface Updatable { public interface Updatable {
/** /**
* Gibt an, ob das Objekt gerade auf Aktualisierungen reagiert. * Gibt an, ob das Objekt gerade auf Aktualisierungen reagiert.
* @return {@code true}, wenn das Objekt aktiv ist. * <p>
* Wie mit dieser Information umgegangen wird, ist nicht weiter festgelegt.
* In der Regel sollte eine aufrufende Instanz zunächst prüfen, ob das
* Objekt aktiv ist, und nur dann{@link #update(double)} aufrufen. Für
* implementierende Klassen ist es aber gegebenenfalls auch sinnvoll, bei
* Inaktivität den Aufruf von {@code update(double)} schnell abzubrechen:
* <pre><code>
* void update( double delta ) {
* if( !isActive() ) {
* return;
* }
*
* // Aktualisierung ausführen..
* }
* </code></pre>
*
* @return {@code true}, wenn das Objekt aktiv ist, {@code false}
* andernfalls.
*/ */
public boolean isActive(); boolean isActive();
/** /**
* Änderung des Zustandes des Objekts abhängig vom Zeitintervall * Änderung des Zustandes des Objekts abhängig vom Zeitintervall
* <var>delta</var> in Sekunden. * {@code delta} in Sekunden.
* <p>
* Die kann, muss aber nicht, die Rückgabe von {@link #isActive()}
* berücksichtigen.
*
* @param delta Zeitintervall seit dem letzten Aufruf (in Sekunden). * @param delta Zeitintervall seit dem letzten Aufruf (in Sekunden).
*/ */
public void update( double delta ); void update( double delta );
} }

View File

@@ -23,6 +23,7 @@ import java.awt.geom.Point2D;
* Der Vektor der Zeichenmaschine erweitert die Klasse {@link Point2D} und lässt * Der Vektor der Zeichenmaschine erweitert die Klasse {@link Point2D} und lässt
* sich dadurch einfach mit den Klassen des {@link java.awt} Pakets benutzen. * sich dadurch einfach mit den Klassen des {@link java.awt} Pakets benutzen.
*/ */
@SuppressWarnings( "unused" )
public class Vector extends Point2D.Double { public class Vector extends Point2D.Double {
/** /**
@@ -127,7 +128,7 @@ public class Vector extends Point2D.Double {
} }
/** /**
* Erzeugt einen neuen Vektor mit derselben Richtun wie der angegebene * Erzeugt einen neuen Vektor mit derselben Richtung wie der angegebene
* Vektor und der Länge 1. * Vektor und der Länge 1.
* *
* @param vector Der original Vektor. * @param vector Der original Vektor.
@@ -197,6 +198,7 @@ public class Vector extends Point2D.Double {
* @param y Der neue y-Wert. * @param y Der neue y-Wert.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( double x, double y ) { public Vector set( double x, double y ) {
this.x = x; this.x = x;
this.y = y; this.y = y;
@@ -210,6 +212,7 @@ public class Vector extends Point2D.Double {
* @param vector Ein Vektor. * @param vector Ein Vektor.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Vector vector ) { public Vector set( Vector vector ) {
x = vector.x; x = vector.x;
y = vector.y; y = vector.y;
@@ -223,6 +226,7 @@ public class Vector extends Point2D.Double {
* @param pPunkt Ein Punkt. * @param pPunkt Ein Punkt.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Point2D pPunkt ) { public Vector set( Point2D pPunkt ) {
x = pPunkt.getX(); x = pPunkt.getX();
x = pPunkt.getY(); x = pPunkt.getY();
@@ -271,6 +275,7 @@ public class Vector extends Point2D.Double {
/** /**
* Legt die Länge des Vektors fest. * Legt die Länge des Vektors fest.
*
* @param length Die neue Länge des Vektors. * @param length Die neue Länge des Vektors.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@@ -294,9 +299,9 @@ public class Vector extends Point2D.Double {
} }
/** /**
*
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector normalize() { public Vector normalize() {
double len = length(); double len = length();
if( len != 0 && len != 1 ) { if( len != 0 && len != 1 ) {
@@ -308,9 +313,11 @@ public class Vector extends Point2D.Double {
/** /**
* Addiert den Vektor {@code vector} zu diesem. * Addiert den Vektor {@code vector} zu diesem.
*
* @param vector Ein anderer Vektor. * @param vector Ein anderer Vektor.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( Vector vector ) { public Vector add( Vector vector ) {
x += vector.x; x += vector.x;
y += vector.y; y += vector.y;
@@ -319,10 +326,12 @@ public class Vector extends Point2D.Double {
/** /**
* Addiert die angegebenen Werte zur x- und y-Komponente des Vektors. * Addiert die angegebenen Werte zur x- und y-Komponente des Vektors.
*
* @param x Summand x-Komponente. * @param x Summand x-Komponente.
* @param y Summand y-Komponente. * @param y Summand y-Komponente.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( double x, double y ) { public Vector add( double x, double y ) {
this.x += x; this.x += x;
this.y += y; this.y += y;
@@ -342,24 +351,28 @@ public class Vector extends Point2D.Double {
return vec; return vec;
} }
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( Vector vector ) { public Vector sub( Vector vector ) {
x -= vector.x; x -= vector.x;
y -= vector.y; y -= vector.y;
return this; return this;
} }
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( double x, double y ) { public Vector sub( double x, double y ) {
this.x -= x; this.x -= x;
this.y -= y; this.y -= y;
return this; return this;
} }
@SuppressWarnings( "UnusedReturnValue" )
public Vector scale( double scalar ) { public Vector scale( double scalar ) {
x *= scalar; x *= scalar;
y *= scalar; y *= scalar;
return this; return this;
} }
@SuppressWarnings( "UnusedReturnValue" )
public Vector div( double scalar ) { public Vector div( double scalar ) {
if( scalar == 0.0 ) { if( scalar == 0.0 ) {
throw new IllegalArgumentException("Can't divide by zero."); throw new IllegalArgumentException("Can't divide by zero.");
@@ -409,8 +422,8 @@ public class Vector extends Point2D.Double {
* dem quadrierten Abstand durchführen, wenn auch die gewünschte Entfernung * dem quadrierten Abstand durchführen, wenn auch die gewünschte Entfernung
* quadriert wird. * quadriert wird.
* *
* @param vector * @param vector Ein anderer Vektor.
* @return * @return Das Quadrat der Entfernung zum anderen Vektor.
*/ */
public double distanceSq( Vector vector ) { public double distanceSq( Vector vector ) {
return super.distanceSq(vector); return super.distanceSq(vector);
@@ -487,6 +500,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
* @see #setLength(double) * @see #setLength(double)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double max ) { public Vector limit( double max ) {
if( lengthSq() > max * max ) { if( lengthSq() > max * max ) {
setLength(max); setLength(max);
@@ -503,6 +517,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
* @see #setLength(double) * @see #setLength(double)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double min, double max ) { public Vector limit( double min, double max ) {
if( min > max ) { if( min > max ) {
double t = min; double t = min;
@@ -556,6 +571,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
* @see #rotate(double) * @see #rotate(double)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector rotateRad( double rad ) { public Vector rotateRad( double rad ) {
double temp = x; double temp = x;
x = x * Math.cos(rad) - y * Math.sin(rad); x = x * Math.cos(rad) - y * Math.sin(rad);
@@ -587,6 +603,7 @@ public class Vector extends Point2D.Double {
* @param t Ein Wert zwischen 0 und 1. * @param t Ein Wert zwischen 0 und 1.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector morph( Vector vector, double t ) { public Vector morph( Vector vector, double t ) {
double tt = Math.min(Math.max(t, 0.0), 1.0); double tt = Math.min(Math.max(t, 0.0), 1.0);
x = x + (vector.x - x) * tt; x = x + (vector.x - x) * tt;
@@ -631,6 +648,7 @@ public class Vector extends Point2D.Double {
* @param t Ein Wert zwischen 0 und 1. * @param t Ein Wert zwischen 0 und 1.
* @return Dieser Vektor selbst (method chaining) * @return Dieser Vektor selbst (method chaining)
*/ */
@SuppressWarnings( "UnusedReturnValue" )
public Vector interpolate( Vector vector, double t ) { public Vector interpolate( Vector vector, double t ) {
x = x + (vector.x - x) * t; x = x + (vector.x - x) * t;
y = y + (vector.y - y) * t; y = y + (vector.y - y) * t;

View File

@@ -1,6 +1,363 @@
package schule.ngb.zm; package schule.ngb.zm;
// TODO: Auslagern des Frames in diese Klasse (Trennung Swing-GUI/Canvas, Zeichenmaschine und Drawing-Thread) import schule.ngb.zm.util.Log;
public class Zeichenfenster { import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
/**
* Ein Zeichenfenster ist das Programmfenster für die Zeichenmaschine.
* <p>
* Das Zeichenfenster implementiert hilfreiche Funktionen, um ein
* Programmfenster mit einer Zeichenleinwand als zentrales Element zu erstellen.
* Ein Zeichenfenster kann auch ohne eine Zeichenmaschine verwendet werden, um
* eigene Programmabläufe zu implementieren.
*/
public class Zeichenfenster extends JFrame {
/**
* Setzt das Look and Feel auf den Standard des Systems.
* <p>
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen
* werden.
*/
public static final void setLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch( Exception ex ) {
LOG.error(ex, "Couldn't set the look and feel: %s", ex.getMessage());
}
}
/**
* Ermittelt ein {@link GraphicsDevice Anzeigegerät}, auf dem ein neues
* Zeichenfenster angezeigt werden soll. In der Regel ist dies der
* Bildschirm, auf dem sich derzeit der Mauszeiger befindet. Kann kein
* solcher Bildschirm ermittelt werden, wird das
* {@link GraphicsEnvironment#getDefaultScreenDevice() Standardgerät}
* zurückgegeben.
*
* @return Das Anzeigegerät, auf dem ein neues Fenster angezeigt werden
* sollte.
*/
public static final GraphicsDevice getGraphicsDevice() {
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
// TODO: (ngb) Wenn wir in BlueJ sind, sollte das Fenster neben dem Editor öffnen.
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = environment.getScreenDevices();
GraphicsDevice displayDevice = null;
for( GraphicsDevice gd : devices ) {
if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) {
displayDevice = gd;
break;
}
}
// Keinen passenden Bildschirm gefunden. Wir nutzen den Standard.
if( displayDevice == null ) {
displayDevice = environment.getDefaultScreenDevice();
}
return displayDevice;
}
/**
* Das Anzeigegerät, auf dem die Zeichenmaschine gestartet wurde (muss nicht
* gleich dem Aktuellen sein, wenn das Fenster verschoben wurde).
*/
private final GraphicsDevice displayDevice;
/**
* Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat es
* Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine
* Garantie für die Größe der Leinwand übernehmen.
*/
private int canvasPreferredWidth, canvasPreferredHeight;
/**
* Speichert, ob die Zeichenmaschine mit {@link #setFullscreen(boolean)} in
* den Vollbildmodus versetzt wurde.
*/
private boolean fullscreen = false;
/**
* {@code KeyListener}, um den Vollbild-Modus mit der Escape-Taste zu
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt.
*/
private final KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
e.consume();
}
}
};
// Die Zeichenleinwand dieses Fensters.
private final Zeichenleinwand canvas;
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und einer
* {@link Zeichenleinwand} in der angegebenen Größe.
*
* @param width Die Breite der Zeichenleinwand.
* @param height Die Höhe der Zeichenleinwand.
* @param title Der Titel des Fensters.
*/
@SuppressWarnings( "unused" )
public Zeichenfenster( int width, int height, String title ) {
this(new Zeichenleinwand(width, height), title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und einer
* {@link Zeichenleinwand} in der angegebenen Größe auf dem angegebenen
* Anzeigegerät.
*
* @param width Die Breite der Zeichenleinwand.
* @param height Die Höhe der Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
@SuppressWarnings( "unused" )
public Zeichenfenster( int width, int height, String title, GraphicsDevice displayDevice ) {
this(new Zeichenleinwand(width, height), title, displayDevice);
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand}.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title ) {
this(canvas, title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand} auf dem angegebenen Anzeigegerät.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
super(Validator.requireNotNull(displayDevice).getDefaultConfiguration());
this.displayDevice = displayDevice;
Validator.requireNotNull(canvas, "Every Zeichenfenster needs a Zeichenleinwand, but got <null>.");
this.canvasPreferredWidth = canvas.getWidth();
this.canvasPreferredHeight = canvas.getHeight();
this.add(canvas, BorderLayout.CENTER);
this.canvas = canvas;
// Konfiguration des Frames
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION : title);
// Kann vom Aufrufenden überschrieben werden
this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png");
if( iconUrl != null ) {
Image icon = ImageIO.read(iconUrl);
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon);
}
} else {
ArrayList<Image> icons = new ArrayList<>(4);
for( int size : new int[]{32, 64, 128, 512} ) {
URL icnUrl = Zeichenmaschine.class.getResource("icon_" + size + ".png");
if( icnUrl != null ) {
icons.add(ImageIO.read(icnUrl));
}
}
this.setIconImages(icons);
}
} catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage());
} catch( SecurityException | UnsupportedOperationException macex ) {
// Dock Icon in macOS konnte nicht gesetzt werden :(
LOG.warn("Could not set dock icon: %s", macex.getMessage());
}
// Fenster zusammenbauen, auf dem Bildschirm positionieren ...
this.pack();
this.setResizable(false);
this.setFocusable(true);
this.setLocationByPlatform(true);
// this.centerFrame();
}
/**
* Liefert das Anzeigegerät, auf dem dieses Fenster erstellt wurde.
* <p>
* Das Anzeigegerät muss nicht unbedingt gleich dem sein, auf dem sich das
* Fenster derzeit befindet, wenn das Fenster verschoben wurde.
*
* @return Das Anzeigegerät.
*/
@SuppressWarnings( "unused" )
public GraphicsDevice getDisplayDevice() {
return displayDevice;
}
/**
* Liefert die Abmessungen des Anzeigegeräts, auf dem das Fenster gestartet
* wurde.
*
* @return Die Abmessungen des Anzeigegeräts.
*/
public Rectangle getScreenBounds() {
// return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return displayDevice.getDefaultConfiguration().getBounds();
}
/**
* Liefert die Zeichenleinwand dieses Fensters.
*
* @return Die Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public Zeichenleinwand getCanvas() {
return canvas;
}
/**
* Liefert die Abmessungen der Zeichenleinwand zurück.
*
* @return Die Abmessungen der Zeichenleinwand.
*/
public Rectangle getCanvasBounds() {
return canvas.getBounds();
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
public final void centerFrame() {
java.awt.Rectangle screenBounds = getScreenBounds();
java.awt.Rectangle frameBounds = getBounds();
this.setLocation(
(int) (screenBounds.x + (screenBounds.width - frameBounds.width) / 2.0),
(int) (screenBounds.y + (screenBounds.height - frameBounds.height) / 2.0)
);
}
/**
* Setzt die Größe der Zeichenleinwand auf die angegebenen Werte.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
*/
public void setCanvasSize( int newWidth, int newHeight ) {
// TODO: (ngb) Put constrains on max/min frame/canvas size
if( fullscreen ) {
canvasPreferredWidth = newWidth;
canvasPreferredHeight = newHeight;
setFullscreen(false);
} else {
canvas.setSize(newWidth, newHeight);
canvasPreferredWidth = canvas.getWidth();
canvasPreferredHeight = canvas.getHeight();
this.pack();
}
}
/**
* Aktiviert oder deaktiviert den Vollbildmodus für die Zeichenmaschine.
* <p>
* Der Vollbildmodus wird abhängig von {@code pEnable} entweder aktiviert
* oder deaktiviert. Wird die Zeichenmaschine in den Vollbildmodus versetzt,
* dann wird automatisch ein {@link KeyListener} aktiviert, der bei
* Betätigung der ESCAPE-Taste den Vollbildmodus verlässt. Wird der
* Vollbildmodus verlassen, wird die zuletzt gesetzte Fenstergröße
* wiederhergestellt.
*
* @param pEnable Wenn {@code true}, wird der Vollbildmodus aktiviert,
* ansonsten deaktiviert.
*/
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
// Temporarily stop rendering
while( canvas.isRendering() ) {
try {
canvas.suspendRendering();
} catch( InterruptedException ex ) {
LOG.info(ex, "setFullsceen(true) was interrupted and canceled.");
return;
}
}
if( pEnable && !fullscreen ) {
// Activate fullscreen
dispose();
setUndecorated(true);
setResizable(false);
displayDevice.setFullScreenWindow(this);
// Register ESC to exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
// Reset canvas size to its new bounds to recreate buffer and drawing surface
java.awt.Rectangle canvasBounds = getCanvasBounds();
canvas.setSize(canvasBounds.width, canvasBounds.height);
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = true;
} else if( !pEnable && fullscreen ) {
displayDevice.setFullScreenWindow(null);
dispose();
setUndecorated(false);
setResizable(false);
canvas.removeKeyListener(fullscreenExitListener);
canvas.setSize(canvasPreferredWidth, canvasPreferredHeight);
setVisible(true);
pack();
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = false;
}
// Resume rendering
canvas.resumeRendering();
}
}
/**
* Prüft, ob sich dieses Zeichenfenster im Vollbild befindet.
*
* @return {@code true}, wenn das Fenster im Vollbild ist, {@code false}
* sonst.
*/
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win.equals(this);
}
private static final Log LOG = Log.getLogger(Zeichenfenster.class);
} }

View File

@@ -1,12 +1,16 @@
package schule.ngb.zm; package schule.ngb.zm;
import schule.ngb.zm.util.Log;
import java.awt.Canvas; import java.awt.Canvas;
import java.awt.Graphics; import java.awt.Graphics;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Toolkit; import java.awt.Toolkit;
import java.awt.image.BufferStrategy; import java.awt.image.BufferStrategy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
/** /**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus * Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -20,10 +24,14 @@ import java.util.LinkedList;
*/ */
public class Zeichenleinwand extends Canvas { public class Zeichenleinwand extends Canvas {
/** // Lokales Lock für Rendervorgänge.
* Liste der hinzugefügten Ebenen. private final Object[] renderLock = new Object[0];
*/
private LinkedList<Layer> layers; // Liste der hinzugefügten Ebenen.
private final List<Layer> layers;
// Status der Zeichenleinwand.
private boolean rendering = false, suspended = false;
/** /**
* Erstellt eine neue Zeichenleinwand mit einer festen Größe. * Erstellt eine neue Zeichenleinwand mit einer festen Größe.
@@ -33,17 +41,55 @@ public class Zeichenleinwand extends Canvas {
*/ */
public Zeichenleinwand( int width, int height ) { public Zeichenleinwand( int width, int height ) {
super.setSize(width, height); super.setSize(width, height);
this.setPreferredSize(this.getSize()); this.setPreferredSize(getSize());
this.setMinimumSize(this.getSize()); this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor()); this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
// Liste der Ebenen initialisieren und die Standardebenen einfügen layers = Collections.synchronizedList(new LinkedList<>());
layers = new LinkedList<>(); }
synchronized( layers ) {
layers.add(new ColorLayer(width, height, Constants.DEFAULT_BACKGROUND)); /**
* Ob die Leinwand ihren Inhalt gerade zeichnet.
*
* @return {@code true}, wenn die Inhalte gerade gezeichnet werden.
*/
public boolean isRendering() {
return rendering;
}
/**
* Pausiert das Zeichnen der Leinwand kurzzeitig.
* <p>
* Falls die Leinwand gerade beim Zeichnen ist
* ({@code isRendering() == true}, blockt die Methode den aufrufenden Thread
* so lange, bis das Rendern beendet ist. Danach wird die Ebene nicht mehr
* neu gezeichnet, bis {@link #resumeRendering()} aufgerufen wird.
* <p>
* Das Zeichnen sollte nur dann unterbrochen werden, wenn sich der Kontext
* der Canvas-Komponente in seinem Elterncontainer ändert, um Fehler bei
* einer fehlenden Container-Hierarchie zu vermeiden.
*
* @throws InterruptedException Falls der Thread beim Warten unterbrochen
* wird.
*/
public void suspendRendering() throws InterruptedException {
synchronized( renderLock ) {
if( isRendering() ) {
renderLock.wait();
}
suspended = true;
} }
} }
/**
* Setzt das Zeichnen der Leinwand fort, falls es zuvor mit
* {@link #suspendRendering()} ausgesetzt wurde.
*/
public void resumeRendering() {
suspended = false;
}
/** /**
* Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln. * Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln.
* <p> * <p>
@@ -56,8 +102,8 @@ public class Zeichenleinwand extends Canvas {
@Override @Override
public void setSize( int width, int height ) { public void setSize( int width, int height ) {
super.setSize(width, height); super.setSize(width, height);
this.setPreferredSize(this.getSize()); this.setPreferredSize(getSize());
this.setMinimumSize(this.getSize()); this.setMinimumSize(getSize());
synchronized( layers ) { synchronized( layers ) {
for( Layer layer : layers ) { for( Layer layer : layers ) {
@@ -74,32 +120,28 @@ public class Zeichenleinwand extends Canvas {
*/ */
public void addLayer( Layer layer ) { public void addLayer( Layer layer ) {
if( layer != null ) { if( layer != null ) {
synchronized( layers ) { layer.setSize(getWidth(), getHeight());
layer.setSize(getWidth(), getHeight()); layers.add(layer);
layers.add(layer);
}
} }
} }
/** /**
* Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn * Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn
* der Index noch nicht existiert (also größer als die * der Index noch nicht existiert (also größer als die
* {@link #getLayerCount() Anzahl der Ebenen} ist, dann wird die neue Ebene * {@link #getLayerCount() Anzahl der Ebenen} ist), dann wird die neue Ebene
* als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein, * als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein,
* dass die neue Ebene am Ende wirklich am Index {@code i} steht. * dass die neue Ebene am Ende wirklich am Index {@code i} steht.
* *
* @param i Index der Ebene, beginnend mit 0. * @param i Index der Ebene beginnend mit 0.
* @param layer Die neue Ebene. * @param layer Die neue Ebene.
*/ */
public void addLayer( int i, Layer layer ) { public void addLayer( int i, Layer layer ) {
if( layer != null ) { if( layer != null ) {
synchronized( layers ) { layer.setSize(getWidth(), getHeight());
layer.setSize(getWidth(), getHeight()); if( i > layers.size() ) {
if( i > layers.size() ) { layers.add(layer);
layers.add(layer); } else {
} else { layers.add(i, layer);
layers.add(i, layer);
}
} }
} }
} }
@@ -114,19 +156,19 @@ public class Zeichenleinwand extends Canvas {
} }
/** /**
* Gibt die Liste der bisher hinzugefügten Ebenen zurück. * Gibt eine Kopie der Liste der bisher hinzugefügten Ebenen zurück.
* *
* @return Liste der Ebenen. * @return Liste der Ebenen.
*/ */
public java.util.List<Layer> getLayers() { public List<Layer> getLayers() {
return layers; return List.copyOf(layers);
} }
/** /**
* Holt die Ebene am Index <var>i</var> (beginnend bei 0). * Holt die Ebene am Index {@code i} (beginnend bei 0).
* *
* @param i Index der Ebene (beginnend bei 0). * @param i Index der Ebene (beginnend bei 0).
* @return Die Ebene am Index <var>i</var> oder {@code null}. * @return Die Ebene am Index {@code i} oder {@code null}.
* @throws IndexOutOfBoundsException Falls der Index nicht existiert. * @throws IndexOutOfBoundsException Falls der Index nicht existiert.
*/ */
public Layer getLayer( int i ) { public Layer getLayer( int i ) {
@@ -141,14 +183,16 @@ public class Zeichenleinwand extends Canvas {
* Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen. * Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben. * Existiert keine solche Ebene, wird {@code null} zurückgegeben.
* *
* @param clazz Typ der Ebene. * @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> * @param <L> Typ der Ebenen, die abgefragt werden.
* @return Erste Ebene vom angegeben Typ. * @return Erste Ebene vom angegeben Typ.
*/ */
public <L extends Layer> L getLayer( Class<L> clazz ) { public <L extends Layer> L getLayer( Class<L> type ) {
for( Layer layer : layers ) { synchronized( layers ) {
if( layer.getClass().equals(clazz) ) { for( Layer layer : layers ) {
return clazz.cast(layer); if( layer.getClass().equals(type) ) {
return type.cast(layer);
}
} }
} }
return null; return null;
@@ -159,43 +203,67 @@ public class Zeichenleinwand extends Canvas {
* gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der * gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der
* Reihenfolge der Ebenen in der Leinwand (von unten nach oben). * Reihenfolge der Ebenen in der Leinwand (von unten nach oben).
* *
* @param pClazz * @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> * @param <L> Typ der Ebenen, die abgefragt werden.
* @return * @return Eine Liste mit den vorhandenen Ebenen des abgefragten Typs.
*/ */
public <L extends Layer> java.util.List<L> getLayers( Class<L> pClazz ) { @SuppressWarnings( "unused" )
public <L extends Layer> List<L> getLayers( Class<L> type ) {
ArrayList<L> result = new ArrayList<>(layers.size()); ArrayList<L> result = new ArrayList<>(layers.size());
for( Layer layer : layers ) { synchronized( layers ) {
if( layer.getClass().equals(pClazz) ) { for( Layer layer : layers ) {
result.add(pClazz.cast(layer)); if( layer.getClass().equals(type) ) {
result.add(type.cast(layer));
}
} }
} }
return result; return result;
} }
/**
* Entfernt die angegebene Ebene von dieser Zeichenleinwand.
*
* @param pLayer Die Ebene, die entfernt werden soll.
* @return {@code true}, wenn die Liste vorhanden war und entfernt wurde,
* {@code false} sonst.
*/
@SuppressWarnings( "unused" )
public boolean removeLayer( Layer pLayer ) { public boolean removeLayer( Layer pLayer ) {
synchronized( layers ) { return layers.remove(pLayer);
return layers.remove(pLayer);
}
} }
public void removeLayers( Layer... pLayers ) { /**
* Entfernt alle angegebenen Ebenen von dieser Zeichenleinwand.
*
* @param removeLayers Die Ebenen, die entfernt werden sollen.
*/
@SuppressWarnings( "unused" )
public void removeLayers( Layer... removeLayers ) {
synchronized( layers ) { synchronized( layers ) {
for( Layer layer : pLayers ) { for( Layer layer : removeLayers ) {
layers.remove(layer); layers.remove(layer);
} }
} }
} }
/**
* Entfernt alle vorhandenen Ebenen von dieser Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public void clearLayers() { public void clearLayers() {
synchronized( layers ) { layers.clear();
layers.clear();
}
} }
/**
* Aktualisiert alle {@link Layer Ebenen}, die dieser Zeichenleinwand
* hinzugefügt wurden.
*
* @param delta Die Zeit seit dem letzten Aufruf in Sekunden.
* @see Layer#update(double)
*/
public void updateLayers( double delta ) { public void updateLayers( double delta ) {
synchronized( layers ) { synchronized( layers ) {
for( Layer layer : layers ) { for( Layer layer : List.copyOf(layers) ) {
layer.update(delta); layer.update(delta);
} }
} }
@@ -225,9 +293,9 @@ public class Zeichenleinwand extends Canvas {
} }
/** /**
* Zeichnet den Inhalt aller {@link Layer Ebenen} in den Grafik-Kontext. * Zeichnet den Inhalt aller {@link Layer Ebenen} in den Grafikkontext.
* *
* @param graphics * @param graphics Der Grafikkontext.
*/ */
public void draw( Graphics graphics ) { public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create(); Graphics2D g2d = (Graphics2D) graphics.create();
@@ -243,38 +311,45 @@ public class Zeichenleinwand extends Canvas {
* Zeigt den aktuellen Inhalt der Zeichenleinwand an. * Zeigt den aktuellen Inhalt der Zeichenleinwand an.
*/ */
public void render() { public void render() {
if( getBufferStrategy() == null ) { if( !suspended && isDisplayable() ) {
allocateBuffer(); if( getBufferStrategy() == null ) {
} allocateBuffer();
}
if( isDisplayable() ) { synchronized( renderLock ) {
BufferStrategy strategy = this.getBufferStrategy(); rendering = true;
if( strategy != null ) { BufferStrategy strategy = this.getBufferStrategy();
do { if( strategy != null ) {
do { do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics(); do {
g2d.clearRect(0, 0, getWidth(), getHeight()); Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) { synchronized( layers ) {
for( Layer layer : layers ) { for( Layer layer : List.copyOf(layers) ) {
layer.draw(g2d); layer.draw(g2d);
}
} }
g2d.dispose();
} while( strategy.contentsRestored() );
// Display the buffer
if( !strategy.contentsLost() ) {
strategy.show();
Toolkit.getDefaultToolkit().sync();
} }
g2d.dispose(); // Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsRestored() ); } while( strategy.contentsLost() );
}
// Display the buffer rendering = false;
if( !strategy.contentsLost() ) { renderLock.notifyAll();
strategy.show();
Toolkit.getDefaultToolkit().sync();
}
// Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() );
} }
} }
} }
private static final Log LOG = Log.getLogger(Zeichenleinwand.class);
} }

View File

@@ -1,12 +1,14 @@
package schule.ngb.zm; package schule.ngb.zm;
import schule.ngb.zm.anim.Animation; import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.shapes.ShapesLayer; import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.tasks.TaskRunner; import schule.ngb.zm.layers.DrawingLayer;
import schule.ngb.zm.util.ImageLoader; import schule.ngb.zm.layers.ImageLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.tasks.TaskRunner;
import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.MouseInputListener; import javax.swing.event.MouseInputListener;
import java.awt.*; import java.awt.*;
@@ -15,7 +17,7 @@ import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.logging.Level; import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Hauptklasse der Zeichenmaschine. * Hauptklasse der Zeichenmaschine.
@@ -24,7 +26,7 @@ import java.util.logging.Level;
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der * Die Klasse übernimmt die Initialisierung eines Programmfensters und der
* nötigen Komponenten. * nötigen Komponenten.
*/ */
// TODO: Refactorings (besonders in Bezug auf Nebenläufigkeit) @SuppressWarnings( "unused" )
public class Zeichenmaschine extends Constants { public class Zeichenmaschine extends Constants {
/** /**
@@ -36,43 +38,6 @@ public class Zeichenmaschine extends Constants {
IN_BLUEJ = System.getProperty("java.class.path").contains("bluej"); IN_BLUEJ = System.getProperty("java.class.path").contains("bluej");
} }
/**
* Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde.
*/
public static final boolean MACOS;
/**
* Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde.
*/
public static final boolean WINDOWS;
/**
* Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde.
*/
public static final boolean LINUX;
static {
final String name = System.getProperty("os.name");
if( name.contains("Mac") ) {
MACOS = true;
WINDOWS = false;
LINUX = false;
} else if( name.contains("Windows") ) {
MACOS = false;
WINDOWS = true;
LINUX = false;
} else if( name.equals("Linux") ) { // true for the ibm vm
MACOS = false;
WINDOWS = false;
LINUX = true;
} else {
MACOS = false;
WINDOWS = false;
LINUX = false;
}
}
/* /*
* Objektvariablen, die von Unterklassen benutzt werden können. * Objektvariablen, die von Unterklassen benutzt werden können.
*/ */
@@ -110,54 +75,14 @@ public class Zeichenmaschine extends Constants {
/** /**
* Das Zeichenfenster der Zeichenmaschine * Das Zeichenfenster der Zeichenmaschine
*/ */
private JFrame frame; protected Zeichenfenster frame;
/**
* Die Graphics-Umgebung für das aktuelle Fenster.
*/
private GraphicsEnvironment environment;
/**
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich
* dem Aktuellen sein, wenn das Fenster verschoben wurde).
*/
private GraphicsDevice displayDevice;
/**
* Speichert, ob die Zeichenmaschine mit {@link #setFullscreen(boolean)}
* in den Vollbildmodus versetzt wurde.
*/
private boolean fullscreen = false;
/**
* Höhe und Breite der Zeichenmaschine, bevor sie mit
* {@link #setFullscreen(boolean)} in den Vollbild-Modus versetzt wurde.
* Wird verwendet, um die Fenstergröße wiederherzustellen, sobald der
* Vollbild-Modus verlassen wird.
*/
private int initialWidth, initialHeight;
/**
* {@code KeyListener}, um den Vollbild-Modus mit der Escape-Taste zu
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt.
*/
private KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
}
}
};
// Aktueller Zustand der Zeichenmaschine. // Aktueller Zustand der Zeichenmaschine.
/** /**
* Zustand der Zeichenmaschine insgesamt * Zustand der Zeichenmaschine insgesamt
*/ */
private Options.AppState state = Options.AppState.INITIALIZING; private Options.AppState state;
/** /**
* Zustand des update/draw Threads * Zustand des update/draw Threads
@@ -167,7 +92,9 @@ public class Zeichenmaschine extends Constants {
/** /**
* Ob der Zeichenthread noch laufen soll, oder beendet. * Ob der Zeichenthread noch laufen soll, oder beendet.
*/ */
private boolean running = false; private boolean running;
private boolean terminateImediately = false;
/** /**
* Ob die ZM nach dem nächsten Frame pausiert werden soll. * Ob die ZM nach dem nächsten Frame pausiert werden soll.
@@ -178,7 +105,7 @@ public class Zeichenmaschine extends Constants {
* Ob die ZM bei nicht überschriebener update() Methode stoppen soll, * Ob die ZM bei nicht überschriebener update() Methode stoppen soll,
* oder trotzdem weiterläuft. * oder trotzdem weiterläuft.
*/ */
private boolean run_once = true; private final boolean run_once;
/** /**
* Aktuelle Frames pro Sekunde der Zeichenmaschine. * Aktuelle Frames pro Sekunde der Zeichenmaschine.
@@ -188,23 +115,23 @@ public class Zeichenmaschine extends Constants {
/** /**
* Hauptthread der Zeichenmaschine. * Hauptthread der Zeichenmaschine.
*/ */
private Thread mainThread; private final Thread mainThread;
/** /**
* Queue für geplante Aufgaben * Queue für geplante Aufgaben
*/ */
private DelayQueue<DelayedTask> taskQueue = new DelayQueue<>(); private final DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
/** /**
* Queue für abgefangene InputEvents * Queue für abgefangene InputEvents
*/ */
private BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>(); private final BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
/** /**
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll, * Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
* oder das Zeichenfenster weiter geöffnet bleibt. * oder das Zeichenfenster weiter geöffnet bleibt.
*/ */
private boolean quitAfterTeardown = false; private boolean quitAfterShutdown = false;
// Mauszeiger // Mauszeiger
/** /**
@@ -255,7 +182,7 @@ public class Zeichenmaschine extends Constants {
* @param title Der Titel, der oben im Fenster steht. * @param title Der Titel, der oben im Fenster steht.
*/ */
public Zeichenmaschine( String title ) { public Zeichenmaschine( String title ) {
this(STD_WIDTH, STD_HEIGHT, title, true); this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, true);
} }
/** /**
@@ -270,7 +197,7 @@ public class Zeichenmaschine extends Constants {
* Aufruf von {@code draw()}. * Aufruf von {@code draw()}.
*/ */
public Zeichenmaschine( String title, boolean run_once ) { public Zeichenmaschine( String title, boolean run_once ) {
this(STD_WIDTH, STD_HEIGHT, title, run_once); this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, run_once);
} }
/** /**
@@ -311,78 +238,41 @@ public class Zeichenmaschine extends Constants {
* @param run_once {@code true} beendet die Zeichenmaschine nach einem * @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}. * Aufruf von {@code draw()}.
*/ */
@SuppressWarnings("static-access")
public Zeichenmaschine( int width, int height, String title, boolean run_once ) { public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
LOG.info("Starting " + APP_NAME + " " + APP_VERSION); LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
// Setzen des "Look&Feel" // Register Cmd+Q on macOS
try { if( Constants.MACOS ) {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
} catch( Exception ex ) {
LOG.log(Level.SEVERE, "Error setting the look and feel: " + ex.getMessage(), ex);
} }
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = environment.getScreenDevices();
for( GraphicsDevice gd : devices ) {
if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) {
displayDevice = gd;
break;
}
}
// Keinen passenden Bildschirm gefunden. Wir nutzen den Standard.
if( displayDevice == null ) {
displayDevice = environment.getDefaultScreenDevice();
}
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
this.canvasWidth = width;
this.canvasHeight = height;
java.awt.Rectangle displayBounds = displayDevice.getDefaultConfiguration().getBounds();
this.screenWidth = (int) displayBounds.getWidth();
this.screenHeight = (int) displayBounds.getHeight();
// Erstellen des Zeichenfensters
frame = new JFrame(displayDevice.getDefaultConfiguration());
frame.setTitle(title);
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
// TODO: Add image sizes
ImageIcon icon = new ImageIcon(ImageIO.read(new File("res/icon_64.png")));
if( MACOS ) {
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon.getImage());
} else {
// Kleines Icon des Frames setzen
frame.setIconImage(icon.getImage());
}
} catch( IOException e ) {
}
// Erstellen der Leinwand // Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height); canvas = new Zeichenleinwand(width, height);
frame.add(canvas);
// Die drei Standardebenen merken, für den einfachen Zugriff aus unterklassen. // Erstellen des Zeichenfensters
frame = createFrame(canvas, title);
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
this.canvasWidth = canvasBounds.width;
this.canvasHeight = canvasBounds.height;
java.awt.Rectangle displayBounds = frame.getScreenBounds();
this.screenWidth = displayBounds.width;
this.screenHeight = displayBounds.height;
// Die drei Standardebenen merken, für den einfachen Zugriff aus Unterklassen.
background = getBackgroundLayer(); background = getBackgroundLayer();
drawing = getDrawingLayer(); drawing = getDrawingLayer();
shapes = getShapesLayer(); shapes = getShapesLayer();
// FPS setzen // FPS setzen
framesPerSecondInternal = STD_FPS; framesPerSecondInternal = DEFAULT_FPS;
this.run_once = run_once; this.run_once = run_once;
// Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen // Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen
// verändert werden soll. // verändert werden soll.
// TODO: When to call settings? // TODO: (ngb) Wann sollte settings() aufgerufen werden?
settings(); settings();
// Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören. // Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören.
@@ -392,31 +282,28 @@ public class Zeichenmaschine extends Constants {
canvas.addMouseWheelListener(inputListener); canvas.addMouseWheelListener(inputListener);
canvas.addKeyListener(inputListener); canvas.addKeyListener(inputListener);
/*KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent( KeyEvent e ) {
enqueueEvent(e);
return false;
}
});*/
// Programm beenden, wenn Fenster geschlossen wird // Programm beenden, wenn Fenster geschlossen wird
frame.addWindowListener(new WindowAdapter() { frame.addWindowListener(new WindowAdapter() {
@Override @Override
public void windowClosing( WindowEvent e ) { public void windowClosing( WindowEvent e ) {
if( running ) { if( isTerminated() ) {
running = false;
teardown();
cleanup();
}
// Give the app a minimum amount of time to shut down
// then kill it.
try {
Thread.sleep(5);
} catch( InterruptedException ex ) {
} finally {
quit(true); quit(true);
} else {
exitNow();
} }
} }
}); });
// Fenster zusammenbauen, auf dem Bildschirm zentrieren ... // Fenster anzeigen
frame.pack(); frame.centerFrame();
frame.setResizable(false);
centerFrame();
// ... und anzeigen!
frame.setVisible(true); frame.setVisible(true);
// Nach dem Anzeigen kann die Pufferstrategie erstellt werden. // Nach dem Anzeigen kann die Pufferstrategie erstellt werden.
@@ -440,25 +327,28 @@ public class Zeichenmaschine extends Constants {
* *
* @param title * @param title
*/ */
// TODO: Implement in conjunction with Zeichenfenster private Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
private final Zeichenfenster createFrame( String title ) { while( frame == null ) {
return null; try {
TaskRunner.invokeLater(() -> {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}).get();
} catch( InterruptedException e ) {
// Keep waiting
} catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e);
}
}
return frame;
} }
/** /**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm. * Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/ */
public final void centerFrame() { public final void centerFrame() {
// TODO: Center on current display (not main display by default) frame.centerFrame();
// TODO: Position at current BlueJ windows if IN_BLUEJ
//frame.setLocationRelativeTo(null);
//frame.setLocationRelativeTo(displayDevice.getFullScreenWindow());
java.awt.Rectangle bounds = displayDevice.getDefaultConfiguration().getBounds();
frame.setLocation(
(int) (bounds.x + (screenWidth - frame.getWidth()) / 2.0),
(int) (bounds.y + (screenHeight - frame.getHeight()) / 2.0)
);
} }
/** /**
@@ -475,37 +365,33 @@ public class Zeichenmaschine extends Constants {
* ansonsten deaktiviert. * ansonsten deaktiviert.
*/ */
public final void setFullscreen( boolean pEnable ) { public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html if( pEnable && !frame.isFullscreen() ) {
if( displayDevice.isFullScreenSupported() ) { frame.setFullscreen(true);
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
frame.setResizable(false); // Should be set anyway
displayDevice.setFullScreenWindow(frame);
// Update width / height
initialWidth = canvasWidth;
initialHeight = canvasHeight;
changeSize(screenWidth, screenHeight);
// Register ESC as exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
fullscreen = true; canvasWidth = canvas.getWidth();
fullscreenChanged(); canvasHeight = canvas.getHeight();
} else if( !pEnable && fullscreen ) {
fullscreen = false;
canvas.removeKeyListener(fullscreenExitListener); if( frame.isFullscreen() )
displayDevice.setFullScreenWindow(null); fullscreenChanged();
changeSize(initialWidth, initialHeight); } else if( !pEnable && frame.isFullscreen() ) {
frame.pack(); frame.setFullscreen(false);
// frame.setUndecorated(false);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( !frame.isFullscreen() )
fullscreenChanged(); fullscreenChanged();
}
} }
} }
/**
* Prüft, ob das Zeichenfenster im Vollbild läuft.
*
* @return {@code true}, wenn sich das Fesnter im Vollbildmodus befindet,
* {@code false} sonst.
*/
public boolean isFullscreen() { public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow(); return frame.isFullscreen();
return fullscreen && win != null;
} }
/** /**
@@ -541,9 +427,18 @@ public class Zeichenmaschine extends Constants {
* im Zeichenfenster an. * im Zeichenfenster an.
*/ */
public final void redraw() { public final void redraw() {
if( state == Options.AppState.PAUSED ) { if( state == Options.AppState.PAUSED
|| state == Options.AppState.TERMINATED ) {
draw(); draw();
} }
render();
}
/**
* Zeigt den aktuellen Inhalt der {@link Zeichenleinwand}
* im Zeichenfenster an, ohne vorher {@link #draw()} aufzurufen.
*/
public final void render() {
canvas.render(); canvas.render();
// canvas.invalidate(); // canvas.invalidate();
// frame.repaint(); // frame.repaint();
@@ -583,14 +478,26 @@ public class Zeichenmaschine extends Constants {
return state == Options.AppState.PAUSED; return state == Options.AppState.PAUSED;
} }
public final boolean isTerminated() {
return state == Options.AppState.TERMINATED;
}
public final boolean isTerminating() {
return state == Options.AppState.STOPPED || state == Options.AppState.TERMINATED;
}
/** /**
* Stoppt die Zeichenmaschine. * Stoppt die Zeichenmaschine.
* <p> * <p>
* Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine * Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine
* in den Zustand {@link Options.AppState#STOPPED} und ruft * in den Zustand {@link Options.AppState#STOPPED} und ruft
* {@link #teardown()} auf. Nachdem {@code teardown()} ausgeführt wurde * {@link #shutdown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das * wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das
* Zeichenfenster bleibt weiter geöffnet. * Zeichenfenster bleibt weiter geöffnet.
* <p>
* Die Zeichenmaschine reagiert in diesem Zustand weiter auf Eingaben,
* allerdings muss die Zeichnung nun manuell mit {@link #redraw()}
* aktualisiert werden.
*/ */
public final void stop() { public final void stop() {
running = false; running = false;
@@ -601,7 +508,7 @@ public class Zeichenmaschine extends Constants {
* <p> * <p>
* Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und * Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und
* verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die * verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die
* Äquivalente Methode für Unterklassen ist {@link #teardown()}, die direkt * Äquivalente Methode für Unterklassen ist {@link #shutdown()}, die direkt
* vor {@code cleanup()} aufgerufen wird. * vor {@code cleanup()} aufgerufen wird.
*/ */
private void cleanup() { private void cleanup() {
@@ -624,7 +531,23 @@ public class Zeichenmaschine extends Constants {
public final void exit() { public final void exit() {
if( running ) { if( running ) {
running = false; running = false;
this.quitAfterTeardown = true; quitAfterShutdown = true;
} else {
quit(true);
}
}
public final void exitNow() {
// Do nothing, when already quitting
if( state == Options.AppState.QUITING ) {
return;
}
if( running ) {
running = false;
terminateImediately = true;
quitAfterShutdown = true;
mainThread.interrupt();
} else { } else {
quit(true); quit(true);
} }
@@ -632,6 +555,10 @@ public class Zeichenmaschine extends Constants {
/** /**
* Beendet das Programm vollständig. * Beendet das Programm vollständig.
* <p>
* Enspricht dem Aufruf {@code quit(true)}.
*
* @see #quit(boolean)
*/ */
public final void quit() { public final void quit() {
//quit(!IN_BLUEJ); //quit(!IN_BLUEJ);
@@ -641,40 +568,29 @@ public class Zeichenmaschine extends Constants {
/** /**
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird * Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird
* die komplette VM beendet. * die komplette VM beendet.
* <p>
* Die Methode sorgt nicht für ein ordnungsgemäßes herunterfahren und
* freigeben aller Ressourcen, da die Zeichenmaschine gegebenenfalls
* geöffnet bleiben und weitere Aufgaben erfüllen soll. Aufrufende Methoden
* sollten dies berücksichtigen.
* <p>
* Soll das Programm vollständig beendet werden, ist es ratsamer
* {@link #exit()} zu verwenden.
* *
* @param exit Ob die VM beendet werden soll. * @param exit Ob die VM beendet werden soll.
* @see System#exit(int) * @see System#exit(int)
*/ */
public final void quit( boolean exit ) { public final void quit( boolean exit ) {
frame.setVisible(false); state = Options.AppState.QUITING;
canvas.dispose(); TaskRunner.invokeLater(() -> {
frame.dispose(); frame.setVisible(false);
canvas.dispose();
frame.dispose();
if( exit ) { if( exit ) {
System.exit(0); System.exit(0);
} }
} });
/**
* Interne Methode um die Größe der Zeichenfläche zu ändern.
* <p>
* Die Methode berücksichtigt nicht den Zustand des Fensters (z.B.
* Vollbildmodus) und geht davon aus, dass die aufrufende Methode
* sichergestellt hat, dass eine Änderung der Größe der Zeichenfläche
* zulässig und sinnvoll ist.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
* @see #setSize(int, int)
* @see #setFullscreen(boolean)
*/
private void changeSize( int newWidth, int newHeight ) {
canvasWidth = Math.min(Math.max(newWidth, 100), screenWidth);
canvasHeight = Math.min(Math.max(newHeight, 100), screenHeight);
if( canvas != null ) {
canvas.setSize(canvasWidth, canvasHeight);
}
} }
/** /**
@@ -685,16 +601,11 @@ public class Zeichenmaschine extends Constants {
* @see Zeichenleinwand#setSize(int, int) * @see Zeichenleinwand#setSize(int, int)
*/ */
public final void setSize( int width, int height ) { public final void setSize( int width, int height ) {
if( fullscreen ) { frame.setCanvasSize(width, height);
initialWidth = Math.min(Math.max(width, 100), screenWidth);
initialHeight = Math.min(Math.max(height, 100), screenHeight);
setFullscreen(false);
} else {
changeSize(width, height);
//frame.setSize(width, height); java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
frame.pack(); canvasWidth = canvasBounds.width;
} canvasHeight = canvasBounds.height;
} }
/** /**
@@ -768,9 +679,9 @@ public class Zeichenmaschine extends Constants {
* Gibt die erste (unterste) {@link Layer Ebene} der angegebenen Klasse * Gibt die erste (unterste) {@link Layer Ebene} der angegebenen Klasse
* zurück. * zurück.
* *
* <pre> * <pre><code>
* DrawingLayer draw = getLayer(DrawingLayer.class); * DrawingLayer draw = getLayer(DrawingLayer.class);
* </pre> * </code></pre>
* *
* @param layerClass * @param layerClass
* @param <LT> * @param <LT>
@@ -952,13 +863,15 @@ public class Zeichenmaschine extends Constants {
return; return;
} }
if( state != Options.AppState.RUNNING ) { if( state == Options.AppState.INITIALIZING ||
state == Options.AppState.INITIALIZED ) {
LOG.warn("Don't use delay(int) from within settings() or setup()."); LOG.warn("Don't use delay(int) from within settings() or setup().");
return; return;
} }
long timer = 0L; long timer = 0L;
if( updateState == Options.AppState.DRAWING ) { if( /*updateState == Options.AppState.DRAWING*/
isTerminating() ) {
// Falls gerade draw() ausgeführt wird, zeigen wir den aktuellen // Falls gerade draw() ausgeführt wird, zeigen wir den aktuellen
// Stand der Zeichnung auf der Leinwand an. Die Zeit für das // Stand der Zeichnung auf der Leinwand an. Die Zeit für das
// Rendern wird gemessen und von der Wartezeit abgezogen. // Rendern wird gemessen und von der Wartezeit abgezogen.
@@ -967,6 +880,7 @@ public class Zeichenmaschine extends Constants {
timer = System.nanoTime() - timer; timer = System.nanoTime() - timer;
} }
Options.AppState oldState = updateState;
try { try {
int sub = (int) Math.ceil(timer / 1000000.0); int sub = (int) Math.ceil(timer / 1000000.0);
@@ -974,9 +888,12 @@ public class Zeichenmaschine extends Constants {
return; return;
} }
updateState = Options.AppState.DELAYED;
Thread.sleep(ms - sub, (int) (timer % 1000000L)); Thread.sleep(ms - sub, (int) (timer % 1000000L));
} catch( InterruptedException ex ) { } catch( InterruptedException ignored ) {
// Nothing // Nothing
} finally {
updateState = oldState;
} }
} }
@@ -1030,9 +947,9 @@ public class Zeichenmaschine extends Constants {
* <p> * <p>
* Die Konstanten der Klasse {@link Cursor} definieren 13 Standardzeiger, * Die Konstanten der Klasse {@link Cursor} definieren 13 Standardzeiger,
* die durch angabe der Nummer geladen werden können. * die durch angabe der Nummer geladen werden können.
* <pre> * <pre><code>
* setCursor(Cursor.HAND_CURSOR); * setCursor(Cursor.HAND_CURSOR);
* </pre> * </code></pre>
* *
* @param pPredefinedCursor Eine der Cursor-Konstanten. * @param pPredefinedCursor Eine der Cursor-Konstanten.
* @see Cursor * @see Cursor
@@ -1105,9 +1022,9 @@ public class Zeichenmaschine extends Constants {
* {@code delta} wird in Sekunden angegeben. Um eine Form zum Beispiel um * {@code delta} wird in Sekunden angegeben. Um eine Form zum Beispiel um
* {@code 50} Pixel pro Sekunde in {@code x}-Richtung zu bewegen, kann so * {@code 50} Pixel pro Sekunde in {@code x}-Richtung zu bewegen, kann so
* vorgegangen werden: * vorgegangen werden:
* <pre> * <pre><code>
* shape.move(50*delta, 0.0); * shape.move(50*delta, 0.0);
* </pre> * </code></pre>
* *
* @param delta * @param delta
*/ */
@@ -1140,7 +1057,7 @@ public class Zeichenmaschine extends Constants {
* Spiels oder der Abspann einer Animation angezeigt werden, oder mit * Spiels oder der Abspann einer Animation angezeigt werden, oder mit
* {@link #saveImage()} die erstellte Zeichnung abgespeichert werden. * {@link #saveImage()} die erstellte Zeichnung abgespeichert werden.
*/ */
public void teardown() { public void shutdown() {
// Intentionally left blank // Intentionally left blank
} }
@@ -1175,39 +1092,44 @@ public class Zeichenmaschine extends Constants {
} }
/* /*
* Mouse handling * Input handling
*/ */
private void enqueueEvent( InputEvent evt ) { private void enqueueEvent( InputEvent evt ) {
eventQueue.add(evt); if( updateState != Options.AppState.DELAYED ) {
eventQueue.add(evt);
}
if( isPaused() ) { if( isPaused() || isTerminated() ) {
if( MouseEvent.class.isInstance(evt) ) {
saveMousePosition((MouseEvent)evt);
}
dispatchEvents(); dispatchEvents();
} }
} }
private void dispatchEvents() { private void dispatchEvents() {
synchronized( eventQueue ) { //synchronized( eventQueue ) {
while( !eventQueue.isEmpty() ) { while( !eventQueue.isEmpty() ) {
InputEvent evt = eventQueue.poll(); InputEvent evt = eventQueue.poll();
switch( evt.getID() ) { switch( evt.getID() ) {
case KeyEvent.KEY_TYPED: case KeyEvent.KEY_TYPED:
case KeyEvent.KEY_PRESSED: case KeyEvent.KEY_PRESSED:
case KeyEvent.KEY_RELEASED: case KeyEvent.KEY_RELEASED:
handleKeyEvent((KeyEvent) evt); handleKeyEvent((KeyEvent) evt);
break; break;
case MouseEvent.MOUSE_CLICKED: case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_PRESSED: case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED: case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_MOVED: case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED: case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_WHEEL: case MouseEvent.MOUSE_WHEEL:
handleMouseEvent((MouseEvent) evt); handleMouseEvent((MouseEvent) evt);
break; break;
}
} }
} }
//}
} }
private void handleKeyEvent( KeyEvent evt ) { private void handleKeyEvent( KeyEvent evt ) {
@@ -1245,7 +1167,7 @@ public class Zeichenmaschine extends Constants {
case MouseEvent.MOUSE_RELEASED: case MouseEvent.MOUSE_RELEASED:
mousePressed = false; mousePressed = false;
mouseButton = NOMOUSE; mouseButton = NOMOUSE;
mousePressed(evt); mouseReleased(evt);
break; break;
case MouseEvent.MOUSE_DRAGGED: case MouseEvent.MOUSE_DRAGGED:
//saveMousePosition(evt); //saveMousePosition(evt);
@@ -1376,9 +1298,9 @@ public class Zeichenmaschine extends Constants {
* // Next frame has started * // Next frame has started
* </code></pre> * </code></pre>
* <p> * <p>
* Die {@link schule.ngb.zm.tasks.FrameSynchronizedTask} implementiert eine * Die {@link schule.ngb.zm.util.tasks.FrameSynchronizedTask} implementiert
* {@link schule.ngb.zm.tasks.Task}, die sich automatisch auf diese Wiese * eine {@link schule.ngb.zm.util.tasks.Task}, die sich automatisch auf
* mit dem Zeichenthread synchronisiert. * diese Wiese mit dem Zeichenthread synchronisiert.
*/ */
public static final Object globalSyncLock = new Object[0]; public static final Object globalSyncLock = new Object[0];
@@ -1393,48 +1315,51 @@ public class Zeichenmaschine extends Constants {
public final void run() { public final void run() {
// Wait for full initialization before start // Wait for full initialization before start
while( state != Options.AppState.INITIALIZED ) { while( state != Options.AppState.INITIALIZED ) {
delay(1); Thread.yield();
} }
// ThreadExecutor for the update/draw Thread // ThreadExecutor for the update/draw Thread
final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor(); final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor();
// start of thread in ms // Start des Thread in ms
final long start = System.currentTimeMillis(); final long start = System.currentTimeMillis();
// current time in ns // Aktuelle Zeit in ns
long beforeTime = System.nanoTime(); long beforeTime;
long updateBeforeTime = System.nanoTime(); long updateBeforeTime = System.nanoTime();
// store for deltas // Speicher für Änderung
long overslept = 0L; long overslept = 0L;
// internal counters for tick and runtime // Interne Zähler für tick und runtime
int _tick = 0; int _tick = 0;
long _runtime = 0; long _runtime;
// public counters for access by subclasses // Öffentliche Zähler für Unterklassen
tick = 0; tick = 0;
runtime = 0; runtime = 0;
// call setup of subclass and wait // setup() der Unterklasse aufrufen
setup(); setup();
// Alles startklar ...
state = Options.AppState.RUNNING; state = Options.AppState.RUNNING;
while( running ) { while( running ) {
// delta in seconds // Aktuelle Zeit in ns merken
beforeTime = System.nanoTime(); beforeTime = System.nanoTime();
// Mausposition einmal pro Frame merken
saveMousePosition(mouseEvent); saveMousePosition(mouseEvent);
if( state != Options.AppState.PAUSED ) { if( state != Options.AppState.PAUSED ) {
//handleUpdate(delta); // update() und draw() der Unterklasse werden in einem
//handleDraw(); // eigenen Thread ausgeführt, aber der Zeichenthread
// wartet, bis der Thread fertig ist. Außer die Unterklasse
// Update and draw are executed in a new thread, // ruft delay() auf und lässt den Thread eine länger Zeit
// but we wait for them to finish unless the user // schlafen. Dann wird der nächst Frame vorzeitig gerendert,
// did call any blocking method, that would also block // bis der update-Thread wieder bereit ist. Dadurch können
// rendering of new frames. // nebenläufige Aufgaben (z.B. Animationen) weiterlaufen.
if( !updateThreadExecutor.isRunning() ) { if( !updateThreadExecutor.isRunning() ) {
delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0; delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0;
updateBeforeTime = System.nanoTime(); updateBeforeTime = System.nanoTime();
// uddate()/draw() ausführen
updateThreadExecutor.execute(() -> { updateThreadExecutor.execute(() -> {
if( state == Options.AppState.RUNNING if( state == Options.AppState.RUNNING
&& updateState == Options.AppState.IDLE ) { && updateState == Options.AppState.IDLE ) {
@@ -1446,10 +1371,11 @@ public class Zeichenmaschine extends Constants {
// Call to draw() // Call to draw()
updateState = Options.AppState.DRAWING; updateState = Options.AppState.DRAWING;
Zeichenmaschine.this.draw(); Zeichenmaschine.this.draw();
updateState = Options.AppState.IDLE; updateState = Options.AppState.DISPATCHING;
// Send latest input events after finishing draw // Send latest input events after finishing draw
// since these may also block // since these may also block
dispatchEvents(); dispatchEvents();
updateState = Options.AppState.IDLE;
} }
}); });
} }
@@ -1458,16 +1384,21 @@ public class Zeichenmaschine extends Constants {
while( updateThreadExecutor.isRunning() while( updateThreadExecutor.isRunning()
&& !updateThreadExecutor.isWaiting() ) { && !updateThreadExecutor.isWaiting() ) {
Thread.yield(); Thread.yield();
if( Thread.interrupted() ) {
running = false;
terminateImediately = true;
break;
}
} }
// Display the current buffer content // Display the current buffer content
if( canvas != null ) { if( canvas != null && frame.isDisplayable() ) {
canvas.render(); canvas.render();
// canvas.invalidate(); // canvas.invalidate();
// frame.repaint(); // frame.repaint();
} }
// dispatchEvents(); // dispatchEvents();
} }
@@ -1510,41 +1441,32 @@ public class Zeichenmaschine extends Constants {
pause_pending = false; pause_pending = false;
} }
} }
// Shutdown the updateThreads
updateThreadExecutor.shutdownNow();
state = Options.AppState.STOPPED; state = Options.AppState.STOPPED;
// Shutdown the updateThread
// Cleanup while( !terminateImediately && updateThreadExecutor.isRunning() ) {
teardown(); Thread.yield();
cleanup();
state = Options.AppState.TERMINATED;
if( quitAfterTeardown ) {
quit();
} }
} updateThreadExecutor.shutdownNow();
try {
updateThreadExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
public void handleUpdate( double delta ) { } finally {
if( state == Options.AppState.RUNNING ) { // Cleanup
state = Options.AppState.UPDATING; shutdown();
update(delta); cleanup();
canvas.updateLayers(delta); state = Options.AppState.TERMINATED;
state = Options.AppState.RUNNING;
}
}
public void handleDraw() { if( quitAfterShutdown ) {
if( state == Options.AppState.RUNNING ) { quit();
state = Options.AppState.DRAWING; }
draw();
state = Options.AppState.RUNNING;
} }
} }
} }
// TODO: Remove // TODO: Remove
class DelayedTask implements Delayed { static class DelayedTask implements Delayed {
long startTime; // in ms long startTime; // in ms
@@ -1633,25 +1555,42 @@ public class Zeichenmaschine extends Constants {
} }
// TODO: (ngb) exception handling when update/draw throws ex
class UpdateThreadExecutor extends ThreadPoolExecutor { class UpdateThreadExecutor extends ThreadPoolExecutor {
private Thread updateThread; private Thread updateThread;
private boolean running = false, waiting = false; private boolean running = false;
public UpdateThreadExecutor() { public UpdateThreadExecutor() {
super(1, 1, 0L, super(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread( Runnable r ) {
Thread t = new Thread(mainThread.getThreadGroup(), r,
"updateThread-" + threadNumber.getAndIncrement(),
0);
t.setDaemon(true);
return t;
}
});
updateState = Options.AppState.IDLE; updateState = Options.AppState.IDLE;
} }
@Override
public void execute( Runnable command ) {
running = true;
super.execute(command);
}
@Override @Override
protected void beforeExecute( Thread t, Runnable r ) { protected void beforeExecute( Thread t, Runnable r ) {
// We store the one Thread this Executor holds // We store the one Thread this Executor holds,
// but it might change if a new Thread needed to be spawned // but it might change if a new Thread needed to be spawned
// due to en error. // due to en error.
updateThread = t; updateThread = t;
running = true;
} }
@Override @Override
@@ -1682,7 +1621,9 @@ public class Zeichenmaschine extends Constants {
* @return * @return
*/ */
public boolean isWaiting() { public boolean isWaiting() {
return running && updateThread.getState() == Thread.State.TIMED_WAITING; //return running && updateThread.getState() == Thread.State.TIMED_WAITING;
//return running && updateThread != null && updateThread.getState() == Thread.State.TIMED_WAITING;
return running && updateThread != null && updateState == Options.AppState.DELAYED;
} }
} }

View File

@@ -2,17 +2,16 @@ package schule.ngb.zm.anim;
import schule.ngb.zm.Constants; import schule.ngb.zm.Constants;
import schule.ngb.zm.Updatable; import schule.ngb.zm.Updatable;
import schule.ngb.zm.events.EventDispatcher; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.tasks.FrameSynchronizedTask; import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.tasks.TaskRunner;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
public abstract class Animation<T> implements Updatable { public abstract class Animation<T> extends Constants implements Updatable {
protected int runtime; protected int runtime;
protected int elapsed_time = 0; protected int elapsedTime = 0;
protected boolean running = false, finished = false; protected boolean running = false, finished = false;
@@ -25,7 +24,7 @@ public abstract class Animation<T> implements Updatable {
public Animation( DoubleUnaryOperator easing ) { public Animation( DoubleUnaryOperator easing ) {
this.runtime = Constants.DEFAULT_ANIM_RUNTIME; this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
this.easing = easing; this.easing = Validator.requireNotNull(easing);
} }
public Animation( int runtime ) { public Animation( int runtime ) {
@@ -35,7 +34,7 @@ public abstract class Animation<T> implements Updatable {
public Animation( int runtime, DoubleUnaryOperator easing ) { public Animation( int runtime, DoubleUnaryOperator easing ) {
this.runtime = runtime; this.runtime = runtime;
this.easing = easing; this.easing = Validator.requireNotNull(easing);
} }
public int getRuntime() { public int getRuntime() {
@@ -58,17 +57,17 @@ public abstract class Animation<T> implements Updatable {
public final void start() { public final void start() {
this.initialize(); this.initialize();
elapsed_time = 0; elapsedTime = 0;
running = true; running = true;
finished = false; finished = false;
interpolate(easing.applyAsDouble(0.0)); animate(easing.applyAsDouble(0.0));
initializeEventDispatcher().dispatchEvent("start", this); initializeEventDispatcher().dispatchEvent("start", this);
} }
public final void stop() { public final void stop() {
running = false; running = false;
// Make sure the last animation frame was interpolated correctly // Make sure the last animation frame was interpolated correctly
interpolate(easing.applyAsDouble((double) elapsed_time / (double) runtime)); animate(easing.applyAsDouble((double) elapsedTime / (double) runtime));
this.finish(); this.finish();
finished = true; finished = true;
initializeEventDispatcher().dispatchEvent("stop", this); initializeEventDispatcher().dispatchEvent("stop", this);
@@ -84,11 +83,7 @@ public abstract class Animation<T> implements Updatable {
public final void await() { public final void await() {
while( !finished ) { while( !finished ) {
try { Thread.yield();
Thread.sleep(1);
} catch( InterruptedException ex ) {
// Keep waiting
}
} }
} }
@@ -99,16 +94,16 @@ public abstract class Animation<T> implements Updatable {
@Override @Override
public void update( double delta ) { public void update( double delta ) {
elapsed_time += (int) (delta * 1000); elapsedTime += (int) (delta * 1000);
if( elapsed_time > runtime ) if( elapsedTime > runtime )
elapsed_time = runtime; elapsedTime = runtime;
double t = (double) elapsed_time / (double) runtime; double t = (double) elapsedTime / (double) runtime;
if( t >= 1.0 ) { if( t >= 1.0 ) {
running = false; running = false;
stop(); stop();
} else { } else {
interpolate(easing.applyAsDouble(t)); animate(easing.applyAsDouble(t));
} }
} }
@@ -123,9 +118,10 @@ public abstract class Animation<T> implements Updatable {
* e = Constants.limit(e, 0, 1); * e = Constants.limit(e, 0, 1);
* </code></pre> * </code></pre>
* *
* @param e * @param e Fortschritt der Animation nachdem die Easingfunktion angewandt
* wurde.
*/ */
public abstract void interpolate( double e ); public abstract void animate( double e );
EventDispatcher<Animation, AnimationListener> eventDispatcher; EventDispatcher<Animation, AnimationListener> eventDispatcher;

View File

@@ -19,8 +19,8 @@ public class AnimationFacade<S> extends Animation<S> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
anim.interpolate(e); anim.animate(e);
} }
@Override @Override

View File

@@ -1,76 +1,116 @@
package schule.ngb.zm.anim; package schule.ngb.zm.anim;
import schule.ngb.zm.shapes.Shape; import java.util.Collection;
import java.util.List;
import java.util.Arrays;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
public class AnimationGroup extends Animation<Shape> { @SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> {
Animation<? extends Shape>[] anims; List<Animation<T>> anims;
private boolean overrideRuntime = false;
public AnimationGroup( DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) { private boolean overrideEasing = false;
super(easing);
this.anims = anims;
int maxRuntime = Arrays.stream(this.anims).mapToInt((a) -> a.getRuntime()).reduce(0, Integer::max); private int overrideRuntime = -1;
setRuntime(maxRuntime);
private int lag = 0;
private int active = 0;
public AnimationGroup( Collection<Animation<T>> anims ) {
this(0, -1, null, anims);
} }
public AnimationGroup( int runtime, DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) { public AnimationGroup( int lag, Collection<Animation<T>> anims ) {
super(runtime, easing); this(lag, -1, null, anims);
this.anims = anims; }
overrideRuntime = true;
public AnimationGroup( DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(0, -1, easing, anims);
}
public AnimationGroup( int lag, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(lag, -1, easing, anims);
}
public AnimationGroup( int lag, int runtime, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
super();
this.anims = List.copyOf(anims);
this.lag = lag;
if( easing != null ) {
this.easing = easing;
overrideEasing = true;
}
if( runtime > 0 ) {
this.runtime = anims.size() * lag + runtime;
this.overrideRuntime = runtime;
} else {
this.runtime = 0;
for( int i = 0; i < this.anims.size(); i++ ) {
if( i * lag + this.anims.get(i).getRuntime() > this.runtime ) {
this.runtime = i * lag + this.anims.get(i).getRuntime();
}
}
}
} }
@Override @Override
public Shape getAnimationTarget() { public T getAnimationTarget() {
return null; for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getAnimationTarget();
}
}
return anims.get(anims.size() - 1).getAnimationTarget();
} }
@Override @Override
public void update( double delta ) { public void update( double delta ) {
if( overrideRuntime ) { elapsedTime += (int) (delta * 1000);
synchronized( anims ) { // Animation is done. Stop all Animations.
for( Animation<? extends Shape> anim: anims ) { if( elapsedTime > runtime ) {
if( anim.isActive() ) { for( int i = 0; i < anims.size(); i++ ) {
anim.update(delta); if( anims.get(i).isActive() ) {
} anims.get(i).elapsedTime = anims.get(i).runtime;
anims.get(i).stop();
} }
} }
} else { running = false;
super.update(delta); this.stop();
} }
}
@Override while( active < anims.size() && elapsedTime >= active * lag ) {
public void interpolate( double e ) { anims.get(active).start();
synchronized( anims ) { active += 1;
for( Animation<? extends Shape> anim: anims ) { }
anim.interpolate(e);
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);
} }
} }
} }
@Override @Override
public void initialize() { public void animate( double e ) {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.initialize();
}
}
}
@Override
public void finish() {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.finish();
}
}
} }
} }

View File

@@ -1,6 +1,6 @@
package schule.ngb.zm.anim; package schule.ngb.zm.anim;
import schule.ngb.zm.events.Listener; import schule.ngb.zm.util.events.Listener;
public interface AnimationListener extends Listener<Animation> { public interface AnimationListener extends Listener<Animation> {

View File

@@ -3,9 +3,8 @@ package schule.ngb.zm.anim;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Constants; import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector; import schule.ngb.zm.Vector;
import schule.ngb.zm.tasks.FrameSynchronizedTask; import schule.ngb.zm.util.tasks.FramerateLimitedTask;
import schule.ngb.zm.tasks.FramerateLimitedTask; import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
@@ -15,6 +14,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.function.*; import java.util.function.*;
@SuppressWarnings( "unused" )
public class Animations { public class Animations {
public static final <T> Future<T> animateProperty( String propName, T target, double to, int runtime, DoubleUnaryOperator easing ) { public static final <T> Future<T> animateProperty( String propName, T target, double to, int runtime, DoubleUnaryOperator easing ) {
@@ -92,27 +92,28 @@ public class Animations {
}); });
} }
private static final <T, R> R callGetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { @SuppressWarnings( "unchecked" )
private static <T, R> R callGetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String getterName = makeMethodName("get", propName); String getterName = makeMethodName("get", propName);
Method getter = target.getClass().getMethod(getterName); Method getter = target.getClass().getMethod(getterName);
if( getter != null && getter.getReturnType().equals(propType) ) { if( getter.getReturnType().equals(propType) ) {
return (R) getter.invoke(target); return (R) getter.invoke(target);
} else { } else {
throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName)); throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName));
} }
} }
private static final <T, R> Method findSetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException { private static <T, R> Method findSetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException {
String setterName = makeMethodName("set", propName); String setterName = makeMethodName("set", propName);
Method setter = target.getClass().getMethod(setterName, propType); Method setter = target.getClass().getMethod(setterName, propType);
if( setter != null && setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) { if( setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) {
return setter; return setter;
} else { } else {
throw new NoSuchMethodException(String.format("No setter for property <%s> found.", propName)); throw new NoSuchMethodException(String.format("No setter for property <%s> found.", propName));
} }
} }
private static final String makeMethodName( String prefix, String propName ) { private static String makeMethodName( String prefix, String propName ) {
String firstChar = propName.substring(0, 1).toUpperCase(); String firstChar = propName.substring(0, 1).toUpperCase();
String tail = ""; String tail = "";
if( propName.length() > 1 ) { if( propName.length() > 1 ) {
@@ -124,31 +125,33 @@ public class Animations {
public static final <T> Future<T> animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) { public static final <T> Future<T> animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) {
Validator.requireNotNull(target); Validator.requireNotNull(target);
Validator.requireNotNull(propSetter); Validator.requireNotNull(propSetter);
return animate(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e))); return play(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
} }
public static final <T> Future<T> animateProperty( T target, final Color from, final Color to, int runtime, DoubleUnaryOperator easing, Consumer<Color> propSetter ) { public static final <T> Future<T> animateProperty( T target, final Color from, final Color to, int runtime, DoubleUnaryOperator easing, Consumer<Color> propSetter ) {
return animate(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e))); return play(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e)));
} }
public static final <T> Future<T> animateProperty( T target, final Vector from, final Vector to, int runtime, DoubleUnaryOperator easing, Consumer<Vector> propSetter ) { public static final <T> Future<T> animateProperty( T target, final Vector from, final Vector to, int runtime, DoubleUnaryOperator easing, Consumer<Vector> propSetter ) {
return animate(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e))); return play(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e)));
} }
public static final <T, R> Future<T> animateProperty( T target, R from, R to, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, Consumer<R> propSetter ) { public static final <T, R> Future<T> animateProperty( T target, R from, R to, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, Consumer<R> propSetter ) {
return animate(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r)); return play(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
} }
public static final <T, R> Future<T> animate( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, BiConsumer<T, R> applicator ) { public static final <T, R> Future<T> play( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, BiConsumer<T, R> applicator ) {
return animate(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e))); return play(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e)));
} }
public static final <T> Future<T> animate( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) { public static final <T> Future<T> play( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
return TaskRunner.run(new FramerateLimitedTask() { return TaskRunner.run(new FramerateLimitedTask() {
double t = 0.0; double t = 0.0;
final long starttime = System.currentTimeMillis(); final long starttime = System.currentTimeMillis();
@Override @Override
public void update( double delta ) { public void update( double delta ) {
// One animation step for t in [0,1] // One animation step for t in [0,1]
@@ -164,8 +167,8 @@ public class Animations {
}, target); }, target);
} }
public static final <T> T animateAndWait( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) { public static final <T> T playAndWait( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
Future<T> future = animate(target, runtime, easing, stepper); Future<T> future = play(target, runtime, easing, stepper);
while( !future.isDone() ) { while( !future.isDone() ) {
try { try {
return future.get(); return future.get();
@@ -179,16 +182,17 @@ public class Animations {
return target; return target;
} }
public static final <T, R> Future<T> animate( T target, int runtime, Animator<T, R> animator ) { /*public static final <T, R> Future<T> animate( T target, int runtime, Animator<T, R> animator ) {
return animate( return animate(
target, runtime, target, runtime,
animator::easing, animator::easing,
animator::interpolator, animator::interpolator,
animator::applicator animator::applicator
); );
} }*/
public static <T> Future<Animation<T>> animate( Animation<T> animation ) { public static <T> Future<Animation<T>> play( Animation<T> animation ) {
// TODO: (ngb) Don't start when running
return TaskRunner.run(new FramerateLimitedTask() { return TaskRunner.run(new FramerateLimitedTask() {
@Override @Override
protected void initialize() { protected void initialize() {
@@ -203,13 +207,13 @@ public class Animations {
}, animation); }, animation);
} }
public static <T> Animation<T> animateAndWait( Animation<T> animation ) { public static <T> Animation<T> playAndWait( Animation<T> animation ) {
Future<Animation<T>> future = animate(animation); Future<Animation<T>> future = play(animation);
animation.await(); animation.await();
return animation; return animation;
} }
public static <T> Future<Animation<T>> animate( Animation<T> animation, DoubleUnaryOperator easing ) { public static <T> Future<Animation<T>> play( Animation<T> animation, DoubleUnaryOperator easing ) {
final AnimationFacade<T> facade = new AnimationFacade<>(animation, animation.getRuntime(), easing); final AnimationFacade<T> facade = new AnimationFacade<>(animation, animation.getRuntime(), easing);
return TaskRunner.run(new FramerateLimitedTask() { return TaskRunner.run(new FramerateLimitedTask() {
@Override @Override

View File

@@ -1,11 +0,0 @@
package schule.ngb.zm.anim;
public interface Animator<T, R> {
double easing(double t);
R interpolator(double e);
void applicator(T target, R value);
}

View File

@@ -0,0 +1,39 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class CircleAnimation extends Animation<Shape> {
private Shape object;
private double centerx, centery, radius, startangle;
public CircleAnimation( Shape target, double cx, double cy, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
object = target;
centerx = cx;
centery = cy;
Vector vec = new Vector(target.getX(), target.getY()).sub(cx, cy);
startangle = vec.heading();
radius = vec.length();
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
double angle = startangle + Constants.radians(Constants.interpolate(0, 360, e));
double x = centerx + radius * Constants.cos(angle);
double y = centery + radius * Constants.sin(angle);
object.moveTo(x, y);
}
}

View File

@@ -0,0 +1,74 @@
package schule.ngb.zm.anim;
@SuppressWarnings( "unused" )
public class ContinousAnimation<T> extends Animation<T> {
private final Animation<T> baseAnimation;
private int lag = 0;
/**
* Speichert eine Approximation der aktuellen Steigung der Easing-Funktion,
* um im Fall {@code easeInOnly == true} nach dem ersten Durchlauf die
* passende Geschwindigkeit beizubehalten.
*/
private double m = 1.0, lastEase = 0.0;
private boolean easeInOnly = false;
public ContinousAnimation( Animation<T> baseAnimation ) {
this(baseAnimation, 0, false);
}
public ContinousAnimation( Animation<T> baseAnimation, int lag ) {
this(baseAnimation, lag, false);
}
public ContinousAnimation( Animation<T> baseAnimation, boolean easeInOnly ) {
this(baseAnimation, 0, easeInOnly);
}
private ContinousAnimation( Animation<T> baseAnimation, int lag, boolean easeInOnly ) {
super(baseAnimation.getRuntime(), baseAnimation.getEasing());
this.baseAnimation = baseAnimation;
this.lag = lag;
this.easeInOnly = easeInOnly;
}
@Override
public T getAnimationTarget() {
return baseAnimation.getAnimationTarget();
}
@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 animate( double e ) {
baseAnimation.animate(e);
}
}

View File

@@ -36,7 +36,7 @@ public class FadeAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e))); object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e)));
object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e))); object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e)));
} }

View File

@@ -26,7 +26,7 @@ public class FillAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.setFillColor(Color.interpolate(oFill, tFill, e)); object.setFillColor(Color.interpolate(oFill, tFill, e));
} }

View File

@@ -27,7 +27,7 @@ public class MorphAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.setX(Constants.interpolate(original.getX(), target.getX(), e)); object.setX(Constants.interpolate(original.getX(), target.getX(), e));
object.setY(Constants.interpolate(original.getY(), target.getY(), e)); object.setY(Constants.interpolate(original.getY(), target.getY(), e));
object.setFillColor(Color.interpolate(original.getFillColor(), target.getFillColor(), e)); object.setFillColor(Color.interpolate(original.getFillColor(), target.getFillColor(), e));

View File

@@ -31,7 +31,7 @@ public class MoveAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e)); object.setX(Constants.interpolate(oX, tX, e));
object.setY(Constants.interpolate(oY, tY, e)); object.setY(Constants.interpolate(oY, tY, e));
} }

View File

@@ -25,7 +25,7 @@ public class RotateAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.rotateTo(Constants.interpolate(oA, tA, e)); object.rotateTo(Constants.interpolate(oA, tA, e));
} }

View File

@@ -25,7 +25,7 @@ public class StrokeAnimation extends Animation<Shape> {
} }
@Override @Override
public void interpolate( double e ) { public void animate( double e ) {
object.setStrokeColor(Color.interpolate(oFill, tFill, e)); object.setStrokeColor(Color.interpolate(oFill, tFill, e));
} }

View File

@@ -0,0 +1,37 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Options;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class WaveAnimation extends Animation<Shape> {
private Shape object;
private double strength, sinOffset, previousDelta = 0.0;
private Options.Direction dir;
public WaveAnimation( Shape target, double strength, Options.Direction dir, double sinOffset, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = target;
this.dir = dir;
this.strength = strength;
this.sinOffset = sinOffset;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
double delta = this.strength * Constants.sin(Constants.interpolate(0.0, Constants.TWO_PI, e) + sinOffset);
object.move((delta - previousDelta) * dir.x, (delta - previousDelta) * dir.y);
previousDelta = delta;
}
}

View File

@@ -0,0 +1,227 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.GradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
/**
* Eine Ebene, die nur aus einer Farbe (oder einem Farbverlauf) besteht.
* <p>
* Ein {@code ColorLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
* <p>
* Die Farbe der Ebene kann beliebig gesetzt werden und kann gut als
* Hintergrundfarbe für eine Szene dienen, oder als halbtransparente
* "Abdeckung", wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt
* wird.
*/
@SuppressWarnings( "unused" )
public class ColorLayer extends Layer {
/**
* Farbe der Ebene.
*/
private Color color;
/**
* Verlauf der Ebene, falls verwendet.
*/
private Paint background;
/**
* Erstellt eine neue Farbebene mit der angegebenen Farbe.
*
* @param color Die Hintergrundfarbe.
*/
public ColorLayer( Color color ) {
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* Erstellt eine neue Farbebene mit der angegebenen Größe und Farbe.
*
* @param width Breite der Ebene.
* @param height Höhe der Ebene.
* @param color Die Hintergrundfarbe.
*/
public ColorLayer( int width, int height, Color color ) {
super(width, height);
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* {@inheritDoc}
*/
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
clear();
}
/**
* @return Die aktuelle Hintergrundfarbe der Ebene.
*/
public Color getColor() {
return color;
}
/**
* Setzt die Farbe der Ebene auf die angegebene Farbe.
*
* @param color Die neue Hintergrundfarbe.
*/
public void setColor( Color color ) {
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der in die
* angegebene Richtung verläuft.
*
* @param from Farbe am Startpunkt.
* @param to Farbe am Endpunkt.
* @param dir Richtung des Farbverlaufs.
*/
public void setGradient( Color from, Color to, Options.Direction dir ) {
double halfW = getWidth() * .5;
double halfH = getHeight() * .5;
Options.Direction inv = dir.inverse();
int fromX = (int) (halfW + inv.x * halfW);
int fromY = (int) (halfH + inv.y * halfH);
int toX = (int) (halfW + dir.x * halfW);
int toY = (int) (halfH + dir.y * halfH);
setGradient(fromX, fromY, from, toX, toY, to);
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
this.color = from;
background = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
clear();
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, der im Zentrum beginnt.
*
* @param from Farbe im Zentrum.
* @param to Farbe am Rand.
*/
public void setGradient( Color from, Color to ) {
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, to);
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, mit dem Zentrum im Punkt ({@code centerX}, {@code centerY})
* und dem angegebenen Radius. Der Verlauf starte im Zentrum mit der Farbe
* {@code from} und endet am Rand des durch den Radius beschriebenen Kreises
* mit der Farbe {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
this.color = from;
background = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
clear();
}
/**
* Zeichnet den Hintergrund der Ebene mit der gesetzten Füllung neu.
*/
@Override
public void clear() {
drawing.setPaint(background);
drawing.fillRect(0, 0, getWidth(), getHeight());
}
}

View File

@@ -0,0 +1,109 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Layer;
import java.awt.Graphics2D;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Ein Layer um {@link Drawable} Objekte zu zeichnen.
* <p>
* Objekte, die das {@code Drawable} Interface implementieren, können der Ebene
* hinzugefügt werden. Die Ebene sorgt dafür, dass alle {@code Drawable}s einmal
* pro Frame über ihre {@link Drawable#draw(Graphics2D)} Methode gezeichnet.
*/
@SuppressWarnings( "unused" )
public class DrawableLayer extends Layer {
/**
* Liste der {@link Drawable}s.
*/
protected final List<Drawable> drawables;
/**
* Ob die Ebene bei jedem Aufruf von {@link #draw(Graphics2D)} geleert
* werden soll.
*/
protected boolean clearBeforeDraw = true;
/**
* Erstellt eine Ebene in der Standardgröße.
*/
public DrawableLayer() {
drawables = new LinkedList<>();
}
/**
* Erstellt eine Ebene mit der angegebenen Größe.
*
* @param width Die Breite der Ebene.
* @param height Die Höhe der Ebene.
*/
public DrawableLayer( int width, int height ) {
super(width, height);
drawables = new LinkedList<>();
}
/**
* Fügt alle angegebenen {@code Drawable}s der Ebene hinzu.
*
* @param drawables Die {@code Drawable} Objekte.
*/
public void add( Drawable... drawables ) {
synchronized( this.drawables ) {
Collections.addAll(this.drawables, drawables);
}
}
/**
* Gibt eine Liste aller {@code Drawable} Objekte dieser Ebene zurück.
*
* @return Die Liste der {@code Drawable} Objekte.
*/
public java.util.List<Drawable> getDrawables() {
return drawables;
}
/**
* Ob die Ebene bei jedem Frame automatisch gelöscht wird.
*
* @return {@code true}, wenn die Ebene vorm Zeichnen gelöscht wird,
* {@code false} sonst.
*/
public boolean isClearBeforeDraw() {
return clearBeforeDraw;
}
/**
* Stellt ein, ob die Ebene vorm Zeichnen gelöscht werden soll.
*
* @param pClearBeforeDraw Ob die Ebene vorm Zeichnen gelöscht werden
* soll.
*/
public void setClearBeforeDraw( boolean pClearBeforeDraw ) {
this.clearBeforeDraw = pClearBeforeDraw;
}
/**
* {@inheritDoc}
*/
@Override
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) {
if( d.isVisible() ) {
d.draw(drawing);
}
}
super.draw(graphics);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Graphics2D;
import java.awt.Image;
/**
* Eine Ebene, die ein statisches Bild anzeigt.
* <p>
* Die Ebene wird mit einem Bild initialisiert und zeigt dieses Bild als
* einzigen Inhalt an. Optional kann die Position des Bildes verändert werden,
* sodass es nicht im Ursprung der Ebene gezeichnet wird.
*/
@SuppressWarnings( "unused" )
public class ImageLayer extends Layer {
/**
* Das Bild, das angezeigt wird.
*/
protected Image image;
/**
* x-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double x = 0;
/**
* y-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double y = 0;
/**
* Interner Schalter, ob die Ebene neu gezeichnet werden muss.
*/
protected boolean redraw = true;
/**
* Erstellt eine Bildebene in der Standardgröße aus der angegebenen
* Bildquelle.
*
* @param source Eine Bildquelle.
* @see ImageLoader#loadImage(String)
*/
public ImageLayer( String source ) {
image = ImageLoader.loadImage(source);
}
/**
* Erstellt eine Bildebene in der Standardgröße aus dem angegebenen Bild.
*
* @param image Ein Bild-Objekt.
*/
public ImageLayer( Image image ) {
this.image = image;
}
/**
* Erstellt eine Bildebene in der angegebenen Größe aus dem angegebenen
* Bild.
*
* @param width Breite der Bildebene.
* @param height Höhe der Bildebene.
* @param image Ein Bild-Objekt.
*/
public ImageLayer( int width, int height, Image image ) {
super(width, height);
this.image = image;
}
/**
* Setzt das Bild der Ebene auf das angegebene Bild-Objekt.
*
* @param image Ein Bild-Objekt.
*/
public void setImage( Image image ) {
this.image = image;
redraw = true;
}
/**
* @return Die x-Koordinate des Bildes in der Ebene.
*/
public double getX() {
return x;
}
/**
* Setzt die {@code x}-Koordinate des BIldes in der Ebene auf den
* angegebenen Wert.
*
* @param pX Die x-Koordinate des Bildes.
*/
public void setX( double pX ) {
this.x = pX;
redraw = true;
}
/**
* @return Die y-Koordinate des Bildes in der Ebene.
*/
public double getY() {
return y;
}
/**
* Setzt die {@code y}-Koordinate des BIldes in der Ebene auf den
* angegebenen Wert.
*
* @param pY Die y-Koordinate des Bildes.
*/
public void setY( double pY ) {
this.y = pY;
redraw = true;
}
/**
* Löscht die Ebene und zeichnet das Bild neu.
* <p>
* In der Regel muss die Ebene nicht gelöscht werden, da sie automatisch neu
* gezeichnet wird, sobald sich das zugrundeliegende Bild ändert.
*/
@Override
public void clear() {
super.clear();
redraw = true;
}
@Override
public void draw( Graphics2D graphics ) {
if( redraw && visible ) {
drawing.drawImage(image, (int) x, (int) y, null);
redraw = false;
}
super.draw(graphics);
}
}

View File

@@ -1,45 +1,53 @@
package schule.ngb.zm; package schule.ngb.zm.layers;
import java.awt.*; import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.util.LinkedList; import java.util.LinkedList;
@SuppressWarnings( "unused" )
public final class Shape2DLayer extends Layer { public final class Shape2DLayer extends Layer {
protected Color strokeColor = STD_STROKECOLOR; private schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected Color fillColor = STD_FILLCOLOR; private schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected double strokeWeight = STD_STROKEWEIGHT; private double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID; private Options.StrokeType strokeType = SOLID;
private LinkedList<java.awt.Shape> shapes; private final LinkedList<java.awt.Shape> shapes;
private boolean instantDraw = false; private boolean instantDraw = false;
public Shape2DLayer() { public Shape2DLayer() {
super(); super();
shapes = new LinkedList<java.awt.Shape>(); shapes = new LinkedList<>();
} }
public Shape2DLayer( boolean instantDraw ) { public Shape2DLayer( boolean instantDraw ) {
super(); super();
shapes = new LinkedList<java.awt.Shape>(); shapes = new LinkedList<>();
this.instantDraw = instantDraw; this.instantDraw = instantDraw;
} }
public Shape2DLayer( int width, int height ) { public Shape2DLayer( int width, int height ) {
super(width, height); super(width, height);
shapes = new LinkedList<java.awt.Shape>(); shapes = new LinkedList<>();
} }
public Shape2DLayer( int width, int height, boolean instantDraw ) { public Shape2DLayer( int width, int height, boolean instantDraw ) {
super(width, height); super(width, height);
shapes = new LinkedList<java.awt.Shape>(); shapes = new LinkedList<>();
this.instantDraw = instantDraw; this.instantDraw = instantDraw;
} }
public Color getFillColor() { public schule.ngb.zm.Color getFillColor() {
return fillColor; return fillColor;
} }
@@ -47,7 +55,7 @@ public final class Shape2DLayer extends Layer {
setFillColor(gray, gray, gray, 255); setFillColor(gray, gray, gray, 255);
} }
public void setFillColor( Color pColor ) { public void setFillColor( schule.ngb.zm.Color pColor ) {
fillColor = pColor; fillColor = pColor;
drawing.setColor(pColor.getJavaColor()); drawing.setColor(pColor.getJavaColor());
} }
@@ -65,10 +73,10 @@ public final class Shape2DLayer extends Layer {
} }
public void setFillColor( int red, int green, int blue, int alpha ) { public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha)); setFillColor(new schule.ngb.zm.Color(red, green, blue, alpha));
} }
public Color getStrokeColor() { public schule.ngb.zm.Color getStrokeColor() {
return strokeColor; return strokeColor;
} }
@@ -76,7 +84,7 @@ public final class Shape2DLayer extends Layer {
setStrokeColor(gray, gray, gray, 255); setStrokeColor(gray, gray, gray, 255);
} }
public void setStrokeColor( Color pColor ) { public void setStrokeColor( schule.ngb.zm.Color pColor ) {
strokeColor = pColor; strokeColor = pColor;
drawing.setColor(pColor.getJavaColor()); drawing.setColor(pColor.getJavaColor());
} }
@@ -102,7 +110,7 @@ public final class Shape2DLayer extends Layer {
drawing.setStroke(createStroke()); drawing.setStroke(createStroke());
} }
protected Stroke createStroke() { private Stroke createStroke() {
switch( strokeType ) { switch( strokeType ) {
case DOTTED: case DOTTED:
return new BasicStroke( return new BasicStroke(
@@ -149,7 +157,7 @@ public final class Shape2DLayer extends Layer {
} }
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D graphics ) {
if( !instantDraw ) { if( !instantDraw ) {
for( Shape shape : shapes ) { for( Shape shape : shapes ) {
drawing.setColor(fillColor.getJavaColor()); drawing.setColor(fillColor.getJavaColor());
@@ -160,7 +168,7 @@ public final class Shape2DLayer extends Layer {
} }
} }
super.draw(pGraphics); super.draw(graphics);
} }
} }

View File

@@ -1,24 +1,32 @@
package schule.ngb.zm.shapes; package schule.ngb.zm.layers;
import schule.ngb.zm.Layer; import schule.ngb.zm.Layer;
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;
import schule.ngb.zm.shapes.Shape;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.util.Collection; import java.util.*;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
/**
* Eine Ebene um {@link Shape} Objekte zu zeichnen.
* <p>
* Ein {@code ShapesLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
*/
@SuppressWarnings( "unused" )
public class ShapesLayer extends Layer { public class ShapesLayer extends Layer {
/**
*
*/
protected boolean clearBeforeDraw = true; protected boolean clearBeforeDraw = true;
private List<Shape> shapes; private final List<Shape> shapes;
private List<Animation<? extends Shape>> animations; private final List<Animation<? extends Shape>> animations;
public ShapesLayer() { public ShapesLayer() {
super(); super();
@@ -39,55 +47,49 @@ public class ShapesLayer extends Layer {
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) { public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) { for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) { if( shapeClass.isInstance(s) ) {
return (ST) s; return shapeClass.cast(s);
} }
} }
return null; return null;
} }
public java.util.List<Shape> getShapes() { public List<Shape> getShapes() {
return shapes; return shapes;
} }
public <ST extends Shape> java.util.List<ST> getShapes( Class<ST> shapeClass ) { public <ST extends Shape> List<ST> getShapes( Class<ST> shapeClass ) {
java.util.List<ST> result = new LinkedList<>(); List<ST> result = new LinkedList<>();
for( Shape s : shapes ) { for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) { if( shapeClass.isInstance(s) ) {
result.add((ST) s); result.add(shapeClass.cast(s));
} }
} }
return result; return result;
} }
public void add( Shape... shapes ) { public void add( Shape... shapes ) {
synchronized( shapes ) { synchronized( this.shapes ) {
for( Shape s : shapes ) { Collections.addAll(this.shapes, shapes);
this.shapes.add(s);
}
} }
} }
public void add( Collection<Shape> shapes ) { public void add( Collection<Shape> shapes ) {
synchronized( shapes ) { synchronized( this.shapes ) {
for( Shape s : shapes ) { this.shapes.addAll(shapes);
this.shapes.add(s);
}
} }
} }
public void remove( Shape... shapes ) { public void remove( Shape... shapes ) {
synchronized( shapes ) { synchronized( this.shapes ) {
for( Shape s: shapes ) { for( Shape s : shapes ) {
this.shapes.remove(s); this.shapes.remove(s);
} }
} }
} }
public void remove( Collection<Shape> shapes ) { public void remove( Collection<Shape> shapes ) {
synchronized( shapes ) { synchronized( this.shapes ) {
for( Shape s: shapes ) { this.shapes.removeAll(shapes);
this.shapes.remove(s);
}
} }
} }
@@ -99,16 +101,16 @@ public class ShapesLayer extends Layer {
public void showAll() { public void showAll() {
synchronized( shapes ) { synchronized( shapes ) {
for( Shape pShape : shapes ) { for( Shape s : shapes ) {
pShape.show(); s.show();
} }
} }
} }
public void hideAll() { public void hideAll() {
synchronized( shapes ) { synchronized( shapes ) {
for( Shape pShape : shapes ) { for( Shape s : shapes ) {
pShape.hide(); s.hide();
} }
} }
} }
@@ -118,6 +120,14 @@ public class ShapesLayer extends Layer {
anim.start(); anim.start();
} }
@SafeVarargs
public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim);
anim.start();
}
}
public <S extends Shape> void play( Animation<S> anim, int runtime ) { public <S extends Shape> void play( Animation<S> anim, int runtime ) {
play(anim, runtime, Easing.DEFAULT_EASING); play(anim, runtime, Easing.DEFAULT_EASING);
} }
@@ -135,26 +145,27 @@ public class ShapesLayer extends Layer {
anim.update(delta); anim.update(delta);
if( !anim.isActive() ) { if( !anim.isActive() ) {
animations.remove(anim); it.remove();
} }
} }
} }
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) { if( clearBeforeDraw ) {
clear(); clear();
} }
synchronized( shapes ) { synchronized( shapes ) {
for( Shape pShape : shapes ) { List<Shape> it = List.copyOf(shapes);
if( pShape.isVisible() ) { for( Shape s : it ) {
pShape.draw(drawing); if( s.isVisible() ) {
s.draw(drawing);
} }
} }
} }
super.draw(pGraphics); super.draw(graphics);
} }
} }

View File

@@ -1,18 +1,25 @@
package schule.ngb.zm.turtle; package schule.ngb.zm.layers;
import schule.ngb.zm.*;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Layer; import schule.ngb.zm.Fillable;
import schule.ngb.zm.Options; import schule.ngb.zm.Strokeable;
import schule.ngb.zm.Vector;
import schule.ngb.zm.shapes.FilledShape;
import java.awt.*; import java.awt.*;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.Stack; import java.util.Stack;
public class TurtleLayer extends Layer { /**
* Eine Ebene, auf der eine Turtle gesteuert werden kann, die Grafiken plottet.
* <p>
* Die Turtle verhält sich ähnlich zu ihren Entsprechungen in Logo oder
* TigerJython.
*/
@SuppressWarnings( "unused" )
public class TurtleLayer extends Layer implements Strokeable, Fillable {
// Rotating by the clock // Rotating by the clock
public static final int H1 = 30; public static final int H1 = 30;
@@ -39,7 +46,7 @@ public class TurtleLayer extends Layer {
public static final int H12 = 360; public static final int H12 = 360;
private static Stack<Color> turtleColors; private final static Stack<Color> turtleColors;
static { static {
turtleColors = new Stack<>(); turtleColors = new Stack<>();
@@ -51,9 +58,9 @@ public class TurtleLayer extends Layer {
turtleColors.add(Color.BLUE); turtleColors.add(Color.BLUE);
} }
private Turtle mainTurtle = null; private final Turtle mainTurtle;
private ArrayList<Turtle> turtles = new ArrayList<Turtle>(6); private final List<Turtle> turtles = new ArrayList<>(6);
public TurtleLayer() { public TurtleLayer() {
super(); super();
@@ -117,7 +124,20 @@ public class TurtleLayer extends Layer {
} }
} }
// Begin of delegate methods (auto-generated) // begin of delegate methods (auto-generated)
@Override
public boolean isVisible() {
return mainTurtle.isVisible();
}
public void beginPath() {
mainTurtle.beginPath();
}
public void closePath() {
mainTurtle.closePath();
}
public void fill() { public void fill() {
mainTurtle.fill(); mainTurtle.fill();
@@ -175,100 +195,196 @@ public class TurtleLayer extends Layer {
mainTurtle.moveTo(x, y); mainTurtle.moveTo(x, y);
} }
public Color getFillColor() { @Override
return mainTurtle.getFillColor(); public void setFill( Paint fill ) {
mainTurtle.setFill(fill);
} }
@Override
public Paint getFill() {
return mainTurtle.getFill();
}
@Override
public void setFillColor( Color color ) { public void setFillColor( Color color ) {
mainTurtle.setFillColor(color); mainTurtle.setFillColor(color);
} }
public void setFillColor( int gray ) { @Override
mainTurtle.setFillColor(gray); public Color getFillColor() {
} return mainTurtle.getFillColor();
public void noFill() {
mainTurtle.noFill();
}
public void setFillColor( int gray, int alpha ) {
mainTurtle.setFillColor(gray, alpha);
}
public void setFillColor( int red, int green, int blue ) {
mainTurtle.setFillColor(red, green, blue);
}
public void setFillColor( int red, int green, int blue, int alpha ) {
mainTurtle.setFillColor(red, green, blue, alpha);
}
public void resetFill() {
mainTurtle.resetFill();
}
public Color getStrokeColor() {
return mainTurtle.getStrokeColor();
} }
@Override
public void setStrokeColor( Color color ) { public void setStrokeColor( Color color ) {
mainTurtle.setStrokeColor(color); mainTurtle.setStrokeColor(color);
} }
public void setStrokeColor( int gray ) { @Override
mainTurtle.setStrokeColor(gray);
}
public void noStroke() {
mainTurtle.noStroke();
}
public void setStrokeColor( int gray, int alpha ) {
mainTurtle.setStrokeColor(gray, alpha);
}
public void setStrokeColor( int red, int green, int blue ) {
mainTurtle.setStrokeColor(red, green, blue);
}
public void setStrokeColor( int red, int green, int blue, int alpha ) {
mainTurtle.setStrokeColor(red, green, blue, alpha);
}
public double getStrokeWeight() {
return mainTurtle.getStrokeWeight();
}
public void setStrokeWeight( double weight ) { public void setStrokeWeight( double weight ) {
mainTurtle.setStrokeWeight(weight); mainTurtle.setStrokeWeight(weight);
} }
@Override
public Options.StrokeType getStrokeType() { public Options.StrokeType getStrokeType() {
return mainTurtle.getStrokeType(); return mainTurtle.getStrokeType();
} }
@Override
public void setStrokeType( Options.StrokeType type ) { public void setStrokeType( Options.StrokeType type ) {
mainTurtle.setStrokeType(type); mainTurtle.setStrokeType(type);
} }
@Override
public void setGradient( Color from, Color to, Options.Direction dir ) {
mainTurtle.setGradient(from, to, dir);
}
@Override
public void setGradient( Color from, Color to ) {
mainTurtle.setGradient(from, to);
}
@Override
public boolean hasFill() {
return mainTurtle.hasFill();
}
@Override
public boolean hasFillColor() {
return mainTurtle.hasFillColor();
}
@Override
public boolean hasGradient() {
return mainTurtle.hasGradient();
}
@Override
public void setFillColor( Color color, int alpha ) {
mainTurtle.setFillColor(color, alpha);
}
@Override
public void setFillColor( int gray ) {
mainTurtle.setFillColor(gray);
}
@Override
public void setFillColor( int gray, int alpha ) {
mainTurtle.setFillColor(gray, alpha);
}
@Override
public void setFillColor( int red, int green, int blue ) {
mainTurtle.setFillColor(red, green, blue);
}
@Override
public void setFillColor( int red, int green, int blue, int alpha ) {
mainTurtle.setFillColor(red, green, blue, alpha);
}
@Override
public void noFill() {
mainTurtle.noFill();
}
@Override
public void resetFill() {
mainTurtle.resetFill();
}
@Override
public MultipleGradientPaint getGradient() {
return mainTurtle.getGradient();
}
@Override
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
mainTurtle.setGradient(fromX, fromY, from, toX, toY, to);
}
@Override
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
mainTurtle.setGradient(centerX, centerY, radius, from, to);
}
@Override
public void noGradient() {
mainTurtle.noGradient();
}
@Override
public void setStroke( Stroke stroke ) {
mainTurtle.setStroke(stroke);
}
@Override
public Stroke getStroke() {
return mainTurtle.getStroke();
}
@Override
public boolean hasStroke() {
return mainTurtle.hasStroke();
}
@Override
public Color getStrokeColor() {
return mainTurtle.getStrokeColor();
}
@Override
public void setStrokeColor( Color color, int alpha ) {
mainTurtle.setStrokeColor(color, alpha);
}
@Override
public void setStrokeColor( int gray ) {
mainTurtle.setStrokeColor(gray);
}
@Override
public void setStrokeColor( int gray, int alpha ) {
mainTurtle.setStrokeColor(gray, alpha);
}
@Override
public void setStrokeColor( int red, int green, int blue ) {
mainTurtle.setStrokeColor(red, green, blue);
}
@Override
public void setStrokeColor( int red, int green, int blue, int alpha ) {
mainTurtle.setStrokeColor(red, green, blue, alpha);
}
@Override
public void noStroke() {
mainTurtle.noStroke();
}
@Override
public void resetStroke() { public void resetStroke() {
mainTurtle.resetStroke(); mainTurtle.resetStroke();
} }
public void addPosToPath() { @Override
mainTurtle.addPosToPath(); public double getStrokeWeight() {
return mainTurtle.getStrokeWeight();
} }
public void closePath() {
mainTurtle.closePath();
}
// End of delegate methods (auto-generated) // end of delegate methods (auto-generated)
public class Turtle extends FilledShape { /**
* Die Turtle der Zeichenmaschine.
*/
public class Turtle extends BasicDrawable {
private static final int STD_SIZE = 12; private static final int DEFAULT_SIZE = 12;
boolean penDown = true; boolean penDown = true;
@@ -282,7 +398,31 @@ public class TurtleLayer extends Layer {
boolean pathOpen = false; boolean pathOpen = false;
Turtle() {} /**
* Path-Objekt für die Darstellung der Turtle.
*/
Path2D.Double turtlePath;
Turtle() {
}
public boolean isVisible() {
return visible;
}
public void beginPath() {
pathOpen = false;
addPosToPath();
}
public void closePath() {
if( pathOpen ) {
addPosToPath();
path.closePath();
path.trimToSize();
pathOpen = false;
}
}
private void addPosToPath() { private void addPosToPath() {
if( !pathOpen ) { if( !pathOpen ) {
@@ -294,53 +434,39 @@ public class TurtleLayer extends Layer {
} }
} }
private void closePath() {
if( pathOpen ) {
addPosToPath();
path.closePath();
path.trimToSize();
pathOpen = false;
}
}
public void fill() { public void fill() {
closePath(); closePath();
if( fillColor != null && fillColor.getAlpha() > 0 ) { if( hasFill() ) {
drawing.setColor(fillColor.getJavaColor()); drawing.setPaint(getFill());
drawing.fill(path); drawing.fill(path);
} }
} }
public boolean isVisible() {
return visible;
}
@Override @Override
public void draw( Graphics2D graphics ) { public void draw( Graphics2D graphics ) {
/*Shape shape = new RoundRectangle2D.Double( if( turtlePath == null ) {
-12, -5, 16, 10, 5, 3 turtlePath = new Path2D.Double();
);*/ path.moveTo(DEFAULT_SIZE, 0);
Path2D path = new Path2D.Double(); path.lineTo(-DEFAULT_SIZE, -DEFAULT_SIZE / 2.0);
path.moveTo(STD_SIZE, 0); path.lineTo(-DEFAULT_SIZE, DEFAULT_SIZE / 2.0);
path.lineTo(-STD_SIZE, -STD_SIZE/2); path.lineTo(DEFAULT_SIZE, 0);
path.lineTo(-STD_SIZE, STD_SIZE/2); }
path.lineTo(STD_SIZE, 0);
AffineTransform verzerrung = new AffineTransform(); AffineTransform verzerrung = new AffineTransform();
verzerrung.translate(position.x, position.y); verzerrung.translate(position.x, position.y);
verzerrung.rotate(Math.toRadians(direction.angle())); verzerrung.rotate(Math.toRadians(direction.angle()));
Shape shape = verzerrung.createTransformedShape(path); java.awt.Shape shape = verzerrung.createTransformedShape(turtlePath);
if( strokeColor != null ) { if( hasStroke() ) {
graphics.setColor(strokeColor.getJavaColor()); graphics.setColor(strokeColor.getJavaColor());
} else { } else {
graphics.setColor(STD_STROKECOLOR.getJavaColor()); graphics.setColor(DEFAULT_STROKECOLOR.getJavaColor());
} }
graphics.fill(shape); graphics.fill(shape);
graphics.setColor(Color.BLACK.getJavaColor()); graphics.setColor(Color.BLACK.getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
graphics.draw(shape); graphics.draw(shape);
} }
@@ -354,9 +480,9 @@ public class TurtleLayer extends Layer {
Vector positionStart = position.copy(); Vector positionStart = position.copy();
position.add(Vector.setLength(direction, dist)); position.add(Vector.setLength(direction, dist));
if( penDown && strokeColor != null ) { if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor()); drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke()); drawing.setStroke(getStroke());
drawing.drawLine((int) positionStart.x, (int) positionStart.y, (int) position.x, (int) position.y); drawing.drawLine((int) positionStart.x, (int) positionStart.y, (int) position.x, (int) position.y);
} }
} }
@@ -407,9 +533,9 @@ public class TurtleLayer extends Layer {
position.x = x; position.x = x;
position.y = y; position.y = y;
if( penDown && strokeColor != null ) { if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor()); drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke()); drawing.setStroke(getStroke());
drawing.drawLine((int) x, (int) y, (int) position.x, (int) position.y); drawing.drawLine((int) x, (int) y, (int) position.x, (int) position.y);
} }
} }

View File

@@ -80,4 +80,19 @@ public interface Audio {
*/ */
void dispose(); void dispose();
/**
* Fügt dem Medium das angegbene Objekt als {@code AudioListener} hinzu, der
* bei Start und Stopp der Wiedergabe informiert werden soll.
*
* @param listener Das zu informierende Objekt.
*/
void addAudioListener( AudioListener listener );
/**
* Entfernt den angegebenen {@code AudioListener} vom Medium.
*
* @param listener Das Listener-Objekt.
*/
void removeAudioListener( AudioListener listener );
} }

View File

@@ -1,11 +1,34 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.events.Listener; import schule.ngb.zm.util.events.Listener;
/**
* Interface für Klassen, die auf das starten und stoppen der Wiedergabe von
* {@link Audio}-Objekten reagieren möchten.
* <p>
* Implementierende Klassen können sich bei einem Auido-Objekt mittels
* {@link Audio#addAudioListener(AudioListener)} anmelden und werden über die
* jeweilige Methode informiert, sobald die Wiedergabe gestartet oder gestoppt
* wird.
*/
public interface AudioListener extends Listener<Audio> { public interface AudioListener extends Listener<Audio> {
void start( Audio source ); /**
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Objektes startet, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Objekt, dessen Wiedergabe gestartet wurde.
*/
void playbackStarted( Audio source );
void stop( Audio source ); /**
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Objektes stoppt, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Objekt, dessen Wiedergabe gestoppt wurde.
*/
void playbackStopped( Audio source );
} }

View File

@@ -1,9 +1,11 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.Constants; import schule.ngb.zm.Constants;
import schule.ngb.zm.tasks.TaskRunner; import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
/** /**
@@ -18,24 +20,14 @@ import java.util.List;
* Darüber hinaus kann ein Mixer Effekte wie einen * Darüber hinaus kann ein Mixer Effekte wie einen
* {@link #fade(double, int) fadeIn} auf die Medien anwenden. * {@link #fade(double, int) fadeIn} auf die Medien anwenden.
*/ */
public class Mixer implements Audio { @SuppressWarnings( "unused" )
public class Mixer implements Audio, AudioListener {
private List<AudioWrapper> audios; private List<AudioWrapper> audios;
private float volume = 0.8f; private float volume = 0.8f;
class AudioWrapper { EventDispatcher<Audio, AudioListener> eventDispatcher;
Audio audio;
float volumeFactor;
public AudioWrapper( Audio audio, float volumeFactor ) {
this.audio = audio;
this.volumeFactor = volumeFactor;
}
}
public Mixer() { public Mixer() {
this.audios = new ArrayList<>(4); this.audios = new ArrayList<>(4);
@@ -45,16 +37,61 @@ public class Mixer implements Audio {
return ""; return "";
} }
public void add( Audio pAudio ) { private AudioWrapper findWrapper( Audio pAudio ) {
add(pAudio, 1f); for( AudioWrapper aw: audios ) {
if( aw.audio == pAudio ) {
return aw;
}
}
return null;
} }
public boolean contains( Audio pAudio ) {
return findWrapper(pAudio) != null;
}
public void add( Audio pAudio ) {
add(pAudio, 1);
}
/**
* Fügt ein Audio-Objekt dem Mixer mit dem angegebenen Lautstärke-Faktor
* hinzu.
* <p>
* Der Lautstärke-Faktor setzt die Lautstärke des Audio-Objektes relativ zur
* Lautstärke des Mixers. Bei einem Faktor von 1.0 wird die Lautstärke des
* Mixers übernommen. Bei einem Wert von 0.5 wird das Objekt halb so laut
* abgespielt. Auf diese Weise lässt sich die Lautstärke aller Audio-Objekte
* des Mixers gleichzeitig anpassen, während ihre relative Lautstärke
* zueinander gleich bleibt.
*
* @param pAudio Ein Audio-Objekt.
* @param pVolumeFactor Der Lautstärke-Faktor.
*/
public void add( Audio pAudio, double pVolumeFactor ) { public void add( Audio pAudio, double pVolumeFactor ) {
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor)); if( !contains(pAudio) ) {
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
} else {
findWrapper(pAudio).volumeFactor = (float) pVolumeFactor;
}
pAudio.setVolume(pVolumeFactor * volume); pAudio.setVolume(pVolumeFactor * volume);
} }
/**
* Entfernt die das angegebene Audio-Objekt aus dem Mixer. Ist das Objekt
* nicht Teil des Mixers, passiert nichts.
*
* @param pAudio Ein Audio-Objekt.
*/
public void remove( Audio pAudio ) { public void remove( Audio pAudio ) {
Iterator<AudioWrapper> it = audios.listIterator();
while( it.hasNext() ) {
AudioWrapper aw = it.next();
if( aw.audio == pAudio ) {
it.remove();
break;
}
}
} }
public void removeAll() { public void removeAll() {
@@ -178,4 +215,57 @@ public class Mixer implements Audio {
}); });
} }
@Override
public void playbackStarted( Audio source ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Mixer.this);
}
}
@Override
public void playbackStopped( Audio source ) {
if( !isPlaying() ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Mixer.this);
}
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, soblad sich auch ein Listener registrieren möchte.
* @return
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
}
return eventDispatcher;
}
class AudioWrapper {
Audio audio;
float volumeFactor;
public AudioWrapper( Audio audio, float volumeFactor ) {
this.audio = audio;
this.volumeFactor = volumeFactor;
}
}
} }

View File

@@ -1,11 +1,9 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.anim.Animation; import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.anim.AnimationListener; import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.events.EventDispatcher;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider; import schule.ngb.zm.util.io.ResourceStreamProvider;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import javax.sound.sampled.*; import javax.sound.sampled.*;
@@ -20,6 +18,7 @@ import java.net.URL;
* nicht komplett in den Speicher geladen, sondern direkt aus der Audioquelle * nicht komplett in den Speicher geladen, sondern direkt aus der Audioquelle
* gestreamt und wiedergegeben. * gestreamt und wiedergegeben.
*/ */
@SuppressWarnings("unused")
public class Music implements Audio { public class Music implements Audio {
// size of the byte buffer used to read/write the audio stream // size of the byte buffer used to read/write the audio stream
@@ -168,7 +167,7 @@ public class Music implements Audio {
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void dispose() { public synchronized void dispose() {
if( audioLine != null ) { if( audioLine != null ) {
if( audioLine.isRunning() ) { if( audioLine.isRunning() ) {
playing = false; playing = false;
@@ -177,7 +176,6 @@ public class Music implements Audio {
if( audioLine.isOpen() ) { if( audioLine.isOpen() ) {
audioLine.drain(); audioLine.drain();
audioLine.close(); audioLine.close();
} }
} }
try { try {
@@ -191,7 +189,7 @@ public class Music implements Audio {
audioStream = null; audioStream = null;
} }
private void stream() { private synchronized void stream() {
audioLine.start(); audioLine.start();
playing = true; playing = true;
if( eventDispatcher != null ) { if( eventDispatcher != null ) {
@@ -279,11 +277,13 @@ public class Music implements Audio {
} }
} }
public void addListener( AudioListener listener ) { @Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener); initializeEventDispatcher().addListener(listener);
} }
public void removeListener( AudioListener listener ) { @Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener); initializeEventDispatcher().removeListener(listener);
} }
@@ -295,8 +295,8 @@ public class Music implements Audio {
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() { private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) { if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>(); eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.start(a)); eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.stop(a)); eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
} }
return eventDispatcher; return eventDispatcher;
} }

View File

@@ -1,12 +1,12 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
/** /**
@@ -55,6 +55,8 @@ public class Sound implements Audio {
*/ */
private float volume = 0.8f; private float volume = 0.8f;
EventDispatcher<Audio, AudioListener> eventDispatcher;
/** /**
* Erstellt einen Sound aus der angegebene Quelle. * Erstellt einen Sound aus der angegebene Quelle.
* *
@@ -206,8 +208,9 @@ public class Sound implements Audio {
} }
/** /**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und stoppt * Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und
* die Wiedergabe dann. * stoppt die Wiedergabe dann.
*
* @param count Anzahl der Wiederholungen. * @param count Anzahl der Wiederholungen.
*/ */
public void loop( int count ) { public void loop( int count ) {
@@ -232,17 +235,17 @@ public class Sound implements Audio {
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public void dispose() { public synchronized void dispose() {
if( audioClip != null ) { if( audioClip != null ) {
if( audioClip.isRunning() ) { if( audioClip.isRunning() ) {
audioClip.stop(); stop();
} }
audioClip.close(); audioClip.close();
audioClip = null; audioClip = null;
} }
} }
private boolean openClip() { private synchronized boolean openClip() {
if( audioClip != null ) { if( audioClip != null ) {
audioClip.setFramePosition(0); audioClip.setFramePosition(0);
return true; return true;
@@ -259,8 +262,16 @@ public class Sound implements Audio {
audioClip.addLineListener(new LineListener() { audioClip.addLineListener(new LineListener() {
@Override @Override
public void update( LineEvent event ) { public void update( LineEvent event ) {
if( event.getType() == LineEvent.Type.STOP ) { if( event.getType() == LineEvent.Type.START ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Sound.this);
}
} else if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped(); playbackStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
} }
} }
}); });
@@ -302,12 +313,37 @@ public class Sound implements Audio {
*/ */
private void playbackStopped() { private void playbackStopped() {
playing = false; playing = false;
if( disposeAfterPlay ) { if( disposeAfterPlay ) {
this.dispose(); this.dispose();
disposeAfterPlay = false; disposeAfterPlay = false;
} }
} }
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, soblad sich auch ein Listener registrieren möchte.
* @return
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Sound.class); private static final Log LOG = Log.getLogger(Sound.class);
} }

View File

@@ -0,0 +1,400 @@
package schule.ngb.zm.ml;
import schule.ngb.zm.Constants;
import java.util.function.DoubleUnaryOperator;
/**
* Eine einfache Implementierung der {@link MLMatrix} zur Verwendung in
* {@link NeuralNetwork}s.
* <p>
* Diese Klasse stellt die interne Implementierung der Matrixoperationen dar,
* die zur Berechnung der Gewichte in einem {@link NeuronLayer} notwendig sind.
* <p>
* Die Klasse ist nur minimal optimiert und sollte nur für kleine Netze
* verwendet werden. Für größere Netze sollte auf eine der optionalen
* Bibliotheken wie
* <a href="https://dst.lbl.gov/ACSSoftware/colt/">Colt</a> zurückgegriffen
* werden.
*/
public final class DoubleMatrix implements MLMatrix {
/**
* Anzahl Zeilen der Matrix.
*/
private int rows;
/**
* Anzahl Spalten der Matrix.
*/
private int columns;
/**
* Die Koeffizienten der Matrix.
* <p>
* Um den Overhead bei Speicher und Zugriffszeiten von zweidimensionalen
* Arrays zu vermeiden wird ein eindimensionales Array verwendet und die
* Indizes mit Spaltenpriorität berechnet. Der Index i des Koeffizienten
* {@code r,c} in Zeile {@code r} und Spalte {@code c} wird bestimmt durch
* <pre>
* i = c * rows + r
* </pre>
* <p>
* Die Werte einer Spalte liegen also hintereinander im Array. Dies sollte
* einen leichten Vorteil bei der {@link #colSums() Spaltensummen} geben.
* Generell sollte eine Iteration über die Matrix der Form
* <pre><code>
* for( int j = 0; j < columns; j++ ) {
* for( int i = 0; i < rows; i++ ) {
* // ...
* }
* }
* </code></pre>
* etwas schneller sein als
* <pre><code>
* for( int i = 0; i < rows; i++ ) {
* for( int j = 0; j < columns; j++ ) {
* // ...
* }
* }
* </code></pre>
*/
double[] coefficients;
public DoubleMatrix( int rows, int cols ) {
this.rows = rows;
this.columns = cols;
coefficients = new double[rows * cols];
}
public DoubleMatrix( double[][] coefficients ) {
this.rows = coefficients.length;
this.columns = coefficients[0].length;
this.coefficients = new double[rows * columns];
for( int j = 0; j < columns; j++ ) {
for( int i = 0; i < rows; i++ ) {
this.coefficients[idx(i, j)] = coefficients[i][j];
}
}
}
/**
* Initialisiert diese Matrix als Kopie der angegebenen Matrix.
*
* @param other Die zu kopierende Matrix.
*/
public DoubleMatrix( DoubleMatrix other ) {
this.rows = other.rows();
this.columns = other.columns();
this.coefficients = new double[rows * columns];
System.arraycopy(
other.coefficients, 0,
this.coefficients, 0,
rows * columns);
}
/**
* {@inheritDoc}
*/
@Override
public int columns() {
return columns;
}
/**
* {@inheritDoc}
*/
@Override
public int rows() {
return rows;
}
/**
* {@inheritDoc}
*/
int idx( int r, int c ) {
return c * rows + r;
}
/**
* {@inheritDoc}
*/
@Override
public double get( int row, int col ) {
try {
return coefficients[idx(row, col)];
} catch( ArrayIndexOutOfBoundsException ex ) {
throw new IllegalArgumentException("No element at row=" + row + ", column=" + col, ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix set( int row, int col, double value ) {
try {
coefficients[idx(row, col)] = value;
} catch( ArrayIndexOutOfBoundsException ex ) {
throw new IllegalArgumentException("No element at row=" + row + ", column=" + col, ex);
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix initializeRandom() {
return initializeRandom(-1.0, 1.0);
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix initializeRandom( double lower, double upper ) {
applyInPlace(( d ) -> ((upper - lower) * Constants.random()) + lower);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix initializeOne() {
applyInPlace(( d ) -> 1.0);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix initializeZero() {
applyInPlace(( d ) -> 0.0);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix duplicate() {
return new DoubleMatrix(this);
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix multiplyTransposed( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
( i ) -> IntStream.range(0, B.rows()).mapToDouble(
( j ) -> IntStream.range(0, columns).mapToDouble(
( k ) -> get(i, k) * B.get(j, k)
).sum()
).toArray()
).toArray(double[][]::new));*/
DoubleMatrix result = new DoubleMatrix(rows, B.rows());
for( int i = 0; i < rows; i++ ) {
for( int j = 0; j < B.rows(); j++ ) {
result.coefficients[result.idx(i, j)] = 0.0;
for( int k = 0; k < columns; k++ ) {
result.coefficients[result.idx(i, j)] += get(i, k) * B.get(j, k);
}
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix multiplyAddBias( final MLMatrix B, final MLMatrix C ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
( i ) -> IntStream.range(0, B.columns()).mapToDouble(
( j ) -> IntStream.range(0, columns).mapToDouble(
( k ) -> get(i, k) * B.get(k, j)
).sum() + C.get(0, j)
).toArray()
).toArray(double[][]::new));*/
DoubleMatrix result = new DoubleMatrix(rows, B.columns());
for( int i = 0; i < rows; i++ ) {
for( int j = 0; j < B.columns(); j++ ) {
result.coefficients[result.idx(i, j)] = 0.0;
for( int k = 0; k < columns; k++ ) {
result.coefficients[result.idx(i, j)] += get(i, k) * B.get(k, j);
}
result.coefficients[result.idx(i, j)] += C.get(0, j);
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix transposedMultiplyAndScale( final MLMatrix B, final double scalar ) {
/*return new DoubleMatrix(IntStream.range(0, columns).parallel().mapToObj(
( i ) -> IntStream.range(0, B.columns()).mapToDouble(
( j ) -> IntStream.range(0, rows).mapToDouble(
( k ) -> get(k, i) * B.get(k, j) * scalar
).sum()
).toArray()
).toArray(double[][]::new));*/
DoubleMatrix result = new DoubleMatrix(columns, B.columns());
for( int i = 0; i < columns; i++ ) {
for( int j = 0; j < B.columns(); j++ ) {
result.coefficients[result.idx(i, j)] = 0.0;
for( int k = 0; k < rows; k++ ) {
result.coefficients[result.idx(i, j)] += get(k, i) * B.get(k, j);
}
result.coefficients[result.idx(i, j)] *= scalar;
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix add( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
( i ) -> IntStream.range(0, columns).mapToDouble(
( j ) -> get(i, j) + B.get(i, j)
).toArray()
).toArray(double[][]::new));*/
DoubleMatrix sum = new DoubleMatrix(rows, columns);
for( int j = 0; j < columns; j++ ) {
for( int i = 0; i < rows; i++ ) {
sum.coefficients[idx(i, j)] = coefficients[idx(i, j)] + B.get(i, j);
}
}
return sum;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix addInPlace( MLMatrix B ) {
for( int j = 0; j < columns; j++ ) {
for( int i = 0; i < rows; i++ ) {
coefficients[idx(i, j)] += B.get(i, j);
}
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix sub( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
( i ) -> IntStream.range(0, columns).mapToDouble(
( j ) -> get(i, j) - B.get(i, j)
).toArray()
).toArray(double[][]::new));*/
DoubleMatrix diff = new DoubleMatrix(rows, columns);
for( int j = 0; j < columns; j++ ) {
for( int i = 0; i < rows; i++ ) {
diff.coefficients[idx(i, j)] = coefficients[idx(i, j)] - B.get(i, j);
}
}
return diff;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix colSums() {
/*DoubleMatrix colSums = new DoubleMatrix(1, columns);
colSums.coefficients = IntStream.range(0, columns).parallel().mapToDouble(
( j ) -> IntStream.range(0, rows).mapToDouble(
( i ) -> get(i, j)
).sum()
).toArray();
return colSums;*/
DoubleMatrix colSums = new DoubleMatrix(1, columns);
for( int j = 0; j < columns; j++ ) {
colSums.coefficients[j] = 0.0;
for( int i = 0; i < rows; i++ ) {
colSums.coefficients[j] += coefficients[idx(i, j)];
}
}
return colSums;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix scaleInPlace( final double scalar ) {
for( int i = 0; i < coefficients.length; i++ ) {
coefficients[i] *= scalar;
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix scaleInPlace( final MLMatrix S ) {
for( int j = 0; j < columns; j++ ) {
for( int i = 0; i < rows; i++ ) {
coefficients[idx(i, j)] *= S.get(i, j);
}
}
return this;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix apply( DoubleUnaryOperator op ) {
DoubleMatrix result = new DoubleMatrix(rows, columns);
for( int i = 0; i < coefficients.length; i++ ) {
result.coefficients[i] = op.applyAsDouble(coefficients[i]);
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public MLMatrix applyInPlace( DoubleUnaryOperator op ) {
for( int i = 0; i < coefficients.length; i++ ) {
coefficients[i] = op.applyAsDouble(coefficients[i]);
}
return this;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(rows);
sb.append(" x ");
sb.append(columns);
sb.append(" Matrix");
sb.append('\n');
for( int i = 0; i < rows; i++ ) {
for( int j = 0; j < columns; j++ ) {
sb.append(get(i, j));
if( j < columns - 1 )
sb.append(' ');
}
sb.append('\n');
}
return sb.toString();
}
}

View File

@@ -0,0 +1,316 @@
package schule.ngb.zm.ml;
import java.util.function.DoubleUnaryOperator;
/**
* Interface für Matrizen, die in {@link NeuralNetwork} Klassen verwendet
* werden.
* <p>
* Eine implementierende Klasse muss generell zwei Konstruktoren bereitstellen:
* <ol>
* <li> {@code MLMatrix(int rows, int columns)} erstellt eine Matrix mit den
* angegebenen Dimensionen und setzt alle Koeffizienten auf 0.
* <li> {@code MLMatrix(double[][] coefficients} erstellt eine Matrix mit der
* durch das Array gegebenen Dimensionen und setzt die Werte auf die
* jeweiligen Werte des Arrays.
* </ol>
* <p>
* Das Interface ist nicht dazu gedacht eine allgemeine Umsetzung für
* Matrizen-Algebra abzubilden, sondern soll gezielt die im Neuralen Netzwerk
* verwendeten Algorithmen umsetzen. Einerseits würde eine ganz allgemeine
* Matrizen-Klasse nicht im Rahmen der Zeichenmaschine liegen und auf der
* anderen Seite bietet eine Konzentration auf die verwendeten Algorithmen mehr
* Spielraum zur Optimierung.
* <p>
* Intern wird das Interface von {@link DoubleMatrix} implementiert. Die Klasse
* ist eine weitestgehend naive Implementierung der Algorithmen mit kleineren
* Optimierungen. Die Verwendung eines generalisierten Interfaces erlaubt aber
* zukünftig die optionale Integration spezialisierterer Algebra-Bibliotheken
* wie
* <a href="https://dst.lbl.gov/ACSSoftware/colt/">Colt</a>, um auch große
* Netze effizient berechnen zu können.
*/
public interface MLMatrix {
/**
* Die Anzahl der Spalten der Matrix.
*
* @return Spaltenzahl.
*/
int columns();
/**
* Die Anzahl der Zeilen der Matrix.
*
* @return Zeilenzahl.
*/
int rows();
/**
* Gibt den Wert an der angegebenen Stelle der Matrix zurück.
*
* @param row Die Spaltennummer zwischen 0 und {@code rows()-1}.
* @param col Die Zeilennummer zwischen 0 und {@code columns()-1}
* @return Den Koeffizienten in der Zeile {@code row} und der Spalte
* {@code col}.
* @throws IllegalArgumentException Falls {@code row >= rows()} oder
* {@code col >= columns()}.
*/
double get( int row, int col ) throws IllegalArgumentException;
/**
* Setzt den Wert an der angegebenen Stelle der Matrix.
*
* @param row Die Spaltennummer zwischen 0 und {@code rows()-1}.
* @param col Die Zeilennummer zwischen 0 und {@code columns()-1}
* @param value Der neue Wert.
* @return Diese Matrix selbst (method chaining).
* @throws IllegalArgumentException Falls {@code row >= rows()} oder
* {@code col >= columns()}.
*/
MLMatrix set( int row, int col, double value ) throws IllegalArgumentException;
/**
* Setzt jeden Wert in der Matrix auf eine Zufallszahl zwischen -1 und 1.
* <p>
* Nach Möglichkeit sollte der
* {@link schule.ngb.zm.Constants#random(int, int) Zufallsgenerator der
* Zeichenmaschine} verwendet werden.
*
* @return Diese Matrix selbst (method chaining).
*/
MLMatrix initializeRandom();
/**
* Setzt jeden Wert in der Matrix auf eine Zufallszahl innerhalb der
* angegebenen Grenzen.
* <p>
* Nach Möglichkeit sollte der
* {@link schule.ngb.zm.Constants#random(int, int) Zufallsgenerator der
* Zeichenmaschine} verwendet werden.
*
* @param lower Untere Grenze der Zufallszahlen.
* @param upper Obere Grenze der Zufallszahlen.
* @return Diese Matrix selbst (method chaining).
*/
MLMatrix initializeRandom( double lower, double upper );
/**
* Setzt alle Werte der Matrix auf 1.
*
* @return Diese Matrix selbst (method chaining).
*/
MLMatrix initializeOne();
/**
* Setzt alle Werte der Matrix auf 0.
*
* @return Diese Matrix selbst (method chaining).
*/
MLMatrix initializeZero();
/**
* Erzeugt eine neue Matrix {@code C} mit dem Ergebnis der Matrixoperation
* <pre>
* C = this . B + V'
* </pre>
* wobei {@code this} dieses Matrixobjekt ist und {@code .} für die
* Matrixmultiplikation steht. {@code V'} ist die Matrix {@code V}
* {@code rows()}-mal untereinander wiederholt.
* <p>
* Wenn diese Matrix die Dimension r x c hat, dann muss die Matrix {@code B}
* die Dimension c x m haben und {@code V} eine 1 x m Matrix sein. Die
* Matrix {@code V'} hat also die Dimension r x m, ebenso wie das Ergebnis
* der Operation.
*
* @param B Eine {@code columns()} x m Matrix mit der Multipliziert wird.
* @param V Eine 1 x {@code B.columns()} Matrix mit den Bias-Werten.
* @return Eine {@code rows()} x m Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.columns() != B.rows()} oder
* {@code B.columns() != V.columns()} oder
* {@code V.rows() != 1}.
*/
MLMatrix multiplyAddBias( MLMatrix B, MLMatrix V ) throws IllegalArgumentException;
/**
* Erzeugt eine neue Matrix {@code C} mit dem Ergebnis der Matrixoperation
* <pre>
* C = this . t(B)
* </pre>
* wobei {@code this} dieses Matrixobjekt ist, {@code t(B)} die
* Transposition der Matrix {@code B} ist und {@code .} für die
* Matrixmultiplikation steht.
* <p>
* Wenn diese Matrix die Dimension r x c hat, dann muss die Matrix {@code B}
* die Dimension m x c haben und das Ergebnis ist eine r x m Matrix.
*
* @param B Eine m x {@code columns()} Matrix.
* @return Eine {@code rows()} x m Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.columns() != B.columns()}.
*/
MLMatrix multiplyTransposed( MLMatrix B ) throws IllegalArgumentException;
/**
* Erzeugt eine neue Matrix {@code C} mit dem Ergebnis der Matrixoperation
* <pre>
* C = t(this) . B * scalar
* </pre>
* wobei {@code this} dieses Matrixobjekt ist, {@code t(this)} die
* Transposition dieser Matrix ist und {@code .} für die
* Matrixmultiplikation steht. {@code *} bezeichnet die
* Skalarmultiplikation, bei der jeder Wert der Matrix mit {@code scalar}
* multipliziert wird.
* <p>
* Wenn diese Matrix die Dimension r x c hat, dann muss die Matrix {@code B}
* die Dimension r x m haben und das Ergebnis ist eine c x m Matrix.
*
* @param B Eine m x {@code columns()} Matrix.
* @return Eine {@code rows()} x m Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.rows() != B.rows()}.
*/
MLMatrix transposedMultiplyAndScale( MLMatrix B, double scalar ) throws IllegalArgumentException;
/**
* Erzeugt eine neue Matrix {@code C} mit dem Ergebnis der komponentenweisen
* Matrix-Addition
* <pre>
* C = this + B
* </pre>
* wobei {@code this} dieses Matrixobjekt ist. Für ein Element {@code C_ij}
* in {@code C} gilt
* <pre>
* C_ij = A_ij + B_ij
* </pre>
* <p>
* Die Matrix {@code B} muss dieselbe Dimension wie diese Matrix haben.
*
* @param B Eine {@code rows()} x {@code columns()} Matrix.
* @return Eine {@code rows()} x {@code columns()} Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.rows() != B.rows()} oder
* {@code this.columns() != B.columns()}.
*/
MLMatrix add( MLMatrix B ) throws IllegalArgumentException;
/**
* Setzt diese Matrix auf das Ergebnis der komponentenweisen
* Matrix-Addition
* <pre>
* A' = A + B
* </pre>
* wobei {@code A} dieses Matrixobjekt ist und {@code A'} diese Matrix nach
* der Operation. Für ein Element {@code A'_ij} in {@code A'} gilt
* <pre>
* A'_ij = A_ij + B_ij
* </pre>
* <p>
* Die Matrix {@code B} muss dieselbe Dimension wie diese Matrix haben.
*
* @param B Eine {@code rows()} x {@code columns()} Matrix.
* @return Eine {@code rows()} x {@code columns()} Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.rows() != B.rows()} oder
* {@code this.columns() != B.columns()}.
*/
MLMatrix addInPlace( MLMatrix B ) throws IllegalArgumentException;
/**
* Erzeugt eine neue Matrix {@code C} mit dem Ergebnis der komponentenweisen
* Matrix-Subtraktion
* <pre>
* C = A - B
* </pre>
* wobei {@code A} dieses Matrixobjekt ist. Für ein Element {@code C_ij} in
* {@code C} gilt
* <pre>
* C_ij = A_ij - B_ij
* </pre>
* <p>
* Die Matrix {@code B} muss dieselbe Dimension wie diese Matrix haben.
*
* @param B Eine {@code rows()} x {@code columns()} Matrix.
* @return Eine {@code rows()} x {@code columns()} Matrix.
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.rows() != B.rows()} oder
* {@code this.columns() != B.columns()}.
*/
MLMatrix sub( MLMatrix B ) throws IllegalArgumentException;
/**
* Multipliziert jeden Wert dieser Matrix mit dem angegebenen Skalar.
* <p>
* Ist {@code A} dieses Matrixobjekt und {@code A'} diese Matrix nach der
* Operation, dann gilt für ein Element {@code A'_ij} in {@code A'}
* <pre>
* A'_ij = A_ij * scalar
* </pre>
*
* @param scalar Ein Skalar.
* @return Diese Matrix selbst (method chaining)
*/
MLMatrix scaleInPlace( double scalar );
/**
* Multipliziert jeden Wert dieser Matrix mit dem entsprechenden Wert in der
* Matrix {@code S}.
* <p>
* Ist {@code A} dieses Matrixobjekt und {@code A'} diese Matrix nach der
* Operation, dann gilt für ein Element {@code A'_ij} in {@code A'}
* <pre>
* A'_ij = A_ij * S_ij
* </pre>
*
* @param S Eine {@code rows()} x {@code columns()} Matrix.
* @return Diese Matrix selbst (method chaining)
* @throws IllegalArgumentException Falls die Dimensionen der Matrizen nicht
* zur Operation passen. Also
* {@code this.rows() != B.rows()} oder
* {@code this.columns() != B.columns()}.
*/
MLMatrix scaleInPlace( MLMatrix S ) throws IllegalArgumentException;
/**
* Berechnet eine neue Matrix mit nur einer Zeile, die die Spaltensummen
* dieser Matrix enthalten.
*
* @return Eine 1 x {@code columns()} Matrix.
*/
MLMatrix colSums();
/**
* Erzeugt eine neue Matrix, deren Werte gleich den Werten dieser Matrix
* nach der Anwendung der angegebenen Funktion sind.
*
* @param op Eine Operation {@code (double) -> double}.
* @return Eine {@code rows()} x {@code columns()} Matrix.
*/
MLMatrix apply( DoubleUnaryOperator op );
/**
* Endet die gegebene Funktion auf jeden Wert der Matrix an.
*
* @param op Eine Operation {@code (double) -> double}.
* @return Diese Matrix selbst (method chaining)
*/
MLMatrix applyInPlace( DoubleUnaryOperator op );
/**
* Erzeugt eine neue Matrix mit denselben Dimensionen und Koeffizienten wie
* diese Matrix.
*
* @return Eine Kopie dieser Matrix.
*/
MLMatrix duplicate();
String toString();
}

View File

@@ -1,82 +0,0 @@
package schule.ngb.zm.ml;
import schule.ngb.zm.Constants;
import java.util.Arrays;
// TODO: Move Math into Matrix class
// TODO: Implement support for optional sci libs
public class Matrix {
private int columns, rows;
double[][] coefficients;
public Matrix( int rows, int cols ) {
this.rows = rows;
this.columns = cols;
coefficients = new double[rows][cols];
}
public Matrix( double[][] coefficients ) {
this.coefficients = coefficients;
this.rows = coefficients.length;
this.columns = coefficients[0].length;
}
public int getColumns() {
return columns;
}
public int getRows() {
return rows;
}
public double[][] getCoefficients() {
return coefficients;
}
public double get( int row, int col ) {
return coefficients[row][col];
}
public void initializeRandom() {
coefficients = MLMath.matrixApply(coefficients, (d) -> Constants.randomGaussian());
}
public void initializeRandom( double lower, double upper ) {
coefficients = MLMath.matrixApply(coefficients, (d) -> ((upper-lower) * (Constants.randomGaussian()+1) * .5) + lower);
}
public void initializeIdentity() {
initializeZero();
for( int i = 0; i < Math.min(rows, columns); i++ ) {
this.coefficients[i][i] = 1.0;
}
}
public void initializeOne() {
coefficients = MLMath.matrixApply(coefficients, (d) -> 1.0);
}
public void initializeZero() {
coefficients = MLMath.matrixApply(coefficients, (d) -> 0.0);
}
@Override
public String toString() {
//return Arrays.deepToString(coefficients);
StringBuilder sb = new StringBuilder();
sb.append('[');
sb.append('\n');
for( int i = 0; i < coefficients.length; i++ ) {
sb.append('\t');
sb.append(Arrays.toString(coefficients[i]));
sb.append('\n');
}
sb.append(']');
return sb.toString();
}
}

View File

@@ -0,0 +1,246 @@
package schule.ngb.zm.ml;
import cern.colt.matrix.DoubleFactory2D;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.Log;
import java.util.function.DoubleUnaryOperator;
/**
* Zentrale Klasse zur Erstellung neuer Matrizen. Generell sollten neue Matrizen
* nicht direkt erstellt werden, sondern durch den Aufruf von
* {@link #create(int, int)} oder {@link #create(double[][])}. Die Fabrik
* ermittelt automatisch die beste verfügbare Implementierung und initialisiert
* eine entsprechende Implementierung von {@link MLMatrix}.
* <p>
* Derzeit werden die optionale Bibliothek <a
* href="https://dst.lbl.gov/ACSSoftware/colt/">Colt</a> und die interne
* Implementierung {@link DoubleMatrix} unterstützt.
*/
public class MatrixFactory {
/**
* Erstellt eine neue Matrix mit den angegebenen Dimensionen und
* initialisiert alle Werte mit 0.
*
* @param rows Anzahl der Zeilen.
* @param cols Anzahl der Spalten.
* @return Eine {@code rows} x {@code cols} Matrix.
*/
public static final MLMatrix create( int rows, int cols ) {
try {
return getMatrixType().getDeclaredConstructor(int.class, int.class).newInstance(rows, cols);
} catch( Exception ex ) {
LOG.error(ex, "Could not initialize matrix implementation for class <%s>. Using internal implementation.", matrixType);
}
return new DoubleMatrix(rows, cols);
}
/**
* Erstellt eine neue Matrix mit den Dimensionen des angegebenen Arrays und
* initialisiert die Werte mit den entsprechenden Werten des Arrays.
*
* @param values Die Werte der Matrix.
* @return Eine {@code values.length} x {@code values[0].length} Matrix mit
* den Werten des Arrays.
*/
public static final MLMatrix create( double[][] values ) {
try {
return getMatrixType().getDeclaredConstructor(double[][].class).newInstance((Object) values);
} catch( Exception ex ) {
LOG.error(ex, "Could not initialize matrix implementation for class <%s>. Using internal implementation.", matrixType);
}
return new DoubleMatrix(values);
}
/**
* Die verwendete {@link MLMatrix} Implementierung, aus der Matrizen erzeugt
* werden.
*/
static Class<? extends MLMatrix> matrixType = null;
/**
* Ermittelt die beste verfügbare Implementierung von {@link MLMatrix}.
*
* @return Die verwendete {@link MLMatrix} Implementierung.
*/
private static final Class<? extends MLMatrix> getMatrixType() {
if( matrixType == null ) {
try {
Class<?> clazz = Class.forName("cern.colt.matrix.impl.DenseDoubleMatrix2D", false, MatrixFactory.class.getClassLoader());
matrixType = ColtMatrix.class;
LOG.info("Colt library found. Using <cern.colt.matrix.impl.DenseDoubleMatrix2D> as matrix implementation.");
} catch( ClassNotFoundException e ) {
LOG.info("Colt library not found. Falling back on internal implementation.");
matrixType = DoubleMatrix.class;
}
}
return matrixType;
}
private static final Log LOG = Log.getLogger(MatrixFactory.class);
/**
* Interner Wrapper der DoubleMatrix2D Klasse aus der Colt Bibliothek, um
* das {@link MLMatrix} Interface zu implementieren.
*/
static class ColtMatrix implements MLMatrix {
cern.colt.matrix.DoubleMatrix2D matrix;
public ColtMatrix( double[][] doubles ) {
matrix = new cern.colt.matrix.impl.DenseDoubleMatrix2D(doubles);
}
public ColtMatrix( int rows, int cols ) {
matrix = new cern.colt.matrix.impl.DenseDoubleMatrix2D(rows, cols);
}
public ColtMatrix( ColtMatrix matrix ) {
this.matrix = matrix.matrix.copy();
}
@Override
public int columns() {
return matrix.columns();
}
@Override
public int rows() {
return matrix.rows();
}
@Override
public double get( int row, int col ) {
return matrix.get(row, col);
}
@Override
public MLMatrix set( int row, int col, double value ) {
matrix.set(row, col, value);
return this;
}
@Override
public MLMatrix initializeRandom() {
return initializeRandom(-1.0, 1.0);
}
@Override
public MLMatrix initializeRandom( double lower, double upper ) {
matrix.assign(( d ) -> ((upper - lower) * Constants.random()) + lower);
return this;
}
@Override
public MLMatrix initializeOne() {
this.matrix.assign(1.0);
return this;
}
@Override
public MLMatrix initializeZero() {
this.matrix.assign(0.0);
return this;
}
@Override
public MLMatrix duplicate() {
ColtMatrix newMatrix = new ColtMatrix(matrix.rows(), matrix.columns());
newMatrix.matrix.assign(this.matrix);
return newMatrix;
}
@Override
public MLMatrix multiplyTransposed( MLMatrix B ) {
ColtMatrix CB = (ColtMatrix) B;
ColtMatrix newMatrix = new ColtMatrix(0, 0);
newMatrix.matrix = matrix.zMult(CB.matrix, null, 1.0, 0.0, false, true);
return newMatrix;
}
@Override
public MLMatrix multiplyAddBias( MLMatrix B, MLMatrix C ) {
ColtMatrix CB = (ColtMatrix) B;
ColtMatrix newMatrix = new ColtMatrix(0, 0);
newMatrix.matrix = DoubleFactory2D.dense.repeat(((ColtMatrix) C).matrix, rows(), 1);
matrix.zMult(CB.matrix, newMatrix.matrix, 1.0, 1.0, false, false);
return newMatrix;
}
@Override
public MLMatrix transposedMultiplyAndScale( final MLMatrix B, final double scalar ) {
ColtMatrix CB = (ColtMatrix) B;
ColtMatrix newMatrix = new ColtMatrix(0, 0);
newMatrix.matrix = matrix.zMult(CB.matrix, null, scalar, 0.0, true, false);
return newMatrix;
}
@Override
public MLMatrix add( MLMatrix B ) {
ColtMatrix CB = (ColtMatrix) B;
ColtMatrix newMatrix = new ColtMatrix(this);
newMatrix.matrix.assign(CB.matrix, ( d1, d2 ) -> d1 + d2);
return newMatrix;
}
@Override
public MLMatrix addInPlace( MLMatrix B ) {
ColtMatrix CB = (ColtMatrix) B;
matrix.assign(CB.matrix, ( d1, d2 ) -> d1 + d2);
return this;
}
@Override
public MLMatrix sub( MLMatrix B ) {
ColtMatrix CB = (ColtMatrix) B;
ColtMatrix newMatrix = new ColtMatrix(this);
newMatrix.matrix.assign(CB.matrix, ( d1, d2 ) -> d1 - d2);
return newMatrix;
}
@Override
public MLMatrix colSums() {
double[][] sums = new double[1][matrix.columns()];
for( int c = 0; c < matrix.columns(); c++ ) {
for( int r = 0; r < matrix.rows(); r++ ) {
sums[0][c] += matrix.getQuick(r, c);
}
}
return new ColtMatrix(sums);
}
@Override
public MLMatrix scaleInPlace( double scalar ) {
this.matrix.assign(( d ) -> d * scalar);
return this;
}
@Override
public MLMatrix scaleInPlace( MLMatrix S ) {
this.matrix.forEachNonZero(( r, c, d ) -> d * S.get(r, c));
return this;
}
@Override
public MLMatrix apply( DoubleUnaryOperator op ) {
ColtMatrix newMatrix = new ColtMatrix(matrix.rows(), matrix.columns());
newMatrix.matrix.assign(matrix);
newMatrix.matrix.assign(( d ) -> op.applyAsDouble(d));
return newMatrix;
}
@Override
public MLMatrix applyInPlace( DoubleUnaryOperator op ) {
this.matrix.assign(( d ) -> op.applyAsDouble(d));
return this;
}
@Override
public String toString() {
return matrix.toString();
}
}
}

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm.ml; package schule.ngb.zm.ml;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider; import schule.ngb.zm.util.io.ResourceStreamProvider;
import java.io.*; import java.io.*;
import java.util.LinkedList; import java.util.LinkedList;
@@ -15,7 +15,7 @@ public class NeuralNetwork {
Writer writer = ResourceStreamProvider.getWriter(source); Writer writer = ResourceStreamProvider.getWriter(source);
PrintWriter out = new PrintWriter(writer) PrintWriter out = new PrintWriter(writer)
) { ) {
for( NeuronLayer layer: network.layers ) { for( NeuronLayer layer : network.layers ) {
out.print(layer.getNeuronCount()); out.print(layer.getNeuronCount());
out.print(' '); out.print(' ');
out.print(layer.getInputCount()); out.print(layer.getInputCount());
@@ -23,20 +23,44 @@ public class NeuralNetwork {
for( int i = 0; i < layer.getInputCount(); i++ ) { for( int i = 0; i < layer.getInputCount(); i++ ) {
for( int j = 0; j < layer.getNeuronCount(); j++ ) { for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.weights.coefficients[i][j]); out.print(layer.weights.get(i, j));
out.print(' '); out.print(' ');
} }
out.println(); out.println();
} }
for( int j = 0; j < layer.getNeuronCount(); j++ ) { for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.biases[j]); out.print(layer.biases.get(0, j));
out.print(' '); out.print(' ');
} }
out.println(); out.println();
} }
out.flush(); out.flush();
} catch( IOException ex ) { } catch( IOException ex ) {
LOG.warn(ex, ""); LOG.error(ex, "");
}
}
public static void saveToDataFile( String source, NeuralNetwork network ) {
try(
OutputStream stream = ResourceStreamProvider.getOutputStream(source);
DataOutputStream out = new DataOutputStream(stream)
) {
for( NeuronLayer layer : network.layers ) {
out.writeInt(layer.getNeuronCount());
out.writeInt(layer.getInputCount());
for( int i = 0; i < layer.getInputCount(); i++ ) {
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.writeDouble(layer.weights.get(i, j));
}
}
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.writeDouble(layer.biases.get(0, j));
}
}
out.flush();
} catch( IOException ex ) {
LOG.error(ex, "");
} }
} }
@@ -56,13 +80,13 @@ public class NeuralNetwork {
for( int i = 0; i < inputs; i++ ) { for( int i = 0; i < inputs; i++ ) {
split = in.readLine().split(" "); split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) { for( int j = 0; j < neurons; j++ ) {
layer.weights.coefficients[i][j] = Double.parseDouble(split[j]); layer.weights.set(i, j, Double.parseDouble(split[j]));
} }
} }
// Load Biases // Load Biases
split = in.readLine().split(" "); split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) { for( int j = 0; j < neurons; j++ ) {
layer.biases[j] = Double.parseDouble(split[j]); layer.biases.set(0, j, Double.parseDouble(split[j]));
} }
layers.add(layer); layers.add(layer);
@@ -70,29 +94,30 @@ public class NeuralNetwork {
return new NeuralNetwork(layers); return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) { } catch( IOException | NoSuchElementException ex ) {
LOG.warn(ex, "Could not load neural network from source <%s>", source); LOG.error(ex, "Could not load neural network from source <%s>", source);
} }
return null; return null;
} }
/*public static NeuralNetwork loadFromFile( String source ) { public static NeuralNetwork loadFromDataFile( String source ) {
try( try(
InputStream stream = ResourceStreamProvider.getInputStream(source); InputStream stream = ResourceStreamProvider.getInputStream(source);
Scanner in = new Scanner(stream) DataInputStream in = new DataInputStream(stream)
) { ) {
List<NeuronLayer> layers = new LinkedList<>(); List<NeuronLayer> layers = new LinkedList<>();
while( in.hasNext() ) { while( in.available() > 0 ) {
int neurons = in.nextInt(); int neurons = in.readInt();
int inputs = in.nextInt(); int inputs = in.readInt();
NeuronLayer layer = new NeuronLayer(neurons, inputs); NeuronLayer layer = new NeuronLayer(neurons, inputs);
for( int i = 0; i < inputs; i++ ) { for( int i = 0; i < inputs; i++ ) {
for( int j = 0; j < neurons; j++ ) { for( int j = 0; j < neurons; j++ ) {
layer.weights.coefficients[i][j] = in.nextDouble(); layer.weights.set(i, j, in.readDouble());
} }
} }
// Load Biases
for( int j = 0; j < neurons; j++ ) { for( int j = 0; j < neurons; j++ ) {
layer.biases[j] = in.nextDouble(); layer.biases.set(0, j, in.readDouble());
} }
layers.add(layer); layers.add(layer);
@@ -100,14 +125,14 @@ public class NeuralNetwork {
return new NeuralNetwork(layers); return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) { } catch( IOException | NoSuchElementException ex ) {
LOG.warn(ex, "Could not load neural network from source <%s>", source); LOG.error(ex, "Could not load neural network from source <%s>", source);
} }
return null; return null;
}*/ }
private NeuronLayer[] layers; private NeuronLayer[] layers;
private double[][] output; private MLMatrix output;
private double learningRate = 0.1; private double learningRate = 0.1;
@@ -128,7 +153,7 @@ public class NeuralNetwork {
for( int i = 0; i < layers.size(); i++ ) { for( int i = 0; i < layers.size(); i++ ) {
this.layers[i] = layers.get(i); this.layers[i] = layers.get(i);
if( i > 0 ) { if( i > 0 ) {
this.layers[i-1].setNextLayer(this.layers[i]); this.layers[i - 1].setNextLayer(this.layers[i]);
} }
} }
} }
@@ -138,7 +163,7 @@ public class NeuralNetwork {
for( int i = 0; i < layers.length; i++ ) { for( int i = 0; i < layers.length; i++ ) {
this.layers[i] = layers[i]; this.layers[i] = layers[i];
if( i > 0 ) { if( i > 0 ) {
this.layers[i-1].setNextLayer(this.layers[i]); this.layers[i - 1].setNextLayer(this.layers[i]);
} }
} }
} }
@@ -146,6 +171,7 @@ public class NeuralNetwork {
public int getLayerCount() { public int getLayerCount() {
return layers.length; return layers.length;
} }
public NeuronLayer[] getLayers() { public NeuronLayer[] getLayers() {
return layers; return layers;
} }
@@ -162,17 +188,28 @@ public class NeuralNetwork {
this.learningRate = pLearningRate; this.learningRate = pLearningRate;
} }
public double[][] getOutput() { public MLMatrix getOutput() {
return output; return output;
} }
public double[][] predict( double[][] inputs ) { public MLMatrix predict( double[] inputs ) {
//this.output = layers[1].apply(layers[0].apply(inputs)); return predict(MatrixFactory.create(new double[][]{inputs}));
}
public MLMatrix predict( double[][] inputs ) {
return predict(MatrixFactory.create(inputs));
}
public MLMatrix predict( MLMatrix inputs ) {
this.output = layers[0].apply(inputs); this.output = layers[0].apply(inputs);
return this.output; return this.output;
} }
public void learn( double[][] expected ) { public void learn( double[][] expected ) {
learn(MatrixFactory.create(expected));
}
public void learn( MLMatrix expected ) {
layers[layers.length - 1].backprop(expected, learningRate); layers[layers.length - 1].backprop(expected, learningRate);
} }

View File

@@ -1,50 +1,66 @@
package schule.ngb.zm.ml; package schule.ngb.zm.ml;
import java.util.Arrays;
import java.util.function.DoubleUnaryOperator; import java.util.function.DoubleUnaryOperator;
import java.util.function.Function; import java.util.function.Function;
public class NeuronLayer implements Function<double[][], double[][]> { /**
* Implementierung einer Neuronenebene in einem Neuonalen Netz.
* <p>
* Eine Ebene besteht aus einer Anzahl an <em>Neuronen</em> die jeweils eine
* Anzahl <em>Eingänge</em> haben. Die Eingänge erhalten als Signal die Ausgabe
* der vorherigen Ebene und berechnen die Ausgabe des jeweiligen Neurons.
*/
public class NeuronLayer implements Function<MLMatrix, MLMatrix> {
public static NeuronLayer fromArray( double[][] weights, boolean transpose ) {
NeuronLayer layer;
if( transpose ) {
layer = new NeuronLayer(weights.length, weights[0].length);
} else {
layer = new NeuronLayer(weights[0].length, weights.length);
}
public static NeuronLayer fromArray( double[][] weights ) {
NeuronLayer layer = new NeuronLayer(weights[0].length, weights.length);
for( int i = 0; i < weights[0].length; i++ ) { for( int i = 0; i < weights[0].length; i++ ) {
for( int j = 0; j < weights.length; j++ ) { for( int j = 0; j < weights.length; j++ ) {
layer.weights.coefficients[i][j] = weights[i][j]; if( transpose ) {
layer.weights.set(j, i, weights[i][j]);
} else {
layer.weights.set(i, j, weights[i][j]);
}
} }
} }
return layer; return layer;
} }
public static NeuronLayer fromArray( double[][] weights, double[] biases ) { public static NeuronLayer fromArray( double[][] weights, double[] biases, boolean transpose ) {
NeuronLayer layer = new NeuronLayer(weights[0].length, weights.length); NeuronLayer layer = fromArray(weights, transpose);
for( int i = 0; i < weights[0].length; i++ ) {
for( int j = 0; j < weights.length; j++ ) {
layer.weights.coefficients[i][j] = weights[i][j];
}
}
for( int j = 0; j < biases.length; j++ ) { for( int j = 0; j < biases.length; j++ ) {
layer.biases[j] = biases[j]; layer.biases.set(0, j, biases[j]);
} }
return layer; return layer;
} }
Matrix weights; MLMatrix weights;
double[] biases; MLMatrix biases;
NeuronLayer previous, next; NeuronLayer previous, next;
DoubleUnaryOperator activationFunction, activationFunctionDerivative; DoubleUnaryOperator activationFunction, activationFunctionDerivative;
double[][] lastOutput, lastInput; MLMatrix lastOutput, lastInput;
public NeuronLayer( int neurons, int inputs ) { public NeuronLayer( int neurons, int inputs ) {
weights = new Matrix(inputs, neurons); weights = MatrixFactory
weights.initializeRandom(-1, 1); .create(inputs, neurons)
.initializeRandom();
biases = new double[neurons]; biases = MatrixFactory
Arrays.fill(biases, 0.0); // TODO: Random? .create(1, neurons)
.initializeZero();
activationFunction = MLMath::sigmoid; activationFunction = MLMath::sigmoid;
activationFunctionDerivative = MLMath::sigmoidDerivative; activationFunctionDerivative = MLMath::sigmoidDerivative;
@@ -85,45 +101,42 @@ public class NeuronLayer implements Function<double[][], double[][]> {
} }
} }
public Matrix getWeights() { public MLMatrix getWeights() {
return weights; return weights;
} }
public MLMatrix getBiases() {
return biases;
}
public int getNeuronCount() { public int getNeuronCount() {
return weights.coefficients[0].length; return weights.columns();
} }
public int getInputCount() { public int getInputCount() {
return weights.coefficients.length; return weights.rows();
} }
public double[][] getLastOutput() { public MLMatrix getLastOutput() {
return lastOutput; return lastOutput;
} }
public void setWeights( double[][] newWeights ) { public void setWeights( MLMatrix newWeights ) {
weights.coefficients = MLMath.copyMatrix(newWeights); weights = newWeights.duplicate();
}
public void adjustWeights( double[][] adjustment ) {
weights.coefficients = MLMath.matrixAdd(weights.coefficients, adjustment);
} }
@Override @Override
public String toString() { public String toString() {
return weights.toString() + "\n" + Arrays.toString(biases); return "weights:\n" + weights.toString() + "\nbiases:\n" + biases.toString();
} }
@Override @Override
public double[][] apply( double[][] inputs ) { public MLMatrix apply( MLMatrix inputs ) {
lastInput = inputs; lastInput = inputs.duplicate();
lastOutput = MLMath.matrixApply( lastOutput = inputs
MLMath.biasAdd( .multiplyAddBias(weights, biases)
MLMath.matrixMultiply(inputs, weights.coefficients), .applyInPlace(activationFunction);
biases
),
activationFunction
);
if( next != null ) { if( next != null ) {
return next.apply(lastOutput); return next.apply(lastOutput);
} else { } else {
@@ -132,36 +145,41 @@ public class NeuronLayer implements Function<double[][], double[][]> {
} }
@Override @Override
public <V> Function<V, double[][]> compose( Function<? super V, ? extends double[][]> before ) { public <V> Function<V, MLMatrix> compose( Function<? super V, ? extends MLMatrix> before ) {
return ( in ) -> apply(before.apply(in)); return ( in ) -> apply(before.apply(in));
} }
@Override @Override
public <V> Function<double[][], V> andThen( Function<? super double[][], ? extends V> after ) { public <V> Function<MLMatrix, V> andThen( Function<? super MLMatrix, ? extends V> after ) {
return ( in ) -> after.apply(apply(in)); return ( in ) -> after.apply(apply(in));
} }
public void backprop( double[][] expected, double learningRate ) { public void backprop( MLMatrix expected, double learningRate ) {
double[][] error, delta, adjustment; MLMatrix error, adjustment;
if( next == null ) { if( next == null ) {
error = MLMath.matrixSub(expected, this.lastOutput); error = expected.sub(lastOutput);
} else { } else {
error = MLMath.matrixMultiply(expected, MLMath.matrixTranspose(next.weights.coefficients)); error = expected.multiplyTransposed(next.weights);
} }
delta = MLMath.matrixScale(error, MLMath.matrixApply(this.lastOutput, this.activationFunctionDerivative)); error.scaleInPlace(
lastOutput.apply(this.activationFunctionDerivative)
);
// Hier schon leraningRate anwenden? // Hier schon leraningRate anwenden?
// See https://towardsdatascience.com/understanding-and-implementing-neural-networks-in-java-from-scratch-61421bb6352c // See https://towardsdatascience.com/understanding-and-implementing-neural-networks-in-java-from-scratch-61421bb6352c
//delta = MLMath.matrixApply(delta, ( x ) -> learningRate * x); //delta = MLMath.matrixApply(delta, ( x ) -> learningRate * x);
if( previous != null ) { if( previous != null ) {
previous.backprop(delta, learningRate); previous.backprop(error, learningRate);
} }
biases = MLMath.biasAdjust(biases, MLMath.matrixApply(delta, ( x ) -> learningRate * x)); biases.addInPlace(
error.colSums().scaleInPlace(
-learningRate / (double) error.rows()
)
);
adjustment = MLMath.matrixMultiply(MLMath.matrixTranspose(lastInput), delta); adjustment = lastInput.transposedMultiplyAndScale(error, learningRate);
adjustment = MLMath.matrixApply(adjustment, ( x ) -> learningRate * x); weights.addInPlace(adjustment);
this.adjustWeights(adjustment);
} }
} }

View File

@@ -1,45 +0,0 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
public abstract class FilledShape extends StrokedShape {
protected Color fillColor = STD_FILLCOLOR;
public Color getFillColor() {
return fillColor;
}
public void setFillColor( Color color ) {
fillColor = color;
}
public void setFillColor( Color color, int alpha ) {
setFillColor(new Color(color, alpha));
}
public void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
public void noFill() {
setFillColor(null);
}
public void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
public void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
public void resetFill() {
setFillColor(STD_FILLCOLOR);
}
}

View File

@@ -3,7 +3,7 @@ package schule.ngb.zm.shapes;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Options; import schule.ngb.zm.Options;
import schule.ngb.zm.util.ImageLoader; import schule.ngb.zm.util.io.ImageLoader;
import java.awt.*; import java.awt.*;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;

View File

@@ -15,6 +15,8 @@ public class Rectangle extends Shape {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.anchor = Options.Direction.NORTHWEST; this.anchor = Options.Direction.NORTHWEST;
//this.cacheEnabled = getClass().equals(Rectangle.class);
} }
public Rectangle( Rectangle pRechteck ) { public Rectangle( Rectangle pRechteck ) {
@@ -24,9 +26,25 @@ public class Rectangle extends Shape {
copyFrom(pRechteck); copyFrom(pRechteck);
} }
public Rectangle( Bounds pBounds ) {
this(
pBounds.x, pBounds.y,
pBounds.width, pBounds.height);
}
/**
* Erstellt ein Rechteck zur Darstellung der
* @param pShape
*/
public Rectangle( Shape pShape ) { public Rectangle( Shape pShape ) {
this(pShape, true);
}
public Rectangle( Shape pShape, boolean transformed ) {
java.awt.Shape s = pShape.getShape(); java.awt.Shape s = pShape.getShape();
s = pShape.getTransform().createTransformedShape(s); if( transformed ) {
s = pShape.getTransform().createTransformedShape(s);
}
Rectangle2D bounds = s.getBounds2D(); Rectangle2D bounds = s.getBounds2D();
x = bounds.getX(); x = bounds.getX();
y = bounds.getY(); y = bounds.getY();

View File

@@ -1,47 +1,115 @@
package schule.ngb.zm.shapes; package schule.ngb.zm.shapes;
import schule.ngb.zm.Options; import schule.ngb.zm.*;
import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
public abstract class Shape extends FilledShape { /**
* Basisklasse für alle Formen in der Zeichenmaschine.
* <p>
* Alle Formen sind als Unterklassen von {@code Shape} implementiert.
* <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die Werte einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert:
*
* <pre><code>
* public Circle( double x, double y, double radius ) {
* super(x, y);
* this.radius = radius;
* }
*
* public Circle( Circle circle ) {
* super(circle.x, circle.y);
* copyFrom(circle);
* }
* </code></pre>
* <p>
* Außerdem implementieren Unterklassen eine passende {@link #toString()} und
* eine {@link #equals(Object)} Methode.
*/
@SuppressWarnings( "unused" )
public abstract class Shape extends BasicDrawable {
/**
* x-Koordinate der Form.
*/
protected double x; protected double x;
/**
* y-Koordinate der Form.
*/
protected double y; protected double y;
/**
* Rotation in Grad um den Punkt (x, y).
*/
protected double rotation = 0.0; protected double rotation = 0.0;
/**
* Skalierungsfaktor.
*/
protected double scale = 1.0; protected double scale = 1.0;
protected boolean visible = true; /**
* Ankerpunkt der Form.
*/
protected Options.Direction anchor = Options.Direction.CENTER; protected Options.Direction anchor = Options.Direction.CENTER;
/**
* Setzt die x- und y-Koordinate der Form auf 0.
*/
public Shape() {
this(0.0, 0.0);
}
/**
* Setzt die x- und y-Koordinate der Form.
*
* @param x
* @param y
*/
public Shape( double x, double y ) { public Shape( double x, double y ) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
public Shape() { /**
this(0.0, 0.0); * Gibt die x-Koordinate der Form zurück.
} *
* @return Die x-Koordinate.
*/
public double getX() { public double getX() {
return x; return x;
} }
/**
* Setzt die x-Koordinate der Form.
*
* @param x Die neue x-Koordinate.
*/
public void setX( double x ) { public void setX( double x ) {
this.x = x; this.x = x;
} }
/**
* Gibt die y-Koordinate der Form zurück.
*
* @return Die y-Koordinate.
*/
public double getY() { public double getY() {
return y; return y;
} }
/**
* Setzt die y-Koordinate der Form.
*
* @param y Die neue y-Koordinate.
*/
public void setY( double y ) { public void setY( double y ) {
this.y = y; this.y = y;
} }
@@ -49,8 +117,9 @@ public abstract class Shape extends FilledShape {
/** /**
* Gibt die Breite dieser Form zurück. * Gibt die Breite dieser Form zurück.
* <p> * <p>
* Die Breite einer Form ist immer die Breite ihrer Begrenzung, <b>bevor</b> * Die Breite einer Form ist immer die Breite ihrer Begrenzung,
* Drehungen und andere Transformationen auf sei angewandt wurden. * <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p> * <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit * Die Begrenzungen der tatsächlich gezeichneten Form kann mit
* {@link #getBounds()} abgerufen werden. * {@link #getBounds()} abgerufen werden.
@@ -62,8 +131,9 @@ public abstract class Shape extends FilledShape {
/** /**
* Gibt die Höhe dieser Form zurück. * Gibt die Höhe dieser Form zurück.
* <p> * <p>
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung, <b>bevor</b> * Die Höhe einer Form ist immer die Höhe ihrer Begrenzung,
* Drehungen und andere Transformationen auf sei angewandt wurden. * <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p> * <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit * Die Begrenzungen der tatsächlich gezeichneten Form kann mit
* {@link #getBounds()} abgerufen werden. * {@link #getBounds()} abgerufen werden.
@@ -72,28 +142,80 @@ public abstract class Shape extends FilledShape {
*/ */
public abstract double getHeight(); public abstract double getHeight();
/**
* {@inheritDoc}
*/
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
/**
* {@inheritDoc}
*/
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
/**
* Gibt die Rotation in Grad zurück.
*
* @return Rotation in Grad.
*/
public double getRotation() { public double getRotation() {
return rotation; return rotation;
} }
/**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
*
* @param angle Drehwinkel in Grad.
*/
public void rotate( double angle ) {
this.rotation += angle % 360;
}
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
public void rotate( Point2D center, double angle ) {
rotate(center.getX(), center.getY(), angle);
}
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
this.x = x2 + x;
this.y = y2 + y;
}
/**
* Gibt den aktuellen Skalierungsfaktor zurück.
*
* @return Der Skalierungsfaktor.
*/
public double getScale() { public double getScale() {
return scale; return scale;
} }
public boolean isVisible() { public void scale( double factor ) {
return visible; scale = factor;
} }
public void hide() { public void scaleBy( double factor ) {
visible = false; scale(scale * factor);
}
public void show() {
visible = true;
}
public void toggle() {
visible = !visible;
} }
public Options.Direction getAnchor() { public Options.Direction getAnchor() {
@@ -117,16 +239,26 @@ public abstract class Shape extends FilledShape {
} }
} }
protected static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) { /**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkts basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p>
* Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt.
*
* @param width Breite des umschließenden Rechtecks.
* @param height Höhe des umschließenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten.
*/
public static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5; double wHalf = width * .5, hHalf = height * .5;
// anchor == CENTER return new Point2D.Double(
Point2D.Double anchorPoint = new Point2D.Double(
wHalf + wHalf * anchor.x, wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y hHalf + hHalf * anchor.y
); );
return anchorPoint;
} }
/** /**
@@ -140,12 +272,10 @@ public abstract class Shape extends FilledShape {
double wHalf = getWidth() * .5, hHalf = getHeight() * .5; double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER // anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double( return new Point2D.Double(
wHalf * (anchor.x - this.anchor.x), wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y) hHalf * (anchor.y - this.anchor.y)
); );
return anchorPoint;
} }
/** /**
@@ -164,14 +294,22 @@ public abstract class Shape extends FilledShape {
} }
/** /**
* Kopiert die Eigenschaften der übergebenen Form in diese. * Kopiert die Eigenschaften der angegebenen Form in diese.
* <p> * <p>
* Unterklassen sollten diese Methode überschreiben, um weitere * Unterklassen sollten diese Methode überschreiben, um weitere
* Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises). Mit * Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises).
* dem Aufruf {@code super.copyFrom(shape)} sollten die Basiseigenschaften * Unterklassen sollten immer mit dem Aufruf {@code super.copyFrom(shape)}
* kopiert werden. * die Basiseigenschaften kopieren.
* <p>
* Die Methode sollte so viele Eigenschaften wie möglich von der anderen
* Form in diese kopieren. Wenn die andere Form einen anderen Typ hat, dann
* werden trotzdem die Basiseigenschaften (Konturlinie, Füllung, Position,
* Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese Form kopiert.
* Implementierende Unterklassen können soweit sinnvoll auch andere Werte
* übernehmen. Eine {@link Ellipse} kann beispielsweise auch die Breite und
* Höhe eines {@link Rectangle} übernehmen.
* *
* @param shape * @param shape Die Originalform, von der kopiert werden soll.
*/ */
public void copyFrom( Shape shape ) { public void copyFrom( Shape shape ) {
if( shape != null ) { if( shape != null ) {
@@ -181,16 +319,55 @@ public abstract class Shape extends FilledShape {
setStrokeWeight(shape.getStrokeWeight()); setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType()); setStrokeType(shape.getStrokeType());
visible = shape.isVisible(); visible = shape.isVisible();
rotation = shape.rotation; rotation = shape.getRotation();
scale(shape.scale); scale(shape.getScale());
setAnchor(shape.getAnchor()); setAnchor(shape.getAnchor());
} }
} }
/**
* Erzeugt eine Kopie dieser Form mit denselben Eigenschaften.
* <p>
* Unterklassen implementieren diese Methode mit dem genauen Typ der
* Unterklasse. In {@link Rectangle} sieht die Umsetzung beispielsweise so
* aus:
* <pre><code>
* public Rectangle copy() {
* return new Rectangle(this);
* }
* </code></pre>
* <p>
* Die Methode kann beliebig umgesetzt werden, um eine 1-zu-1-Kopie dieser
* Form zu erhalten. In der Regel sollte aber jede Form einen Konstruktor
* besitzen, die alle Werte einer andern Form übernimmt. Die gezeigte
* Implementierung dürfte daher im Regelfall ausreichend sein.
*
* @return Eine genaue Kopie dieser Form.
*/
public abstract Shape copy(); public abstract Shape copy();
/**
* Gibt eine {@link java.awt.Shape Java-AWT Shape} Version dieser Form
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen.
* <p>
* Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, kann die
* Methode {@code null} zurückgeben.
*
* @return Eine Java-AWT {@code Shape} die diese Form repräsentiert oder
* {@code null}.
*/
public abstract java.awt.Shape getShape(); public abstract java.awt.Shape getShape();
/**
* Gibt die Begrenzungen der Form zurück.
* <p>
* Ein {@code Bounds}-Objekt beschreibt eine "<a
* href="https://gdbooks.gitbooks.io/3dcollisions/content/Chapter1/aabb.html">Axis
* Aligned Bounding Box</a>" (AABB).
*
* @return Die Abgrenzungen der Form nach Anwendung der Transformationen.
*/
public Bounds getBounds() { public Bounds getBounds() {
return new Bounds(this); return new Bounds(this);
} }
@@ -273,7 +450,7 @@ public abstract class Shape extends FilledShape {
} }
public void nextTo( Shape shape, Options.Direction dir ) { public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, STD_BUFFER); nextTo(shape, dir, DEFAULT_BUFFER);
} }
/** /**
@@ -295,40 +472,6 @@ public abstract class Shape extends FilledShape {
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff; this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
} }
public void scale( double factor ) {
scale = factor;
}
public void scaleBy( double factor ) {
scale(scale * factor);
}
public void rotate( double angle ) {
this.rotation += angle % 360;
}
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
public void rotate( Point2D center, double angle ) {
rotate(center.getX(), center.getY(), angle);
}
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
this.x = x2 + x;
this.y = y2 + y;
}
/*public void shear( double dx, double dy ) { /*public void shear( double dx, double dy ) {
verzerrung.shear(dx, dy); verzerrung.shear(dx, dy);
}*/ }*/
@@ -375,17 +518,9 @@ public abstract class Shape extends FilledShape {
shape = transform.createTransformedShape(shape); shape = transform.createTransformedShape(shape);
} }
Color currentColor = graphics.getColor(); java.awt.Color currentColor = graphics.getColor();
if( fillColor != null && fillColor.getAlpha() > 0 ) { fillShape(shape, graphics);
graphics.setColor(fillColor.getJavaColor()); strokeShape(shape, graphics);
graphics.fill(shape);
}
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.draw(shape);
}
graphics.setColor(currentColor); graphics.setColor(currentColor);
} }
} }
@@ -396,8 +531,8 @@ public abstract class Shape extends FilledShape {
* verglichen. Unterklassen überschreiben die Methode, um weitere * verglichen. Unterklassen überschreiben die Methode, um weitere
* Eigenschaften zu berücksichtigen. * Eigenschaften zu berücksichtigen.
* <p> * <p>
* Die Eigenschaften von {@link FilledShape} und {@link StrokedShape} werden * Die Eigenschaften, die durch {@link Fillable} und {@link Strokeable}
* nicht verglichen. * impliziert werden, werden nicht verglichen.
* *
* @param o Ein anderes Objekt. * @param o Ein anderes Objekt.
* @return * @return
@@ -413,4 +548,43 @@ public abstract class Shape extends FilledShape {
Double.compare(pShape.scale, scale) == 0; Double.compare(pShape.scale, scale) == 0;
} }
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit den aktuellen
* Kontureigenschaften auf den Grafik-Kontext zu zeichnen. Die Methode
* verändert gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt
* sie nicht auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void strokeShape( java.awt.Shape shape, Graphics2D graphics ) {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(getStroke());
graphics.draw(shape);
}
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit der aktuellen
* Füllung auf den Grafik-Kontext zu zeichnen. Die Methode verändert
* gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt sie nicht
* auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void fillShape( java.awt.Shape shape, Graphics2D graphics ) {
if( fill != null ) {
graphics.setPaint(fill);
graphics.fill(shape);
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fill(shape);
}
}
} }

View File

@@ -7,6 +7,7 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@@ -35,7 +36,7 @@ import java.util.List;
* {@link #arrangeInGrid(int, Options.Direction, double, int)} und * {@link #arrangeInGrid(int, Options.Direction, double, int)} und
* {@link #align(Options.Direction)} verwendet werden, die jeweils die Position * {@link #align(Options.Direction)} verwendet werden, die jeweils die Position
* der Formen in der Gruppe ändern und nicht die Position der Gruppe selbst (so * der Formen in der Gruppe ändern und nicht die Position der Gruppe selbst (so
* wie z.B. {@link #alignTo(Shape, Options.Direction)}. * wie z.B. {@link #alignTo(Shape, Options.Direction)}).
*/ */
public class ShapeGroup extends Shape { public class ShapeGroup extends Shape {
@@ -43,7 +44,7 @@ public class ShapeGroup extends Shape {
public static final int ARRANGE_COLS = 1; public static final int ARRANGE_COLS = 1;
private List<Shape> shapes; private final List<Shape> shapes;
private double groupWidth = -1.0; private double groupWidth = -1.0;
@@ -51,7 +52,7 @@ public class ShapeGroup extends Shape {
public ShapeGroup() { public ShapeGroup() {
super(); super();
shapes = new ArrayList<>(10); shapes = Collections.synchronizedList(new ArrayList<>(10));
} }
public ShapeGroup( double x, double y ) { public ShapeGroup( double x, double y ) {
@@ -62,9 +63,7 @@ public class ShapeGroup extends Shape {
public ShapeGroup( double x, double y, Shape... shapes ) { public ShapeGroup( double x, double y, Shape... shapes ) {
super(x, y); super(x, y);
this.shapes = new ArrayList<>(shapes.length); this.shapes = new ArrayList<>(shapes.length);
for( Shape pShape : shapes ) { Collections.addAll(this.shapes, shapes);
this.shapes.add(pShape);
}
} }
public Shape copy() { public Shape copy() {
@@ -99,9 +98,11 @@ public class ShapeGroup extends Shape {
public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) { public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) {
LinkedList<ShapeType> list = new LinkedList<>(); LinkedList<ShapeType> list = new LinkedList<>();
for( Shape s : shapes ) { synchronized( shapes ) {
if( typeClass.getClass().isInstance(s) ) { for( Shape shape : shapes ) {
list.add((ShapeType) s); if( typeClass.isInstance(shape) ) {
list.add(typeClass.cast(shape));
}
} }
} }
return list; return list;
@@ -170,19 +171,15 @@ public class ShapeGroup extends Shape {
int rows, cols; int rows, cols;
if( mode == ARRANGE_ROWS ) { if( mode == ARRANGE_ROWS ) {
rows = n; rows = n;
cols = (int) ceil(shapes.size() / n); cols = (int) ceil(shapes.size() / (double)n);
} else { } else {
cols = n; cols = n;
rows = (int) ceil(shapes.size() / n); rows = (int) ceil(shapes.size() / (double)n);
} }
// Calculate grid cell size // Calculate grid cell size
double maxHeight = shapes.stream().mapToDouble( double maxHeight = shapes.stream().mapToDouble(Shape::getHeight).reduce(0.0, Double::max);
( s ) -> s.getHeight() double maxWidth = shapes.stream().mapToDouble(Shape::getWidth).reduce(0.0, Double::max);
).reduce(0.0, Double::max);
double maxWidth = shapes.stream().mapToDouble(
( s ) -> s.getWidth()
).reduce(0.0, Double::max);
double halfHeight = maxHeight * .5; double halfHeight = maxHeight * .5;
double halfWidth = maxWidth * .5; double halfWidth = maxWidth * .5;

View File

@@ -1,127 +0,0 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import schule.ngb.zm.util.Noise;
import java.awt.*;
import java.awt.Shape;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.util.Arrays;
public abstract class StrokedShape extends Constants implements Drawable {
protected Color strokeColor = STD_STROKECOLOR;
protected double strokeWeight = STD_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
protected Stroke stroke = null;
public Color getStrokeColor() {
return strokeColor;
}
public void setStrokeColor( Color color ) {
this.strokeColor = color;
}
public void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha));
}
public void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
public void noStroke() {
setStrokeColor(null);
}
public void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
public void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
public void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
public double getStrokeWeight() {
return strokeWeight;
}
public void setStrokeWeight( double weight ) {
this.strokeWeight = weight;
this.stroke = null;
}
public Options.StrokeType getStrokeType() {
return strokeType;
}
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
*
* @param type
*/
public void setStrokeType( Options.StrokeType type ) {
this.strokeType = type;
this.stroke = null;
}
@Override
public abstract void draw( Graphics2D graphics );
/**
* Erstellt ein {@link Stroke} Objekt für den Konturtyp.
*
* @return
*/
protected Stroke createStroke() {
// TODO: Used global cached Stroke Objects?
if( stroke == null ) {
switch( strokeType ) {
case DOTTED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
break;
case DASHED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
break;
case SOLID:
default:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
break;
}
}
return stroke;
}
public void resetStroke() {
setStrokeColor(STD_STROKECOLOR);
setStrokeWeight(STD_STROKEWEIGHT);
setStrokeType(SOLID);
}
}

View File

@@ -2,7 +2,7 @@ package schule.ngb.zm.shapes;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Options; import schule.ngb.zm.Options;
import schule.ngb.zm.util.FontLoader; import schule.ngb.zm.util.io.FontLoader;
import java.awt.Canvas; import java.awt.Canvas;
import java.awt.Font; import java.awt.Font;
@@ -22,15 +22,16 @@ public class Text extends Shape {
protected int width = 0, height = 0, ascent = 0; protected int width = 0, height = 0, ascent = 0;
public Text( double x, double y, String text ) { public Text( double x, double y, String text ) {
this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE)); this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, DEFAULT_FONTSIZE));
} }
public Text( double x, double y, String text, String fontname ) { public Text( double x, double y, String text, String fontname ) {
super(x, y); super(x, y);
Font userfont = FontLoader.loadFont(fontname); Font userfont = FontLoader.loadFont(fontname);
if( userfont != null ) { if( userfont != null ) {
font = userfont; font = userfont;
} else { } else {
font = new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE); font = new Font(Font.SANS_SERIF, Font.PLAIN, DEFAULT_FONTSIZE);
} }
setText(text); setText(text);
fillColor = null; fillColor = null;
@@ -60,29 +61,58 @@ public class Text extends Shape {
return height; return height;
} }
public void setFont( String fontname ) { public Font getFont() {
Font newFont = FontLoader.loadFont(fontname); return font;
}
public void setFont( Font newFont ) {
//font = newFont.deriveFont(font.getStyle(), font.getSize2D());
font = new Font(newFont.getFontName(), newFont.getStyle(), newFont.getSize());
//font = newFont;
calculateBounds();
}
public void setFont( String fontName ) {
Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) { if( newFont != null ) {
setFont(newFont); setFont(newFont);
} }
} }
public void setFont( Font newFont ) { public void setFont( String fontName, double fontSize ) {
font = newFont.deriveFont(font.getSize2D()); Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
font = newFont.deriveFont((float) fontSize);
calculateBounds(); calculateBounds();
} }
public Font getFont() { public void setFont( String fontName, double fontSize, int style ) {
return font; Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
font = newFont.deriveFont(style, (float) fontSize);
calculateBounds();
} }
public void setFontsize( double size ) { public double getFontSize() {
return font.getSize2D();
}
public void setFontSize( double size ) {
font = font.deriveFont((float) size); font = font.deriveFont((float) size);
calculateBounds(); calculateBounds();
} }
public double getFontsize() { public int getFontStyle() {
return font.getSize2D(); return font.getStyle();
}
public void setFontStyle( int fontStyle ) {
font = font.deriveFont(fontStyle);
calculateBounds();
} }
public String getText() { public String getText() {
@@ -151,19 +181,19 @@ public class Text extends Shape {
@Override @Override
public void copyFrom( Shape shape ) { public void copyFrom( Shape shape ) {
super.copyFrom(shape);
if( shape instanceof Text ) { if( shape instanceof Text ) {
Text pText = (Text) shape; Text pText = (Text) shape;
this.text = pText.getText(); this.text = pText.getText();
this.font = pText.getFont(); this.font = pText.getFont();
calculateBounds();
} }
super.copyFrom(shape);
calculateBounds();
} }
@Override @Override
public void scale( double factor ) { public void scale( double factor ) {
super.scale(factor); super.scale(factor);
setFontsize(font.getSize2D() * factor); setFontSize(font.getSize2D() * factor);
} }
@Override @Override
@@ -191,7 +221,7 @@ public class Text extends Shape {
if( strokeColor != null && strokeColor.getAlpha() > 0 if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) { && strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor()); graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
graphics.drawRect(0, 0, width, height); graphics.drawRect(0, 0, width, height);
} }

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.shapes.Rectangle; import schule.ngb.zm.shapes.Rectangle;
@@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@SuppressWarnings("unused")
public class BarChart extends Rectangle { public class BarChart extends Rectangle {
public static String DEFAULT_LABEL = "%.2f"; public static String DEFAULT_LABEL = "%.2f";
@@ -105,10 +106,10 @@ public class BarChart extends Rectangle {
} }
public void addValue( double pValue, Color pColor ) { public void addValue( double pValue, Color pColor ) {
addValue(pValue, pColor, String.format(DEFAULT_LABEL, pValue)); addValue(pValue, String.format(DEFAULT_LABEL, pValue), pColor);
} }
public void addValue( double pValue, Color pColor, String pLabel ) { public void addValue( double pValue, String pLabel, Color pColor ) {
addValue(new BasicChartValue(pValue, pLabel, pColor)); addValue(new BasicChartValue(pValue, pLabel, pColor));
} }
@@ -288,7 +289,7 @@ public class BarChart extends Rectangle {
barY += barHeight + gap; barY += barHeight + gap;
} }
graphics.setColor(getStrokeColor().getJavaColor()); graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
if( inverted ) { if( inverted ) {
graphics.drawLine((int) width, 0, (int) width, (int) height); graphics.drawLine((int) width, 0, (int) width, (int) height);
} else { } else {
@@ -360,7 +361,7 @@ public class BarChart extends Rectangle {
} }
graphics.setColor(getStrokeColor().getJavaColor()); graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
if( inverted ) { if( inverted ) {
graphics.drawLine(0, 0, (int) width, 0); graphics.drawLine(0, 0, (int) width, 0);
} else { } else {

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;

View File

@@ -1,7 +1,6 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Options; import schule.ngb.zm.Options;
import schule.ngb.zm.shapes.Rectangle; import schule.ngb.zm.shapes.Rectangle;
@@ -53,7 +52,7 @@ public class ChartAxes extends Rectangle {
} }
graphics.setColor(strokeColor.getJavaColor()); graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
graphics.drawLine(0, (int)(height), (int)((xMax-xMin) * xUnit), (int)(height)); graphics.drawLine(0, (int)(height), (int)((xMax-xMin) * xUnit), (int)(height));
graphics.drawLine(0, (int)(height), 0, (int)(height - (yMax-yMin) * yUnit)); graphics.drawLine(0, (int)(height), 0, (int)(height - (yMax-yMin) * yUnit));
if( showArrows ) { if( showArrows ) {

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;

View File

@@ -1,7 +1,6 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.shapes.Rectangle; import schule.ngb.zm.shapes.Rectangle;
import schule.ngb.zm.util.Validator;
import java.awt.BasicStroke; import java.awt.BasicStroke;
import java.awt.Graphics2D; import java.awt.Graphics2D;
@@ -92,7 +91,7 @@ public class LineChart extends Rectangle {
for( ChartValue lcv : val ) { for( ChartValue lcv : val ) {
if( drawLines && lastLcv != null ) { if( drawLines && lastLcv != null ) {
graphics.setColor(getStrokeColor().getJavaColor()); graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
graphics.drawLine((int)(lastLcv.getX()*xUnit), (int)(height - lastLcv.getValue()*yUnit), (int)(lcv.getX()*xUnit), (int)(height - lcv.getValue()*yUnit)); graphics.drawLine((int)(lastLcv.getX()*xUnit), (int)(height - lastLcv.getValue()*yUnit), (int)(lcv.getX()*xUnit), (int)(height - lcv.getValue()*yUnit));
drawDot(graphics, lastLcv, xUnit, yUnit); drawDot(graphics, lastLcv, xUnit, yUnit);
} }

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.shapes.Circle; import schule.ngb.zm.shapes.Circle;
@@ -326,7 +326,7 @@ public class PieChart extends Circle {
} }
graphics.setColor(getStrokeColor().getJavaColor()); graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke()); graphics.setStroke(getStroke());
graphics.drawOval(0, 0, (int) (radius * 2), (int) (radius * 2)); graphics.drawOval(0, 0, (int) (radius * 2), (int) (radius * 2));
graphics.setTransform(originalTransform); graphics.setTransform(originalTransform);

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts; package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color; import schule.ngb.zm.Color;
import schule.ngb.zm.shapes.Circle; import schule.ngb.zm.shapes.Circle;

Some files were not shown because too many files have changed in this diff Show More