90 Commits

Author SHA1 Message Date
936043bf85 changelog 2022-12-14 20:36:30 +01:00
d75f67c0fa build tasks neu strukturiert 2022-12-14 20:36:24 +01:00
adbc29dabe Dokumentation 2022-12-14 20:36:08 +01:00
b687483e6d Versionsnummer 2022-12-14 20:35:55 +01:00
19bacd15e9 Bezir-Kurven im DrawingLayer 2022-12-14 20:35:48 +01:00
2d4abf6f0d Laden von Dock-Icon (macos) angepasst 2022-12-14 20:35:21 +01:00
cefe7c8cfa overview.html verschoben 2022-12-14 20:34:57 +01:00
b04e68c7bd Einführung erweitert 2022-12-14 20:34:32 +01:00
25ce3a35e9 Timing-Problem beim Aufruf des AudioListeners behoben 2022-12-13 10:17:25 +01:00
44d0f79c6c Changelog 2022-12-13 08:19:19 +01:00
43195aa63c Cache Methoden im ImageLoader direkt verwendet 2022-12-13 08:17:18 +01:00
6cc23de620 Dokumentation und umbenannte Methode 2022-12-13 08:14:07 +01:00
836571ca95 Merge branch 'main' into softcache 2022-12-12 21:26:57 +01:00
5232057b15 SoftCache zu Cache generalisiert
Ein Cache kann nun auch mit `WeakReference`n genutzt werden.
2022-12-12 21:26:24 +01:00
f0a3c65552 Gradle Task buildAll hinzugefügt 2022-12-11 21:50:10 +01:00
2c40e1ba31 SoftCache in ImageLoader benutzt 2022-12-11 21:49:51 +01:00
26e3593f2c SoftCache Klasse als allgemeine Cache Map
Die SoftCache Klasse implementiert eine Map, die Inhalte in SoftReference Objekte wrapped. Sie kann vor allem als Cache für Ressourcen-Objekte genutzt werden.
2022-12-11 19:58:00 +01:00
13cad69e1d Abi-NRW Klassen zum Testen eingefügt
Die Klassen werden von der Qualitäts- und UnterstützungsAgentur - Landesinstitut für Schule herausgegeben.
2022-12-11 17:32:21 +01:00
bd718ba27d Changelog 2022-12-11 16:34:45 +01:00
1895378978 Name der mp3spi jar angepasst 2022-12-11 16:34:41 +01:00
2f845bdcd9 Überschirft in overview.html angepasst 2022-12-11 16:34:30 +01:00
788ed888e9 Dokumentatione 2022-12-11 13:35:30 +01:00
ce3ffee4da Testklassen auf letzte Änderungen angepasst 2022-12-11 13:35:21 +01:00
74c85e0f61 Shape2DLayer ins Test-Paket verschoben 2022-12-11 13:35:03 +01:00
c7b2a520c4 Overview-Datei für Javadoc ergänzt 2022-12-11 13:34:40 +01:00
c5c046521b Doku und Formatierungen 2022-12-11 13:34:21 +01:00
9834b9c389 package-info.java ergänzt 2022-12-11 13:33:55 +01:00
eaaca6b90f Validator Methods erwarten nun einen Parameter
Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird. Dadurch sollen die Methoden hilfreicher werden, indem geneuere Fehlermeldungen generiert werden können.
2022-12-11 13:33:19 +01:00
1275af55f3 Dokumentation random(int) 2022-12-10 14:43:59 +01:00
632038030e main Methode entfernt 2022-12-10 13:40:15 +01:00
cec17f0d7c choice Methoden wiederhergestellt 2022-12-10 13:33:00 +01:00
b29532bf6e Dokumentation 2022-12-10 13:32:45 +01:00
d500c130ed Changelog update 2022-12-10 11:55:26 +01:00
b4d390cd9b Feheler beim buildtask der mp3-jar behoben 2022-12-10 11:55:19 +01:00
97ea610f34 Dokumentation 2022-12-10 11:14:38 +01:00
e2e1f24e3e Verison auf 0.0.34 erhöht 2022-12-10 11:11:26 +01:00
a09b956b48 Dokumentation
Ebene der Unterüberschrift auf h2 angepasst
2022-12-10 11:11:17 +01:00
c92a4517b3 Faker Klasse um Fake-Daten zu generieren 2022-12-10 11:07:54 +01:00
1260a38bb7 Imports bereinigt 2022-12-10 11:07:17 +01:00
20772da813 {@inheritDoc} Kommentare entfernt
Javadoc Kommentare, die nur {@inheritDoc} enthalten, sind redundant und wurden entfernt.
2022-12-10 11:06:51 +01:00
8898d6e8ee MKDocs Dokumentation erweitert 2022-12-10 11:05:09 +01:00
cb0ee9c842 Dokumentation 2022-12-10 11:04:48 +01:00
3cf7871591 Weitere image-Methoden ergänzt
Die neuen Methoden erlauben es, Bilder auch mit einer festen Größe auf die Zeichenebene zu zeichnen.
2022-12-09 18:01:52 +01:00
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
125 changed files with 16158 additions and 1864 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,6 +6,37 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased] ## [Unreleased]
## Added
- Dokumentation erweitert.
- Caching-Mechanismen in Klasse `util.Cache` ausgelagert.
- `util.io.ImageLoader` und `util.io.FontLoader` verwenden `Cache`.
## Changed
- Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird.
## Fixed
- `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt.
- Timing-Problem beim Aufruf von `AudioListener.playbackStopped()` in `Sound` behoben.
## Removed
- `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar.
## Version 0.0.34
### Added
- `Faker`-Klasse zur Erzeugung von Fake-Daten hinzugefügt.
- Dokumentation unter [zeichenmaschine.xyz](https://zeichenmaschine.xyz) mit
[MkDocs](https://www.mkdocs.org) und [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).
- Neue `image` methoden im `DrawingLayer`.
### 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. - System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle. - `AudioListener` und `AnimationListener` als erste Anwendungsfälle.

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

BIN
docs/assets/icon_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
docs/assets/icon_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
docs/assets/icon_512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
docs/assets/icon_64.png Normal file

Binary file not shown.

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

70
docs/einfuehrung.md Normal file
View File

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

View File

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

46
docs/installation.md Normal file
View File

@@ -0,0 +1,46 @@
# 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
## Unterstützung für MP3

128
docs/macros.py Normal file
View File

@@ -0,0 +1,128 @@
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
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](installation.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: einfuehrung.md
- Schnellstart: schnellstart.md
- Installation: installation.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,194 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.Stroke;
/**
* 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
@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;
}
}
@Override
public Paint getFill() {
if( fill != null ) {
return fill;
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
return fillColor;
} else {
return null;
}
}
@Override
public boolean hasFillColor() {
return fillColor != null;
}
@Override
public boolean hasGradient() {
return fill != null;
}
@Override
public Color getFillColor() {
return fillColor;
}
@Override
public void setFillColor( Color color ) {
fillColor = color;
fill = null;
}
@Override
public MultipleGradientPaint getGradient() {
return fill;
}
// Implementierung Strokeable Interface
@Override
public void setStroke( Stroke stroke ) {
this.stroke = stroke;
}
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight);
}
return stroke;
}
@Override
public Color getStrokeColor() {
return strokeColor;
}
@Override
public void setStrokeColor( Color color ) {
strokeColor = color;
}
@Override
public double getStrokeWeight() {
return strokeWeight;
}
@Override
public void setStrokeWeight( double weight ) {
strokeWeight = weight;
this.stroke = null;
}
@Override
public Options.StrokeType getStrokeType() {
return strokeType;
}
@Override
public void setStrokeType( Options.StrokeType type ) {
strokeType = type;
this.stroke = null;
}
}

View File

@@ -4,6 +4,10 @@ import java.awt.*;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D; import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel; 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.
@@ -14,6 +18,7 @@ import java.awt.image.ColorModel;
* 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).
*/ */
@SuppressWarnings( "unused" )
public class Color implements Paint { public class Color implements Paint {
@@ -47,38 +52,67 @@ public class Color implements Paint {
* 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.
*/ */
@@ -88,6 +122,7 @@ public class Color implements Paint {
* 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.
*/ */
@@ -107,7 +142,7 @@ public class Color implements Paint {
} }
/** /**
* 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.
*/ */
@@ -116,8 +151,8 @@ public class Color implements Paint {
} }
/** /**
* 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.
*/ */
@@ -126,9 +161,9 @@ public class Color implements Paint {
} }
/** /**
* 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.
@@ -139,11 +174,11 @@ public class Color implements Paint {
} }
/** /**
* 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.
@@ -151,24 +186,24 @@ public class Color implements Paint {
* @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);
@@ -203,46 +238,97 @@ public class Color implements Paint {
/** /**
* 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("#") ) {
@@ -259,8 +345,6 @@ public class Color implements Paint {
} 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));
@@ -281,7 +365,7 @@ public class Color implements Paint {
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 ) {
@@ -307,12 +391,12 @@ public class Color implements Paint {
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_;
@@ -331,6 +415,14 @@ public class Color implements Paint {
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);
} }
@@ -339,9 +431,9 @@ public class Color implements Paint {
* 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.
@@ -484,16 +576,17 @@ public class Color implements Paint {
} }
} }
@Override
/** /**
* 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 ) { if( obj == null ) {
return false; return false;

View File

@@ -1,26 +1,29 @@
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.io.ImageLoader;
import schule.ngb.zm.util.Noise; import schule.ngb.zm.util.Noise;
import schule.ngb.zm.util.io.ImageLoader;
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 einfacher im
* Programm nutzen zu können. * Programm nutzen zu können.
* <pre><code> * <pre><code>
* class MyClass extends Constants { * class MyClass extends Constants {
@@ -64,13 +67,49 @@ public class Constants {
/** /**
* Patchversion der Zeichenmaschine. * Patchversion der Zeichenmaschine.
*/ */
public static final int APP_VERSION_REV = 22; public static final int APP_VERSION_REV = 34;
/** /**
* 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.
@@ -405,6 +444,21 @@ 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
@@ -731,6 +785,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.
* *
@@ -755,6 +833,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.
* *
@@ -1000,7 +1102,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)}.
@@ -1010,7 +1112,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)}.
@@ -1150,6 +1252,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.
*/ */
@@ -1212,7 +1320,8 @@ public class Constants {
} }
/** /**
* Erzeugt eine ganze Pseudozufallszahl zwischen {@code 0} und {@code max}. * Erzeugt eine ganze Pseudozufallszahl zwischen {@code 0} und {@code max}
* (einschließlich der Grenzen).
* *
* @param max Obere Grenze. * @param max Obere Grenze.
* @return Eine Zufallszahl. * @return Eine Zufallszahl.
@@ -1223,7 +1332,7 @@ public class Constants {
/** /**
* Erzeugt eine ganze Pseudozufallsganzzahl zwischen {@code min} und * Erzeugt eine ganze Pseudozufallsganzzahl zwischen {@code min} und
* {@code max}. * {@code max} (einschließlich der Grenzen).
* *
* @param min Untere Grenze. * @param min Untere Grenze.
* @param max Obere Grenze. * @param max Obere Grenze.
@@ -1280,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.
* *
@@ -1287,7 +1416,8 @@ 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 ) { @SafeVarargs
public static final <T> T choice( T... values ) {
return values[random(0, values.length - 1)]; return values[random(0, values.length - 1)];
} }
@@ -1296,15 +1426,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);
} }
/** /**
@@ -1473,38 +1737,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);
@@ -1513,38 +1832,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);
} }
@@ -1605,7 +1979,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

@@ -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

@@ -12,10 +12,10 @@ import java.awt.image.BufferedImage;
* Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die * Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte * übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken. * der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
* * <p>
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer {@link Zeichenmaschine}. * Ebenen sind ein zentraler Bestandteil bei der Implementierung einer
* Es werden * {@link Zeichenmaschine}. Sie erben von {@code Constants}, da neue Ebenentypen
* Sie erben von {@code Constants}, damit sie beim * von Nutzern implementiert werden können.
*/ */
public abstract class Layer extends Constants implements Drawable, Updatable { public abstract class Layer extends Constants implements Drawable, Updatable {
@@ -43,24 +43,39 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
protected boolean active = true; protected boolean active = true;
/**
* Erstellt eine neue Ebene mit den Standardmaßen.
*/
public Layer() { public Layer() {
this(DEFAULT_WIDTH, DEFAULT_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 angegebene Größe. * Ändert die Größe der Ebene auf die angegebenen Maße.
* *
* @param width Die neue Breite. * @param width Die neue Breite.
* @param height Die neue Höhe. * @param height Die neue Höhe.
@@ -73,11 +88,15 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
} else { } else {
createCanvas(width, height); createCanvas(width, height);
} }
}
// TODO: prevent access to graphics context?
public Graphics2D getGraphics() {
return this.drawing;
} }
/** /**
* Gibt die Resourcen der Ebene frei. * Gibt die Ressourcen der Ebene frei.
*/ */
public void dispose() { public void dispose() {
drawing.dispose(); drawing.dispose();
@@ -88,7 +107,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
/** /**
* 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 ) {
@@ -115,7 +134,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 ) {
@@ -139,14 +158,14 @@ 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);
} }
} }
@@ -155,14 +174,26 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
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;
} }
@@ -176,4 +207,22 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
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,24 +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,
/**
* 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, 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, DELAYED,
/**
* Die Zeichenmaschine sendet gereade gesammelte Events und führt Tasks
* aus.
*/
DISPATCHING 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),
@@ -88,31 +218,117 @@ 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

@@ -108,7 +108,7 @@ public class Spielemaschine extends Zeichenmaschine {
} }
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D graphics ) {
clear(); clear();
List<Drawable> it = List.copyOf(drawables); List<Drawable> it = List.copyOf(drawables);
for( Drawable d: it ) { for( Drawable d: it ) {
@@ -116,7 +116,7 @@ public class Spielemaschine extends Zeichenmaschine {
d.draw(drawing); d.draw(drawing);
} }
} }
super.draw(pGraphics); super.draw(graphics);
} }
} }

View File

@@ -1,51 +1,68 @@
package schule.ngb.zm.shapes; package schule.ngb.zm;
import schule.ngb.zm.Color; import schule.ngb.zm.shapes.Shape;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import java.awt.BasicStroke; import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Stroke; 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 {
/** /**
* Basisklasse für Formen, die eine Konturlinie besitzen. * Setzt den {@code Stroke} für die Konturlinie direkt.
*
* @param stroke Ein {@code Stroke}-Objekt.
*/ */
public abstract class StrokedShape extends Constants implements Drawable { void setStroke( Stroke stroke );
/** /**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn die Form ohne * Gibt ein {@code Stroke}-Objekt mit den aktuell gesetzten Eigenschaften
* kontur dargestellt werden soll. * zurück.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/ */
protected Color strokeColor = DEFAULT_STROKECOLOR; Stroke getStroke();
/** /**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0. * 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.
*/ */
protected double strokeWeight = DEFAULT_STROKEWEIGHT; default boolean hasStroke() {
Color strokeColor = getStrokeColor();
/** double strokeWeight = getStrokeWeight();
* Die Art der Konturlinie. return strokeColor != null && strokeColor.getAlpha() > 0 && strokeWeight > 0;
*/ }
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;
/** /**
* Gibt die aktuelle Farbe der Konturlinie zurück. * Gibt die aktuelle Farbe der Konturlinie zurück.
* *
* @return Die Konturfarbe oder {@code null}. * @return Die Konturfarbe oder {@code null}.
*/ */
public Color getStrokeColor() { Color getStrokeColor();
return strokeColor;
}
/** /**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe. * Setzt die Farbe der Konturlinie auf die angegebene Farbe.
@@ -53,9 +70,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param color Die neue Farbe der Konturlinie. * @param color Die neue Farbe der Konturlinie.
* @see Color * @see Color
*/ */
public void setStrokeColor( Color color ) { void setStrokeColor( Color color );
this.strokeColor = color;
}
/** /**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die * Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die
@@ -66,7 +81,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param alpha Ein Transparenzwert zwischen 0 und 255. * @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int) * @see Color#Color(Color, int)
*/ */
public void setStrokeColor( Color color, int alpha ) { default void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha)); setStrokeColor(new Color(color, alpha));
} }
@@ -77,7 +92,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param gray Ein Grauwert zwischen 0 und 255. * @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int) * @see Color#Color(int)
*/ */
public void setStrokeColor( int gray ) { default void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255); setStrokeColor(gray, gray, gray, 255);
} }
@@ -90,7 +105,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param alpha Ein Transparenzwert zwischen 0 und 255. * @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int) * @see Color#Color(int, int)
*/ */
public void setStrokeColor( int gray, int alpha ) { default void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha); setStrokeColor(gray, gray, gray, alpha);
} }
@@ -105,7 +120,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <a * @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a> * href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/ */
public void setStrokeColor( int red, int green, int blue ) { default void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255); setStrokeColor(red, green, blue, 255);
} }
@@ -121,14 +136,14 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <a * @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a> * href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/ */
public void setStrokeColor( int red, int green, int blue, int alpha ) { default void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha)); setStrokeColor(new Color(red, green, blue, alpha));
} }
/** /**
* Entfernt die Kontur der Form. * Entfernt die Kontur der Form.
*/ */
public void noStroke() { default void noStroke() {
setStrokeColor(null); setStrokeColor(null);
} }
@@ -139,10 +154,10 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT * @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT
* @see schule.ngb.zm.Constants#SOLID * @see schule.ngb.zm.Constants#SOLID
*/ */
public void resetStroke() { default void resetStroke() {
setStrokeColor(DEFAULT_STROKECOLOR); setStrokeColor(Constants.DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT); setStrokeWeight(Constants.DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID); setStrokeType(Constants.SOLID);
} }
/** /**
@@ -150,9 +165,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* *
* @return Die aktuelle Dicke der Linie. * @return Die aktuelle Dicke der Linie.
*/ */
public double getStrokeWeight() { double getStrokeWeight();
return strokeWeight;
}
/** /**
* Setzt die Dicke der Konturlinie. Die Dicke muss größer 0 sein. Wird 0 * Setzt die Dicke der Konturlinie. Die Dicke muss größer 0 sein. Wird 0
@@ -160,9 +173,8 @@ public abstract class StrokedShape extends Constants implements Drawable {
* *
* @param weight Die Dicke der Konturlinie. * @param weight Die Dicke der Konturlinie.
*/ */
public void setStrokeWeight( double weight ) { default void setStrokeWeight( double weight ) {
this.strokeWeight = max(0.0, weight); setStroke(createStroke(getStrokeType(), weight));
this.stroke = null;
} }
/** /**
@@ -171,78 +183,48 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @return Die aktuelle Art der Konturlinie. * @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType * @see Options.StrokeType
*/ */
public Options.StrokeType getStrokeType() { Options.StrokeType getStrokeType();
return strokeType;
}
/** /**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED}, * Setzt den Typ der Kontur. Erlaubte Werte sind {@link Constants#DASHED},
* {@link #DOTTED} und {@link #SOLID}. * {@link Constants#DOTTED} und {@link Constants#SOLID}.
* *
* @param type Eine der möglichen Konturarten. * @param type Eine der möglichen Konturarten.
* @see Options.StrokeType * @see Options.StrokeType
*/ */
public void setStrokeType( Options.StrokeType type ) { default void setStrokeType( Options.StrokeType type ) {
this.strokeType = type; setStroke(createStroke(type, getStrokeWeight()));
this.stroke = null;
} }
@Override
public abstract void draw( Graphics2D graphics );
/** /**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen * Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird * Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
* zwischengespeichert. * zwischengespeichert.
* *
* @param strokeType
* @param strokeWeight
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften. * @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/ */
protected Stroke createStroke() { static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight ) {
// TODO: Used global cached Stroke Objects?
if( stroke == null ) {
switch( strokeType ) { switch( strokeType ) {
case DOTTED: case DOTTED:
stroke = new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f); 10.0f, new float[]{1.0f, 5.0f}, 0.0f);
break;
case DASHED: case DASHED:
stroke = new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f); 10.0f, new float[]{5.0f}, 0.0f);
break;
case SOLID: case SOLID:
default: default:
stroke = new BasicStroke( return new BasicStroke(
(float) strokeWeight, (float) strokeWeight,
BasicStroke.CAP_ROUND, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND); BasicStroke.JOIN_ROUND);
break;
}
}
return stroke;
}
/**
* 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(createStroke());
graphics.draw(shape);
} }
} }

View File

@@ -28,7 +28,7 @@ public interface Updatable {
* @return {@code true}, wenn das Objekt aktiv ist, {@code false} * @return {@code true}, wenn das Objekt aktiv ist, {@code false}
* andernfalls. * andernfalls.
*/ */
public boolean isActive(); boolean isActive();
/** /**
* Änderung des Zustandes des Objekts abhängig vom Zeitintervall * Änderung des Zustandes des Objekts abhängig vom Zeitintervall
@@ -39,6 +39,6 @@ public interface Updatable {
* *
* @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

@@ -2,6 +2,7 @@ package schule.ngb.zm;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
@@ -9,8 +10,8 @@ import java.awt.*;
import java.awt.event.KeyAdapter; import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent; import java.awt.event.KeyEvent;
import java.awt.event.KeyListener; import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
@@ -24,6 +25,12 @@ import java.util.ArrayList;
*/ */
public class Zeichenfenster extends JFrame { 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() { public static final void setLookAndFeel() {
try { try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
@@ -32,6 +39,17 @@ public class Zeichenfenster extends JFrame {
} }
} }
/**
* 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() { public static final GraphicsDevice getGraphicsDevice() {
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um // Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren. // das Zeichenfenster dort zu zentrieren.
@@ -54,14 +72,14 @@ public class Zeichenfenster extends JFrame {
} }
/** /**
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich dem * Das Anzeigegerät, auf dem die Zeichenmaschine gestartet wurde (muss nicht
* Aktuellen sein, wenn das Fenster verschoben wurde). * gleich dem Aktuellen sein, wenn das Fenster verschoben wurde).
*/ */
private GraphicsDevice displayDevice; private final GraphicsDevice displayDevice;
/** /**
* Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat * Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat es
* es Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter * Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine * Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine
* Garantie für die Größe der Leinwand übernehmen. * Garantie für die Größe der Leinwand übernehmen.
*/ */
@@ -78,40 +96,76 @@ public class Zeichenfenster extends JFrame {
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch * verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt. * hinzugefügt und entfernt.
*/ */
private KeyListener fullscreenExitListener = new KeyAdapter() { private final KeyListener fullscreenExitListener = new KeyAdapter() {
@Override @Override
public void keyPressed( KeyEvent e ) { public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) { if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this); // canvas.removeKeyListener(this);
setFullscreen(false); setFullscreen(false);
e.consume();
} }
} }
}; };
private Zeichenleinwand canvas; // 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 ) { public Zeichenfenster( int width, int height, String title ) {
this(new Zeichenleinwand(width, height), title, getGraphicsDevice()); 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 ) { public Zeichenfenster( int width, int height, String title, GraphicsDevice displayDevice ) {
this(new Zeichenleinwand(width, height), title, 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 ) { public Zeichenfenster( Zeichenleinwand canvas, String title ) {
this(canvas, title, getGraphicsDevice()); 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 ) { public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
super(Validator.requireNotNull(displayDevice).getDefaultConfiguration()); super(Validator.requireNotNull(displayDevice, "displayDevice").getDefaultConfiguration());
this.displayDevice = displayDevice; this.displayDevice = displayDevice;
Validator.requireNotNull(canvas, "Every Zeichenfenster needs a Zeichenleinwand, but got <null>."); Validator.requireNotNull(canvas, "Every Zeichenfenster needs a Zeichenleinwand, but got <null>.");
this.canvasPreferredWidth = canvas.getWidth(); this.canvasPreferredWidth = canvas.getWidth();
this.canvasPreferredHeight = canvas.getHeight(); this.canvasPreferredHeight = canvas.getHeight();
//this.add(canvas, BorderLayout.CENTER); this.add(canvas, BorderLayout.CENTER);
this.add(canvas);
this.canvas = canvas; this.canvas = canvas;
// Konfiguration des Frames // Konfiguration des Frames
@@ -122,19 +176,30 @@ public class Zeichenfenster extends JFrame {
// Das Icon des Fensters ändern // Das Icon des Fensters ändern
try { try {
if( Zeichenmaschine.MACOS ) { if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png"); InputStream iconStream = this.getClass().getResourceAsStream("icon_512.png");
Image icon = ImageIO.read(iconUrl); if( iconStream != null ) {
Image icon = ImageIO.read(iconStream);
// Dock Icon in macOS setzen // Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar(); Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon); taskbar.setIconImage(icon);
} else {
LOG.warn("Could not load dock-icon");
}
} else { } else {
ArrayList<Image> icons = new ArrayList<>(4); ArrayList<Image> icons = new ArrayList<>(4);
for( int size = 32; size <= 512; size *= size ) { for( int size : new int[]{32, 64, 128, 512} ) {
icons.add(ImageIO.read(new File("icon_" + size + ".png"))); URL icnUrl = Zeichenmaschine.class.getResource("icon_" + size + ".png");
if( icnUrl != null ) {
icons.add(ImageIO.read(icnUrl));
}
} }
if( icons.isEmpty() ) {
LOG.warn("Could not load dock-icon");
} else {
this.setIconImages(icons); this.setIconImages(icons);
} }
}
} catch( IllegalArgumentException | IOException e ) { } catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage()); LOG.warn("Could not load image icons: %s", e.getMessage());
} catch( SecurityException | UnsupportedOperationException macex ) { } catch( SecurityException | UnsupportedOperationException macex ) {
@@ -144,25 +209,54 @@ public class Zeichenfenster extends JFrame {
// Fenster zusammenbauen, auf dem Bildschirm positionieren ... // Fenster zusammenbauen, auf dem Bildschirm positionieren ...
this.pack(); this.pack();
this.setResizable(false); this.setResizable(false);
this.setFocusable(true);
this.setLocationByPlatform(true); this.setLocationByPlatform(true);
// this.centerFrame(); // 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() { public GraphicsDevice getDisplayDevice() {
return displayDevice; return displayDevice;
} }
/**
* Liefert die Abmessungen des Anzeigegeräts, auf dem das Fenster gestartet
* wurde.
*
* @return Die Abmessungen des Anzeigegeräts.
*/
public Rectangle getScreenBounds() { public Rectangle getScreenBounds() {
// return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return displayDevice.getDefaultConfiguration().getBounds(); return displayDevice.getDefaultConfiguration().getBounds();
} }
/**
* Liefert die Zeichenleinwand dieses Fensters.
*
* @return Die Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public Zeichenleinwand getCanvas() { public Zeichenleinwand getCanvas() {
return canvas; return canvas;
} }
/**
* Liefert die Abmessungen der Zeichenleinwand zurück.
*
* @return Die Abmessungen der Zeichenleinwand.
*/
public Rectangle getCanvasBounds() { public Rectangle getCanvasBounds() {
return canvas.getBounds(); return canvas.getBounds();
} }
/** /**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm. * Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/ */
@@ -175,8 +269,14 @@ public class Zeichenfenster extends JFrame {
); );
} }
/**
* 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 ) { public void setCanvasSize( int newWidth, int newHeight ) {
// TODO: (ngb) Put constains on max/min frame/canvas size // TODO: (ngb) Put constrains on max/min frame/canvas size
if( fullscreen ) { if( fullscreen ) {
canvasPreferredWidth = newWidth; canvasPreferredWidth = newWidth;
canvasPreferredHeight = newHeight; canvasPreferredHeight = newHeight;
@@ -205,30 +305,62 @@ public class Zeichenfenster extends JFrame {
public final void setFullscreen( boolean pEnable ) { public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html // See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) { 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 ) { if( pEnable && !fullscreen ) {
// frame.setUndecorated(true); // Activate fullscreen
this.setResizable(false); // Should be set anyway dispose();
setUndecorated(true);
setResizable(false);
displayDevice.setFullScreenWindow(this); displayDevice.setFullScreenWindow(this);
java.awt.Rectangle bounds = getScreenBounds();
// TODO: (ngb) We need to switch layouts to allow the LayoutManger to maximize the canvas
canvas.setSize(bounds.width, bounds.height);
// Register ESC to exit fullscreen // Register ESC to exit fullscreen
canvas.addKeyListener(fullscreenExitListener); 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; fullscreen = true;
} else if( !pEnable && fullscreen ) { } else if( !pEnable && fullscreen ) {
fullscreen = false; displayDevice.setFullScreenWindow(null);
dispose();
setUndecorated(false);
setResizable(false);
canvas.removeKeyListener(fullscreenExitListener); canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
canvas.setSize(canvasPreferredWidth, canvasPreferredHeight); canvas.setSize(canvasPreferredWidth, canvasPreferredHeight);
this.pack();
// frame.setUndecorated(false); 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() { public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow(); Window win = displayDevice.getFullScreenWindow();
return fullscreen && win.equals(this); return fullscreen && win.equals(this);

View File

@@ -1,16 +1,16 @@
package schule.ngb.zm; package schule.ngb.zm;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import java.awt.*; import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
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; import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/** /**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus * Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -24,10 +24,14 @@ import java.util.concurrent.LinkedBlockingQueue;
*/ */
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.
@@ -41,11 +45,49 @@ public class Zeichenleinwand extends Canvas {
this.setMinimumSize(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;
} }
/** /**
@@ -78,26 +120,23 @@ 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);
@@ -106,7 +145,6 @@ public class Zeichenleinwand extends Canvas {
} }
} }
} }
}
/** /**
* Gibt die Anzahl der {@link Layer Ebenen} in dieser Leinwand zurück. * Gibt die Anzahl der {@link Layer Ebenen} in dieser Leinwand zurück.
@@ -118,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 ) {
@@ -145,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 ) {
synchronized( layers ) {
for( Layer layer : layers ) { for( Layer layer : layers ) {
if( layer.getClass().equals(clazz) ) { if( layer.getClass().equals(type) ) {
return clazz.cast(layer); return type.cast(layer);
}
} }
} }
return null; return null;
@@ -163,44 +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());
synchronized( layers ) {
for( Layer layer : layers ) { for( Layer layer : layers ) {
if( layer.getClass().equals(pClazz) ) { if( layer.getClass().equals(type) ) {
result.add(pClazz.cast(layer)); 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 ) {
List<Layer> it = List.copyOf(layers); for( Layer layer : List.copyOf(layers) ) {
for( Layer layer : it ) {
layer.update(delta); layer.update(delta);
} }
} }
@@ -230,15 +293,14 @@ 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();
synchronized( layers ) { synchronized( layers ) {
List<Layer> it = List.copyOf(layers); for( Layer layer : layers ) {
for( Layer layer : it ) {
layer.draw(g2d); layer.draw(g2d);
} }
} }
@@ -249,11 +311,13 @@ 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( !suspended && isDisplayable() ) {
if( getBufferStrategy() == null ) { if( getBufferStrategy() == null ) {
allocateBuffer(); allocateBuffer();
} }
if( isDisplayable() ) { synchronized( renderLock ) {
rendering = true;
BufferStrategy strategy = this.getBufferStrategy(); BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) { if( strategy != null ) {
do { do {
@@ -262,8 +326,7 @@ public class Zeichenleinwand extends Canvas {
g2d.clearRect(0, 0, getWidth(), getHeight()); g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) { synchronized( layers ) {
List<Layer> it = List.copyOf(layers); for( Layer layer : List.copyOf(layers) ) {
for( Layer layer : it ) {
layer.draw(g2d); layer.draw(g2d);
} }
} }
@@ -281,6 +344,9 @@ public class Zeichenleinwand extends Canvas {
// Repeat the rendering if the drawing buffer was lost // Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() ); } while( strategy.contentsLost() );
} }
rendering = false;
renderLock.notifyAll();
}
} }
} }

View File

@@ -17,6 +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.concurrent.atomic.AtomicInteger;
/** /**
* Hauptklasse der Zeichenmaschine. * Hauptklasse der Zeichenmaschine.
@@ -37,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.
*/ */
@@ -111,14 +75,14 @@ public class Zeichenmaschine extends Constants {
/** /**
* Das Zeichenfenster der Zeichenmaschine * Das Zeichenfenster der Zeichenmaschine
*/ */
private Zeichenfenster frame; protected Zeichenfenster frame;
// 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
@@ -128,7 +92,7 @@ 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; private boolean terminateImediately = false;
@@ -141,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.
@@ -151,17 +115,17 @@ 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,
@@ -274,9 +238,15 @@ 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);
// Register Cmd+Q on macOS
if( Constants.MACOS ) {
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
}
// Erstellen der Leinwand // Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height); canvas = new Zeichenleinwand(width, height);
@@ -312,8 +282,15 @@ 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
// TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern
frame.addWindowListener(new WindowAdapter() { frame.addWindowListener(new WindowAdapter() {
@Override @Override
public void windowClosing( WindowEvent e ) { public void windowClosing( WindowEvent e ) {
@@ -350,17 +327,15 @@ public class Zeichenmaschine extends Constants {
* *
* @param title * @param title
*/ */
private final Zeichenfenster createFrame( Zeichenleinwand c, String title ) { private Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
while( frame == null ) { while( frame == null ) {
try { try {
TaskRunner.invokeLater(new Runnable() { TaskRunner.invokeLater(() -> {
@Override
public void run() {
Zeichenfenster.setLookAndFeel(); Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title); frame = new Zeichenfenster(canvas, title);
}
}).get(); }).get();
} catch( InterruptedException e ) { } catch( InterruptedException e ) {
// Keep waiting
} catch( ExecutionException e ) { } catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage()); LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -392,10 +367,18 @@ public class Zeichenmaschine extends Constants {
public final void setFullscreen( boolean pEnable ) { public final void setFullscreen( boolean pEnable ) {
if( pEnable && !frame.isFullscreen() ) { if( pEnable && !frame.isFullscreen() ) {
frame.setFullscreen(true); frame.setFullscreen(true);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( frame.isFullscreen() ) if( frame.isFullscreen() )
fullscreenChanged(); fullscreenChanged();
} else if( !pEnable && frame.isFullscreen() ) { } else if( !pEnable && frame.isFullscreen() ) {
frame.setFullscreen(false); frame.setFullscreen(false);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( !frame.isFullscreen() ) if( !frame.isFullscreen() )
fullscreenChanged(); fullscreenChanged();
} }
@@ -444,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();
@@ -546,6 +538,11 @@ public class Zeichenmaschine extends Constants {
} }
public final void exitNow() { public final void exitNow() {
// Do nothing, when already quitting
if( state == Options.AppState.QUITING ) {
return;
}
if( running ) { if( running ) {
running = false; running = false;
terminateImediately = true; terminateImediately = true;
@@ -584,6 +581,8 @@ public class Zeichenmaschine extends Constants {
* @see System#exit(int) * @see System#exit(int)
*/ */
public final void quit( boolean exit ) { public final void quit( boolean exit ) {
state = Options.AppState.QUITING;
TaskRunner.invokeLater(() -> {
frame.setVisible(false); frame.setVisible(false);
canvas.dispose(); canvas.dispose();
frame.dispose(); frame.dispose();
@@ -591,6 +590,7 @@ public class Zeichenmaschine extends Constants {
if( exit ) { if( exit ) {
System.exit(0); System.exit(0);
} }
});
} }
/** /**
@@ -601,11 +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 ) {
frame.setSize(width, height); frame.setCanvasSize(width, height);
java.awt.Rectangle canvasBounds = frame.getCanvasBounds(); java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
canvasWidth = (int) canvasBounds.getWidth(); canvasWidth = canvasBounds.width;
canvasHeight = (int) canvasBounds.getHeight(); canvasHeight = canvasBounds.height;
} }
/** /**
@@ -679,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>
@@ -797,6 +797,7 @@ public class Zeichenmaschine extends Constants {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight()); BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics(); Graphics2D g = img.createGraphics();
// TODO: Transparente Hintergründe beim Speichern von png erlauben
g.setColor(DEFAULT_BACKGROUND.getJavaColor()); g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight()); g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g); canvas.draw(g);
@@ -947,9 +948,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
@@ -1022,9 +1023,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
*/ */
@@ -1100,6 +1101,9 @@ public class Zeichenmaschine extends Constants {
} }
if( isPaused() || isTerminated() ) { if( isPaused() || isTerminated() ) {
if( MouseEvent.class.isInstance(evt) ) {
saveMousePosition((MouseEvent)evt);
}
dispatchEvents(); dispatchEvents();
} }
} }
@@ -1327,7 +1331,7 @@ public class Zeichenmaschine extends Constants {
long overslept = 0L; long overslept = 0L;
// Interne Zähler für tick und runtime // Interne Zähler für tick und runtime
int _tick = 0; int _tick = 0;
long _runtime = 0; long _runtime;
// Öffentliche Zähler für Unterklassen // Öffentliche Zähler für Unterklassen
tick = 0; tick = 0;
runtime = 0; runtime = 0;
@@ -1390,7 +1394,7 @@ public class Zeichenmaschine extends Constants {
} }
// 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();
@@ -1444,7 +1448,11 @@ public class Zeichenmaschine extends Constants {
Thread.yield(); Thread.yield();
} }
updateThreadExecutor.shutdownNow(); updateThreadExecutor.shutdownNow();
try {
updateThreadExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
} finally {
// Cleanup // Cleanup
shutdown(); shutdown();
cleanup(); cleanup();
@@ -1454,11 +1462,12 @@ public class Zeichenmaschine extends Constants {
quit(); quit();
} }
} }
}
} }
// TODO: Remove // TODO: Remove
class DelayedTask implements Delayed { static class DelayedTask implements Delayed {
long startTime; // in ms long startTime; // in ms
@@ -1547,6 +1556,7 @@ 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;
@@ -1555,7 +1565,18 @@ public class Zeichenmaschine extends Constants {
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;
} }

View File

@@ -24,7 +24,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
public Animation( DoubleUnaryOperator easing ) { public Animation( DoubleUnaryOperator easing ) {
this.runtime = Constants.DEFAULT_ANIM_RUNTIME; this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
this.easing = Validator.requireNotNull(easing); this.easing = Validator.requireNotNull(easing, "easing");
} }
public Animation( int runtime ) { public Animation( int runtime ) {
@@ -34,7 +34,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
public Animation( int runtime, DoubleUnaryOperator easing ) { public Animation( int runtime, DoubleUnaryOperator easing ) {
this.runtime = runtime; this.runtime = runtime;
this.easing = Validator.requireNotNull(easing); this.easing = Validator.requireNotNull(easing, "easing");
} }
public int getRuntime() { public int getRuntime() {

View File

@@ -10,7 +10,7 @@ public class AnimationFacade<S> extends Animation<S> {
public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) { public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing); super(runtime, easing);
this.anim = Validator.requireNotNull(anim); this.anim = Validator.requireNotNull(anim, "anim");
} }
@Override @Override

View File

@@ -9,7 +9,6 @@ public class AnimationGroup<T> extends Animation<T> {
List<Animation<T>> anims; List<Animation<T>> anims;
private boolean overrideEasing = false; private boolean overrideEasing = false;
private int overrideRuntime = -1; private int overrideRuntime = -1;

View File

@@ -123,8 +123,8 @@ 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, "target");
Validator.requireNotNull(propSetter); Validator.requireNotNull(propSetter, "propSetter");
return play(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e))); return play(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
} }

View File

@@ -0,0 +1,12 @@
/**
* Dieses Paket enthält Klassen zur Animation von
* {@link schule.ngb.zm.shapes.Shape} Objekten auf einem
* {@link schule.ngb.zm.layers.ShapesLayer}.
* <p>
* Mit den Animationsklassen lassen sich neben {@code Shape} Objekten aber auch
* andere Objekte animieren.
* <p>
* Das Paket setzt auf den funktionalen Programmierschnittstellen von Java auf
* und kann als Einführung in das Paradigma dienen.
*/
package schule.ngb.zm.anim;

View File

@@ -11,9 +11,13 @@ import java.awt.RadialGradientPaint;
/** /**
* Eine Ebene, die nur aus einer Farbe (oder einem Farbverlauf) besteht. * Eine Ebene, die nur aus einer Farbe (oder einem Farbverlauf) besteht.
* <p> * <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 * Die Farbe der Ebene kann beliebig gesetzt werden und kann gut als
* Hintergundfarbe für eine Szene dienen, oder als halbtransparente "Abdeckung", * Hintergrundfarbe für eine Szene dienen, oder als halbtransparente
* wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt wird. * "Abdeckung", wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt
* wird.
*/ */
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public class ColorLayer extends Layer { public class ColorLayer extends Layer {
@@ -53,9 +57,6 @@ public class ColorLayer extends Layer {
clear(); clear();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setSize( int width, int height ) { public void setSize( int width, int height ) {
super.setSize(width, height); super.setSize(width, height);
@@ -63,16 +64,14 @@ public class ColorLayer extends Layer {
} }
/** /**
* Gibt die Hintergrundfarbe der Ebene zurück. * @return Die aktuelle Hintergrundfarbe der Ebene.
*
* @return Die aktuelle Hintergrundfarbe.
*/ */
public Color getColor() { public Color getColor() {
return color; return color;
} }
/** /**
* Setzt die Farbe der Ebene neu. * Setzt die Farbe der Ebene auf die angegebene Farbe.
* *
* @param color Die neue Hintergrundfarbe. * @param color Die neue Hintergrundfarbe.
*/ */
@@ -82,22 +81,69 @@ public class ColorLayer extends Layer {
clear(); 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 ) { public void setColor( int gray ) {
setColor(gray, gray, gray, 255); 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 ) { public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, 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 ) { public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255); 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 ) { public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, 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 ) { public void setGradient( Color from, Color to, Options.Direction dir ) {
double halfW = getWidth() * .5; double halfW = getWidth() * .5;
double halfH = getHeight() * .5; double halfH = getHeight() * .5;
@@ -112,6 +158,18 @@ public class ColorLayer extends Layer {
setGradient(fromX, fromY, from, toX, toY, to); 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 ) { public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
this.color = from; this.color = from;
background = new GradientPaint( background = new GradientPaint(
@@ -121,10 +179,30 @@ public class ColorLayer extends Layer {
clear(); 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 ) { public void setGradient( Color from, Color to ) {
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, 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 ) { public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
this.color = from; this.color = from;
background = new RadialGradientPaint( background = new RadialGradientPaint(
@@ -134,6 +212,9 @@ public class ColorLayer extends Layer {
clear(); clear();
} }
/**
* Zeichnet den Hintergrund der Ebene mit der gesetzten Füllung neu.
*/
@Override @Override
public void clear() { public void clear() {
drawing.setPaint(background); drawing.setPaint(background);

View File

@@ -8,55 +8,99 @@ import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; 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 { public class DrawableLayer extends Layer {
protected List<Drawable> drawables = new LinkedList<>(); /**
* 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; protected boolean clearBeforeDraw = true;
/**
* Erstellt eine Ebene in der Standardgröße.
*/
public DrawableLayer() { 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 ) { public DrawableLayer( int width, int height ) {
super(width, 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 ) { public void add( Drawable... drawables ) {
synchronized( drawables ) { synchronized( this.drawables ) {
for( Drawable d : drawables ) { Collections.addAll(this.drawables, drawables);
this.drawables.add(d);
}
} }
} }
/**
* Gibt eine Liste aller {@code Drawable} Objekte dieser Ebene zurück.
*
* @return Die Liste der {@code Drawable} Objekte.
*/
public java.util.List<Drawable> getDrawables() { public java.util.List<Drawable> getDrawables() {
return drawables; 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() { public boolean isClearBeforeDraw() {
return clearBeforeDraw; 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 ) { public void setClearBeforeDraw( boolean pClearBeforeDraw ) {
this.clearBeforeDraw = pClearBeforeDraw; this.clearBeforeDraw = pClearBeforeDraw;
} }
@Override @Override
public void draw( Graphics2D pGraphics ) { public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) { if( clearBeforeDraw ) {
clear(); clear();
} }
synchronized( drawables ) {
List<Drawable> it = List.copyOf(drawables); List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) { for( Drawable d : it ) {
if( d.isVisible() ) { if( d.isVisible() ) {
d.draw(drawing); d.draw(drawing);
} }
} }
}
super.draw(pGraphics); super.draw(graphics);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,126 @@
package schule.ngb.zm.layers; package schule.ngb.zm.layers;
import java.awt.Graphics2D;
import java.awt.Image;
import schule.ngb.zm.Layer; import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader; 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 { public class ImageLayer extends Layer {
/**
* Das Bild, das angezeigt wird.
*/
protected Image image; protected Image image;
/**
* x-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double x = 0; protected double x = 0;
/**
* y-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double y = 0; protected double y = 0;
/**
* Interner Schalter, ob die Ebene neu gezeichnet werden muss.
*/
protected boolean redraw = true; 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 ) { public ImageLayer( String source ) {
image = ImageLoader.loadImage(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 ) { public ImageLayer( Image image ) {
this.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 ) { public ImageLayer( int width, int height, Image image ) {
super(width, height); super(width, height);
this.image = image; this.image = image;
} }
/**
* Setzt das Bild der Ebene auf das angegebene Bild-Objekt.
*
* @param image Ein Bild-Objekt.
*/
public void setImage( Image image ) { public void setImage( Image image ) {
this.image = image; this.image = image;
redraw = true; redraw = true;
} }
/**
* @return Die x-Koordinate des Bildes in der Ebene.
*/
public double getX() { public double getX() {
return x; 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 ) { public void setX( double pX ) {
this.x = pX; this.x = pX;
redraw = true; redraw = true;
} }
/**
* @return Die y-Koordinate des Bildes in der Ebene.
*/
public double getY() { public double getY() {
return y; 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 ) { public void setY( double pY ) {
this.y = pY; this.y = pY;
redraw = true; 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 @Override
public void clear() { public void clear() {
super.clear(); super.clear();

View File

@@ -10,13 +10,23 @@ import java.awt.Graphics2D;
import java.util.*; import java.util.*;
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();
@@ -37,7 +47,7 @@ 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;
@@ -51,7 +61,7 @@ public class ShapesLayer extends Layer {
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;
@@ -59,17 +69,13 @@ public class ShapesLayer extends Layer {
public void add( Shape... shapes ) { public void add( Shape... shapes ) {
synchronized( this.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( this.shapes ) { synchronized( this.shapes ) {
for( Shape s : shapes ) { this.shapes.addAll(shapes);
this.shapes.add(s);
}
} }
} }
@@ -83,9 +89,7 @@ public class ShapesLayer extends Layer {
public void remove( Collection<Shape> shapes ) { public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) { synchronized( this.shapes ) {
for( Shape s : shapes ) { this.shapes.removeAll(shapes);
this.shapes.remove(s);
}
} }
} }
@@ -116,8 +120,8 @@ public class ShapesLayer extends Layer {
anim.start(); anim.start();
} }
@SafeVarargs
public void play( Animation<? extends Shape>... anims ) { public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) { for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim); this.animations.add(anim);
anim.start(); anim.start();
@@ -141,13 +145,13 @@ 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();
} }
@@ -161,7 +165,7 @@ public class ShapesLayer extends Layer {
} }
} }
super.draw(pGraphics); super.draw(graphics);
} }
} }

View File

@@ -1,18 +1,25 @@
package schule.ngb.zm.layers; 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(DEFAULT_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

@@ -0,0 +1,8 @@
/**
* Dieses Paket enthält implementationen der abstrakten
* {@link schule.ngb.zm.Layer} Klasse.
* <p>
* {@code Layer} sind Ebenen, die der {@link schule.ngb.zm.Zeichenleinwand}
* hinzugefügt und pro Frame gerendert werden.
*/
package schule.ngb.zm.layers;

View File

@@ -1,10 +1,21 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
/** /**
* Interface für Audio-Medien. * Schnittstelle für Audio-Medien.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/ */
public interface Audio { public interface Audio {
/**
* @return Die Quelle, aus der das Medium geladen wurde.
*/
String getSource(); String getSource();
/** /**
@@ -17,7 +28,7 @@ public interface Audio {
/** /**
* Prüft, ob das Medium gerade in einer Schleife abgespielt wird. Wenn * Prüft, ob das Medium gerade in einer Schleife abgespielt wird. Wenn
* {@code isLooping() == true}, dann muss auch immer * {@code isLooping() == true} gilt, dann muss auch immer
* {@code isPlaying() == true} gelten. * {@code isPlaying() == true} gelten.
* *
* @return {@code true}, wenn das Medium in einer Schleife abgespielt wird, * @return {@code true}, wenn das Medium in einer Schleife abgespielt wird,
@@ -30,7 +41,7 @@ public interface Audio {
* <p> * <p>
* Die Lautstärke wird auf einer linearen Skale festgelegt, wobei 0 kein Ton * Die Lautstärke wird auf einer linearen Skale festgelegt, wobei 0 kein Ton
* und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des * und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
* Mediums. * Mediums. Negative Werte setzen die Lautstärke aud 0.
* *
* @param volume Die neue Lautstärke zwischen 0 und 1. * @param volume Die neue Lautstärke zwischen 0 und 1.
* @see <a * @see <a
@@ -39,7 +50,7 @@ public interface Audio {
void setVolume( double volume ); void setVolume( double volume );
/** /**
* Gibt die aktuelle Lautstärkeeinstellung dieses Mediums zurück. * Liefert die aktuelle Lautstärke dieses Mediums.
* <p> * <p>
* Die Lautstärke wird auf einer linearen Skale angegeben, wobei 0 kein Ton * Die Lautstärke wird auf einer linearen Skale angegeben, wobei 0 kein Ton
* und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des * und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
@@ -50,8 +61,16 @@ public interface Audio {
double getVolume(); double getVolume();
/** /**
* Startet die Wiedergabe des Mediums und beendet die Methode. Das * Startet die Wiedergabe des Mediums. Das Audio-Medium wird einmal
* Audio-Medium wird einmal abgespielt und stoppt dann. * abgespielt und stoppt dann.
* <p>
* Die Methode beendet sofort und die Wiedergabe erfolgt im Hintergrund.
* Soll die Programmausführung erst nach Wiedergabe des Mediums fortgesetzt
* werden, sollte {@link #playAndWait()} verwendet werden.
* <p>
* Soll die Wiedergabe im Hintergrund ablaufen, aber dennoch auf das Ende
* reagiert werden, kann ein
* {@link #addAudioListener(AudioListener) AudioListener} verwendet werden.
*/ */
void play(); void play();
@@ -63,21 +82,36 @@ public interface Audio {
/** /**
* Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode * Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode
* startet die Wiedergabe und beendet dann direkt die Methode. Um die * startet die Wiedergabe im Hintergrund und beendet dann sofort. Um die
* Wiedergabe zu stoppen muss {@link #stop()} aufgerufen werden. * Wiedergabe zu stoppen, muss {@link #stop()} aufgerufen werden.
*/ */
void loop(); void loop();
/** /**
* Stoppt die Wiedergabe. Wird das Medium gerade nicht abgespielt * Stoppt die Wiedergabe. Wird das Medium gerade nicht abgespielt
* {@code isPlaying() == false}, dann passiert nichts. * ({@code isPlaying() == false}), dann passiert nichts.
*/ */
void stop(); void stop();
/** /**
* Stoppt die Wiedergabe und gibt alle Resourcen, die für das Medium * Stoppt die Wiedergabe und gibt alle Ressourcen, die für das Medium
* verwendet werden, frei. * verwendet werden, frei.
*/ */
void dispose(); void dispose();
/**
* Fügt dem Medium das angegebene Objekt als {@code AudioListener} hinzu,
* der bei Start und Stopp der Wiedergabe informiert wird.
*
* @param listener Das Listener-Objekt.
*/
void addAudioListener( AudioListener listener );
/**
* Entfernt den angegebenen {@code AudioListener} vom Medium.
*
* @param listener Das Listener-Objekt.
*/
void removeAudioListener( AudioListener listener );
} }

View File

@@ -2,10 +2,33 @@ package schule.ngb.zm.media;
import schule.ngb.zm.util.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-Mediums startet, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Medium, dessen Wiedergabe gestartet wurde.
*/
void playbackStarted( Audio source );
void stop( Audio source ); /**
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Mediums stoppt, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Medium, 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.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner; 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,66 +37,93 @@ 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 ) {
if( !contains(pAudio) ) {
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor)); 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() {
audios.clear(); audios.clear();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isPlaying() { public boolean isPlaying() {
return audios.stream().anyMatch(aw -> aw.audio.isPlaying()); return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isLooping() { public boolean isLooping() {
return audios.stream().anyMatch(aw -> aw.audio.isLooping()); return audios.stream().anyMatch(aw -> aw.audio.isLooping());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setVolume( double pVolume ) { public void setVolume( double volume ) {
volume = (float) pVolume; this.volume = volume < 0 ? 0f : (float) volume;
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * pVolume)); audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * volume));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getVolume() { public double getVolume() {
return volume; return volume;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void play() { public void play() {
audios.stream().forEach(aw -> aw.audio.play()); audios.stream().forEach(aw -> aw.audio.play());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void playAndWait() { public void playAndWait() {
audios.stream().forEach(aw -> aw.audio.play()); audios.stream().forEach(aw -> aw.audio.play());
@@ -117,25 +136,16 @@ public class Mixer implements Audio {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void loop() { public void loop() {
audios.stream().forEach(aw -> aw.audio.loop()); audios.stream().forEach(aw -> aw.audio.loop());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void stop() { public void stop() {
audios.stream().forEach(aw -> aw.audio.stop()); audios.stream().forEach(aw -> aw.audio.stop());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void dispose() { public void dispose() {
if( isPlaying() ) { if( isPlaying() ) {
@@ -178,4 +188,58 @@ 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, sobald 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,83 +1,103 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.io.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 schule.ngb.zm.util.tasks.TaskRunner;
import javax.sound.sampled.*; import javax.sound.sampled.*;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
/** /**
* Ein Musikstück, dass im Projekt abgespielt werden soll. * Eine Musik, die abgespielt werden kann.
* <p> * <p>
* Im gegensatz zu einem {@link Sound} sind Musikstücke längere Audiodateien, * Im Gegensatz zu einem {@link Sound} wird {@code Music} für längere
* die zum Beispiel als Hintergrundmusik ablaufen sollen. Die Musik wird daher * Audiodateien benutzt, die zum Beispiel als Hintergrundmusik gespielt werden.
* nicht komplett in den Speicher geladen, sondern direkt aus der Audioquelle * Die Audiodaten werden daher nicht vollständig in den Speicher geladen,
* gestreamt und wiedergegeben. * sondern direkt aus der Quelle gestreamt und direkt wiedergegeben.
* <p>
* Daher ist es nicht möglich, die länge der Musik im Vorfeld abzufragen oder zu
* einer bestimmten Stelle im Stream zu springen.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/ */
// TODO: Wann sollten Listener beim Loopen informiert werden? Nach jedem Loop oder erst ganz am Ende?
@SuppressWarnings( "unused" )
public class Music implements Audio { public class Music implements Audio {
// size of the byte buffer used to read/write the audio stream /**
* Größe des verwendeten Input-Puffers für die Audiodaten.
*/
private static final int BUFFER_SIZE = 4096; private static final int BUFFER_SIZE = 4096;
/** /**
* Ob der Sound gerade abgespielt wird. * Ob der Sound aktuell abgespielt wird.
*/ */
private boolean playing = false; private boolean playing = false;
/** /**
* Ob der Sound gerade in einer Schleife abgespielt wird. * Ob der Sound aktuell in einer Schleife abgespielt wird.
*/ */
private boolean looping = false; private boolean looping = false;
/** /**
* Die Quelle des Musikstücks. * Die Quelle der Audiodaten.
*/ */
private String audioSource; private String audioSource;
/** /**
* Der AudioStream, um die AUdiosdaten zulsen, falls dieser schon geöffnet * Der {@link AudioInputStream}, um die Audiosdaten zu lesen. {@code null},
* wurde. Sonst {@code null}. * falls noch kein Stream geöffnet wurde.
*/ */
private AudioInputStream audioStream; private AudioInputStream audioStream;
/** /**
* Die Line für die Ausgabe, falls diese schon geöffnet wurde. Sonst * Die {@link SourceDataLine} für die Ausgabe. {@code null}, falls die
* {@code null}. * Audiodatei noch nicht geöffnet wurde.
*/ */
private SourceDataLine audioLine; private SourceDataLine audioLine;
/** /**
* Die Lautstärke der Musik. * Die aktuelle Lautstärke des Mediums.
*/ */
private float volume = 0.8f; private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher; EventDispatcher<Audio, AudioListener> eventDispatcher;
public Music( String source ) { /**
Validator.requireNotNull(source); * Erstellt eine Musik aus der angegebenen Audioquelle.
this.audioSource = source; *
* @param audioSource Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/
public Music( String audioSource ) {
Validator.requireNotNull(audioSource, "audioSource");
this.audioSource = audioSource;
} }
@Override
public String getSource() { public String getSource() {
return audioSource; return audioSource;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isPlaying() { public boolean isPlaying() {
return playing; return playing;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isLooping() { public boolean isLooping() {
if( !playing ) { if( !playing ) {
@@ -86,28 +106,21 @@ public class Music implements Audio {
return looping; return looping;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setVolume( double volume ) { public void setVolume( double volume ) {
this.volume = (float) volume; this.volume = volume < 0 ? 0f : (float) volume;
if( audioLine != null ) { if( audioLine != null ) {
applyVolume(); applyVolume();
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getVolume() { public double getVolume() {
return volume; return volume;
} }
/** /**
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen * Wendet die Lautstärke vor dem Abspielen auf den Audiostream an.
* anzuwenden.
*/ */
private void applyVolume() { private void applyVolume() {
FloatControl gainControl = FloatControl gainControl =
@@ -118,24 +131,13 @@ public class Music implements Audio {
gainControl.setValue(vol); gainControl.setValue(vol);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void play() { public void play() {
if( openLine() ) { if( openLine() ) {
TaskRunner.run(new Runnable() { TaskRunner.run(this::stream);
@Override
public void run() {
stream();
}
});
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void playAndWait() { public void playAndWait() {
if( openLine() ) { if( openLine() ) {
@@ -143,18 +145,12 @@ public class Music implements Audio {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void loop() { public void loop() {
looping = true; looping = true;
play(); play();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void stop() { public void stop() {
playing = false; playing = false;
@@ -162,9 +158,6 @@ public class Music implements Audio {
dispose(); dispose();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public synchronized void dispose() { public synchronized void dispose() {
if( audioLine != null ) { if( audioLine != null ) {
@@ -188,7 +181,17 @@ public class Music implements Audio {
audioStream = null; audioStream = null;
} }
/**
* Startet den Stream der Audiodaten und damit die Wiedergabe.
* <p>
* Die {@link #audioLine} muss vorher mit {@link #openLine()} geöffnet
* werden, ansonsten passiert nichts.
*/
private synchronized void stream() { private synchronized void stream() {
if( audioLine == null ) {
return;
}
audioLine.start(); audioLine.start();
playing = true; playing = true;
if( eventDispatcher != null ) { if( eventDispatcher != null ) {
@@ -225,6 +228,14 @@ public class Music implements Audio {
} }
} }
/**
* Öffnet eine {@link SourceDataLine} für die
* {@link #audioSource Audioquelle} und bereitet die Wiedergabe vor. Es wird
* noch nichts abgespielt.
*
* @return {@code true}, wenn die Line geöffnet werden konnte, {@code false}
* sonst.
*/
private boolean openLine() { private boolean openLine() {
if( audioLine != null ) { if( audioLine != null ) {
return true; return true;
@@ -261,6 +272,15 @@ public class Music implements Audio {
return false; return false;
} }
/**
* Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Aufruf von {@link #stop()} oder weil keine Audiodaten mehr vorhanden
* sind.
* <p>
* Nach dem Ende des Streams wird {@link #dispose()} aufgerufen und, falls
* das Musikstück in einer Schleife abgespielt wird, der Stream direkt
* wieder gestartet.
*/
private void streamingStopped() { private void streamingStopped() {
dispose(); dispose();
@@ -276,24 +296,27 @@ 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);
} }
/** /**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst * Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, soblad sich auch ein Listener registrieren möchte. * aufgerufen, sobald sich der erste Listener anmelden möchte.
* @return *
* @return Der {@code EventDispatcher} für dieses Objekt.
*/ */
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

@@ -2,6 +2,7 @@ package schule.ngb.zm.media;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider; import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*; import javax.sound.sampled.*;
@@ -9,78 +10,87 @@ import java.io.IOException;
import java.net.URL; import java.net.URL;
/** /**
* Wiedergabe kurzer Soundclips, die mehrmals wiederverwendet werden. * Ein kurzer Soundclip, der mehrmals wiederverwendet werden kann.
* <p> * <p>
* In Spielen und anderen Projekten gibt es oftmals eine Reihe kurzer Sounds, * In Spielen und anderen Projekten gibt es oftmals eine Reihe kurzer
* die zusammen mit bestimmten Aktionen wiedergegeben werden (zum Beispiel, wenn * Soundclips, die zusammen mit bestimmten Aktionen wiedergegeben werden (zum
* die Spielfigur springt, wenn zwei Objekte kollidieren, usw.). Sounds werden * Beispiel, wenn die Spielfigur springt, wenn zwei Objekte kollidieren, usw.).
* komplett in den Speicher geladen und können dadurch immer wieder, als * Sounds werden vollständig in den Speicher geladen und können immer wieder,
* Schleife oder auch nur Abschnittsweise abgespielt werden. * als Schleife oder auch nur Abschnittsweise, abgespielt werden.
* <p> * <p>
* Für längre Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher die * Für längere Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher
* KLasse {@link Music} an. * die Klasse {@link Music} an.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/ */
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public class Sound implements Audio { public class Sound implements Audio {
/** /**
* Ob der Sound gerade abgespielt wird. * Ob der Sound aktuell abgespielt wird.
*/ */
private boolean playing = false; private boolean playing = false;
/** /**
* Ob der Sound gerade in einer Schleife abgespielt wird. * Ob der Sound aktuell in einer Schleife abgespielt wird.
*/ */
private boolean looping = false; private boolean looping = false;
/** /**
* Die Quelle des Musikstücks. * Die Quelle der Audiodaten.
*/ */
private String audioSource; private String audioSource;
/** /**
* Der Clip, falls er schon geladen wurde, sonst {@code null}. * Der Clip, falls er schon geladen wurde. Ansonsten {@code null}.
*/ */
private Clip audioClip; private Clip audioClip;
/** /**
* Ob die Resourcen des Clips im Speicher nach dem nächsten Abspielen * Ob die Ressourcen des Clips im Speicher nach dem nächsten Abspielen
* freigegeben werden sollen. * freigegeben werden sollen.
*/ */
private boolean disposeAfterPlay = false; private boolean disposeAfterPlay = false;
/** /**
* Die Lautstärke des Clips. * Die aktuelle Lautstärke des Clips.
*/ */
private float volume = 0.8f; private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher;
/** /**
* Erstellt einen Sound aus der angegebene Quelle. * Erstellt einen Sound aus der angegebene Quelle.
* *
* @param source Ein Dateipfad oder eine Webadresse. * @param source Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist. * @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/ */
public Sound( String source ) { public Sound( String source ) {
Validator.requireNotNull(source); Validator.requireNotNull(source, "source");
this.audioSource = source; this.audioSource = source;
} }
@Override
public String getSource() { public String getSource() {
return audioSource; return audioSource;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isPlaying() { public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning(); // return audioClip != null && audioClip.isRunning();
return playing; return playing;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isLooping() { public boolean isLooping() {
if( !playing ) { if( !playing ) {
@@ -89,28 +99,21 @@ public class Sound implements Audio {
return looping; return looping;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setVolume( double volume ) { public void setVolume( double volume ) {
this.volume = (float) volume; this.volume = volume < 0 ? 0f : (float) volume;
if( audioClip != null ) { if( audioClip != null ) {
applyVolume(); applyVolume();
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getVolume() { public double getVolume() {
return volume; return volume;
} }
/** /**
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen * Wendet die Lautstärke vor dem Abspielen auf den Clip an.
* anzuwenden.
*/ */
private void applyVolume() { private void applyVolume() {
FloatControl gainControl = FloatControl gainControl =
@@ -121,9 +124,6 @@ public class Sound implements Audio {
gainControl.setValue(vol); gainControl.setValue(vol);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void stop() { public void stop() {
looping = false; looping = false;
@@ -133,9 +133,6 @@ public class Sound implements Audio {
playing = false; playing = false;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void play() { public void play() {
if( this.openClip() ) { if( this.openClip() ) {
@@ -144,9 +141,6 @@ public class Sound implements Audio {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void playAndWait() { public void playAndWait() {
this.play(); this.play();
@@ -165,7 +159,7 @@ public class Sound implements Audio {
} }
/** /**
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips * Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei. * frei.
* <p> * <p>
* Der Aufruf ist effektiv gleich zu * Der Aufruf ist effektiv gleich zu
@@ -182,7 +176,7 @@ public class Sound implements Audio {
} }
/** /**
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips * Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei. * frei.
* <p> * <p>
* Der Aufruf entspricht * Der Aufruf entspricht
@@ -196,17 +190,18 @@ public class Sound implements Audio {
playAndWait(); playAndWait();
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void loop() { public void loop() {
loop(Clip.LOOP_CONTINUOUSLY); loop(Clip.LOOP_CONTINUOUSLY);
} }
/** /**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und * Wiederholt den Sound die angegebene Anzahl an Wiederholungen und stoppt
* stoppt die Wiedergabe dann. * die Wiedergabe dann.
* <p>
* Wird {@code count} auf {@link Clip#LOOP_CONTINUOUSLY} gesetzt (-1), wird
* der Clip unendlich oft wiederholt. Der Aufruf entspricht dann
* {@link #loop()}.
* *
* @param count Anzahl der Wiederholungen. * @param count Anzahl der Wiederholungen.
*/ */
@@ -228,20 +223,24 @@ public class Sound implements Audio {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public synchronized 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;
} }
} }
/**
* Lädt, falls nötig, den {@link Clip} für die
* {@link #audioSource Audioquelle}.
*
* @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false}
* sonst.
*/
private synchronized boolean openClip() { private synchronized boolean openClip() {
if( audioClip != null ) { if( audioClip != null ) {
audioClip.setFramePosition(0); audioClip.setFramePosition(0);
@@ -259,7 +258,11 @@ 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();
} }
} }
@@ -296,18 +299,51 @@ public class Sound implements Audio {
}*/ }*/
/** /**
* Interne Methode, die aufgerufen wird, wenn die Wiedergabe gestoppt wird. * Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Entweder durch einen Aufruf von {@link #stop()} oder, weil die Wiedergabe * Aufruf von {@link #stop()} oder, weil die Wiedergabe nach
* nach {@link #playOnce()} beendet wurde. * {@link #playOnce()} beendet wurde.
* <p>
* Falls {@link #disposeAfterPlay} gesetzt ist, wird nach dem Ende der
* Wiedergabe {@link #dispose()} aufgerufen.
*/ */
private void playbackStopped() { private void playbackStopped() {
playing = false; playing = false;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
if( disposeAfterPlay ) { if( disposeAfterPlay ) {
this.dispose(); this.dispose();
disposeAfterPlay = false; disposeAfterPlay = false;
} }
} }
@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, sobald sich der erste Listener anmelden möchte.
*
* @return Der {@code EventDispatcher} für dieses Objekt.
*/
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,7 @@
/**
* Dieses Paket enthält Klassen zur Einbettung von Mediendateien.
* <p>
* Mit Medien sind vor allem Audio und Videodateien gemeint. Aktuell kann die
* Zeichenmaschine Audiodateien verwenden.
*/
package schule.ngb.zm.media;

View File

@@ -93,32 +93,20 @@ public final class DoubleMatrix implements MLMatrix {
rows * columns); rows * columns);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public int columns() { public int columns() {
return columns; return columns;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public int rows() { public int rows() {
return rows; return rows;
} }
/**
* {@inheritDoc}
*/
int idx( int r, int c ) { int idx( int r, int c ) {
return c * rows + r; return c * rows + r;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double get( int row, int col ) { public double get( int row, int col ) {
try { try {
@@ -128,9 +116,6 @@ public final class DoubleMatrix implements MLMatrix {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix set( int row, int col, double value ) { public MLMatrix set( int row, int col, double value ) {
try { try {
@@ -141,52 +126,34 @@ public final class DoubleMatrix implements MLMatrix {
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix initializeRandom() { public MLMatrix initializeRandom() {
return initializeRandom(-1.0, 1.0); return initializeRandom(-1.0, 1.0);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix initializeRandom( double lower, double upper ) { public MLMatrix initializeRandom( double lower, double upper ) {
applyInPlace(( d ) -> ((upper - lower) * Constants.random()) + lower); applyInPlace(( d ) -> ((upper - lower) * Constants.random()) + lower);
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix initializeOne() { public MLMatrix initializeOne() {
applyInPlace(( d ) -> 1.0); applyInPlace(( d ) -> 1.0);
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix initializeZero() { public MLMatrix initializeZero() {
applyInPlace(( d ) -> 0.0); applyInPlace(( d ) -> 0.0);
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix duplicate() { public MLMatrix duplicate() {
return new DoubleMatrix(this); return new DoubleMatrix(this);
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix multiplyTransposed( MLMatrix B ) { public MLMatrix multiplyTransposed( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj( /*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
@@ -208,9 +175,6 @@ public final class DoubleMatrix implements MLMatrix {
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix multiplyAddBias( final MLMatrix B, final MLMatrix C ) { public MLMatrix multiplyAddBias( final MLMatrix B, final MLMatrix C ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj( /*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
@@ -233,9 +197,6 @@ public final class DoubleMatrix implements MLMatrix {
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix transposedMultiplyAndScale( final MLMatrix B, final double scalar ) { public MLMatrix transposedMultiplyAndScale( final MLMatrix B, final double scalar ) {
/*return new DoubleMatrix(IntStream.range(0, columns).parallel().mapToObj( /*return new DoubleMatrix(IntStream.range(0, columns).parallel().mapToObj(
@@ -258,9 +219,6 @@ public final class DoubleMatrix implements MLMatrix {
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix add( MLMatrix B ) { public MLMatrix add( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj( /*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
@@ -277,9 +235,6 @@ public final class DoubleMatrix implements MLMatrix {
return sum; return sum;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix addInPlace( MLMatrix B ) { public MLMatrix addInPlace( MLMatrix B ) {
for( int j = 0; j < columns; j++ ) { for( int j = 0; j < columns; j++ ) {
@@ -290,9 +245,6 @@ public final class DoubleMatrix implements MLMatrix {
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix sub( MLMatrix B ) { public MLMatrix sub( MLMatrix B ) {
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj( /*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
@@ -309,9 +261,6 @@ public final class DoubleMatrix implements MLMatrix {
return diff; return diff;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix colSums() { public MLMatrix colSums() {
/*DoubleMatrix colSums = new DoubleMatrix(1, columns); /*DoubleMatrix colSums = new DoubleMatrix(1, columns);
@@ -331,9 +280,6 @@ public final class DoubleMatrix implements MLMatrix {
return colSums; return colSums;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix scaleInPlace( final double scalar ) { public MLMatrix scaleInPlace( final double scalar ) {
for( int i = 0; i < coefficients.length; i++ ) { for( int i = 0; i < coefficients.length; i++ ) {
@@ -342,9 +288,6 @@ public final class DoubleMatrix implements MLMatrix {
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix scaleInPlace( final MLMatrix S ) { public MLMatrix scaleInPlace( final MLMatrix S ) {
for( int j = 0; j < columns; j++ ) { for( int j = 0; j < columns; j++ ) {
@@ -355,9 +298,6 @@ public final class DoubleMatrix implements MLMatrix {
return this; return this;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix apply( DoubleUnaryOperator op ) { public MLMatrix apply( DoubleUnaryOperator op ) {
DoubleMatrix result = new DoubleMatrix(rows, columns); DoubleMatrix result = new DoubleMatrix(rows, columns);
@@ -367,9 +307,6 @@ public final class DoubleMatrix implements MLMatrix {
return result; return result;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public MLMatrix applyInPlace( DoubleUnaryOperator op ) { public MLMatrix applyInPlace( DoubleUnaryOperator op ) {
for( int i = 0; i < coefficients.length; i++ ) { for( int i = 0; i < coefficients.length; i++ ) {

View File

@@ -0,0 +1,16 @@
/**
* Dieses Paekt enthält Klassen für Experimente mit Verfahren des maschinellen
* Lernens (ML).
* <p>
* Die hier implementierten Klassen sind eine prototypische Umsetzung von
* einfachen neuronalen Netzwerken, mit denen an kleinen Problemstellungen
* experimentell Modelle trainiert und angewandt werden können.
* <p>
* Die Implementierungen sind nicht optimiert und setzen auf native
* Java-Methoden. Daher sind sie nur für die Anwendung auf extrem kleine Modelle
* in Bildungskontexten gedacht.
* <p>
* Durch Einbettung wissenschaftlicher Bibliotheken mit optimierten Operationen
* lassen sich bessere Ergebnisse erreichen.
*/
package schule.ngb.zm.ml;

View File

@@ -0,0 +1,13 @@
/**
* <h2>Die Zeichenmaschine</h2>
* <p>
* Die <b>Zeichenmaschine</b> ist eine für den Informatikunterricht entwickelte
* Bibliothek, die unter anderem an <a
* href="https://processing.org/">Processing</a> 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.
* <p>
* Eine umfassende Dokumentation ist unter <a
* href="https://zeichenmaschine.xyz">zeichenmaschine.xyz</a> verfügbar.
*/
package schule.ngb.zm;

View File

@@ -1,202 +0,0 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
/**
* Basisklasse für Formen, die eine Füllung besitzen können.
* <p>
* Formen mit einer Füllung können auch immer eine Konturlinie besitzen.
*/
public abstract class FilledShape extends StrokedShape {
/**
* Die aktuelle Füllfarbe der Form oder {@code null}, wenn die Form nicht
* gefüllt werden soll.
*/
protected Color fillColor = DEFAULT_FILLCOLOR;
/**
* Der aktuelle Farbverlauf der Form oder {@code null}, wenn die Form keinen
* Farbverlauf besitzt.
*/
protected Paint fill = null;
/**
* Gibt die aktuelle Füllfarbe der Form zurück.
*
* @return Die aktuelle Füllfarbe oder {@code null}.
*/
public Color getFillColor() {
return fillColor;
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @see Color
*/
public void setFillColor( Color color ) {
fillColor = 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)
*/
public 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)
*/
public 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)
*/
public 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>
*/
public 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>
*/
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Füllung der Form.
*/
public void noFill() {
setFillColor(null);
noGradient();
}
/**
* Setzt die Füllfarbe auf den Standardwert zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_FILLCOLOR
*/
public void resetFill() {
setFillColor(DEFAULT_FILLCOLOR);
noGradient();
}
/**
* 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.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
setFillColor(from);
fill = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
}
/**
* 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.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
setFillColor(from);
fill = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
}
/**
* Entfernt den Farbverlauf von der Form.
*/
public void noGradient() {
fill = null;
}
/**
* 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

@@ -1,23 +1,25 @@
package schule.ngb.zm.shapes; package schule.ngb.zm.shapes;
import schule.ngb.zm.BasicDrawable;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Options; import schule.ngb.zm.Options;
import schule.ngb.zm.Strokeable;
import schule.ngb.zm.util.Validator;
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;
/** /**
* Basisklasse für alle Formen in der Zeichenmaschine. * Dies ist die Basisklasse für alle Formen in der Zeichenmaschine.
* <p> * <p>
* Alle Formen sind als Unterklassen von {@code Shape} implementiert. * Alle Formen sind als Unterklassen von {@code Shape} implementiert.
* <p> * <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei * Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen * Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die Werten einer anderen Form * Parametern initialisiert und einen, der die Werte einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren * desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert: * beispielsweise so implementiert:
*
* <pre><code> * <pre><code>
* public Circle( double x, double y, double radius ) { * public Circle( double x, double y, double radius ) {
* super(x, y); * super(x, y);
@@ -34,50 +36,45 @@ import java.awt.geom.Point2D;
* eine {@link #equals(Object)} Methode. * eine {@link #equals(Object)} Methode.
*/ */
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public abstract class Shape extends FilledShape { public abstract class Shape extends BasicDrawable {
/** /**
* x-Koordinate der Form. * Speichert die x-Koordinate der Form.
*/ */
protected double x; protected double x;
/** /**
* y-Koordinate der Form. * Speichert die y-Koordinate der Form.
*/ */
protected double y; protected double y;
/** /**
* Rotation in Grad um den Punkt (x, y). * Speichert die Rotation in Grad um den Punkt (x, y).
*/ */
protected double rotation = 0.0; protected double rotation = 0.0;
/** /**
* Skalierungsfaktor. * Speichert den Skalierungsfaktor.
*/ */
protected double scale = 1.0; protected double scale = 1.0;
/** /**
* Ob die Form angezeigt werden soll. * Speichert den Ankerpunkt.
*/
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. * Erstellt eine neue Form mit den Koordinaten {@code (0,0)}.
*/ */
public Shape() { public Shape() {
this(0.0, 0.0); this(0.0, 0.0);
} }
/** /**
* Setzt die x- und y-Koordinate der Form. * Erstellt eine Form mit den angegebenen Koordinaten.
* *
* @param x * @param x Die x-Koordinate.
* @param y * @param y Die y-Koordinate.
*/ */
public Shape( double x, double y ) { public Shape( double x, double y ) {
this.x = x; this.x = x;
@@ -85,39 +82,7 @@ public abstract class Shape extends FilledShape {
} }
/** /**
* Ob die Form angezeigt wird oder nicht. * Liefert die aktuelle x-Koordinate der Form.
*
* @return {@code true}, wenn die From angezeigt werden soll, {@code false}
* sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Form.
*/
public void hide() {
visible = false;
}
/**
* Zeigt die Form an.
*/
public void show() {
visible = true;
}
/**
* Versteckt die Form, wenn sie derzeit angezeigt wird und zeigt sie
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
/**
* Gibt die x-Koordinate der Form zurück.
* *
* @return Die x-Koordinate. * @return Die x-Koordinate.
*/ */
@@ -135,7 +100,7 @@ public abstract class Shape extends FilledShape {
} }
/** /**
* Gibt die y-Koordinate der Form zurück. * Liefert die aktuelle y-Koordinate der Form.
* *
* @return Die y-Koordinate. * @return Die y-Koordinate.
*/ */
@@ -144,44 +109,44 @@ public abstract class Shape extends FilledShape {
} }
/** /**
* Setzt die y-Koordinate der Form. * Setzt die x-Koordinate der Form.
* *
* @param y Die neue y-Koordinate. * @param y Die neue y-Koordinate der Form.
*/ */
public void setY( double y ) { public void setY( double y ) {
this.y = y; this.y = y;
} }
/** /**
* Gibt die Breite dieser Form zurück. * Liefert die aktuelle Breite dieser Form.
* <p> * <p>
* Die Breite einer Form ist immer die Breite ihrer Begrenzung, * Die Breite einer Form ist immer die Breite ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie * <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden. * angewandt wurden.
* <p> * <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit * Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen werden. * {@link #getBounds()} abgerufen.
* *
* @return * @return Die Breite der Form.
*/ */
public abstract double getWidth(); public abstract double getWidth();
/** /**
* Gibt die Höhe dieser Form zurück. * Liefert die aktuelle Höhe dieser Form.
* <p> * <p>
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung, * Die Höhe einer Form ist immer die Höhe ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie * <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden. * angewandt wurden.
* <p> * <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit * Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen werden. * {@link #getBounds()} abgerufen.
* *
* @return * @return Die Höhe der Form.
*/ */
public abstract double getHeight(); public abstract double getHeight();
/** /**
* Gibt die Rotation in Grad zurück. * Liefert die Rotation in Grad.
* *
* @return Rotation in Grad. * @return Rotation in Grad.
*/ */
@@ -191,20 +156,32 @@ public abstract class Shape extends FilledShape {
/** /**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt. * Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
*
* @param angle Drehwinkel in Grad. * @param angle Drehwinkel in Grad.
*/ */
public void rotate( double angle ) { public void rotate( double angle ) {
this.rotation += angle % 360; this.rotation += angle % 360;
} }
public void rotateTo( double angle ) { /**
this.rotation = angle % 360; * Dreht die Form um den angegebenen Winkel um das angegebene Drehzentrum.
} *
* @param center Das Drehzentrum der Drehung.
* @param angle Der Drehwinkel.
*/
public void rotate( Point2D center, double angle ) { public void rotate( Point2D center, double angle ) {
Validator.requireNotNull(center, "center");
rotate(center.getX(), center.getY(), angle); rotate(center.getX(), center.getY(), angle);
} }
/**
* Dreht die Form um den angegebenen Drehwinkel um die angegbenen
* Koordinaten als Drehzentrum.
*
* @param x x-Koordiante des Drehzentrums.
* @param y y-Koordiante des Drehzentrums.
* @param angle Drehwinkel in Grad.
*/
public void rotate( double x, double y, double angle ) { public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360; this.rotation += angle % 360;
@@ -219,6 +196,15 @@ public abstract class Shape extends FilledShape {
this.y = y2 + y; this.y = y2 + y;
} }
/**
* Setzt die Drehung der Form auf den angegebenen Winkel.
*
* @param angle Drehwinkel in Grad.
*/
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
/** /**
* Gibt den aktuellen Skalierungsfaktor zurück. * Gibt den aktuellen Skalierungsfaktor zurück.
* *
@@ -227,96 +213,161 @@ public abstract class Shape extends FilledShape {
public double getScale() { public double getScale() {
return scale; return scale;
} }
/**
* Setzt den Skalierungsfaktor auf den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Das Seitenverhältnis wird immer beibehalten.
*
* @param factor Der Skalierungsfaktor.
*/
public void scale( double factor ) { public void scale( double factor ) {
scale = factor; scale = factor;
} }
/**
* Skaliert die Form um den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Die Skalierung wird zusätzlich zur aktuellen Skalierung angewandt. Wurde
* die Form zuvor um den Faktor 0.5 verkleinert und wird dann um 1.5
* vergrößert, dann ist die Form im Anschluss ein Drittel kleiner als zu
* Beginn ({@code 0.5 * 1.5 = 0.75}).
*
* @param factor Der Skalierungsfaktor.
*/
public void scaleBy( double factor ) { public void scaleBy( double factor ) {
scale(scale * factor); scale(scale * factor);
} }
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) { /**
Point2D apDir = getAbsAnchorPoint(dir); * Liefert den aktuellen Ankerpunkt der Form.
Point2D apInv = getAbsAnchorPoint(dir.inverse()); *
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to); * @return Der Ankerpunkt.
} */
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);
}
public Options.Direction getAnchor() { public Options.Direction getAnchor() {
return anchor; return anchor;
} }
/** /**
* Setzt den Ankerpunkt der Form basierend auf der angegebenen * Setzt den Ankerpunkt der Form auf die angegebene Richtung.
* {@link Options.Direction Richtung}.
* <p> * <p>
* Für das Setzen des Ankers muss das * Jede Form hat einen Ankerpunkt, von dem aus sie gezeichnet wird. Jede
* {@link #getBounds() begrenzende Rechteck} berechnet werden. Unterklassen * {@link schule.ngb.zm.Options.Direction Richtung} beschreibt einen der
* sollten die Methode überschreiben, wenn der Anker auch direkt gesetzt * Neun Ankerpunkte:
* werden kann. * <pre>
* NW────N────NE
* │ │
* │ │
* W C E
* │ │
* │ │
* SW────S────SE
* </pre>
* <p>
* Für den Ankerpunkt {@link #CENTER} wird die Form also ausgehend von den
* Koordinaten {@link #x} und {@link #y} um die Hälfte der Breite nach links
* und rechts, sowie um die Hälfte der Höhe nach oben und unten gezeichnet.
* Fpr den Ankerpunkt {@link #NORTHWEST} dagegen um die gesamte Breite nach
* rechts und die Höhe nach unten.
* <pre>
* setAnchor(CENTER) │ setAnchor(NORTHWEST)
* ┌───────────┐ │
* │ │ │
* │ │ │
* │ (x,y) │ │ (x,y)─────────┐
* │ │ │ │ │
* │ │ │ │ │
* └───────────┘ │ │ │
* │ │ │
* │ │ │
* │ └───────────┘
* </pre>
* <p>
* Der Ankerpunkt der Form bestimmt bei Transformationen auch die Position
* des Drehzentrums und anderer relativer Koordinaten bezüglich der Form.
* *
* @param anchor * @param anchor Der Ankerpunkt.
*/ */
public void setAnchor( Options.Direction anchor ) { public void setAnchor( Options.Direction anchor ) {
if( anchor != null ) { Validator.requireNotNull(anchor, "anchor");
this.anchor = anchor; this.anchor = anchor;
} }
}
/** /**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkt basierend * Bestimmt die relativen Koordinaten des angegebenen Ankerpunkts basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks. * auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p> * <p>
* Die Koordinaten des Ankerpunkt werden relativ zur oberen linken Ecke des * Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* Rechtecks mit der Breite {@code width} und der Höhe {@code height} * des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt. * bestimmt. Der Ankerpunkt {@link #NORTHWEST} hat daher immer das Ergebnis
* {@code (0,0)} und {@link #SOUTHEAST} {@code (width, height)}.
* <pre>
* (0,0)───(w/2,0)───(w,0)
* │ │
* │ │
* │ │
* (0,h/2) (w/2,h/2) (w,h/2)
* │ │
* │ │
* │ │
* (0,h)───(w/2,h)───(w,h)
* </pre>
* *
* @param width Breite des umschließdenden Rechtecks. * @param width Breite des umschließenden Rechtecks.
* @param height Höhe des umschließdenden Rechtecks. * @param height Höhe des umschließenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt. * @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten. * @return Ein {@link Point2D} mit den relativen Koordinaten.
*/ */
protected static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) { 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;
} }
/** /**
* Bestimmt den Ankerpunkt der Form relativ zum gesetzten * Bestimmt die Koordinaten des angegebenen Ankers der Form relativ zum
* {@link #setAnchor(Options.Direction) Ankerpunkt}. * aktuellen {@link #setAnchor(Options.Direction) Ankerpunkt}.
* *
* @param anchor Die Richtung des Ankerpunktes. * @param anchor Die Richtung des Ankers.
* @return Der relative Ankerpunkt. * @return Der relative Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/ */
public Point2D.Double getAnchorPoint( Options.Direction anchor ) { public Point2D.Double getAnchorPoint( Options.Direction anchor ) {
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;
} }
/** /**
* Ermittelt die absoluten Koordinaten eines angegebenen * Ermittelt die absoluten Koordinaten des angegebenen
* {@link #setAnchor(Options.Direction) Ankers}. * {@link #setAnchor(Options.Direction) Ankers}.
* <p>
* Die absoluten Koordinaten werden bestimmt durch die Position der Form
* {@code (x,y)} plus die
* {@link #getAnchorPoint(Options.Direction) relativen Koordinaten} des
* Ankers.
* *
* @param anchor Die Richtung des Ankerpunktes. * <b>Wichtig:</b> Die Berechnung berücksichtigt derzeit keine Rotationen
* und Transformationen der Form.
*
* @param anchor Die Richtung des Ankers.
* @return Der absolute Ankerpunkt. * @return Der absolute Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/ */
public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) { public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) {
// TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen. // TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen.
@@ -329,20 +380,22 @@ public abstract class Shape extends FilledShape {
/** /**
* Kopiert die Eigenschaften der angegebenen Form in diese. * Kopiert die Eigenschaften der angegebenen Form in diese.
* <p> * <p>
* Unterklassen sollten diese Methode überschreiben, um weitere * Unterklassen überschreiben diese Methode, um weitere Eigenschaften zu
* Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises). * kopieren (zum Beispiel den Radius eines Kreises). Überschreibende
* Unterklassen sollten immer mit dem Aufruf {@code super.copyFrom(shape)} * Methoden sollten immer mit dem Aufruf {@code super.copyFrom(shape)} die
* die Basiseigenschaften kopieren. * Basiseigenschaften kopieren.
* <p> * <p>
* Die Methode sollte so viele Eigenschaften wie möglich von der anderen * Die Methode kopiert so viele Eigenschaften wie möglich von der
* Form in diese kopieren. Wenn die andere Form einen anderen Typ hat, dann * angegebenen Form in diese. Wenn die andere Form einen anderen Typ hat,
* werden trotzdem die Basiseigenschaften (Konturlinie, Füllung, Position, * dann werden trotzdem die Basiseigenschaften (Konturlinie, Füllung,
* Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese Form kopiert. * Position, Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese
* Implementierende Unterklassen können soweit sinnvoll auch andere Werte * Form kopiert. Soweit sinnvoll übernehmen implementierende Unterklassen
* übernehmen. Eine {@link Ellipse} kann beispielsweise auch die Breite und * auch andere Werte. Eine {@link Ellipse} kopiert beispielsweise auch die
* Höhe eines {@link Rectangle} übernehmen. * Breite und Höhe eines {@link Rectangle}.
* <p>
* Wird {@code null} übergeben, dann passiert nichts.
* *
* @param shape Die Originalform, von der kopiert werden soll. * @param shape Die Originalform, von der kopiert wird.
*/ */
public void copyFrom( Shape shape ) { public void copyFrom( Shape shape ) {
if( shape != null ) { if( shape != null ) {
@@ -352,8 +405,8 @@ 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());
} }
} }
@@ -371,9 +424,9 @@ public abstract class Shape extends FilledShape {
* </code></pre> * </code></pre>
* <p> * <p>
* Die Methode kann beliebig umgesetzt werden, um eine 1-zu-1-Kopie dieser * 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 * Form zu erhalten. In der Regel besitzt aber jede Form einen Konstruktor,
* besitzen, die alle Werte einer andern Form übernimmt. Die gezeigte * der alle Werte einer andern Form übernimmt. Die gezeigte Implementierung
* Implementierung dürfte daher im Regelfall ausreichend sein. * ist daher im Regelfall ausreichend.
* *
* @return Eine genaue Kopie dieser Form. * @return Eine genaue Kopie dieser Form.
*/ */
@@ -384,19 +437,10 @@ public abstract class Shape extends FilledShape {
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den * zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen. * {@link Graphics2D Grafikkontext} zu zeichnen.
* <p> * <p>
* Da die AWT-Shape bei jedem Zeichnen (also mindestens einmal pro Frame) * Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, liefert die
* benötigt wird, wird das aktuelle Shape-Objekt in {@link #awtShape} * Methode {@code null}.
* zwischengespeichert. Bei Änderungen der Objekteigenschaften muss daher
* intern {@link #invalidate()} aufgerufen werden, damit beim nächsten
* Aufruf von {@link #draw(Graphics2D)} die Shape mit einem Aufurf von
* {@code getShape()} neu erzeugt wird. Unterklassen können aber auch die
* zwischengespeicherte Shape direkt modifizieren, um eine Neugenerierung zu
* vermeiden.
* <p>
* Wenn diese Form nicht durch eine AWT-Shape dargstellt wird, kann die
* Methode {@code null} zurückgeben.
* *
* @return Eine Java-AWT {@code Shape} die diess Form repräsentiert oder * @return Eine Java-AWT {@code Shape} die diese Form repräsentiert oder
* {@code null}. * {@code null}.
*/ */
public abstract java.awt.Shape getShape(); public abstract java.awt.Shape getShape();
@@ -414,34 +458,85 @@ public abstract class Shape extends FilledShape {
return new Bounds(this); return new Bounds(this);
} }
/**
* Verschiebt die Form um die angegebenen Werte entlang der
* Koordinatenachsen.
*
* @param dx Verschiebung entlang der x-Achse.
* @param dy Verschiebung entlang der y-Achse.
*/
public void move( double dx, double dy ) { public void move( double dx, double dy ) {
x += dx; x += dx;
y += dy; y += dy;
} }
/**
* Bewegt die Form an die angegebenen Koordinaten.
*
* @param x Die neue x-Koordinate.
* @param y Die neue y-Koordinate.
*/
public void moveTo( double x, double y ) { public void moveTo( double x, double y ) {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
/**
* Bewegt die Form an dieselben Koordinaten wie die angegebene Form.
*
* @param shape Eine andere Form.
*/
public void moveTo( Shape shape ) { public void moveTo( Shape shape ) {
moveTo(shape.getX(), shape.getY()); moveTo(shape.getX(), shape.getY());
} }
/**
* Bewegt die Form zum angegebenen Ankerpunkt der angegebenen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #moveTo(Shape, Options.Direction, double)
*/
public void moveTo( Shape shape, Options.Direction dir ) { public void moveTo( Shape shape, Options.Direction dir ) {
moveTo(shape, dir, 0.0); moveTo(shape, dir, 0.0);
} }
/** /**
* Bewegt den Ankerpunkt dieser Form zu einem Ankerpunkt einer anderen Form. * Bewegt den Ankerpunkt dieser Form zu einem Ankerpunkt einer anderen
* Mit {@code buff} kann ein zusätzlicher Abstand angegeben werden, um den * Form.
* die Form entlang des Ankerpunktes {@code anchor} verschoben werden soll. * <p>
* Mit {@code buff} wird ein zusätzlicher Abstand angegeben, um den die Form
* entlang des Ankerpunktes {@code anchor} verschoben wird.
* <p>
* Ist der Anker zum Beispiel {@code NORTH}, dann wird die Form um * Ist der Anker zum Beispiel {@code NORTH}, dann wird die Form um
* {@code buff} nach oben verschoben. * {@code buff} oberhalb der oberen Kante der zweiten Form verschoben.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────────┐
* │ │
* W B │
* ┌─────┐ │ │
* │ │ └─────────┘
* W A │
* │ │
* └─────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.moveTo(A, DOWN, 0)} in diese Ausrichtung:
* <pre>
* B.moveTo(A, WEST, 0) │ B.moveTo(A, WEST, 10)
* │
* ┌─────┬───┐ │ ┌┬────┬────┐
* │ │ │ │ ││ │ │
* │ A B│ │ │ ││ A B │
* │ │ │ │ ││ │ │
* └─────┴───┘ │ └┴────┴────┘
* </pre>
* *
* @param shape * @param shape Die andere Form.
* @param dir * @param dir Die Richtung des Ankerpunktes.
* @param buff * @param buff Der Abstand zum angegebenen Ankerpunkt.
*/ */
public void moveTo( Shape shape, Options.Direction dir, double buff ) { public void moveTo( Shape shape, Options.Direction dir, double buff ) {
Point2D ap = shape.getAbsAnchorPoint(dir); Point2D ap = shape.getAbsAnchorPoint(dir);
@@ -451,15 +546,22 @@ public abstract class Shape extends FilledShape {
} }
/** /**
* Richtet die Form entlang der angegebenen Richtung am Rand der * Bewegt die Form an den Rand der Zeichenfläche in der angegebenen
* Zeichenfläche aus. * Richtung.
* *
* @param dir Die Richtung der Ausrichtung. * @param dir Die Richtung.
*/ */
public void alignTo( Options.Direction dir ) { public void alignTo( Options.Direction dir ) {
alignTo(dir, 0.0); alignTo(dir, 0.0);
} }
/**
* Bewegt die Form mit dem angegebenen Abstand an den Rand der Zeichenfläche
* in der angegebenen Richtung aus.
*
* @param dir Die Richtung.
* @param buff Der Abstand zum Rand.
*/
public void alignTo( Options.Direction dir, double buff ) { public void alignTo( Options.Direction dir, double buff ) {
Point2D anchorShape = Shape.getAnchorPoint(canvasWidth, canvasHeight, dir); Point2D anchorShape = Shape.getAnchorPoint(canvasWidth, canvasHeight, dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir); Point2D anchorThis = this.getAbsAnchorPoint(dir);
@@ -468,20 +570,58 @@ public abstract class Shape extends FilledShape {
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff; this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
} }
/**
* Bewegt den Ankerpunkt dieser Form in der angegebenen Richtung zum
* Gleichen Ankerpunkt der anderen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #alignTo(Shape, Options.Direction, double)
*/
public void alignTo( Shape shape, Options.Direction dir ) { public void alignTo( Shape shape, Options.Direction dir ) {
alignTo(shape, dir, 0.0); alignTo(shape, dir, 0.0);
} }
/** /**
* Richtet die Form entlang der angegebenen Richtung an einer anderen Form * Richtet die Form entlang der angegebenen Richtung an einer anderen Form
* aus. Für {@code DOWN} wird beispielsweise die y-Koordinate der unteren * aus.
* Kante dieser Form an der unteren Kante der angegebenen Form {@code shape} * <p>
* ausgerichtet. Die x-Koordinate wird nicht verändert. {@code buff} gibt * {@code buff} gibt einen Abstand ab, um den diese From versetzt
* einen Abstand ab, um den diese From versetzt ausgerichtet werden soll. * ausgerichtet wird.
* <p>
* Für {@link #DOWN} wird beispielsweise die y-Koordinate der unteren Kante
* dieser Form an der unteren Kante von {@code shape} ausgerichtet. Die
* x-Koordinate wird in dem Fall nicht verändert.
* <p>
* Befinden sich die Formen beispielsweise in folgender Position:
* <pre>
* ┌─────┐
* │ │
* │ B │
* ┌─────┐ │ │
* │ │ └──D──┘
* │ A │
* │ │
* └──D──┘
* </pre>
* <p>
* <p>
* werden sie durch {@code alignTo} so positioniert:
* <pre>
* B.alignTo(A, EAST, 0) │ B.alignTo(A, EAST, 10)
* │
* ┌─────┐ ┌─────┐ │ ┌─────┐
* │ │ │ │ │ │ │ ┌─────┐
* │ A │ │ B │ │ │ A │ │ │
* │ │ │ │ │ │ │ │ B │
* └──D──┘ └──D──┘ │ └──D──┘ │ │
* │ └──D──┘
* │
* </pre>
* *
* @param shape * @param shape Die andere Form.
* @param dir * @param dir Die Richtung.
* @param buff * @param buff Der Abstand.
*/ */
public void alignTo( Shape shape, Options.Direction dir, double buff ) { public void alignTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir); Point2D anchorShape = shape.getAbsAnchorPoint(dir);
@@ -491,16 +631,46 @@ public abstract class Shape extends FilledShape {
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff; this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
} }
/**
* @param shape
* @param dir
* @see #nextTo(Shape, Options.Direction, double)
*/
public void nextTo( Shape shape, Options.Direction dir ) { public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, DEFAULT_BUFFER); nextTo(shape, dir, DEFAULT_BUFFER);
} }
/** /**
* Bewegt die Form neben eine andere in Richtung des angegebenen * Bewegt die Form neben eine andere in Richtung des angegebenen
* Ankerpunktes. Im Gegensatz zu * Ankerpunktes.
* {@link #moveTo(Shape, Options.Direction, double)} wird die Breite bzw. * <p>
* Höhe der Formen berücksichtigt und die Formen so platziert, dass keine * Im Gegensatz zu {@link #moveTo(Shape, Options.Direction, double)} wird
* Überlappungen vorhanden sind. * die Breite bzw. Höhe der Formen berücksichtigt und die Formen so
* platziert, dass keine Überlappungen vorhanden sind.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────┐
* │ │
* W B │
* ┌──────┐ │ │
* │ │ └─────┘
* │ A E
* │ │
* └──────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.nextTo(A, EAST, 0)} in diese Ausrichtung:
* <pre>
* B.nextTo(A, EAST, 0) │ B.nextTo(A, EAST, 10)
* │
* ┌─────┬─────┐ │ ┌─────┐ ┌─────┐
* │ │ │ │ │ │ │ │
* │ A │ B │ │ │ A │ │ B │
* │ │ │ │ │ │ │ │
* └─────┴─────┘ │ └─────┘ └─────┘
* │
* </pre>
* *
* @param shape * @param shape
* @param dir * @param dir
@@ -514,6 +684,19 @@ public abstract class Shape extends FilledShape {
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff; this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
} }
@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);
}
@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);
}
/*public void shear( double dx, double dy ) { /*public void shear( double dx, double dy ) {
verzerrung.shear(dx, dy); verzerrung.shear(dx, dy);
}*/ }*/
@@ -560,7 +743,7 @@ public abstract class Shape extends FilledShape {
shape = transform.createTransformedShape(shape); shape = transform.createTransformedShape(shape);
} }
Color currentColor = graphics.getColor(); java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics); fillShape(shape, graphics);
strokeShape(shape, graphics); strokeShape(shape, graphics);
graphics.setColor(currentColor); graphics.setColor(currentColor);
@@ -573,8 +756,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
@@ -590,4 +773,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.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

@@ -24,6 +24,7 @@ public class Text extends Shape {
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, DEFAULT_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);
@@ -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

@@ -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

@@ -136,57 +136,36 @@ public class BasicChartValue implements ChartValue {
this.color = color; this.color = color;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getX() { public double getX() {
return xValue; return xValue;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getValue() { public double getValue() {
return value; return value;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setValue( double pValue ) { public void setValue( double pValue ) {
this.value = pValue; this.value = pValue;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public String getLabel() { public String getLabel() {
return label; return label;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setLabel( String pLabel ) { public void setLabel( String pLabel ) {
this.label = pLabel; this.label = pLabel;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public Color getColor() { public Color getColor() {
return color; return color;
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setColor( Color pColor ) { public void setColor( Color pColor ) {
this.color = pColor; this.color = pColor;

View File

@@ -52,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

@@ -91,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

@@ -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

@@ -0,0 +1,4 @@
/**
* Diese Paket enthält Formen, die Diagramme darstellen.
*/
package schule.ngb.zm.shapes.charts;

View File

@@ -0,0 +1,13 @@
/**
* Dieses Paket enthält Implementationen der abstrakten
* {@link schule.ngb.zm.shapes.Shape} Klasse.
*
* Jede Unterklasse von {@code Shape} stellt eine konkrete Form wie ein
* {@link schule.ngb.zm.shapes.Rectangle Rechteck}, ein
* {@link schule.ngb.zm.shapes.Circle Kreis} oder ein
* {@link schule.ngb.zm.shapes.Picture Bild} dar.
*
* Mit {@link schule.ngb.zm.shapes.ShapeGroup} können Formen gruppiert
* und gemeinsam transformiert werden.
*/
package schule.ngb.zm.shapes;

View File

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

View File

@@ -4,55 +4,134 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/**
* Eine Hilfsklasse, um Dinge zu zählen.
* <p>
* Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden,
* die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder
* Methoden verändert werden kann.
* <p>
* Der Zähler kann aber auch Objekte zählen, indem die Instanzen an
* {@link #count(Object)} übergeben werden. Am Ende kann mit {@link #getCount()}
* die Anzahl der Objekte abgerufen werden.
* <p>
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein
* {@code Counter} auch das Maximum, das Minimum, die Summe und den Durchschnitt
* der gezählten Werte.
* <p>
* Ein Zähler kann auch komplette Arrays oder Listen von Zahlen zählen und die
* obigen Statistiken auswerten.
*/
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public final class Counter { public final class Counter {
/**
* Erstellt einen neuen {@code Counter}, der alle Integer im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( int[] values ) {
return new Counter().countAll(values);
}
/**
* Erstellt einen neuen {@code Counter}, der alle Doubles im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( double[] values ) { public static Counter fromArray( double[] values ) {
return new Counter().countAll(values); return new Counter().countAll(values);
} }
/**
* Erstellt einen neuen {@code Counter}, der alle Zahlen im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( Number[] values ) { public static Counter fromArray( Number[] values ) {
return new Counter().countAll(values); return new Counter().countAll(values);
} }
/**
* Erstellt einen neuen {@code Counter}, der alle Zahlen in der angegebenen
* Liste gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromList( List<Number> values ) { public static Counter fromList( List<Number> values ) {
return new Counter().countAll(values); return new Counter().countAll(values);
} }
private AtomicInteger count = new AtomicInteger(0); /**
* Aktuelle Anzahl gezählter Werte.
*/
private final AtomicInteger count = new AtomicInteger(0);
/**
* Statistiken zu den gezählten Werten.
*/
private double min = Double.NaN, max = Double.NaN, sum = Double.NaN; private double min = Double.NaN, max = Double.NaN, sum = Double.NaN;
public void Counter() { /**
* Erstellt einen neuen, leeren {@code Counter}.
*/
public Counter() {
} }
public void Counter( int initial ) { /**
* Ertstellt einen neuen {@code Counter}, der mit dem angegebenen Wert
* initialisiert ist.
*
* @param initial Wert des Zählers zu Beginn.
*/
public Counter( int initial ) {
count.set(initial); count.set(initial);
} }
/**
* @return Die aktuelle Anzahl.
*/
public int getCount() { public int getCount() {
return count.get(); return count.get();
} }
/**
* @return Das Maxium der bisher gezählten Werte.
*/
public double getMax() { public double getMax() {
synchronized( count ) { synchronized( count ) {
return max; return max;
} }
} }
/**
* @return Das Minimum der bisher gezählten Werte.
*/
public double getMin() { public double getMin() {
synchronized( count ) { synchronized( count ) {
return min; return min;
} }
} }
/**
* @return Die Summe der bisher gezählten Werte.
*/
public double getSum() { public double getSum() {
synchronized( count ) { synchronized( count ) {
return sum; return sum;
} }
} }
/**
* @return Der Mittelwert der bisher gezählten Werte.
*/
public double getAvg() { public double getAvg() {
if( Double.isNaN(sum) ) { if( Double.isNaN(sum) ) {
return Double.NaN; return Double.NaN;
@@ -61,11 +140,27 @@ public final class Counter {
} }
} }
/**
* Setzt den Zähler auf den angegebenen Wert.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @param count Der neue Wert des Zählers.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter setCount( int count ) { public Counter setCount( int count ) {
this.count.set(count); this.count.set(count);
return this; return this;
} }
/**
* Setzt den Zähler auf Null.
* <p>
* Die Statistiken werden auf {@link Double#NaN} gesetzt.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter reset() { public Counter reset() {
count.set(0); count.set(0);
@@ -77,18 +172,40 @@ public final class Counter {
return this; return this;
} }
/**
* Erhöht den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter inc() { public Counter inc() {
this.count.incrementAndGet(); this.count.incrementAndGet();
return this; return this;
} }
/**
* Verringert den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter dec() { public Counter dec() {
this.count.decrementAndGet(); this.count.decrementAndGet();
return this; return this;
} }
/**
* Zählt den angegebenen Wert.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param value Der neue Wert.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter count( double value ) { public Counter count( double value ) {
inc(); inc();
@@ -103,11 +220,27 @@ public final class Counter {
return this; return this;
} }
/**
* Zählt die angegebene Zahl.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param num Die neue Zahl.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter count( Number num ) { public Counter count( Number num ) {
return count(num.doubleValue()); return count(num.doubleValue());
} }
/**
* Zählt das angegebenen Objekt.
* <p>
* Erhöht den Zähler um Eins.
*
* @param obj Ein beliebiges Objekt.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Counter count( Object obj ) { public Counter count( Object obj ) {
if( obj instanceof Number ) { if( obj instanceof Number ) {
@@ -118,6 +251,13 @@ public final class Counter {
} }
} }
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) { public synchronized Counter countAll( double[] values ) {
for( double value : values ) { for( double value : values ) {
@@ -126,6 +266,28 @@ public final class Counter {
return this; return this;
} }
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( int[] values ) {
for( double value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Zahlen im angegebenen Array.
*
* @param values Das Array der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) { public synchronized Counter countAll( Number[] values ) {
for( Number value : values ) { for( Number value : values ) {
@@ -134,6 +296,13 @@ public final class Counter {
return this; return this;
} }
/**
* Zöhlt alle Zahlen in der angegebenen Sammlung.
*
* @param values Die Sammlung der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) { public synchronized Counter countAll( Collection<Number> values ) {
for( Number value : values ) { for( Number value : values ) {

View File

@@ -0,0 +1,400 @@
package schule.ngb.zm.util;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.font.LineMetrics;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Math.exp;
import static java.lang.Math.log;
import static schule.ngb.zm.Constants.random;
/**
* Eine Hilfsklasse, um zufällige Beispieldaten zu erzeugen.
* <p>
* Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen.
* Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder.
*/
@SuppressWarnings( "unused" )
public final class Faker {
/**
* URL, von der extern generierte Fake-Bilder geladen werden können.
* <p>
* Die URL wird als Format-String definiert mit zwei {@code %d}
* Platzhaltern, die durch die Breite und Höhe des gewünschten Bildes
* ersetzt werden.
*/
public static final String FAKE_IMG_URL = "https://loremflickr.com/%d/%d";
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Benutzerdaten.
* <p>
* Jeder Datensatz besteht aus einem String-Array mit den folgenden Daten
* <ul>
* <li><code>fakeUser[i][0]</code>: Vorname</li>
* <li><code>fakeUser[i][1]</code>: Nachname</li>
* <li><code>fakeUser[i][2]</code>: Geschlecht</li>
* <li><code>fakeUser[i][3]</code>: Nutzername</li>
* <li><code>fakeUser[i][4]</code>: Passwort</li>
* <li><code>fakeUser[i][5]</code>: E-Mail</li>
* <li><code>fakeUser[i][6]</code>: Geburtsdatum</li>
* </ul>
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[][] fakeUsers( int count ) {
return randomSample("users", count, ( line ) -> line.split(","), String[].class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Vornamen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeNames( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[0], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Namen im Format
* "Vorname Nachname".
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeFullnames( int count ) {
return randomSample("users", count, ( line ) -> {
String[] parts = line.split(",");
return parts[0] + " " + parts[1];
}, String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeUsernames( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[3], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Passwörter.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakePasswords( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[4], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger E-Mail-Adressen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeEmails( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[5], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger deutscher
* Wörter.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeStrings( int count ) {
return randomSample("words", count, ( line ) -> line, String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger
* {@code LocalDate}-Objekte, die ein Datum ohne Uhrzeit beschreiben.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static LocalDate[] fakeDates( int count ) {
long nowEpoch = LocalDate.now().toEpochDay();
long from = LocalDate.ofEpochDay(nowEpoch - 18 * 365).toEpochDay();
long to = LocalDate.ofEpochDay(nowEpoch - 14 * 365).toEpochDay();
LocalDate[] result = new LocalDate[count];
for( int i = 0; i < count; i++ ) {
result[i] = LocalDate.ofEpochDay((int) Constants.interpolate(from, to, random()));
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger
* {@code LocalDateTime}-Objekte, die einen Zeitpunkt mit Dateum und Uhrzeit
* beschreiben,
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static LocalDateTime[] fakeDatetimes( int count ) {
long nowEpoch = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long from = LocalDateTime.ofEpochSecond(nowEpoch - 18 * 365, 0, ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC);
long to = LocalDateTime.ofEpochSecond(nowEpoch - 14 * 365, 0, ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC);
LocalDateTime[] result = new LocalDateTime[count];
for( int i = 0; i < count; i++ ) {
result[i] = LocalDateTime.ofEpochSecond((int) Constants.interpolate(from, to, random()), 0, ZoneOffset.UTC);
}
return result;
}
/**
* Erzeugt einen Blindtext mit der angegebenen Anzahl Worten, aufgeteilt in
* die angegebene Anzahl Absätze.
* <p>
* Abssätze werden duch einen doppelten Zeilenumbruch {@code \n\n}
* getrennt.
*
* @param words Anzahl Wörter im Text insgesamt.
* @param paragraphs Anzahl Absätze.
* @return Ein zufälliger Blindtext.
*/
public static String fakeText( int words, int paragraphs ) {
String basetext = "";
try(
InputStream in = Faker.class.getResourceAsStream("mock-text.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))
) {
basetext = reader.readLine();
} catch( IOException ex ) {
LOG.error(ex, "Error generating fake blindtext: " + ex.getMessage());
}
String result = basetext.repeat(words / 283);
int w = (words % 283);
if( w > 0 ) {
Matcher m = Pattern.compile("([a-zA-Z_0-9\\u00C0-\\u017F]+\\W+){" + w + "}").matcher(basetext);
if( m.find() ) {
result += m.group().stripTrailing();
}
}
if( paragraphs > 1 ) {
int half = words / paragraphs;
final int maxLength = result.length();
Matcher m = Pattern.compile("([a-zA-Z_0-9\\u00C0-\\u017F]+\\W+){" + (half - 10) + "," + (half + 10) + "}([a-zA-Z_0-9\\u00C0-\\u017F]+)\\.").matcher(result);
if( m.find() ) {
result = m.replaceAll(( mr ) -> mr.end() == maxLength ? mr.group().trim() : mr.group().trim() + "\n\n");
}
}
return result;
}
/**
* Erzeugt ein Array mit der angegebenen Anzahl zufällig erzeugter Integer
* im angegebenen Bereich.
*
* @param count Anzahl der Zahlen im Array.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @return Ein Array mit Zufallszahlen.
* @see Constants#random(int, int)
*/
public static int[] fakeIntArray( int count, int min, int max ) {
int[] arr = new int[count];
for( int i = 0; i < count; i++ ) {
arr[i] = random(min, max);
}
return arr;
}
/**
* Erzeugt eine Liste mit der angegebenen Anzahl zufällig erzeugter Integer
* im angegebenen Bereich.
* <p>
* Ist {@code list} ein Listenobjekt, werden dei Zahlen an diese angehängt.
* Wird {@code null} übergeben, wird eine neue {@link ArrayList} erzeugt.
*
* @param count Anzahl der erzeugten Zahlen.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @param list Eine Liste, die befüllt werden soll, oder {@code null}.
* @return Ein Array mit Zufallszahlen.
* @see Constants#random(int, int)
*/
public static List<Integer> fakeIntegerList( int count, int min, int max, List<Integer> list ) {
List<Integer> result = (list == null) ? new ArrayList<>(count) : list;
fakeIntegers(count, min, max, result::add);
return result;
}
/**
* Erzeugt die angegebene Anzahl Zufallszahlen im angegebenen Bereich und
* übergibt sie an den angegebenen {@code Consumer}.
*
* Ein typischer Aufruf, um eine {@code #LinkedList} mit 100 Zufallszahlen
* zu erzeugen könnte so aussehen:
* <pre><code>
* List&lt;Integer&gt; l = new LinkedList&lt;&gt;();
* Faker.fakeIntegers(100, 0, 100, l::add);
* </code></pre>
*
* @param count Anzahl der erzeugten Zahlen.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @param con {@code Consumer} für die Zahlen.
*/
public static void fakeIntegers( int count, int min, int max, Consumer<Integer> con ) {
for( int i = 0; i < count; i++ ) {
con.accept(random(min, max));
}
}
public static <L> L fakeIntegers( int count, int min, int max, Supplier<L> sup, BiConsumer<L, Integer> con ) {
L result = sup.get();
for( int i = 0; i < count; i++ ) {
con.accept(result, random(min, max));
}
return result;
}
@SuppressWarnings( "unchecked" )
private static <T> T[] randomSample( String filename, int count, Function<String, T> transformer, Class<T> type ) {
T[] result = (T[]) Array.newInstance(type, count);
int i = 0;
double k = count; // cast to double
double W = exp(log(random()) / k);
try(
InputStream in = Faker.class.getResourceAsStream("mock-" + filename + ".csv");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))
) {
String line;
while( (line = reader.readLine()) != null ) {
if( i < count ) {
result[i] = transformer.apply(line);
i += 1;
} else {
int j = (int) (log(random()) / log(1 - W)) + 1;
while( j > 0 ) {
line = reader.readLine();
j -= 1;
}
if( line != null ) {
result[random(0, count - 1)] = transformer.apply(line);
i += 1;
} else {
break;
}
W *= exp(log(random()) / k);
}
}
// Fill remaining array
while( i < count ) {
result[i] = result[random(0, i - 1)];
i += 1;
}
} catch( IOException ex ) {
LOG.error(ex, "Error loading mock data file: " + ex.getMessage());
}
return result;
}
/**
* Erzeugt ein Platzhalter-Bild in der angegebenen Größe.
* <p>
* Das Bild ist ein aus dem Internet geladenes, zufälliges Motiv, dass unter
* einer freien Lizenz (Creative Commons) steht.
*
* @param width Breite des Bildes.
* @param height Höhe des Bildes.
* @return Ein zufälliges Bild in der angegebenen Größe.
*/
public static BufferedImage fakeImage( int width, int height ) {
return fakeImage(width, height, true);
}
/**
* Erzeugt ein Platzhalter-Bild in der angegebenen Größe.
* <p>
* Wenn {@code fromWeb} auf {@code true} gesetzt ist, wird ein zufälliges
* Motiv, das unter einer freien Lizenz (Creative Commons) steht, geladen.
* Bei {@code false} wird das Bild lokal generiert.
*
* @param width Breite des Bildes.
* @param height Höhe des Bildes.
* @param fromWeb Bei {@code true} wird das Bild aus dem Internet geladen,
* bei {@code false} wird das Bild lokal erzeugt.
* @return Ein zufälliges Bild in der angegebenen Größe.
*/
public static BufferedImage fakeImage( int width, int height, boolean fromWeb ) {
if( !fromWeb ) {
BufferedImage img = ImageLoader.createImage(width, height);
Graphics2D graphics = (Graphics2D) img.getGraphics().create();
String text = width + " x " + height;
Color clr = Constants.randomNiceColor();
graphics.setBackground(clr.getJavaColor());
graphics.clearRect(0, 0, width, height);
graphics.setColor(clr.textcolor().getJavaColor());
graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, (int) ((width + height) * 0.05)));
FontMetrics fontMerics = graphics.getFontMetrics();
LineMetrics lineMetrics = fontMerics.getLineMetrics(text, graphics);
graphics.drawString(text,
(width - fontMerics.stringWidth(text)) / 2,
(int) (height / 2 - lineMetrics.getDescent() + lineMetrics.getAscent() / 2)
);
graphics.dispose();
return img;
} else {
return ImageLoader.loadImage(String.format(FAKE_IMG_URL, width, height), false);
}
}
private Faker() {
}
private static final Log LOG = Log.getLogger(Faker.class);
}

View File

@@ -26,7 +26,8 @@ import static java.util.logging.Level.*;
* Klasse nur genau ein {@code Log}-Objekt erstellt. Mehrere {@code Log}s nutzen * Klasse nur genau ein {@code Log}-Objekt erstellt. Mehrere {@code Log}s nutzen
* dann aber denselben {@code Logger}. * dann aber denselben {@code Logger}.
* <p> * <p>
* Die API orientiert sich lose an <a href="#">Log4j</a> und vereinfacht die * Die API orientiert sich lose an <a
* href="https://logging.apache.org/log4j/2.x/">Log4j</a> und vereinfacht die
* Nutzung der Java logging API für die häufigsten Anwendungsfälle. * Nutzung der Java logging API für die häufigsten Anwendungsfälle.
*/ */
public final class Log { public final class Log {
@@ -76,7 +77,7 @@ public final class Log {
* mindestens herabgesenkt werden sollen. * mindestens herabgesenkt werden sollen.
*/ */
public static void enableGlobalLevel( Level level ) { public static void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue(); int lvl = Validator.requireNotNull(level, "level").intValue();
ensureRootLoggerInitialized(); ensureRootLoggerInitialized();
// Decrease level of root level ConsoleHandlers for output // Decrease level of root level ConsoleHandlers for output
@@ -133,7 +134,6 @@ public final class Log {
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null if( System.getProperty("java.util.logging.SimpleFormatter.format") == null
&& LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) { && LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) {
// System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) { rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) {
@Override @Override
public synchronized void publish( final LogRecord record ) { public synchronized void publish( final LogRecord record ) {
@@ -141,15 +141,6 @@ public final class Log {
flush(); flush();
} }
}); });
// rootLogger.setUseParentHandlers(false);
}
if( rootLogger.getUseParentHandlers() ) {
// This logger was not configured somewhere else
// Add a Handler and Formatter
//StreamHandler rootHandler = new StreamHandler(System.out, new SimpleFormatter());
//rootLogger.addHandler(rootHandler);
//rootLogger.setUseParentHandlers(false);
} }
LOGGING_INIT = true; LOGGING_INIT = true;
@@ -208,26 +199,15 @@ public final class Log {
} }
} }
private String inferCallerName() {
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
/// if( trace[i].getClassName().equals(sourceClass.getName()) ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
return trace[i].getMethodName();
}
}
return "unknown";
}
private void doLog( Level level, final Throwable throwable, final Supplier<String> msgSupplier ) { private void doLog( Level level, final Throwable throwable, final Supplier<String> msgSupplier ) {
String clazz = sourceClass.getName(); String clazz = sourceClass.getName();
String method = "unknown"; String method = "unknown";
StackTraceElement[] trace = new Throwable().getStackTrace(); StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) { for( StackTraceElement stackTraceElement : trace ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) { if( !stackTraceElement.getClassName().equals(Log.class.getName()) ) {
clazz = trace[i].getClassName(); clazz = stackTraceElement.getClassName();
method = trace[i].getMethodName(); method = stackTraceElement.getMethodName();
break; break;
} }
} }
@@ -368,7 +348,7 @@ public final class Log {
pw.close(); pw.close();
throwable = sw.toString(); throwable = sw.toString();
} }
return String.format(DEFAULT_LOG_FORMAT, return String.format(DEFAULT_DEBUG_FORMAT,
zdt, zdt,
source, source,
record.getLoggerName(), record.getLoggerName(),

View File

@@ -3,13 +3,14 @@ package schule.ngb.zm.util;
import java.util.Random; import java.util.Random;
/** /**
* Zufallsgenerator für Perlin-Noise. * Generator für Perlin-Noise.
* <p> * <p>
* Die Implementierung basiert auf dem von Ken Perlin entwickelten Algorithmus * Die Implementierung basiert auf dem von Ken Perlin entwickelten Algorithmus
* und wurde anhand der <a * und wurde anhand der <a
* href="https://adrianb.io/2014/08/09/perlinnoise.html">Beschreibung von * href="https://adrianb.io/2014/08/09/perlinnoise.html">Beschreibung von
* FLAFLA2</a> implementiert. * FLAFLA2</a> implementiert.
*/ */
@SuppressWarnings( "unused" )
public class Noise { public class Noise {
private static final int N = 256; private static final int N = 256;
@@ -17,7 +18,7 @@ public class Noise {
private static final int M = N - 1; private static final int M = N - 1;
/** /**
* Interne Permutationstabelle für diesen Generator * Interne Permutationstabelle für diesen Generator.
*/ */
private int[] p; private int[] p;
@@ -42,7 +43,7 @@ public class Noise {
/** /**
* Initialisiert diesen Perlin-Noise mit dem angegebenen Zufallsgenerator. * Initialisiert diesen Perlin-Noise mit dem angegebenen Zufallsgenerator.
* *
* @param rand * @param rand Ein Zufallsgenerator-Objekt.
*/ */
public Noise( Random rand ) { public Noise( Random rand ) {
init(rand); init(rand);

View File

@@ -2,77 +2,189 @@ package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**
* Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p>
* Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus
* gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn
* nach einem {@link #stop()} wieder {@link #start()} aufgerufen wird. Soll die
* Zeitmessung wieder bei null beginnen, muss vorher {@link #reset()} genutzt
* werden.
* <p>
* Die gemessene Zeit kann in {@link #getMillis() Millisekunden} oder
* {@link #getSeconds() Sekunden} abgerufen werden. Wird eine noch größere
* Genauigkeit benötigt, können mit {@link #getTime(TimeUnit)} beliebige
* Zeiteinheiten (zum Beispiel {@link TimeUnit#NANOSECONDS Nanosekunden})
* abgerufen werden.
* <p>
* Die Zeit kann auch bei laufender Uhr abgefragt werden. In dem Fall wird die
* bis zu diesem Zeitpunkt gemessene Zeit zurückgegeben.
*/
@SuppressWarnings( "unused" ) @SuppressWarnings( "unused" )
public final class Timer { public final class Timer {
/**
* Die Basiseinheit für die Zeitmessung.
*/
private final TimeUnit baseUnit; private final TimeUnit baseUnit;
/**
* Ob die Zeitmessung gerade läuft.
*/
private boolean running = false; private boolean running = false;
private long starttime = -1; /**
* Startzeit der Zeitmessung. -1, wenn noch keine Messung gestartet wurde.
*/
private long start = -1;
/**
* Zeit, die bisher bei allen Zeitmessungen, die gestoppt wurden, insgesamt
* vergangen ist.
*/
private long elapsed = 0; private long elapsed = 0;
/**
* Erstellt einen neuen {@code Timer} mit Millisekunden als Basiseinheit.
*/
public Timer() { public Timer() {
this(TimeUnit.MILLISECONDS); this(TimeUnit.MILLISECONDS);
} }
/**
* Erstellt einen {@code Timer}, der die angegebene Einheit als Basiseinheit
* für {@link #getTime()} benutzt.
* <p>
* Um eine Zeitmessung in Nanosekunden durchzuführen, kann der {@code Timer}
* beispielsweise so instanziiert werden:
* <pre><code>
* Timer clock = new Timer(TimeUnit.NANOSECONDS);
* </code></pre>
*
* @param baseUnit Die Basiseinheit für die Zeitmessung.
*/
public Timer( TimeUnit baseUnit ) { public Timer( TimeUnit baseUnit ) {
this.baseUnit = baseUnit; this.baseUnit = baseUnit;
} }
/**
* Ob die Zeitmessung gerade läuft.
*
* @return {@code true}, wenn die Zeitmessung mit {@link #start()} gestartet
* wurde.
*/
public boolean isRunning() { public boolean isRunning() {
return running; return running;
} }
/**
* Startet die Zeitmessung.
* <p>
* Wenn zuvor schon eine Zeitmessung gestartet wurde, wird die neue Messung
* zur Summe aller Messungen hinzuaddiert. Soll die Messung bei null
* starten, muss vorher {@link #reset()} verwendet werden:
*
* <pre><code>
* // Timer auf null stellen und sofort starten
* timer.reset().start();
* </code></pre>
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Timer start() { public Timer start() {
starttime = System.nanoTime(); start = System.nanoTime();
running = true; running = true;
return this; return this;
} }
/**
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Dauer wird
* zur Summe aller gemessenen Zeiten hinzuaddiert.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Timer stop() { public Timer stop() {
if( running ) {
running = false; running = false;
elapsed += System.nanoTime() - starttime; elapsed += System.nanoTime() - start;
}
return this; return this;
} }
/**
* Setzt den {@code Timer} auf den Startzustand und löscht alle bisher
* gemessenen Zeiten. Falls die Zeitmessung gerade läuft, stoppt sie nicht,
* sondern startet vom Zeitpunkt des Aufrufs neu.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" ) @SuppressWarnings( "UnusedReturnValue" )
public Timer reset() { public Timer reset() {
running = false;
starttime = -1;
elapsed = 0; elapsed = 0;
start = -1;
if( running ) {
start = System.nanoTime();
}
return this; return this;
} }
/**
* Gibt die Zeit in der Basiseinheit zurück.
*
* @return Die bisher insgesamt gemessene Zeit.
*/
public long getTime() { public long getTime() {
return getTime(baseUnit); return getTime(baseUnit);
} }
/**
* Gibt die Zeit in der angegebenen Einheit zurück.
* <p>
* Größere Zeiteinheiten werden gerundet und verlieren daher an Genauigkeit.
* Eine Zeitmessung von 999 Millisekunden wird als 0 Sekunden
* zurückgegeben.
* <p>
* Um genauere Ergebnisse zu erhalten, kann mit {@link #getMillis()} und
* {@link #getSeconds()} die gemessene Zeit in Minuten beziehungsweise
* Sekunden als Kommazahl abgefragt werden.
*
* @param unit Zeiteinheit
* @return Die bisher insgesamt gemessene Zeit in der gewählten Zeiteinheit.
*/
public long getTime( TimeUnit unit ) { public long getTime( TimeUnit unit ) {
if( running ) { if( running ) {
return unit.convert(System.nanoTime() - starttime + elapsed, TimeUnit.NANOSECONDS); return unit.convert(System.nanoTime() - start + elapsed, TimeUnit.NANOSECONDS);
} else { } else {
return unit.convert(elapsed, TimeUnit.NANOSECONDS); return unit.convert(elapsed, TimeUnit.NANOSECONDS);
} }
} }
/**
* Gibt die gemessene Zeit in Millisekunden (gerundet) zurück.
*
* @return Die gemessene Zeit in ms.
*/
public int getMillis() { public int getMillis() {
if( running ) { if( running ) {
return (int) ((System.nanoTime() - starttime + elapsed) / 1000000); return (int) ((System.nanoTime() - start + elapsed) / 1000000);
} else { } else {
return (int) (elapsed / 1000000); return (int) (elapsed / 1000000);
} }
} }
/**
* Gibt die gemessene Zeit in Sekunden zurück.
*
* @return Die gemessene Zeit in s.
*/
public double getSeconds() { public double getSeconds() {
if( running ) { if( running ) {
return (System.nanoTime() - starttime + elapsed) / 1000000000.0; return (System.nanoTime() - start + elapsed) / 1000000000.0;
} else { } else {
return elapsed / 1000000000.0; return elapsed / 1000000000.0;
} }

View File

@@ -1,37 +1,60 @@
package schule.ngb.zm.util; package schule.ngb.zm.util;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
/**
* Statische Methoden, um Methodenparameter auf Gültigkeit zu prüfen.
*/
@SuppressWarnings( {"unused", "UnusedReturnValue"} )
public class Validator { public class Validator {
public static final <T> T requireNotNull( T obj ) { public static final <T> T requireNotNull( T obj, CharSequence paramName ) {
return Objects.requireNonNull(obj); return requireNotNull(obj, () -> "Parameter <%s> may not be null.".format(paramName.toString()));
} }
public static final <T> T requireNotNull( T obj, CharSequence msg ) { public static final <T> T requireNotNull( T obj, CharSequence paramName, CharSequence msg ) {
return Objects.requireNonNull(obj, msg::toString); return requireNotNull(obj, () -> msg.toString().format(paramName.toString()));
} }
public static final <T> T requireNotNull( T obj, Supplier<String> msg ) { public static final <T> T requireNotNull( T obj, Supplier<String> msg ) {
return Objects.requireNonNull(obj, msg); if( obj == null ) {
throw new NullPointerException(msg == null ? "Parameter may nut be null." : msg.get());
}
return obj;
} }
public static final String requireNotEmpty( String str ) { public static final String requireNotEmpty( String str, CharSequence paramName ) {
return requireNotEmpty(str, (Supplier<String>)null); return requireNotEmpty(str, () -> String.format("Parameter <%s> may not be empty string (<%s> provided)", paramName, str));
} }
public static final String requireNotEmpty( String str, CharSequence msg ) { public static final String requireNotEmpty( String str, CharSequence paramName, CharSequence msg ) {
return requireNotEmpty(str, msg::toString); return requireNotEmpty(str, () -> msg.toString().format(paramName.toString()));
} }
public static final String requireNotEmpty( String str, Supplier<String> msg ) { public static final String requireNotEmpty( String str, Supplier<String> msg ) {
if( str.isEmpty() ) if( str.isEmpty() ) {
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be empty string (<%s> provided)", str) : msg.get()); throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be empty string (<%s> provided)", str) : msg.get());
}
return str; return str;
} }
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName ) {
return requireNotEmpty(arr, () -> String.format("Parameter <%s> may not be empty", paramName));
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName, CharSequence msg ) {
return requireNotEmpty(arr, () -> msg.toString().format(paramName.toString()));
}
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
if( arr.length == 0 )
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
return arr;
}
public static final int requirePositive( int i ) { public static final int requirePositive( int i ) {
return requirePositive(i, (Supplier<String>) null); return requirePositive(i, (Supplier<String>) null);
} }
@@ -136,21 +159,6 @@ public class Validator {
} }
*/ */
public static final <T> T[] requireNotEmpty( T[] arr ) {
return requireNotEmpty(arr, (Supplier<String>)null);
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence msg ) {
return requireNotEmpty(arr, msg::toString);
}
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
if( arr.length == 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array may not be empty") : msg.get());
return arr;
}
public static final <T> T[] requireSize( T[] arr, int size ) { public static final <T> T[] requireSize( T[] arr, int size ) {
return requireSize(arr, size, (Supplier<String>) null); return requireSize(arr, size, (Supplier<String>) null);
} }
@@ -161,21 +169,7 @@ public class Validator {
public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) { public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) {
if( arr.length != size ) if( arr.length != size )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array must have <%d> elements (<%d> provided)", size) : msg.get()); throw new IllegalArgumentException(msg == null ? String.format("Parameter array must have <%d> elements (<%d> provided)", size, arr.length) : msg.get());
return arr;
}
public static final <T> T[] requireValid( T[] arr ) {
return requireValid(arr, (Supplier<String>)null);
}
public static final<T> T[] requireValid( T[] arr, CharSequence msg ) {
return requireValid(arr, msg::toString);
}
public static final <T> T[] requireValid( T[] arr, Supplier<String> msg ) {
if( arr == null || arr.length > 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array may not be null or empty") : msg.get());
return arr; return arr;
} }

View File

@@ -7,11 +7,68 @@ import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
/**
* Implementierung einer EventListener-API basierend auf dem Artikel
* <a href="https://org.coloradomesa.edu/~wmacevoy/listen/paper.html">Skilled
* Listening in Java</a> von Dr. Warren D. MacEvoy.
* <p>
* Der {@code EventDispatcher} verwaltet eine Liste von angemeldeten Listenern
* in einem {@link CopyOnWriteArraySet}, dass besonders für Situationen geeignet
* ist, in denen in der Regel wenige Objekte verwaltet werden müssen, auf die
* schnell zugegriffen werden soll und die sich selten ändern. Das trifft in den
* meisten Fällen auf die Listener eines Objekts zu.
* <p>
* Um einen {@code EventDispatcher} zu nutzen, muss eine Schnittstelle als
* Unterinterface von {@link Listener} erstellt werden, die Methoden für die
* einzelnen Events definiert. In der Regel wird eine Methode pro Event
* angelegt, aber dies ist nicht unbedingt notwendig.
* <p>
* Ein Objekt, dass Events für diese Art von Listener erzeugt, erstellt einen
* {@code EventDispatcher} und
* {@link #registerEventType(String, BiConsumer) registriert} die notwendigen
* Events.
*
* <pre><code>
* EventDispatcher&lt;MyEvent, MyEventListener&gt; dispatcher = new EventDispatcher&lt;&gt;();
* dispatcher.registerEventType("start", (evt, listener) -> listener.started(evt));
* dispatcher.registerEventType("stop", (evt, listener) -> listener.stopped(evt));
* </code></pre>
* <p>
* Hier werden zwei Events registriert "start" und "stop". Die Bezeichnung der
* Events wird nut intern verwendet. Jedes Event registriert eine Funktion, die
* den Aufruf der Listener-Schnittstelle umsetzt. In der Regel ist dies ein
* Lambda-Ausdruck von zwei Parametern: dem Event-Objekt vom Typ {@code E} und
* der Listener, der aufgerufen werden soll, vom Typ {@code L}.
* <p>
* Nun können {@code MyEventListener} angemeldet und Events ausgelöst werden.
*
* <pre><code>
* public void addEventListener( MyEventListener listener ) {
* dispatcher.addListener(listener);
* }
*
* public void removeEventListener( MyEventListener listener ) {
* dispatcher.removeListener(listener);
* }
*
* public void methodThatCreatesEvents() {
* dispatcher.dispatchEvent("start", new MyEvent());
* // Do something
* dispatcher.dispatchEvent("stop", new MyEvent());
* }
* </code></pre>
* <p>
* Siehe {@link schule.ngb.zm.media.AudioListener} und
* {@link schule.ngb.zm.media.Music} für ein Beispiel der Verwendung.
*
* @param <E> Typ der Event-Objekte.
* @param <L> Typ der verwendeten Listener-Schnittstelle.
*/
public class EventDispatcher<E, L extends Listener<E>> { public class EventDispatcher<E, L extends Listener<E>> {
private CopyOnWriteArraySet<L> listeners; private final CopyOnWriteArraySet<L> listeners;
private ConcurrentMap<String, BiConsumer<E, L>> eventRegistry; private final ConcurrentMap<String, BiConsumer<E, L>> eventRegistry;
public EventDispatcher() { public EventDispatcher() {
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
@@ -19,8 +76,8 @@ public class EventDispatcher<E, L extends Listener<E>> {
} }
public void registerEventType( String eventKey, BiConsumer<E, L> dispatcher ) { public void registerEventType( String eventKey, BiConsumer<E, L> dispatcher ) {
Validator.requireNotNull(eventKey); Validator.requireNotNull(eventKey, "eventKey");
Validator.requireNotNull(dispatcher); Validator.requireNotNull(dispatcher, "dispatcher");
if( !eventRegistered(eventKey) ) { if( !eventRegistered(eventKey) ) {
eventRegistry.put(eventKey, dispatcher); eventRegistry.put(eventKey, dispatcher);
@@ -35,6 +92,7 @@ public class EventDispatcher<E, L extends Listener<E>> {
listeners.remove(listener); listeners.remove(listener);
} }
@SuppressWarnings( "unused" )
public boolean hasListeners() { public boolean hasListeners() {
return !listeners.isEmpty(); return !listeners.isEmpty();
} }
@@ -44,14 +102,12 @@ public class EventDispatcher<E, L extends Listener<E>> {
} }
public void dispatchEvent( String eventKey, final E event ) { public void dispatchEvent( String eventKey, final E event ) {
Validator.requireNotNull(eventKey); Validator.requireNotNull(eventKey, "eventKey");
Validator.requireNotNull(event); Validator.requireNotNull(event, "event");
if( eventRegistered(eventKey) ) { if( eventRegistered(eventKey) ) {
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey); final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
listeners.forEach(( listener ) -> { listeners.forEach(( listener ) -> dispatcher.accept(event, listener));
dispatcher.accept(event, listener);
});
} }
} }

View File

@@ -1,7 +1,11 @@
package schule.ngb.zm.util.events; package schule.ngb.zm.util.events;
/**
* Generische Basisschnittstelle für die Implementierung von EventListenern.
*
* @param <E> Typ der Event-Objekte, die von diesem Listener empfangen werden.
* @see EventDispatcher
*/
public interface Listener<E> { public interface Listener<E> {
} }

View File

@@ -0,0 +1,5 @@
/**
* Dieses Paket enthält Hilfsklassen, die das Listener-Entwurfsmuster
* umsetzen.
*/
package schule.ngb.zm.util.events;

View File

@@ -2,86 +2,196 @@ package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.util.ArrayList;
import java.nio.file.Paths;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Function;
/**
* Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
*/
@SuppressWarnings( "unused" )
public final class FileLoader { public final class FileLoader {
/**
* Charset: ASCII
*/
public static final Charset ASCII = StandardCharsets.US_ASCII; public static final Charset ASCII = StandardCharsets.US_ASCII;
/**
* Charset: UTF-8
*/
public static final Charset UTF8 = StandardCharsets.UTF_8; public static final Charset UTF8 = StandardCharsets.UTF_8;
/**
* Charset: UTF-16
*/
public static final Charset UTF16 = StandardCharsets.UTF_16; public static final Charset UTF16 = StandardCharsets.UTF_16;
/**
* Charset: ISO-8859-1
*/
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1; public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
/**
* Lädt die angegebene Textdatei zeilenweise in eine Liste.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Die Quelle der Textdatei.
* @return Eine Liste mit den Zeilen der Textdatei.
* @see #loadLines(String, Charset)
*/
public static List<String> loadLines( String source ) { public static List<String> loadLines( String source ) {
return loadLines(source, UTF8); return loadLines(source, UTF8);
} }
/**
* Lädt die angegebene Textdatei mit dem angegebenen Charset zeilenweise in
* eine Liste.
* <p>
* Am Ende jeder Zeile wird das Symbol für einen Zeilenumbruch ({@code \n})
* entfernt.
*
* @param source Die Quelle der Textdatei.
* @param charset Das zu verwendene {@code Charset}.
* @return Eine Liste mit den Zeilen der Textdatei.
*/
public static List<String> loadLines( String source, Charset charset ) { public static List<String> loadLines( String source, Charset charset ) {
try { try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
return Files.readAllLines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset); List<String> result = new ArrayList<>();
} catch( IOException | URISyntaxException ex ) {
String line;
while( (line = reader.readLine()) != null ) {
result.add(line);
}
return result;
} catch( IOException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source); LOG.error(ex, "Error while loading lines from source <%s>", source);
return Collections.emptyList();
}
} }
return Collections.EMPTY_LIST; /**
} * Lädt den Inhalt der angegebenen Textdatei vollständig in einen String.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source ) { public static String loadText( String source ) {
return loadText(source, UTF8); return loadText(source, UTF8);
} }
/**
* Lädt den Inhalt der angegebenen Textdatei mit dem angegebenen
* {@code Charset} vollständig in einen String.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @param charset Der {@code Charset} der Textdatei.
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source, Charset charset ) { public static String loadText( String source, Charset charset ) {
try { try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
return Files.readString(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset); StringBuilder result = new StringBuilder();
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading text from source <%s>", source); String line;
while( (line = reader.readLine()) != null ) {
result.append(line).append('\n');
} }
return result.toString();
} catch( IOException ex ) {
LOG.error(ex, "Error while loading string from source <%s>", source);
return ""; return "";
} }
}
/**
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
* String-Array.
* <p>
* Der Aufruf entspricht
* <pre><code>
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
* </code></pre>
*
* @param source Eine Quelle für die CSV-Datei (Absoluter Dateipfad,
* * Dateipfad im Classpath oder Netzwerkressource)
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @return Ein Array mit den Daten als {@code String}s.
* @see #loadCsv(String, char, boolean, Charset)
*/
public static String[][] loadCsv( String source, boolean skipFirst ) { public static String[][] loadCsv( String source, boolean skipFirst ) {
return loadCsv(source, ',', skipFirst, UTF8); return loadCsv(source, ',', skipFirst, UTF8);
} }
/**
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
* String-Array.
* <p>
* Die Methode ist nicht in der Lage, komplexe CSV-Dateien zu verarbeiten.
* Insbesondere werden Inhalte, die das Trennzeichen {@code separator}
* enthalten, nicht korrekt erkannt. Das Trennzeichen wird unabhängig
* vom Kontext immer als Zelltrenner erkannt. (Im Normalfall kann das
* Trennzeichen durch die Verwendung doppelter Anführungszeichen in der Art
* {@code Inhalt,"Inhalt, der Komma enthält",Inhalt} maskiert werden.)
* <p>
* Es wird auch keine erweiterte Inhaltserkennung ausgeführt, sondern alle
* Inhalte als {@code String} gelesen. Die weitere Verarbeitung mit den
* passenden Parser-Methoden (beispielsweise
* {@link Double#parseDouble(String)}) obligt dem Nutzer.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static String[][] loadCsv( String source, char separator, boolean skipFirst, Charset charset ) { public static String[][] loadCsv( String source, char separator, boolean skipFirst, Charset charset ) {
try {
int n = skipFirst ? 1 : 0; int n = skipFirst ? 1 : 0;
return Files List<String> lines = loadLines(source, charset);
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset) return lines.stream().skip(n).map(
.skip(n) //( line ) -> line.split(Character.toString(separator))
.map( ( line ) -> line.split("\\s*" + separator + "\\s*")
( line ) -> line.split(Character.toString(separator))
).toArray(String[][]::new); ).toArray(String[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading csv source <%s>", source);
}
return new String[0][0];
} }
public static double[][] loadValues( String source, char separator, boolean skipFirst ) { public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
return loadValues(source, separator, skipFirst, UTF8); return loadValues(source, separator, skipFirst, UTF8);
} }
/**
* Lädt Double-Werte aus einer CSV Datei in ein zweidimensionales Array.
* <p>
* Die gelesenen Strings werden mit {@link Double#parseDouble(String)} in
* {@code double} umgeformt. Es leigt in der Verantwortung des Nutzers
* sicherzustellen, dass die CSV-Datei auch nur Zahlen enthält, die korrekt
* in {@code double} umgewandelt werden können. Zellen für die die
* Umwandlung fehlschlägt werden mit 0.0 befüllt.
* <p>
* Die Methode unterliegt denselben Einschränkungen wie
* {@link #loadCsv(String, char, boolean, Charset)}.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static double[][] loadValues( String source, char separator, boolean skipFirst, Charset charset ) { public static double[][] loadValues( String source, char separator, boolean skipFirst, Charset charset ) {
try {
int n = skipFirst ? 1 : 0; int n = skipFirst ? 1 : 0;
return Files List<String> lines = loadLines(source, charset);
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset) return lines.stream().skip(n).map(
.skip(n)
.map(
( line ) -> Arrays ( line ) -> Arrays
.stream(line.split(Character.toString(separator))) .stream(line.split(Character.toString(separator)))
.mapToDouble( .mapToDouble(
@@ -94,14 +204,9 @@ public final class FileLoader {
} }
).toArray() ).toArray()
).toArray(double[][]::new); ).toArray(double[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading double values from csv source <%s>", source);
} }
return new double[0][0]; private FileLoader() {
}
public FileLoader() {
} }
private static final Log LOG = Log.getLogger(FileLoader.class); private static final Log LOG = Log.getLogger(FileLoader.class);

View File

@@ -1,40 +1,57 @@
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.awt.Font; import java.awt.Font;
import java.awt.FontFormatException; import java.awt.FontFormatException;
import java.awt.Image;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/**
* Eine Hilfsklasse mit Klassenmethoden, um Schriftarten zu laden.
* <p>
* Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten,
* die aus Dateien geladen wurden, werden in einem internen Cache gespeichert
* und nachfolgende Versuche, dieselbe Schriftart zu laden, werden aus dem Cache
* bedient.
*/
public class FontLoader { public class FontLoader {
private static final Map<String, Font> fontCache = new ConcurrentHashMap<>(); private static final Cache<String, Font> fontCache = Cache.newSoftCache();
/** /**
* Lädt eine Schrift aus einer Datei. * Lädt eine Schrift aus einer Datei.
* <p> * <p>
* Die Schrift wird unter ihrem Dateinamen in den Schriftenspeicher geladen * Die Methode kann eine Liste von Schriften bekommen und probiert diese
* und kann danach in der Zeichenmaschine benutzt werden. * nacheinander zu laden. Die erste Schrift, die Fehlerfrei geladen werden
* * kann, wird zurückgegeben. Kann keine der Schriften geladen werden, ist
* Ein Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen * das Ergebnis {@code null}.
* <p>
* Die gefundene Schrift wird unter ihrem Dateinamen in den
* Schriftenspeicher geladen und kann danach in der Zeichenmaschine benutzt
* werden.
* <p>
* Eine Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem * "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");} * {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden. * verwendet werden.
* *
* @param source * @param source
* @return * @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/ */
public static Font loadFont( String source ) { public static Font loadFont( String source ) {
String name = source; String name = source;
// Dateipfad entfernen // Dateipfad entfernen
int lastIndex = source.lastIndexOf(File.separatorChar); int lastIndex = source.lastIndexOf(File.separatorChar);
if( lastIndex > -1 ) { if( lastIndex > -1 ) {
source.substring(lastIndex + 1); source = source.substring(lastIndex + 1);
} }
// Dateiendung entfernen // Dateiendung entfernen
lastIndex = name.lastIndexOf('.'); lastIndex = name.lastIndexOf('.');
@@ -45,10 +62,8 @@ public class FontLoader {
} }
public static Font loadFont( String name, String source ) { public static Font loadFont( String name, String source ) {
Objects.requireNonNull(source, "Font source may not be null"); Validator.requireNotNull(source, "Font source may not be null");
if( source.length() == 0 ) { Validator.requireNotEmpty(source, "Font source may not be empty.");
throw new IllegalArgumentException("Font source may not be empty.");
}
if( fontCache.containsKey(name) ) { if( fontCache.containsKey(name) ) {
LOG.trace("Retrieved font <%s> from font cache.", name); LOG.trace("Retrieved font <%s> from font cache.", name);
@@ -57,7 +72,7 @@ public class FontLoader {
// Look for System fonts // Look for System fonts
Font font = Font.decode(source); Font font = Font.decode(source);
if( font != null && source.toLowerCase().contains(font.getFamily().toLowerCase()) ) { if( source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(name, font); fontCache.put(name, font);
fontCache.put(source, font); fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source); LOG.debug("Loaded system font for <%s>.", source);
@@ -85,6 +100,39 @@ public class FontLoader {
return font; return font;
} }
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Methode kann eine Liste von Schriften bekommen und probiert diese
* nacheinander zu laden. Die erste Schrift, die Fehlerfrei geladen werden
* kann, wird zurückgegeben. Kann keine der Schriften geladen werden, ist
* das Ergebnis {@code null}.
* <p>
* Die gefundene Schrift wird unter ihrem Dateinamen in den
* Schriftenspeicher geladen und kann danach in der Zeichenmaschine benutzt
* werden.
* <p>
* Eine Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden.
*
* @param name
* @param sources
* @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/
public static Font loadFonts( String name, String... sources ) {
for( String fontSource : sources ) {
// TODO: Ignore exceptions in this case and throw own at end?
Font font = loadFont(name, fontSource);
if( font != null ) {
return font;
}
}
return null;
}
private FontLoader() { private FontLoader() {
} }

View File

@@ -1,6 +1,7 @@
package schule.ngb.zm.util.io; package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log; import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
@@ -15,16 +16,19 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
* <p>
* Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die
* Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche,
* dieselbe Quelle zu laden, werden aus dem Cache bedient.
*/
public final class ImageLoader { public final class ImageLoader {
public static boolean caching = true; public static boolean caching = true;
private static Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>(); private static final Cache<String, BufferedImage> imageCache = Cache.newSoftCache();
private static SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
/** /**
* Lädt ein Bild von der angegebenen Quelle {@code source}. * Lädt ein Bild von der angegebenen Quelle {@code source}.
@@ -48,12 +52,12 @@ public final class ImageLoader {
/** /**
* Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das * Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* Bild zurück oder {@code null}, wenn das Bild nicht geladen werden * Bild zurück oder {@code null}, wenn das Bild nicht geladen werden konnte.
* konnte. Ist ein Bild mit der angegebenen Quelle im Cache, wird das * Ist ein Bild mit der angegebenen Quelle im Cache, wird das gespeicherte
* gespeicherte Bild zurückgegeben. Dies kann mit {@code caching = false} * Bild zurückgegeben. Dies kann mit {@code caching = false} verhindert
* verhindert werden. * werden.
* <p> * <p>
* Wurde das Chaching global deaktiviert, kann mit <code>caching = * Wurde das Caching global deaktiviert, kann mit <code>caching =
* true</code> das Bild trotzdem aus dem Cache geladen werden, wenn es * true</code> das Bild trotzdem aus dem Cache geladen werden, wenn es
* vorhanden ist. * vorhanden ist.
* *
@@ -66,8 +70,8 @@ public final class ImageLoader {
Validator.requireNotNull(source, "Image source may not be null"); Validator.requireNotNull(source, "Image source may not be null");
Validator.requireNotEmpty(source, "Image source may not be empty."); Validator.requireNotEmpty(source, "Image source may not be empty.");
if( caching && isCached(source) ) { if( caching && imageCache.containsKey(source) ) {
return getCache(source); return imageCache.get(source);
} }
BufferedImage img = null; BufferedImage img = null;
@@ -78,7 +82,7 @@ public final class ImageLoader {
img = ImageIO.read(in); img = ImageIO.read(in);
if( caching && img != null ) { if( caching && img != null ) {
putCache(source, img); imageCache.put(source, img);
} }
} catch( IOException ioex ) { } catch( IOException ioex ) {
LOG.error(ioex, "Error loading image file from source <%s>.", source); LOG.error(ioex, "Error loading image file from source <%s>.", source);
@@ -106,16 +110,16 @@ public final class ImageLoader {
public static boolean preloadImage( String name, String source ) { public static boolean preloadImage( String name, String source ) {
BufferedImage img = loadImage(source, true); BufferedImage img = loadImage(source, true);
if( img != null ) { if( img != null ) {
putCache(name, img); imageCache.put(name, img);
return true; return true;
} }
return false; return false;
} }
/** /**
* Speiechert das angegebene Bild unter dem angegebenen Namen im Cache. * Speichert das angegebene Bild unter dem angegebenen Namen im Cache.
* <p> * <p>
* Exisitert zu {@code name} schon ein Bild im Zwischenspeicher * Existiert zu {@code name} schon ein Bild im Zwischenspeicher
* ({@code ImageLoader.isCached(name) == true}), dann wird dieses * ({@code ImageLoader.isCached(name) == true}), dann wird dieses
* überschrieben. * überschrieben.
* <p> * <p>
@@ -127,7 +131,7 @@ public final class ImageLoader {
* @param img ZU speicherndes Bild. * @param img ZU speicherndes Bild.
*/ */
public static void preloadImage( String name, BufferedImage img ) { public static void preloadImage( String name, BufferedImage img ) {
putCache(name, img); imageCache.put(name, img);
} }
/** /**
@@ -138,8 +142,7 @@ public final class ImageLoader {
* {@code false}. * {@code false}.
*/ */
public static boolean isCached( String name ) { public static boolean isCached( String name ) {
SoftReference<BufferedImage> imgRef = imageCache.get(name); return imageCache.containsKey(name);
return imgRef != null && imgRef != NOCACHE && imgRef.get() != null;
} }
/** /**
@@ -149,34 +152,8 @@ public final class ImageLoader {
* @param name Name des Bildes im Cache. * @param name Name des Bildes im Cache.
*/ */
public static void invalidateCache( final String name ) { public static void invalidateCache( final String name ) {
if( imageCache.containsKey(name) ) {
imageCache.remove(name); imageCache.remove(name);
} }
}
/**
* Speichert ein Bild als {@link SoftReference} im Cache.
*
* @param name Name des Bildes im Zwischenspeicher.
* @param img Das zu speichernde Bild.
*/
private static void putCache( final String name, final BufferedImage img ) {
imageCache.put(name, new SoftReference<>(img));
}
/**
* Holt ein Bild aus dem Cache.
* <p>
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
* werden kann.
*
* @param name
* @return
*/
private static BufferedImage getCache( final String name ) {
return imageCache.get(name).get();
}
/** /**
* Deaktiviert den Cache für die angegebene Quelle. * Deaktiviert den Cache für die angegebene Quelle.
@@ -187,14 +164,14 @@ public final class ImageLoader {
* *
* @param name Die Quelle des Bildes. * @param name Die Quelle des Bildes.
*/ */
public static void preventCache( final String name ) { public static void disableCache( final String name ) {
imageCache.put(name, NOCACHE); imageCache.disableCache(name);
} }
/** /**
* Leer den Cache und löschte alle bisher gespeicherten Bilder. * Leer den Cache und löschte alle bisher gespeicherten Bilder.
* <p> * <p>
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden * Auch vorher mit {@link #disableCache(String)} verhinderte Caches werden
* gelöscht und müssen neu gesetzt werden. * gelöscht und müssen neu gesetzt werden.
*/ */
public static void clearCache() { public static void clearCache() {
@@ -211,7 +188,7 @@ public final class ImageLoader {
* Bild zurückgegeben und nicht komplett neu geladen. * Bild zurückgegeben und nicht komplett neu geladen.
* <p> * <p>
* <b>Wichtig:</b> Bildreferenzen, die aus dem Cache geladen werden, * <b>Wichtig:</b> Bildreferenzen, die aus dem Cache geladen werden,
* verweisen alle auf dasselbe Objekt. Änderungen schalgen sich daher in * verweisen alle auf dasselbe Objekt. Änderungen schlagen sich daher in
* allen anderen Versionen des Bildes nieder (inklusive dem Bild im * allen anderen Versionen des Bildes nieder (inklusive dem Bild im
* Zwischenspeicher). Für Änderungen sollte daher immer * Zwischenspeicher). Für Änderungen sollte daher immer
* {@link #copyImage(BufferedImage) eine Kopie} des Bildes erstellt werden: * {@link #copyImage(BufferedImage) eine Kopie} des Bildes erstellt werden:
@@ -302,7 +279,7 @@ public final class ImageLoader {
* @param file Die Zieldatei. * @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei * @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen, * überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert. * wenn die Datei schon existiert.
* @throws NullPointerException Falls {@code image} oder {@code file} * @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist. * {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab. * @throws IOException Falls es einen Fehler beim Speichern gab.
@@ -335,7 +312,7 @@ public final class ImageLoader {
* @param file Die Zieldatei. * @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei * @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen, * überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert. * wenn die Datei schon existiert.
* @throws NullPointerException Falls {@code image} oder {@code file} * @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist. * {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab. * @throws IOException Falls es einen Fehler beim Speichern gab.

View File

@@ -5,20 +5,72 @@ import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator; import schule.ngb.zm.util.Validator;
import java.io.*; import java.io.*;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten. * Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
*/ */
@SuppressWarnings("unused")
public class ResourceStreamProvider { public class ResourceStreamProvider {
/** /**
* Sucht eine zur angegebenen Quelle passende Resource und öffnet einen * Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon * Resource Locator</em>). Eine passende Datei-Ressource wird wie folgt
* ab, welche Art an Resource gefunden wird: * ermittelt:
* <ol> * <ol>
* <li>Ist {@code source} eine existierende Datei * <li>Ist {@code source} eine existierende Datei
* {@code new File(source}.isFile() == true}, dann wird ein * ({@code new File(source}.isFile() == true})?</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt ({@code getResource(source) != null})?</li>
* <li>Ist {@code source} im Classpath enthalten ({@code getClassLoader().getResource(source) != null})?</li>
* <li>Ansonsten erstelle ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* <p>
* Auftretende Exceptions werden als {@link IOException} geworfen.
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link java.net.MalformedURLException} beim Konstruieren des {@code URL}
* zu einer Datei verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Ein {@code InputStream} für die Ressource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim Erzeugen einer URL zu
* einer bestehenden Ressource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
// Ist source ein valider Dateipfad?
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
// Ist source im Classpath vorhanden?
URL url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
// Dann versuchen aus source direkt eine URL zu machen.
return new URL(source);
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
* ab, welche Art an Ressource gefunden wird:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* {@code new File(source).isFile() == true}, dann wird ein
* {@link FileInputStream} erstellt.</li> * {@link FileInputStream} erstellt.</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt, wird ein passender * <li>Ist {@code source} ein relativer Pfad im Projekt, wird ein passender
* Stream mit {@link Class#getResourceAsStream(String)} geöffnet.</li> * Stream mit {@link Class#getResourceAsStream(String)} geöffnet.</li>
@@ -28,153 +80,130 @@ public class ResourceStreamProvider {
* {@link URL#openStream() Netwerkstream} geöffnet.</li> * {@link URL#openStream() Netwerkstream} geöffnet.</li>
* </ol> * </ol>
* <p> * <p>
* Die Möglichen Resourcen werden in der gelisteten Reihenfolge durchprobiert * Die Möglichen Ressourcen werden in der gelisteten Reihenfolge durchprobiert
* und der erste gefundene Stream zurückgegeben. Auftretende Exceptions * und der erste gefundene Stream zurückgegeben. Auftretende Exceptions
* werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source} * werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source}
* keine gültige Resource gefunden werden kann, wird am Ende die * keine gültige Ressource gefunden werden kann, wird am Ende die
* von {@link URL#openStream()} erzeugte {@code IOException} geworfen. * von {@link URL#openStream()} erzeugte {@code IOException} geworfen.
* <p> * <p>
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt. * Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link SecurityException} beim Öffnen des {@code FileInputStream} * Eine {@link SecurityException} beim Öffnen des {@code FileInputStream}
* (zum Beispiel, weil auf eine existierende Datei keine Leserechte bestehen) * (zum Beispiel, weil auf eine existierende Datei keine Leserechte bestehen)
* verhindert daher, dass noch im Classpath gesucht wird. * verhindert daher, dass noch im Classpath gesucht wird.
* *
* @param source Eine Quelle für die Resource (Absoluter Dateipfad, * @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource) * Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code InputStream} für die Resource * @return Ein {@code InputStream} für die Ressource.
* @throws NullPointerException Falls {@code source} {@code null} ist. * @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String * @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist. * ist.
* @throws IOException Geworfen beim öffnen des Streams zu * @throws IOException Geworfen beim Öffnen des Streams zu
* einer bestehenden Resource oder falls * einer bestehenden Ressource oder falls
* keine passende Resource gefunden wurde. * keine passende Ressource gefunden
* wurde.
*/ */
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException { public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null"); return getResourceURL(source).openStream();
Validator.requireNotEmpty(source, "Resource source may not be empty.");
InputStream in = null;
// See if source is a readable file
File file = new File(source);
try {
if( file.isFile() ) {
in = new FileInputStream(file);
}
} catch( FileNotFoundException fnfex ) {
// Somehow an exception occurred, but we still try other sources
in = null;
}
// File does not exist, try other means
// load ressource relative to .class-file
if( in == null ) {
in = Zeichenmaschine.class.getResourceAsStream(source);
}
// relative to ClassLoader
if( in == null ) {
in = Zeichenmaschine.class.getClassLoader().getResourceAsStream(source);
}
// load form web or jar-file
if( in == null ) {
in = new URL(source).openStream();
}
// One of the above got a valid Stream,
// otherwise an Exception was thrown
return in;
}
public static InputStream getInputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileInputStream(file);
}
public static InputStream getInputStream( URL url ) throws IOException {
Validator.requireNotNull(url, "Provided URL can't be null.");
return url.openStream();
} }
/** /**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified * Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt * passenden {@link OutputStream}.
* ermittelt:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* ({@code new File(source}.isFile() == true})?</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt ({@code getResource(source) != null})?.</li>
* <li>Ist {@code source} im Classpath enthalten ({@code getClassLoader().getResource(source) != null})?</li>
* <li>Ansonten erstellt ein {@link URL}-Objekt.</li>
* </ol>
* <p> * <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben. * Es wird davon ausgegangen, dass die Quelle auf eine Datei verweist, auf
* Auftretende Exceptions * die Schreibrechte bestehen. Ist dies nicht der Fall, wird eine
* werden als {@link IOException} geworfen. * {@link IOException} geworfen.
* <p>
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt.
* Eine {@link java.net.MalformedURLException} beim Konstruieren des {@code URL}
* zu einer Datei verhindert daher, dass noch im Classpath gesucht wird.
* *
* @param source Eine Quelle für die Resource (Absoluter Dateipfad, * @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource) * Dateipfad im Classpath).
* @return Ein {@code InputStream} für die Resource * @return {@code OutputStream} für die Ressource.
* @throws NullPointerException Falls {@code source} {@code null} ist. * @throws IOException Falls die Quelle kein gültiges Ziel beschreibt.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim erzeugen eines URL zu
* einer bestehenden Resource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
URL url;
url = Zeichenmaschine.class.getResource(source);
if( url != null ) {
return url;
}
url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
return new URL(source);
}
/**
* Ver
* @param source
* @return
* @throws IOException
*/ */
public static OutputStream getOutputStream( String source ) throws IOException { public static OutputStream getOutputStream( String source ) throws IOException {
Validator.requireNotNull(source, "Resource source may not be null"); try {
Validator.requireNotEmpty(source, "Resource source may not be empty."); return Files.newOutputStream(Path.of(getResourceURL(source).toURI()));
URL url = getResourceURL(source); } catch( URISyntaxException ex ) {
return getOutputStream(new File(url.getPath())); throw new IOException(ex);
}
} }
public static OutputStream getOutputStream( File file ) throws IOException { /**
Validator.requireNotNull(file, "Provided file can't be null."); * Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
return new FileOutputStream(file); * {@link BufferedReader}.
* <p>
* Die Suche erfolgt, wie bei {@link #getInputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source ) throws IOException {
return getReader(source, StandardCharsets.UTF_8);
} }
public static Reader getReader( String source ) throws IOException { /**
return new InputStreamReader(getInputStream(source)); * Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* {@link BufferedReader} mit dem angegebenen Charset.
* <p>
* Die Suche erfolgt wie bei {@link #getInputStream(String)} beschrieben.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @param charset Der zu verwendende Charset.
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source, Charset charset ) throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(source), charset.newDecoder()));
} }
public static Writer getWriter( String source ) throws IOException { /**
return new OutputStreamWriter(getOutputStream(source)); * Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source ) throws IOException {
return getWriter(source, StandardCharsets.UTF_8);
} }
/**
* Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @param charset Der zu verwendende Charset.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source, Charset charset ) throws IOException {
return new BufferedWriter(new OutputStreamWriter(getOutputStream(source), charset.newEncoder()));
}
// Privater C'tor
private ResourceStreamProvider() { private ResourceStreamProvider() {
} }

View File

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

View File

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

View File

@@ -5,35 +5,18 @@ import schule.ngb.zm.Zeichenmaschine;
public abstract class FrameSynchronizedTask extends Task { public abstract class FrameSynchronizedTask extends Task {
private static Thread mainThread;
private static final Thread getMainThread() {
if( mainThread == null ) {
mainThread = Thread.currentThread();
if( !mainThread.getName().equals(Constants.APP_NAME) ) {
// Need to search for main Zeichenthread ...
}
}
return mainThread;
}
@Override @Override
public void run() { public void run() {
initialize(); initialize();
running = true; running = true;
Object lock = Zeichenmaschine.globalSyncLock; final Object lock = Zeichenmaschine.globalSyncLock;
// start of thread in ms
final long start = System.currentTimeMillis();
// current time in ns // current time in ns
long beforeTime = System.nanoTime(); long beforeTime = System.nanoTime();
// store for deltas // internal counters for tick and delta time
long overslept = 0L; int _tick;
// internal counters for tick and runtime double delta;
int _tick = 0;
double delta = 0.0;
while( running ) { while( running ) {
// delta in seconds // delta in seconds
@@ -53,10 +36,7 @@ public abstract class FrameSynchronizedTask extends Task {
} }
} }
} }
running = false;
done = true; done = true;
finish(); finish();
} }

View File

@@ -17,7 +17,7 @@ public abstract class RateLimitedTask extends Task {
// store for deltas // store for deltas
long overslept = 0L; long overslept = 0L;
// delta in ms // delta in ms
double delta = 0; double delta;
running = true; running = true;
while( running ) { while( running ) {
@@ -47,10 +47,7 @@ public abstract class RateLimitedTask extends Task {
// Did we sleep to long? // Did we sleep to long?
overslept = (System.nanoTime() - afterTime) - sleep; overslept = (System.nanoTime() - afterTime) - sleep;
} }
running = false;
done = true; done = true;
finish(); finish();
} }

View File

@@ -10,11 +10,6 @@ import java.util.concurrent.*;
*/ */
public class TaskRunner { public class TaskRunner {
private static final int POOL_SIZE = 4;
private static final int SHUTDOWN_TIME = 100;
private static TaskRunner runner; private static TaskRunner runner;
public static TaskRunner getTaskRunner() { public static TaskRunner getTaskRunner() {

View File

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

View File

@@ -0,0 +1,9 @@
<HTML>
<BODY>
Die Zeichenmaschine
<p>
<h1>Die Zeichenmaschine</h1>
@author J. Neugebauer
</BODY>
</HTML>

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