41 Commits

Author SHA1 Message Date
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
82 changed files with 10881 additions and 1394 deletions

8
.gitignore vendored
View File

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

View File

@@ -6,6 +6,14 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Changed
- `FilledShape` und `StrokedShape` durch `Fillable` und `Strokeable` Interfaces ersetzt.
- `Shape` erweitert nun `BasisDrawable` als abstrakte Grundlage.
- `io` Klassen nutzen nun mehr der `java.nio` Funktionen.
- Package-Struktur angepasst.
## Version 0.0.23
### Added
- System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.

View File

@@ -3,17 +3,13 @@ plugins {
id 'java-library'
}
/*properties {
zmVersion {
major = 0;
minor = 0;
rev = 21;
}
}*/
group 'schule.ngb'
version '0.0.22-SNAPSHOT'
//version '{$zmVersion.major}.{$zmVersion.minor}.{$zmVersion.rev}-SNAPSHOT'
version '0.0.32-SNAPSHOT'
java {
withSourcesJar()
withJavadocJar()
}
compileJava {
options.release = 11

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

View File

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

43
docs/index.md Normal file
View File

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

44
docs/install.md Normal file
View File

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

129
docs/macros.py Normal file
View File

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

606
docs/quickstart.md Normal file
View File

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

View File

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

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
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
zipStorePath=wrapper/dists

83
mkdocs.yml Normal file
View File

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

25
requirements.txt Normal file
View File

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

View File

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

View File

@@ -4,6 +4,10 @@ import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* Repräsentiert eine Farbe in der Zeichenmaschine.
@@ -14,6 +18,7 @@ import java.awt.image.ColorModel;
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend).
*/
@SuppressWarnings( "unused" )
public class Color implements Paint {
@@ -47,38 +52,67 @@ public class Color implements Paint {
* Die Farbe Zeichenmaschinen-Rot.
*/
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.
*/
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.
*/
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.
*/
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.
*/
public static final Color ORANGE = new Color(248, 158, 80);
/**
* Die Farbe Zeichenmaschinen-Türkis.
*/
public static final Color CYAN = new Color(java.awt.Color.CYAN);
/**
* Die Farbe Zeichenmaschinen-Magenta.
*/
public static final Color MAGENTA = new Color(java.awt.Color.MAGENTA);
/**
* Die Farbe Zeichenmaschinen-Rosa.
*/
public static final Color PINK = new Color(240, 99, 164);
/**
* Die Farbe Zeichenmaschinen-Lila.
*/
public static final Color PURPLE = new Color(101, 0, 191);
/**
* Die Farbe Zeichenmaschinen-Braun.
*/
@@ -88,6 +122,7 @@ public class Color implements Paint {
* Die Farbe Helmholtz-Grün.
*/
public static final Color HGGREEN = new Color(0, 165, 81);
/**
* 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.
*/
@@ -116,8 +151,8 @@ public class Color implements Paint {
}
/**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var> und
* des Transparentwertes <var>alpha</var>.
* Erstellt eine graue Farbe entsprechend dem Grauwert {@code gray} und dem
* Transparenzwert {@code alpha}.
*
* @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
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen 0 und 255.
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen 0 und 255.
*
* @param red Rotwert 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
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen 0 und 255.
* <var>alpha</var> gibt den den Transparentwert an (auch zwischen
* 0 und 255), wobei 0 komplett durchsichtig ist und 255 komplett deckend.
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen 0 und 255. {@code alpha} gibt den den Transparentwert an
* (auch zwischen 0 und 255), wobei 0 komplett durchsichtig ist und 255
* komplett deckend.
*
* @param red Rotwert 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.
*/
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 ) {
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
* Transparentwert durch <var>alpha</var>.
* Erstellt eine Farbe als Kopie von {@code color} und ersetzt den
* Transparentwert durch {@code alpha}.
*
* @param color
* @param alpha
* @param color Eine Farbe.
* @param alpha Der neue Transparenzwert.
*/
public Color( Color color, int 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.
* <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
* @return
* @param rgba Eine RGBA-Farbe.
* @return Ein Farbobjekt.
*/
public static Color getRGBColor( int rgba ) {
Color c = new Color(rgba, true);
return c;
return new Color(rgba, true);
}
/**
* 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 ) {
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 ) {
int rgb = Color.HSLtoRGB(new float[]{(float) h, (float) s, (float) l});
return Color.getRGBColor(rgb);
}
public static Color parseString( String pColor ) {
pColor = pColor.toLowerCase().strip();
if( pColor.contains("red") || pColor.contains("rot") ) {
return Color.RED.copy();
} else if( pColor.contains("blue") || pColor.contains("blau") ) {
return Color.BLUE.copy();
} else if( pColor.contains("green") || pColor.contains("grün") || pColor.contains("gruen") ) {
return Color.GREEN.copy();
} else if( pColor.contains("yellow") || pColor.contains("gelb") ) {
return Color.YELLOW.copy();
} else {
return new Color();
/**
* Erstellt aus einem Farbnamen ein Farbobjekt.
* <p>
* Die gültigen Farbnamen können unter <a
* href="https://htmlcolors.com/color-names">https://htmlcolors.com/color-names</a>
* nachgeschlagen werden.
*
* @param color Der Name einer Farbe.
* @return Ein Farbobjekt.
*/
public static Color parseString( String 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-
* oder achtstellig sein (wenn ein Transparentwert vorhanden ist). Dem Code
* kann ein {@code #} Zeichen vorangestellt sein.
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann drei-,
* sechs- oder achtstellig sein (wenn ein Transparentwert vorhanden ist).
* 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
* @return
* @param hexcode Eine Farbe als Hexcode.
* @return Ein Farbobjekt.
*/
public static Color parseHexcode( String hexcode ) {
if( hexcode.startsWith("#") ) {
@@ -259,8 +345,6 @@ public class Color implements Paint {
} else if( hexcode.length() == 8 ) {
alpha = Integer.valueOf(hexcode.substring(6, 8), 16);
hexcode = hexcode.substring(0, 6);
} else {
hexcode = hexcode;
}
return Color.getRGBColor((alpha << 24) | Integer.valueOf(hexcode, 16));
@@ -281,7 +365,7 @@ public class Color implements Paint {
if( color1 == null && color2 == 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();
}
if( t > 1.0 || color1 == null ) {
@@ -307,12 +391,12 @@ public class Color implements Paint {
if( c == 0 ) {
h_ = 0;
} else if( max == r ) {
h_ = (float) (g - b) / c;
h_ = (g - b) / c;
if( h_ < 0 ) h_ += 6.f;
} else if( max == g ) {
h_ = (float) (b - r) / c + 2.f;
h_ = (b - r) / c + 2.f;
} else if( max == b ) {
h_ = (float) (r - g) / c + 4.f;
h_ = (r - g) / c + 4.f;
}
float h = 60.f * h_;
@@ -331,6 +415,14 @@ public class Color implements Paint {
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 ) {
return HSLtoRGB(hsl, 255);
}
@@ -339,9 +431,9 @@ public class Color implements Paint {
* Konvertiert eine Farbe mit Komponenten im HSL-Farbraum in den
* RGB-Farbraum.
* <p>
* 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
* l-Wert im Bereich von 0 bis 1.
* 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
* L-Wert im Bereich von 0 bis 1.
*
* @param hsl Die Farbkomponenten im HSL-Farbraum.
* @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.
* <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.
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/
@Override
public boolean equals( Object obj ) {
if( obj == null ) {
return false;

View File

@@ -1,23 +1,27 @@
package schule.ngb.zm;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Noise;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random;
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.
* <p>
* Die Konstanten stellen viele Funktionen zur einfachen Programmierung bereit
* 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>
* Für die Implementierung eigener Klassen ist es oft hilfreich von
* {@code Constants} zu erben, um die Methoden und Konstanten einfach im
@@ -64,13 +68,49 @@ public class Constants {
/**
* Patchversion der Zeichenmaschine.
*/
public static final int APP_VERSION_REV = 22;
public static final int APP_VERSION_REV = 32;
/**
* Version der Zeichenmaschine als Text-String.
*/
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.
@@ -405,6 +445,21 @@ public class Constants {
*/
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
* wirken sich auf die aktuelle Zeichenmaschine aus und sollten nur von der
@@ -731,6 +786,30 @@ public class Constants {
// 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.
*
@@ -755,6 +834,30 @@ public class Constants {
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.
*
@@ -1000,7 +1103,7 @@ public class Constants {
}
/**
* Ermittelt den Arkuskosinus der angegebenen Zahl.
* Ermittelt den Arcuskosinus der angegebenen Zahl.
*
* @param x Eine Zahl.
* @return {@code acos(x)}.
@@ -1010,7 +1113,7 @@ public class Constants {
}
/**
* Ermittelt den Arkusktangens der angegebenen Zahl.
* Ermittelt den Arcusktangens der angegebenen Zahl.
*
* @param x Eine Zahl.
* @return {@code atan(x)}.
@@ -1150,6 +1253,12 @@ public class Constants {
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.
*/
@@ -1280,6 +1389,26 @@ public class Constants {
return getRandom().nextGaussian();
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
/*public static final int choice( int... values ) {
return values[random(0, values.length - 1)];
}*/
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
/*public static final double choice( double... values ) {
return values[random(0, values.length - 1)];
}*/
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
@@ -1287,7 +1416,7 @@ public class Constants {
* @param <T> Datentyp des Elements.
* @return Ein zufälliges Element aus dem Array.
*/
public static final <T> T choice( T[] values ) {
public static final <T> T choice( T... values ) {
return values[random(0, values.length - 1)];
}
@@ -1296,15 +1425,149 @@ public class Constants {
*
* @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 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.
* @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 ) {
Object[] result = new Object[n];
public static final <T> T[] choice( T[] 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)
);
T[] result = Arrays.copyOf(values, n);
T[] valuesCopy = Arrays.copyOf(values, values.length);
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 +1736,93 @@ public class Constants {
}
}
/**
* Konvertiert einen char-Wert in einen double-Wert.
*
* @param value Der char-Wert.
* @return Ein double-Wert.
*/
public static final double asDouble( char value ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
try {
return Double.parseDouble(value);
@@ -1513,38 +1831,93 @@ public class Constants {
}
}
/**
* Konvertiert einen char-Wert in einen boolean-Wert.
*
* @param value Der char-Wert.
* @return Ein boolean-Wert.
*/
public static final boolean asBool( char value ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
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 ) {
return Boolean.parseBoolean(value);
}
@@ -1605,7 +1978,7 @@ public class Constants {
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.

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
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
*
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer {@link Zeichenmaschine}.
* Es werden
* Sie erben von {@code Constants}, damit sie beim
* <p>
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer
* {@link Zeichenmaschine}. Sie erben von {@code Constants}, da neue Ebenentypen
* von Nutzern implementiert werden können.
*/
public abstract class Layer extends Constants implements Drawable, Updatable {
@@ -43,24 +43,39 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
protected boolean active = true;
/**
* Erstellt eine neue Ebene mit den Standardmaßen.
*/
public Layer() {
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 ) {
createCanvas(width, height);
}
/**
* @return Die Breite der Ebene.
*/
public int getWidth() {
return buffer.getWidth();
}
/**
* @return Die Höhe der Ebene.
*/
public int 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 height Die neue Höhe.
@@ -77,7 +92,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Gibt die Resourcen der Ebene frei.
* Gibt die Ressourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
@@ -88,7 +103,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
/**
* 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.
*/
private void createCanvas( int width, int height ) {
@@ -115,7 +130,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe und
* 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.
*/
private void recreateCanvas( int width, int height ) {
@@ -139,41 +154,80 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Zeichnet den Puffer auf die Grafik-Instanz.
* Zeichnet den Puffer auf den Grafikkontext.
*
* @param pGraphics
* @param graphics Der Grafikkontext, auf den gezeichnet wird.
*/
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( visible ) {
pGraphics.drawImage(buffer, 0, 0, null);
graphics.drawImage(buffer, 0, 0, null);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Ebene.
*/
public void hide() {
visible = false;
}
public void view() {
/**
* Zeigt die Ebene an, falls sie versteckt war.
*/
@SuppressWarnings( "unused" )
public void show() {
visible = true;
}
/**
* Versteckt oder zeigt die Ebene, je nachdem, welchen Zustand sie derzeit
* hat.
*/
@SuppressWarnings( "unused" )
public void toggle() {
visible = !visible;
}
/**
* {@inheritDoc}
*/
@Override
public void update( double delta ) {
}
/**
* {@inheritDoc}
*/
@Override
public boolean isActive() {
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() {
}
/**
* Linienstile für Konturlinien.
*/
public enum StrokeType {
SOLID, DASHED, DOTTED
/**
* Durchgezogene Linien.
*/
SOLID,
/**
* Getrichelte Linien.
*/
DASHED,
/**
* Gepunktete Linien.
*/
DOTTED
}
/**
* Stile für Pfeilspitzen.
*/
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 {
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;
PathType( int type ) {
@@ -33,24 +87,100 @@ public final class Options {
* Zustände in denen sich die Zeichenmaschine befinden kann.
*/
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,
/**
* Die Initialisierung der Zeichenmaschine ist beendet, aber der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde noch nicht gestartet.
*/
INITIALIZED,
/**
* Die Zeichenmaschine wurde gestartet und der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* arbeitet.
*/
RUNNING,
UPDATING,
DRAWING,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde pausiert.
*/
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,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* ist beendet.
*/
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,
/**
* Die Zeichenmaschine führt gerade
* {@link Zeichenmaschine#update(double)} aus.
*/
UPDATING,
/**
* Die Zeichenmaschine führt gerade {@link Zeichenmaschine#draw()} aus.
*/
DRAWING,
/**
* Die Ausführung der Zeichenmaschine wurde mit
* {@link Zeichenmaschine#delay(int)} verzögert und wartet auf
* Fortsetzung.
*/
DELAYED,
/**
* Die Zeichenmaschine sendet gereade gesammelte Events und führt Tasks
* aus.
*/
DISPATCHING
}
/**
* Richtungen für die Ausrichtung von Formen. Richtungen sind durch
* Einheitsvektoren bzw. deren Kombination repräsentiert, wodurch mit ihnen
* gerechnet werden kann. Jede Richtung ist zusätzlich als Himmelsrichtung
* definiert.
* Einheitsvektoren bzw. deren Kombination im Koordinatensystem der
* Zeichenfläche repräsentiert, wodurch mit ihnen gerechnet werden kann. Die
* 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 {
CENTER(0, 0),
@@ -88,30 +218,116 @@ public final class Options {
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 ) {
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 ) {
return dir.in(this);
}
/**
* @return Diese Richtung als Vektor-Objekt.
*/
public Vector asVector() {
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() {
for( Direction dir : Direction.values() ) {
if( dir.x == -this.x && dir.y == -this.y ) {
return dir;
}
switch( this ) {
case UP:
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
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
clear();
List<Drawable> it = List.copyOf(drawables);
for( Drawable d: it ) {
@@ -116,7 +116,7 @@ public class Spielemaschine extends Zeichenmaschine {
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.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import schule.ngb.zm.shapes.Shape;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Stroke;
/**
* Basisklasse für Formen, die eine Konturlinie besitzen.
* {@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 abstract class StrokedShape extends Constants implements Drawable {
public interface Strokeable extends Drawable {
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn die Form ohne
* kontur dargestellt werden soll.
* Setzt den {@code Stroke} für die Konturlinie direkt.
*
* @param stroke Ein {@code Stroke}-Objekt.
*/
protected Color strokeColor = DEFAULT_STROKECOLOR;
void setStroke( Stroke stroke );
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
* Gibt ein {@code Stroke}-Objekt mit den aktuell gesetzten Eigenschaften
* zurück.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
Stroke getStroke();
/**
* Die Art der Konturlinie.
* 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 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;
default boolean hasStroke() {
Color strokeColor = getStrokeColor();
double strokeWeight = getStrokeWeight();
return strokeColor != null && strokeColor.getAlpha() > 0 && strokeWeight > 0;
}
/**
* Gibt die aktuelle Farbe der Konturlinie zurück.
*
* @return Die Konturfarbe oder {@code null}.
*/
public Color getStrokeColor() {
return strokeColor;
}
Color getStrokeColor();
/**
* 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.
* @see Color
*/
public void setStrokeColor( Color color ) {
this.strokeColor = color;
}
void setStrokeColor( Color color );
/**
* 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.
* @see Color#Color(Color, int)
*/
public void setStrokeColor( Color color, int alpha ) {
default void setStrokeColor( Color color, int 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.
* @see Color#Color(int)
*/
public void setStrokeColor( int gray ) {
default void setStrokeColor( int gray ) {
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.
* @see Color#Color(int, int)
*/
public void setStrokeColor( int gray, int alpha ) {
default void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
@@ -105,7 +120,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <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);
}
@@ -121,14 +136,14 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <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));
}
/**
* Entfernt die Kontur der Form.
*/
public void noStroke() {
default void noStroke() {
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#SOLID
*/
public void resetStroke() {
setStrokeColor(DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID);
default void resetStroke() {
setStrokeColor(Constants.DEFAULT_STROKECOLOR);
setStrokeWeight(Constants.DEFAULT_STROKEWEIGHT);
setStrokeType(Constants.SOLID);
}
/**
@@ -150,9 +165,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
*
* @return Die aktuelle Dicke der Linie.
*/
public double getStrokeWeight() {
return strokeWeight;
}
double getStrokeWeight();
/**
* 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.
*/
public void setStrokeWeight( double weight ) {
this.strokeWeight = max(0.0, weight);
this.stroke = null;
default void setStrokeWeight( double weight ) {
setStroke(createStroke(getStrokeType(), weight));
}
/**
@@ -171,25 +183,19 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
public Options.StrokeType getStrokeType() {
return strokeType;
}
Options.StrokeType getStrokeType();
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link Constants#DASHED},
* {@link Constants#DOTTED} und {@link Constants#SOLID}.
*
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
public void setStrokeType( Options.StrokeType type ) {
this.strokeType = type;
this.stroke = null;
default void setStrokeType( Options.StrokeType type ) {
setStroke(createStroke(type, getStrokeWeight()));
}
@Override
public abstract void draw( Graphics2D graphics );
/**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
@@ -197,52 +203,26 @@ public abstract class StrokedShape extends Constants implements Drawable {
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected Stroke createStroke() {
// TODO: Used global cached Stroke Objects?
if( stroke == null ) {
switch( strokeType ) {
case DOTTED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
break;
case DASHED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
break;
case SOLID:
default:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
break;
}
}
return stroke;
}
/**
* 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);
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight ) {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
case SOLID:
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
}
}

View File

@@ -28,7 +28,7 @@ public interface Updatable {
* @return {@code true}, wenn das Objekt aktiv ist, {@code false}
* andernfalls.
*/
public boolean isActive();
boolean isActive();
/**
* Änderung des Zustandes des Objekts abhängig vom Zeitintervall
@@ -39,6 +39,6 @@ public interface Updatable {
*
* @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
* sich dadurch einfach mit den Klassen des {@link java.awt} Pakets benutzen.
*/
@SuppressWarnings( "unused" )
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.
*
* @param vector Der original Vektor.
@@ -197,6 +198,7 @@ public class Vector extends Point2D.Double {
* @param y Der neue y-Wert.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( double x, double y ) {
this.x = x;
this.y = y;
@@ -210,6 +212,7 @@ public class Vector extends Point2D.Double {
* @param vector Ein Vektor.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Vector vector ) {
x = vector.x;
y = vector.y;
@@ -223,6 +226,7 @@ public class Vector extends Point2D.Double {
* @param pPunkt Ein Punkt.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Point2D pPunkt ) {
x = pPunkt.getX();
x = pPunkt.getY();
@@ -271,6 +275,7 @@ public class Vector extends Point2D.Double {
/**
* Legt die Länge des Vektors fest.
*
* @param length Die neue Länge des Vektors.
* @return Dieser Vektor selbst (method chaining)
*/
@@ -294,9 +299,9 @@ public class Vector extends Point2D.Double {
}
/**
*
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector normalize() {
double len = length();
if( len != 0 && len != 1 ) {
@@ -308,9 +313,11 @@ public class Vector extends Point2D.Double {
/**
* Addiert den Vektor {@code vector} zu diesem.
*
* @param vector Ein anderer Vektor.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( Vector vector ) {
x += vector.x;
y += vector.y;
@@ -319,10 +326,12 @@ public class Vector extends Point2D.Double {
/**
* Addiert die angegebenen Werte zur x- und y-Komponente des Vektors.
*
* @param x Summand x-Komponente.
* @param y Summand y-Komponente.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( double x, double y ) {
this.x += x;
this.y += y;
@@ -342,24 +351,28 @@ public class Vector extends Point2D.Double {
return vec;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( Vector vector ) {
x -= vector.x;
y -= vector.y;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( double x, double y ) {
this.x -= x;
this.y -= y;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector scale( double scalar ) {
x *= scalar;
y *= scalar;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector div( double scalar ) {
if( scalar == 0.0 ) {
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
* quadriert wird.
*
* @param vector
* @return
* @param vector Ein anderer Vektor.
* @return Das Quadrat der Entfernung zum anderen Vektor.
*/
public double distanceSq( Vector vector ) {
return super.distanceSq(vector);
@@ -487,6 +500,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #setLength(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double max ) {
if( lengthSq() > max * max ) {
setLength(max);
@@ -503,6 +517,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #setLength(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double min, double max ) {
if( min > max ) {
double t = min;
@@ -556,6 +571,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #rotate(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector rotateRad( double rad ) {
double temp = x;
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.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector morph( Vector vector, double t ) {
double tt = Math.min(Math.max(t, 0.0), 1.0);
x = x + (vector.x - x) * tt;
@@ -631,6 +648,7 @@ public class Vector extends Point2D.Double {
* @param t Ein Wert zwischen 0 und 1.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector interpolate( Vector vector, double t ) {
x = x + (vector.x - x) * t;
y = y + (vector.y - y) * t;

View File

@@ -9,7 +9,6 @@ import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
@@ -24,6 +23,12 @@ import java.util.ArrayList;
*/
public class Zeichenfenster extends JFrame {
/**
* Setzt das Look and Feel auf den Standard des Systems.
* <p>
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen
* werden.
*/
public static final void setLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
@@ -32,6 +37,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() {
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
@@ -54,14 +70,14 @@ public class Zeichenfenster extends JFrame {
}
/**
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich dem
* Aktuellen sein, wenn das Fenster verschoben wurde).
* Das Anzeigegerät, auf dem die Zeichenmaschine gestartet wurde (muss nicht
* 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
* es Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat es
* Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine
* Garantie für die Größe der Leinwand übernehmen.
*/
@@ -78,30 +94,67 @@ public class Zeichenfenster extends JFrame {
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt.
*/
private KeyListener fullscreenExitListener = new KeyAdapter() {
private final KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
e.consume();
}
}
};
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 ) {
this(new Zeichenleinwand(width, height), title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und einer
* {@link Zeichenleinwand} in der angegebenen Größe auf dem angegebenen
* Anzeigegerät.
*
* @param width Die Breite der Zeichenleinwand.
* @param height Die Höhe der Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
@SuppressWarnings( "unused" )
public Zeichenfenster( int width, int height, String title, GraphicsDevice displayDevice ) {
this(new Zeichenleinwand(width, height), title, displayDevice);
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand}.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title ) {
this(canvas, title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand} auf dem angegebenen Anzeigegerät.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
super(Validator.requireNotNull(displayDevice).getDefaultConfiguration());
this.displayDevice = displayDevice;
@@ -110,12 +163,11 @@ public class Zeichenfenster extends JFrame {
this.canvasPreferredWidth = canvas.getWidth();
this.canvasPreferredHeight = canvas.getHeight();
//this.add(canvas, BorderLayout.CENTER);
this.add(canvas);
this.add(canvas, BorderLayout.CENTER);
this.canvas = canvas;
// Konfiguration des Frames
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION: title);
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION : title);
// Kann vom Aufrufenden überschrieben werden
this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
@@ -123,14 +175,19 @@ public class Zeichenfenster extends JFrame {
try {
if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png");
Image icon = ImageIO.read(iconUrl);
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon);
if( iconUrl != null ) {
Image icon = ImageIO.read(iconUrl);
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon);
}
} else {
ArrayList<Image> icons = new ArrayList<>(4);
for( int size = 32; size <= 512; size *= size ) {
icons.add(ImageIO.read(new File("icon_" + size + ".png")));
for( int size : new int[]{32, 64, 128, 512} ) {
URL icnUrl = Zeichenmaschine.class.getResource("icon_" + size + ".png");
if( icnUrl != null ) {
icons.add(ImageIO.read(icnUrl));
}
}
this.setIconImages(icons);
@@ -144,25 +201,54 @@ public class Zeichenfenster extends JFrame {
// Fenster zusammenbauen, auf dem Bildschirm positionieren ...
this.pack();
this.setResizable(false);
this.setFocusable(true);
this.setLocationByPlatform(true);
// this.centerFrame();
}
/**
* Liefert das Anzeigegerät, auf dem dieses Fenster erstellt wurde.
* <p>
* Das Anzeigegerät muss nicht unbedingt gleich dem sein, auf dem sich das
* Fenster derzeit befindet, wenn das Fenster verschoben wurde.
*
* @return Das Anzeigegerät.
*/
@SuppressWarnings( "unused" )
public GraphicsDevice getDisplayDevice() {
return displayDevice;
}
/**
* Liefert die Abmessungen des Anzeigegeräts, auf dem das Fenster gestartet
* wurde.
*
* @return Die Abmessungen des Anzeigegeräts.
*/
public Rectangle getScreenBounds() {
// return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return displayDevice.getDefaultConfiguration().getBounds();
}
/**
* Liefert die Zeichenleinwand dieses Fensters.
*
* @return Die Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public Zeichenleinwand getCanvas() {
return canvas;
}
/**
* Liefert die Abmessungen der Zeichenleinwand zurück.
*
* @return Die Abmessungen der Zeichenleinwand.
*/
public Rectangle getCanvasBounds() {
return canvas.getBounds();
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
@@ -175,8 +261,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 ) {
// TODO: (ngb) Put constains on max/min frame/canvas size
// TODO: (ngb) Put constrains on max/min frame/canvas size
if( fullscreen ) {
canvasPreferredWidth = newWidth;
canvasPreferredHeight = newHeight;
@@ -205,30 +297,62 @@ public class Zeichenfenster extends JFrame {
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
// Temporarily stop rendering
while( canvas.isRendering() ) {
try {
canvas.suspendRendering();
} catch( InterruptedException ex ) {
LOG.info(ex, "setFullsceen(true) was interrupted and canceled.");
return;
}
}
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
this.setResizable(false); // Should be set anyway
// Activate fullscreen
dispose();
setUndecorated(true);
setResizable(false);
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
canvas.addKeyListener(fullscreenExitListener);
// Reset canvas size to its new bounds to recreate buffer and drawing surface
java.awt.Rectangle canvasBounds = getCanvasBounds();
canvas.setSize(canvasBounds.width, canvasBounds.height);
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = true;
} else if( !pEnable && fullscreen ) {
fullscreen = false;
displayDevice.setFullScreenWindow(null);
dispose();
setUndecorated(false);
setResizable(false);
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
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() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win.equals(this);

View File

@@ -1,16 +1,16 @@
package schule.ngb.zm;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.ShapesLayer;
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.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -24,10 +24,14 @@ import java.util.concurrent.LinkedBlockingQueue;
*/
public class Zeichenleinwand extends Canvas {
/**
* Liste der hinzugefügten Ebenen.
*/
private LinkedList<Layer> layers;
// Lokales Lock für Rendervorgänge.
private final Object[] renderLock = new Object[0];
// 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.
@@ -41,13 +45,51 @@ public class Zeichenleinwand extends Canvas {
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
// Liste der Ebenen initialisieren und die Standardebenen einfügen
layers = new LinkedList<>();
synchronized( layers ) {
layers.add(new ColorLayer(width, height, Constants.DEFAULT_BACKGROUND));
layers = Collections.synchronizedList(new LinkedList<>());
}
/**
* Ob die Leinwand ihren Inhalt gerade zeichnet.
*
* @return {@code true}, wenn die Inhalte gerade gezeichnet werden.
*/
public boolean isRendering() {
return rendering;
}
/**
* Pausiert das Zeichnen der Leinwand kurzzeitig.
* <p>
* Falls die Leinwand gerade beim Zeichnen ist
* ({@code isRendering() == true}, blockt die Methode den aufrufenden Thread
* so lange, bis das Rendern beendet ist. Danach wird die Ebene nicht mehr
* neu gezeichnet, bis {@link #resumeRendering()} aufgerufen wird.
* <p>
* Das Zeichnen sollte nur dann unterbrochen werden, wenn sich der Kontext
* der Canvas-Komponente in seinem Elterncontainer ändert, um Fehler bei
* einer fehlenden Container-Hierarchie zu vermeiden.
*
* @throws InterruptedException Falls der Thread beim Warten unterbrochen
* wird.
*/
public void suspendRendering() throws InterruptedException {
synchronized( renderLock ) {
if( isRendering() ) {
renderLock.wait();
}
suspended = true;
}
}
/**
* Setzt das Zeichnen der Leinwand fort, falls es zuvor mit
* {@link #suspendRendering()} ausgesetzt wurde.
*/
public void resumeRendering() {
suspended = false;
}
/**
* Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln.
* <p>
@@ -78,32 +120,28 @@ public class Zeichenleinwand extends Canvas {
*/
public void addLayer( Layer layer ) {
if( layer != null ) {
synchronized( layers ) {
layer.setSize(getWidth(), getHeight());
layers.add(layer);
}
layer.setSize(getWidth(), getHeight());
layers.add(layer);
}
}
/**
* Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn
* 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,
* 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.
*/
public void addLayer( int i, Layer layer ) {
if( layer != null ) {
synchronized( layers ) {
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
}
}
@@ -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.
*/
public java.util.List<Layer> getLayers() {
return layers;
public List<Layer> getLayers() {
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).
* @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.
*/
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.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben.
*
* @param clazz Typ der Ebene.
* @param <L>
* @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> Typ der Ebenen, die abgefragt werden.
* @return Erste Ebene vom angegeben Typ.
*/
public <L extends Layer> L getLayer( Class<L> clazz ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(clazz) ) {
return clazz.cast(layer);
public <L extends Layer> L getLayer( Class<L> type ) {
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
return type.cast(layer);
}
}
}
return null;
@@ -163,44 +203,67 @@ public class Zeichenleinwand extends Canvas {
* gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der
* Reihenfolge der Ebenen in der Leinwand (von unten nach oben).
*
* @param pClazz
* @param <L>
* @return
* @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> Typ der Ebenen, die abgefragt werden.
* @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());
for( Layer layer : layers ) {
if( layer.getClass().equals(pClazz) ) {
result.add(pClazz.cast(layer));
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
result.add(type.cast(layer));
}
}
}
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 ) {
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 ) {
for( Layer layer : pLayers ) {
for( Layer layer : removeLayers ) {
layers.remove(layer);
}
}
}
/**
* Entfernt alle vorhandenen Ebenen von dieser Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
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 ) {
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
for( Layer layer : List.copyOf(layers) ) {
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 ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
for( Layer layer : layers ) {
layer.draw(g2d);
}
}
@@ -249,37 +311,41 @@ public class Zeichenleinwand extends Canvas {
* Zeigt den aktuellen Inhalt der Zeichenleinwand an.
*/
public void render() {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
if( !suspended && isDisplayable() ) {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
if( isDisplayable() ) {
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
synchronized( renderLock ) {
rendering = true;
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
synchronized( layers ) {
for( Layer layer : List.copyOf(layers) ) {
layer.draw(g2d);
}
}
g2d.dispose();
} while( strategy.contentsRestored() );
// Display the buffer
if( !strategy.contentsLost() ) {
strategy.show();
Toolkit.getDefaultToolkit().sync();
}
g2d.dispose();
} while( strategy.contentsRestored() );
// Display the buffer
if( !strategy.contentsLost() ) {
strategy.show();
Toolkit.getDefaultToolkit().sync();
}
// Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() );
// Repeat the rendering if the drawing buffer was lost
} 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.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Hauptklasse der Zeichenmaschine.
@@ -37,43 +38,6 @@ public class Zeichenmaschine extends Constants {
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.
*/
@@ -111,14 +75,14 @@ public class Zeichenmaschine extends Constants {
/**
* Das Zeichenfenster der Zeichenmaschine
*/
private Zeichenfenster frame;
protected Zeichenfenster frame;
// Aktueller Zustand der Zeichenmaschine.
/**
* Zustand der Zeichenmaschine insgesamt
*/
private Options.AppState state = Options.AppState.INITIALIZING;
private Options.AppState state;
/**
* Zustand des update/draw Threads
@@ -128,7 +92,7 @@ public class Zeichenmaschine extends Constants {
/**
* Ob der Zeichenthread noch laufen soll, oder beendet.
*/
private boolean running = false;
private boolean running;
private boolean terminateImediately = false;
@@ -141,7 +105,7 @@ public class Zeichenmaschine extends Constants {
* Ob die ZM bei nicht überschriebener update() Methode stoppen soll,
* oder trotzdem weiterläuft.
*/
private boolean run_once = true;
private final boolean run_once;
/**
* Aktuelle Frames pro Sekunde der Zeichenmaschine.
@@ -151,17 +115,17 @@ public class Zeichenmaschine extends Constants {
/**
* Hauptthread der Zeichenmaschine.
*/
private Thread mainThread;
private final Thread mainThread;
/**
* Queue für geplante Aufgaben
*/
private DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
private final DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
/**
* 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,
@@ -274,9 +238,15 @@ public class Zeichenmaschine extends Constants {
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}.
*/
@SuppressWarnings("static-access")
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
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
canvas = new Zeichenleinwand(width, height);
@@ -312,8 +282,15 @@ public class Zeichenmaschine extends Constants {
canvas.addMouseWheelListener(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
// TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
@@ -350,17 +327,15 @@ public class Zeichenmaschine extends Constants {
*
* @param title
*/
private final Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
private Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
while( frame == null ) {
try {
TaskRunner.invokeLater(new Runnable() {
@Override
public void run() {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}
TaskRunner.invokeLater(() -> {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}).get();
} catch( InterruptedException e ) {
// Keep waiting
} catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e);
@@ -392,10 +367,18 @@ public class Zeichenmaschine extends Constants {
public final void setFullscreen( boolean pEnable ) {
if( pEnable && !frame.isFullscreen() ) {
frame.setFullscreen(true);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( frame.isFullscreen() )
fullscreenChanged();
} else if( !pEnable && frame.isFullscreen() ) {
frame.setFullscreen(false);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( !frame.isFullscreen() )
fullscreenChanged();
}
@@ -444,9 +427,18 @@ public class Zeichenmaschine extends Constants {
* im Zeichenfenster an.
*/
public final void redraw() {
if( state == Options.AppState.PAUSED ) {
if( state == Options.AppState.PAUSED
|| state == Options.AppState.TERMINATED ) {
draw();
}
render();
}
/**
* Zeigt den aktuellen Inhalt der {@link Zeichenleinwand}
* im Zeichenfenster an, ohne vorher {@link #draw()} aufzurufen.
*/
public final void render() {
canvas.render();
// canvas.invalidate();
// frame.repaint();
@@ -546,6 +538,11 @@ public class Zeichenmaschine extends Constants {
}
public final void exitNow() {
// Do nothing, when already quitting
if( state == Options.AppState.QUITING ) {
return;
}
if( running ) {
running = false;
terminateImediately = true;
@@ -584,13 +581,16 @@ public class Zeichenmaschine extends Constants {
* @see System#exit(int)
*/
public final void quit( boolean exit ) {
frame.setVisible(false);
canvas.dispose();
frame.dispose();
state = Options.AppState.QUITING;
TaskRunner.invokeLater(() -> {
frame.setVisible(false);
canvas.dispose();
frame.dispose();
if( exit ) {
System.exit(0);
}
if( exit ) {
System.exit(0);
}
});
}
/**
@@ -601,11 +601,11 @@ public class Zeichenmaschine extends Constants {
* @see Zeichenleinwand#setSize(int, int)
*/
public final void setSize( int width, int height ) {
frame.setSize(width, height);
frame.setCanvasSize(width, height);
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
canvasWidth = (int) canvasBounds.getWidth();
canvasHeight = (int) canvasBounds.getHeight();
canvasWidth = canvasBounds.width;
canvasHeight = canvasBounds.height;
}
/**
@@ -679,9 +679,9 @@ public class Zeichenmaschine extends Constants {
* Gibt die erste (unterste) {@link Layer Ebene} der angegebenen Klasse
* zurück.
*
* <pre>
* DrawingLayer draw = getLayer(DrawingLayer.class);
* </pre>
* <pre><code>
* DrawingLayer draw = getLayer(DrawingLayer.class);
* </code></pre>
*
* @param layerClass
* @param <LT>
@@ -947,9 +947,9 @@ public class Zeichenmaschine extends Constants {
* <p>
* Die Konstanten der Klasse {@link Cursor} definieren 13 Standardzeiger,
* die durch angabe der Nummer geladen werden können.
* <pre>
* setCursor(Cursor.HAND_CURSOR);
* </pre>
* <pre><code>
* setCursor(Cursor.HAND_CURSOR);
* </code></pre>
*
* @param pPredefinedCursor Eine der Cursor-Konstanten.
* @see Cursor
@@ -1022,9 +1022,9 @@ public class Zeichenmaschine extends Constants {
* {@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
* vorgegangen werden:
* <pre>
* <pre><code>
* shape.move(50*delta, 0.0);
* </pre>
* </code></pre>
*
* @param delta
*/
@@ -1100,6 +1100,9 @@ public class Zeichenmaschine extends Constants {
}
if( isPaused() || isTerminated() ) {
if( MouseEvent.class.isInstance(evt) ) {
saveMousePosition((MouseEvent)evt);
}
dispatchEvents();
}
}
@@ -1327,7 +1330,7 @@ public class Zeichenmaschine extends Constants {
long overslept = 0L;
// Interne Zähler für tick und runtime
int _tick = 0;
long _runtime = 0;
long _runtime;
// Öffentliche Zähler für Unterklassen
tick = 0;
runtime = 0;
@@ -1390,7 +1393,7 @@ public class Zeichenmaschine extends Constants {
}
// Display the current buffer content
if( canvas != null ) {
if( canvas != null && frame.isDisplayable() ) {
canvas.render();
// canvas.invalidate();
// frame.repaint();
@@ -1444,21 +1447,26 @@ public class Zeichenmaschine extends Constants {
Thread.yield();
}
updateThreadExecutor.shutdownNow();
try {
updateThreadExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
// Cleanup
shutdown();
cleanup();
state = Options.AppState.TERMINATED;
} finally {
// Cleanup
shutdown();
cleanup();
state = Options.AppState.TERMINATED;
if( quitAfterShutdown ) {
quit();
if( quitAfterShutdown ) {
quit();
}
}
}
}
// TODO: Remove
class DelayedTask implements Delayed {
static class DelayedTask implements Delayed {
long startTime; // in ms
@@ -1547,6 +1555,7 @@ public class Zeichenmaschine extends Constants {
}
// TODO: (ngb) exception handling when update/draw throws ex
class UpdateThreadExecutor extends ThreadPoolExecutor {
private Thread updateThread;
@@ -1555,7 +1564,18 @@ public class Zeichenmaschine extends Constants {
public UpdateThreadExecutor() {
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;
}

View File

@@ -11,9 +11,13 @@ import java.awt.RadialGradientPaint;
/**
* Eine Ebene, die nur aus einer Farbe (oder einem Farbverlauf) besteht.
* <p>
* Ein {@code ColorLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
* <p>
* Die Farbe der Ebene kann beliebig gesetzt werden und kann gut als
* Hintergundfarbe für eine Szene dienen, oder als halbtransparente "Abdeckung",
* wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt wird.
* Hintergrundfarbe für eine Szene dienen, oder als halbtransparente
* "Abdeckung", wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt
* wird.
*/
@SuppressWarnings( "unused" )
public class ColorLayer extends Layer {
@@ -63,16 +67,14 @@ public class ColorLayer extends Layer {
}
/**
* Gibt die Hintergrundfarbe der Ebene zurück.
*
* @return Die aktuelle Hintergrundfarbe.
* @return Die aktuelle Hintergrundfarbe der Ebene.
*/
public Color getColor() {
return color;
}
/**
* Setzt die Farbe der Ebene neu.
* Setzt die Farbe der Ebene auf die angegebene Farbe.
*
* @param color Die neue Hintergrundfarbe.
*/
@@ -82,22 +84,69 @@ public class ColorLayer extends Layer {
clear();
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der in die
* angegebene Richtung verläuft.
*
* @param from Farbe am Startpunkt.
* @param to Farbe am Endpunkt.
* @param dir Richtung des Farbverlaufs.
*/
public void setGradient( Color from, Color to, Options.Direction dir ) {
double halfW = getWidth() * .5;
double halfH = getHeight() * .5;
@@ -112,6 +161,18 @@ public class ColorLayer extends Layer {
setGradient(fromX, fromY, from, toX, toY, to);
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
this.color = from;
background = new GradientPaint(
@@ -121,10 +182,30 @@ public class ColorLayer extends Layer {
clear();
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, der im Zentrum beginnt.
*
* @param from Farbe im Zentrum.
* @param to Farbe am Rand.
*/
public void setGradient( Color from, Color to ) {
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, to);
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, mit dem Zentrum im Punkt ({@code centerX}, {@code centerY})
* und dem angegebenen Radius. Der Verlauf starte im Zentrum mit der Farbe
* {@code from} und endet am Rand des durch den Radius beschriebenen Kreises
* mit der Farbe {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
this.color = from;
background = new RadialGradientPaint(
@@ -134,6 +215,9 @@ public class ColorLayer extends Layer {
clear();
}
/**
* Zeichnet den Hintergrund der Ebene mit der gesetzten Füllung neu.
*/
@Override
public void clear() {
drawing.setPaint(background);

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,60 @@
package schule.ngb.zm.layers;
import java.awt.Graphics2D;
import java.awt.Image;
import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Graphics2D;
import java.awt.Image;
/**
* Eine Ebene, die ein statisches Bild anzeigt.
* <p>
* Die Ebene wird mit einem Bild initialisiert und zeigt dieses Bild als
* einzigen Inhalt an. Optional kann die Position des Bildes verändert werden,
* sodass es nicht im Ursprung der Ebene gezeichnet wird.
*/
@SuppressWarnings( "unused" )
public class ImageLayer extends Layer {
/**
* Das Bild, das angezeigt wird.
*/
protected Image image;
/**
* x-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double x = 0;
/**
* y-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double y = 0;
/**
* Interner Schalter, ob die Ebene neu gezeichnet werden muss.
*/
protected boolean redraw = true;
public ImageLayer(String source) {
/**
* Erstellt eine Bildebene aus der angegebenen Bildquelle.
*
* @param source Eine Bildquelle.
*/
public ImageLayer( String source ) {
image = ImageLoader.loadImage(source);
}
public ImageLayer(Image image) {
public ImageLayer( Image image ) {
this.image = image;
}
public ImageLayer(int width, int height, Image image) {
public ImageLayer( int width, int height, Image image ) {
super(width, height);
this.image = image;
}
public void setImage(Image image) {
public void setImage( Image image ) {
this.image = image;
redraw = true;
}
@@ -38,7 +63,7 @@ public class ImageLayer extends Layer {
return x;
}
public void setX(double pX) {
public void setX( double pX ) {
this.x = pX;
redraw = true;
}
@@ -47,7 +72,7 @@ public class ImageLayer extends Layer {
return y;
}
public void setY(double pY) {
public void setY( double pY ) {
this.y = pY;
redraw = true;
}
@@ -59,8 +84,8 @@ public class ImageLayer extends Layer {
}
@Override
public void draw(Graphics2D graphics) {
if (redraw && visible) {
public void draw( Graphics2D graphics ) {
if( redraw && visible ) {
drawing.drawImage(image, (int) x, (int) y, null);
redraw = false;
}

View File

@@ -4,42 +4,46 @@ import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.*;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.util.LinkedList;
@SuppressWarnings( "unused" )
public final class Shape2DLayer extends Layer {
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
private schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
private schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
private double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
private Options.StrokeType strokeType = SOLID;
private LinkedList<java.awt.Shape> shapes;
private final LinkedList<java.awt.Shape> shapes;
private boolean instantDraw = false;
public Shape2DLayer() {
super();
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
}
public Shape2DLayer( boolean instantDraw ) {
super();
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
this.instantDraw = instantDraw;
}
public Shape2DLayer( int width, int height ) {
super(width, height);
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
}
public Shape2DLayer( int width, int height, boolean instantDraw ) {
super(width, height);
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
this.instantDraw = instantDraw;
}
@@ -106,7 +110,7 @@ public final class Shape2DLayer extends Layer {
drawing.setStroke(createStroke());
}
protected Stroke createStroke() {
private Stroke createStroke() {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
@@ -153,7 +157,7 @@ public final class Shape2DLayer extends Layer {
}
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( !instantDraw ) {
for( Shape shape : shapes ) {
drawing.setColor(fillColor.getJavaColor());
@@ -164,7 +168,7 @@ public final class Shape2DLayer extends Layer {
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -10,13 +10,20 @@ import java.awt.Graphics2D;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
/**
* Ein Layer 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 {
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() {
super();
@@ -37,7 +44,7 @@ public class ShapesLayer extends Layer {
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
return (ST) s;
return shapeClass.cast(s);
}
}
return null;
@@ -51,7 +58,7 @@ public class ShapesLayer extends Layer {
List<ST> result = new LinkedList<>();
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
result.add((ST) s);
result.add(shapeClass.cast(s));
}
}
return result;
@@ -59,17 +66,13 @@ public class ShapesLayer extends Layer {
public void add( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
Collections.addAll(this.shapes, shapes);
}
}
public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
this.shapes.addAll(shapes);
}
}
@@ -83,9 +86,7 @@ public class ShapesLayer extends Layer {
public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
this.shapes.removeAll(shapes);
}
}
@@ -116,9 +117,9 @@ public class ShapesLayer extends Layer {
anim.start();
}
public void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim: anims ) {
@SafeVarargs
public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim);
anim.start();
}
@@ -141,13 +142,13 @@ public class ShapesLayer extends Layer {
anim.update(delta);
if( !anim.isActive() ) {
animations.remove(anim);
it.remove();
}
}
}
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
@@ -161,7 +162,7 @@ public class ShapesLayer extends Layer {
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -1,18 +1,19 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.*;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import schule.ngb.zm.Vector;
import schule.ngb.zm.shapes.FilledShape;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Strokeable;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class TurtleLayer extends Layer {
@SuppressWarnings( "unused" )
public class TurtleLayer extends Layer implements Strokeable, Fillable {
// Rotating by the clock
public static final int H1 = 30;
@@ -39,7 +40,7 @@ public class TurtleLayer extends Layer {
public static final int H12 = 360;
private static Stack<Color> turtleColors;
private final static Stack<Color> turtleColors;
static {
turtleColors = new Stack<>();
@@ -51,9 +52,9 @@ public class TurtleLayer extends Layer {
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() {
super();
@@ -117,7 +118,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() {
mainTurtle.fill();
@@ -175,100 +189,196 @@ public class TurtleLayer extends Layer {
mainTurtle.moveTo(x, y);
}
public Color getFillColor() {
return mainTurtle.getFillColor();
@Override
public void setFill( Paint fill ) {
mainTurtle.setFill(fill);
}
@Override
public Paint getFill() {
return mainTurtle.getFill();
}
@Override
public void setFillColor( Color color ) {
mainTurtle.setFillColor(color);
}
public void setFillColor( int gray ) {
mainTurtle.setFillColor(gray);
}
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 Color getFillColor() {
return mainTurtle.getFillColor();
}
@Override
public void setStrokeColor( Color color ) {
mainTurtle.setStrokeColor(color);
}
public void setStrokeColor( int gray ) {
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();
}
@Override
public void setStrokeWeight( double weight ) {
mainTurtle.setStrokeWeight(weight);
}
@Override
public Options.StrokeType getStrokeType() {
return mainTurtle.getStrokeType();
}
@Override
public void setStrokeType( Options.StrokeType 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() {
mainTurtle.resetStroke();
}
public void addPosToPath() {
mainTurtle.addPosToPath();
@Override
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;
@@ -282,7 +392,31 @@ public class TurtleLayer extends Layer {
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() {
if( !pathOpen ) {
@@ -294,53 +428,39 @@ public class TurtleLayer extends Layer {
}
}
private void closePath() {
if( pathOpen ) {
addPosToPath();
path.closePath();
path.trimToSize();
pathOpen = false;
}
}
public void fill() {
closePath();
if( fillColor != null && fillColor.getAlpha() > 0 ) {
drawing.setColor(fillColor.getJavaColor());
if( hasFill() ) {
drawing.setPaint(getFill());
drawing.fill(path);
}
}
public boolean isVisible() {
return visible;
}
@Override
public void draw( Graphics2D graphics ) {
/*Shape shape = new RoundRectangle2D.Double(
-12, -5, 16, 10, 5, 3
);*/
Path2D path = new Path2D.Double();
path.moveTo(STD_SIZE, 0);
path.lineTo(-STD_SIZE, -STD_SIZE/2);
path.lineTo(-STD_SIZE, STD_SIZE/2);
path.lineTo(STD_SIZE, 0);
if( turtlePath == null ) {
turtlePath = new Path2D.Double();
path.moveTo(DEFAULT_SIZE, 0);
path.lineTo(-DEFAULT_SIZE, -DEFAULT_SIZE / 2.0);
path.lineTo(-DEFAULT_SIZE, DEFAULT_SIZE / 2.0);
path.lineTo(DEFAULT_SIZE, 0);
}
AffineTransform verzerrung = new AffineTransform();
verzerrung.translate(position.x, position.y);
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());
} else {
graphics.setColor(DEFAULT_STROKECOLOR.getJavaColor());
}
graphics.fill(shape);
graphics.setColor(Color.BLACK.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.draw(shape);
}
@@ -354,9 +474,9 @@ public class TurtleLayer extends Layer {
Vector positionStart = position.copy();
position.add(Vector.setLength(direction, dist));
if( penDown && strokeColor != null ) {
if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.setStroke(getStroke());
drawing.drawLine((int) positionStart.x, (int) positionStart.y, (int) position.x, (int) position.y);
}
}
@@ -407,9 +527,9 @@ public class TurtleLayer extends Layer {
position.x = x;
position.y = y;
if( penDown && strokeColor != null ) {
if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.setStroke(getStroke());
drawing.drawLine((int) x, (int) y, (int) position.x, (int) position.y);
}
}

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,8 +1,7 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Options;
import schule.ngb.zm.*;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
@@ -14,7 +13,7 @@ import java.awt.geom.Point2D;
* <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die 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
* beispielsweise so implementiert:
*
@@ -34,7 +33,7 @@ import java.awt.geom.Point2D;
* eine {@link #equals(Object)} Methode.
*/
@SuppressWarnings( "unused" )
public abstract class Shape extends FilledShape {
public abstract class Shape extends BasicDrawable {
/**
* x-Koordinate der Form.
@@ -56,11 +55,6 @@ public abstract class Shape extends FilledShape {
*/
protected double scale = 1.0;
/**
* Ob die Form angezeigt werden soll.
*/
protected boolean visible = true;
/**
* Ankerpunkt der Form.
*/
@@ -84,38 +78,6 @@ public abstract class Shape extends FilledShape {
this.y = y;
}
/**
* Ob die Form angezeigt wird oder nicht.
*
* @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.
*
@@ -180,6 +142,25 @@ public abstract class Shape extends FilledShape {
*/
public abstract double getHeight();
/**
* {@inheritDoc}
*/
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
/**
* {@inheritDoc}
*/
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
/**
* Gibt die Rotation in Grad zurück.
*
@@ -191,6 +172,7 @@ public abstract class Shape extends FilledShape {
/**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
*
* @param angle Drehwinkel in Grad.
*/
public void rotate( double angle ) {
@@ -227,6 +209,7 @@ public abstract class Shape extends FilledShape {
public double getScale() {
return scale;
}
public void scale( double factor ) {
scale = factor;
}
@@ -235,17 +218,6 @@ public abstract class Shape extends FilledShape {
scale(scale * factor);
}
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);
}
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() {
return anchor;
}
@@ -268,28 +240,25 @@ public abstract class Shape extends FilledShape {
}
/**
* 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.
* <p>
* Die Koordinaten des Ankerpunkt werden relativ zur oberen linken Ecke des
* Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt.
*
* @param width Breite des umschließdenden Rechtecks.
* @param height Höhe des umschließdenden Rechtecks.
* @param width Breite des umschließenden Rechtecks.
* @param height Höhe des umschließenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten.
*/
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;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(
return new Point2D.Double(
wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y
);
return anchorPoint;
}
/**
@@ -303,12 +272,10 @@ public abstract class Shape extends FilledShape {
double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(
return new Point2D.Double(
wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y)
);
return anchorPoint;
}
/**
@@ -352,8 +319,8 @@ public abstract class Shape extends FilledShape {
setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType());
visible = shape.isVisible();
rotation = shape.rotation;
scale(shape.scale);
rotation = shape.getRotation();
scale(shape.getScale());
setAnchor(shape.getAnchor());
}
}
@@ -384,19 +351,10 @@ public abstract class Shape extends FilledShape {
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen.
* <p>
* Da die AWT-Shape bei jedem Zeichnen (also mindestens einmal pro Frame)
* benötigt wird, wird das aktuelle Shape-Objekt in {@link #awtShape}
* 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
* Wenn diese Form nicht durch eine AWT-Shape dargestellt 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}.
*/
public abstract java.awt.Shape getShape();
@@ -560,7 +518,7 @@ public abstract class Shape extends FilledShape {
shape = transform.createTransformedShape(shape);
}
Color currentColor = graphics.getColor();
java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
@@ -573,8 +531,8 @@ public abstract class Shape extends FilledShape {
* verglichen. Unterklassen überschreiben die Methode, um weitere
* Eigenschaften zu berücksichtigen.
* <p>
* Die Eigenschaften von {@link FilledShape} und {@link StrokedShape} werden
* nicht verglichen.
* Die Eigenschaften, die durch {@link Fillable} und {@link Strokeable}
* impliziert werden, werden nicht verglichen.
*
* @param o Ein anderes Objekt.
* @return
@@ -590,4 +548,43 @@ public abstract class Shape extends FilledShape {
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.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -35,7 +36,7 @@ import java.util.List;
* {@link #arrangeInGrid(int, Options.Direction, double, int)} und
* {@link #align(Options.Direction)} verwendet werden, die jeweils die Position
* 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 {
@@ -43,7 +44,7 @@ public class ShapeGroup extends Shape {
public static final int ARRANGE_COLS = 1;
private List<Shape> shapes;
private final List<Shape> shapes;
private double groupWidth = -1.0;
@@ -51,7 +52,7 @@ public class ShapeGroup extends Shape {
public ShapeGroup() {
super();
shapes = new ArrayList<>(10);
shapes = Collections.synchronizedList(new ArrayList<>(10));
}
public ShapeGroup( double x, double y ) {
@@ -62,9 +63,7 @@ public class ShapeGroup extends Shape {
public ShapeGroup( double x, double y, Shape... shapes ) {
super(x, y);
this.shapes = new ArrayList<>(shapes.length);
for( Shape pShape : shapes ) {
this.shapes.add(pShape);
}
Collections.addAll(this.shapes, shapes);
}
public Shape copy() {
@@ -99,9 +98,11 @@ public class ShapeGroup extends Shape {
public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) {
LinkedList<ShapeType> list = new LinkedList<>();
for( Shape s : shapes ) {
if( typeClass.isInstance(s) ) {
list.add((ShapeType) s);
synchronized( shapes ) {
for( Shape shape : shapes ) {
if( typeClass.isInstance(shape) ) {
list.add(typeClass.cast(shape));
}
}
}
return list;
@@ -170,19 +171,15 @@ public class ShapeGroup extends Shape {
int rows, cols;
if( mode == ARRANGE_ROWS ) {
rows = n;
cols = (int) ceil(shapes.size() / n);
cols = (int) ceil(shapes.size() / (double)n);
} else {
cols = n;
rows = (int) ceil(shapes.size() / n);
rows = (int) ceil(shapes.size() / (double)n);
}
// Calculate grid cell size
double maxHeight = shapes.stream().mapToDouble(
( s ) -> s.getHeight()
).reduce(0.0, Double::max);
double maxWidth = shapes.stream().mapToDouble(
( s ) -> s.getWidth()
).reduce(0.0, Double::max);
double maxHeight = shapes.stream().mapToDouble(Shape::getHeight).reduce(0.0, Double::max);
double maxWidth = shapes.stream().mapToDouble(Shape::getWidth).reduce(0.0, Double::max);
double halfHeight = maxHeight * .5;
double halfWidth = maxWidth * .5;

View File

@@ -24,6 +24,7 @@ public class Text extends Shape {
public Text( double x, double y, String text ) {
this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, DEFAULT_FONTSIZE));
}
public Text( double x, double y, String text, String fontname ) {
super(x, y);
Font userfont = FontLoader.loadFont(fontname);
@@ -60,29 +61,58 @@ public class Text extends Shape {
return height;
}
public void setFont( String fontname ) {
Font newFont = FontLoader.loadFont(fontname);
public Font getFont() {
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 ) {
setFont(newFont);
}
}
public void setFont( Font newFont ) {
font = newFont.deriveFont(font.getSize2D());
public void setFont( String fontName, double fontSize ) {
Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
font = newFont.deriveFont((float) fontSize);
calculateBounds();
}
public Font getFont() {
return font;
public void setFont( String fontName, double fontSize, int style ) {
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);
calculateBounds();
}
public double getFontsize() {
return font.getSize2D();
public int getFontStyle() {
return font.getStyle();
}
public void setFontStyle( int fontStyle ) {
font = font.deriveFont(fontStyle);
calculateBounds();
}
public String getText() {
@@ -151,19 +181,19 @@ public class Text extends Shape {
@Override
public void copyFrom( Shape shape ) {
super.copyFrom(shape);
if( shape instanceof Text ) {
Text pText = (Text) shape;
this.text = pText.getText();
this.font = pText.getFont();
calculateBounds();
}
super.copyFrom(shape);
calculateBounds();
}
@Override
public void scale( double factor ) {
super.scale(factor);
setFontsize(font.getSize2D() * factor);
setFontSize(font.getSize2D() * factor);
}
@Override
@@ -191,7 +221,7 @@ public class Text extends Shape {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawRect(0, 0, width, height);
}

View File

@@ -288,7 +288,7 @@ public class BarChart extends Rectangle {
barY += barHeight + gap;
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
if( inverted ) {
graphics.drawLine((int) width, 0, (int) width, (int) height);
} else {
@@ -360,7 +360,7 @@ public class BarChart extends Rectangle {
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
if( inverted ) {
graphics.drawLine(0, 0, (int) width, 0);
} else {

View File

@@ -52,7 +52,7 @@ public class ChartAxes extends Rectangle {
}
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), 0, (int)(height - (yMax-yMin) * yUnit));
if( showArrows ) {

View File

@@ -91,7 +91,7 @@ public class LineChart extends Rectangle {
for( ChartValue lcv : val ) {
if( drawLines && lastLcv != null ) {
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));
drawDot(graphics, lastLcv, xUnit, yUnit);
}

View File

@@ -326,7 +326,7 @@ public class PieChart extends Circle {
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawOval(0, 0, (int) (radius * 2), (int) (radius * 2));
graphics.setTransform(originalTransform);

View File

@@ -4,33 +4,101 @@ import java.util.Collection;
import java.util.List;
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 Objkete abgerufen werden.
* <p>
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein Zähler 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" )
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 ) {
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 ) {
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 ) {
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;
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);
}
/**
* Gibt die aktuelle Anzahl zurück.
* @return Die aktuelle Anzahl.
*/
public int getCount() {
return count.get();
}
@@ -61,6 +129,8 @@ public final class Counter {
}
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter setCount( int count ) {
this.count.set(count);
return this;
@@ -94,10 +164,10 @@ public final class Counter {
inc();
// Update stats
synchronized( count ) {
sum = Double.isNaN(sum) ? value : sum + value;
sum = Double.isNaN(sum) ? value : sum + value;
if( Double.isNaN(max) || max < value )
max = value;
if( Double.isNaN(min) ||min > value )
if( Double.isNaN(min) || min > value )
min = value;
}
return this;
@@ -120,7 +190,15 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) {
for( double value: values ) {
for( double value : values ) {
count(value);
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( int[] values ) {
for( double value : values ) {
count(value);
}
return this;
@@ -128,7 +206,7 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) {
for( Number value: values ) {
for( Number value : values ) {
count(value);
}
return this;
@@ -136,7 +214,7 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) {
for( Number value: values ) {
for( Number value : values ) {
count(value);
}
return this;

View File

@@ -0,0 +1,190 @@
package schule.ngb.zm.util;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.util.io.FileLoader;
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;
/**
* 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 Platzhalterbilder.
*/
public final class Faker {
public static final String FAKE_IMG_URL = "https://loremflickr.com/%d/%d";
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[][] fakeUsers( int n ) {
String[][] data = loadMockfile("users");
String[][] result = new String[n][];
for( int i = 0; i < n; i++ ) {
result[i] = Constants.choice(data);
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Vornamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeNames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[0];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Namen (Vor- und
* Nachname).
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeFullnames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[0] + " " + row[1];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeUsernames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[3];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Passwörter.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakePasswords( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[4];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger E-Mail-Adressen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeEmails( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[5];
}
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,
(int) ((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);
}
}
public static String fakeText( int words, int paragraphs ) {
return "";
}
private static String[][] loadMockfile( String name ) {
return FileLoader.loadCsv("schule/ngb/zm/util/mock-" + name + ".csv", true);
}
private Faker() {
}
}

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
* dann aber denselben {@code Logger}.
* <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.
*/
public final class Log {
@@ -133,23 +134,13 @@ public final class Log {
if( System.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()) {
@Override
public synchronized void publish(final LogRecord record) {
public synchronized void publish( final LogRecord record ) {
super.publish(record);
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;
@@ -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 ) {
String clazz = sourceClass.getName();
String method = "unknown";
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
clazz = trace[i].getClassName();
method = trace[i].getMethodName();
for( StackTraceElement stackTraceElement : trace ) {
if( !stackTraceElement.getClassName().equals(Log.class.getName()) ) {
clazz = stackTraceElement.getClassName();
method = stackTraceElement.getMethodName();
break;
}
}
@@ -350,9 +330,9 @@ public final class Log {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
if( record.getSourceClassName() != null ) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
if( record.getSourceMethodName() != null ) {
source += " " + record.getSourceMethodName();
}
} else {
@@ -360,7 +340,7 @@ public final class Log {
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
if( record.getThrown() != null ) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
@@ -368,7 +348,7 @@ public final class Log {
pw.close();
throwable = sw.toString();
}
return String.format(DEFAULT_LOG_FORMAT,
return String.format(DEFAULT_DEBUG_FORMAT,
zdt,
source,
record.getLoggerName(),

View File

@@ -10,6 +10,7 @@ import java.util.Random;
* href="https://adrianb.io/2014/08/09/perlinnoise.html">Beschreibung von
* FLAFLA2</a> implementiert.
*/
@SuppressWarnings( "unused" )
public class Noise {
private static final int N = 256;
@@ -42,7 +43,7 @@ public class Noise {
/**
* Initialisiert diesen Perlin-Noise mit dem angegebenen Zufallsgenerator.
*
* @param rand
* @param rand Ein Zufallsgenerator-Objekt.
*/
public Noise( Random rand ) {
init(rand);

View File

@@ -2,77 +2,189 @@ package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit;
/**
* Eine Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p>
* Die Klasse kann zum Beispiel genutzt werden, um die Laufzeit eines
* Quelltextes zu messen. 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" )
public final class Timer {
/**
* Die Basiseinheit für die Zeitmessung.
*/
private final TimeUnit baseUnit;
/**
* Ob die Zeitmessung gerade läuft.
*/
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;
/**
* Erstellt einen neuen {@code Timer} mit Millisekunden als Basiseinheit.
*/
public Timer() {
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 ) {
this.baseUnit = baseUnit;
}
/**
* Ob die Zeitmessung gerade läuft.
*
* @return {@code true}, wenn die Zeitmessung mit {@link #start()} gestartet
* wurde.
*/
public boolean isRunning() {
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" )
public Timer start() {
starttime = System.nanoTime();
start = System.nanoTime();
running = true;
return this;
}
/**
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Zeit wird
* zur Summe aller gemessenen Zeiten hinzuaddiert.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer stop() {
running = false;
elapsed += System.nanoTime() - starttime;
if( running ) {
running = false;
elapsed += System.nanoTime() - start;
}
return this;
}
/**
* Setzt den {@code Timer} auf den Startzustand und löscht alle bisher
* gemessenen Zeiten. Falls die Zeitmessung gerade läuft, wird sie nicht
* gestoppt, sondern läuft vom Zeitpunkt des Aufrufs weiter.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer reset() {
running = false;
starttime = -1;
elapsed = 0;
start = -1;
if( running ) {
start = System.nanoTime();
}
return this;
}
/**
* Gibt die Zeit in der Basiseinheit zurück.
*
* @return Die bisher insgesamt gemessene Zeit.
*/
public long getTime() {
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 ) {
if( running ) {
return unit.convert(System.nanoTime() - starttime + elapsed, TimeUnit.NANOSECONDS);
return unit.convert(System.nanoTime() - start + elapsed, TimeUnit.NANOSECONDS);
} else {
return unit.convert(elapsed, TimeUnit.NANOSECONDS);
}
}
/**
* Gibt die gemessene Zeit in Millisekunden (gerundet) zurück.
*
* @return Die gemessene Zeit in ms.
*/
public int getMillis() {
if( running ) {
return (int) ((System.nanoTime() - starttime + elapsed) / 1000000);
return (int) ((System.nanoTime() - start + elapsed) / 1000000);
} else {
return (int) (elapsed / 1000000);
}
}
/**
* Gibt die gemessene Zeit in Sekunden zurück.
*
* @return Die gemessene Zeit in s.
*/
public double getSeconds() {
if( running ) {
return (System.nanoTime() - starttime + elapsed) / 1000000000.0;
return (System.nanoTime() - start + elapsed) / 1000000000.0;
} else {
return elapsed / 1000000000.0;
}

View File

@@ -1,9 +1,9 @@
package schule.ngb.zm.util;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
@SuppressWarnings( "UnusedReturnValue" )
public class Validator {
public static final <T> T requireNotNull( T obj ) {
@@ -19,7 +19,7 @@ public class Validator {
}
public static final String requireNotEmpty( String str ) {
return requireNotEmpty(str, (Supplier<String>)null);
return requireNotEmpty(str, (Supplier<String>) null);
}
public static final String requireNotEmpty( String str, CharSequence msg ) {
@@ -33,7 +33,7 @@ public class Validator {
}
public static final int requirePositive( int i ) {
return requirePositive(i, (Supplier<String>)null);
return requirePositive(i, (Supplier<String>) null);
}
public static final int requirePositive( int i, CharSequence msg ) {
@@ -47,7 +47,7 @@ public class Validator {
}
public static final int requireNotNegative( int i ) {
return requireNotNegative(i, (Supplier<String>)null);
return requireNotNegative(i, (Supplier<String>) null);
}
public static final int requireNotNegative( int i, CharSequence msg ) {
@@ -61,7 +61,7 @@ public class Validator {
}
public static final int requireInRange( int i, int min, int max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
return requireInRange(i, min, max, (Supplier<String>) null);
}
public static final int requireInRange( int i, int min, int max, CharSequence msg ) {
@@ -75,7 +75,7 @@ public class Validator {
}
public static final double requirePositive( double i ) {
return requirePositive(i, (Supplier<String>)null);
return requirePositive(i, (Supplier<String>) null);
}
public static final double requirePositive( double i, CharSequence msg ) {
@@ -89,7 +89,7 @@ public class Validator {
}
public static final double requireNotNegative( double i ) {
return requireNotNegative(i, (Supplier<String>)null);
return requireNotNegative(i, (Supplier<String>) null);
}
public static final double requireNotNegative( double i, CharSequence msg ) {
@@ -103,7 +103,7 @@ public class Validator {
}
public static final double requireInRange( double i, double min, double max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
return requireInRange(i, min, max, (Supplier<String>) null);
}
public static final double requireInRange( double i, double min, double max, CharSequence msg ) {
@@ -138,7 +138,7 @@ public class Validator {
public static final <T> T[] requireNotEmpty( T[] arr ) {
return requireNotEmpty(arr, (Supplier<String>)null);
return requireNotEmpty(arr, (Supplier<String>) null);
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence msg ) {
@@ -147,12 +147,12 @@ public class Validator {
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());
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
return arr;
}
public static final <T> T[] requireSize( T[] arr, int size ) {
return requireSize(arr, size, (Supplier<String>)null);
return requireSize(arr, size, (Supplier<String>) null);
}
public static final <T> T[] requireSize( T[] arr, int size, CharSequence msg ) {
@@ -161,21 +161,21 @@ public class Validator {
public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) {
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);
return requireValid(arr, (Supplier<String>) null);
}
public static final<T> T[] requireValid( T[] arr, CharSequence msg ) {
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());
throw new IllegalArgumentException(msg == null ? "Parameter array may not be null or empty" : msg.get());
return arr;
}

View File

@@ -9,9 +9,9 @@ import java.util.function.BiConsumer;
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() {
listeners = new CopyOnWriteArraySet<>();
@@ -49,9 +49,7 @@ public class EventDispatcher<E, L extends Listener<E>> {
if( eventRegistered(eventKey) ) {
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
listeners.forEach(( listener ) -> {
dispatcher.accept(event, listener);
});
listeners.forEach(( listener ) -> dispatcher.accept(event, listener));
}
}

View File

@@ -2,6 +2,4 @@ package schule.ngb.zm.util.events;
public interface Listener<E> {
}

View File

@@ -2,15 +2,18 @@ package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public final class FileLoader {
@@ -22,19 +25,31 @@ public final class FileLoader {
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
public static List<String> loadLines( String source ) {
return loadLines(source, UTF8);
}
/**
* Lädt die angegebene Datei Zeilenweise in eine Liste.
*
* @param source
* @param charset
* @return
*/
public static List<String> loadLines( String source, Charset charset ) {
try {
return Files.readAllLines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source);
}
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
List<String> result = new ArrayList<>();
return Collections.EMPTY_LIST;
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);
return Collections.emptyList();
}
}
public static String loadText( String source ) {
@@ -42,66 +57,111 @@ public final class FileLoader {
}
public static String loadText( String source, Charset charset ) {
try {
return Files.readString(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading text from source <%s>", source);
}
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
StringBuilder result = new StringBuilder();
return "";
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 "";
}
}
/**
* Lädt die Daten aus einer CSV Datei in ein zweidimensionales
* String-Array.
* <p>
* Der Aufruf entspricht dem Aufruf von
* <pre><code>
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
* </code></pre>
*
* @param source Die Quelle der CSV-Daten.
* @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 ) {
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 können Inhalte, die das Trennzeichen {@code separator}
* enthalten, nicht korrekt erkannt werden. 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 ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> line.split(Character.toString(separator))
).toArray(String[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading csv source <%s>", source);
}
return new String[0][0];
int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map(
//( line ) -> line.split(Character.toString(separator))
( line ) -> line.split("\\s*" + separator + "\\s*")
).toArray(String[][]::new);
}
public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
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 ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).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];
int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).toArray(double[][]::new);
}
public FileLoader() {
private FileLoader() {
}
private static final Log LOG = Log.getLogger(FileLoader.class);

View File

@@ -1,12 +1,14 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -18,23 +20,29 @@ public class FontLoader {
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Schrift wird unter ihrem Dateinamen in den Schriftenspeicher geladen
* und kann danach in der Zeichenmaschine benutzt werden.
*
* Ein Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* 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 source
* @return
* @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/
public static Font loadFont( String source ) {
String name = source;
// Dateipfad entfernen
int lastIndex = source.lastIndexOf(File.separatorChar);
if( lastIndex > -1 ) {
source.substring(lastIndex + 1);
source = source.substring(lastIndex + 1);
}
// Dateiendung entfernen
lastIndex = name.lastIndexOf('.');
@@ -45,10 +53,8 @@ public class FontLoader {
}
public static Font loadFont( String name, String source ) {
Objects.requireNonNull(source, "Font source may not be null");
if( source.length() == 0 ) {
throw new IllegalArgumentException("Font source may not be empty.");
}
Validator.requireNotNull(source,"Font source may not be null");
Validator.requireNotEmpty(source, "Font source may not be empty.");
if( fontCache.containsKey(name) ) {
LOG.trace("Retrieved font <%s> from font cache.", name);
@@ -57,7 +63,7 @@ public class FontLoader {
// Look for System fonts
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(source, font);
LOG.debug("Loaded system font for <%s>.", source);
@@ -85,6 +91,38 @@ public class FontLoader {
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() {
}

View File

@@ -22,9 +22,9 @@ public final class ImageLoader {
public static boolean caching = true;
private static Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static final Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
private static final SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
/**
* Lädt ein Bild von der angegebenen Quelle {@code source}.
@@ -48,12 +48,12 @@ public final class ImageLoader {
/**
* 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
* konnte. Ist ein Bild mit der angegebenen Quelle im Cache, wird das
* gespeicherte Bild zurückgegeben. Dies kann mit {@code caching = false}
* verhindert werden.
* Bild zurück oder {@code null}, wenn das Bild nicht geladen werden konnte.
* Ist ein Bild mit der angegebenen Quelle im Cache, wird das gespeicherte
* Bild zurückgegeben. Dies kann mit {@code caching = false} verhindert
* werden.
* <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
* vorhanden ist.
*
@@ -113,9 +113,9 @@ public final class ImageLoader {
}
/**
* Speiechert das angegebene Bild unter dem angegebenen Namen im Cache.
* Speichert das angegebene Bild unter dem angegebenen Namen im Cache.
* <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
* überschrieben.
* <p>
@@ -149,9 +149,7 @@ public final class ImageLoader {
* @param name Name des Bildes im Cache.
*/
public static void invalidateCache( final String name ) {
if( imageCache.containsKey(name) ) {
imageCache.remove(name);
}
imageCache.remove(name);
}
/**
@@ -211,7 +209,7 @@ public final class ImageLoader {
* Bild zurückgegeben und nicht komplett neu geladen.
* <p>
* <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
* Zwischenspeicher). Für Änderungen sollte daher immer
* {@link #copyImage(BufferedImage) eine Kopie} des Bildes erstellt werden:
@@ -302,7 +300,7 @@ public final class ImageLoader {
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* ü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}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
@@ -335,7 +333,7 @@ public final class ImageLoader {
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* ü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}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.

View File

@@ -5,13 +5,66 @@ import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.io.*;
import java.net.URISyntaxException;
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.
*/
public class ResourceStreamProvider {
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt
* 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>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* Auftretende Exceptions
* werden als {@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,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @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 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.");
// 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 Resource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
@@ -45,134 +98,41 @@ public class ResourceStreamProvider {
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim öffnen des Streams zu
* @throws IOException Geworfen beim Öffnen des Streams zu
* einer bestehenden Resource oder falls
* keine passende Resource gefunden wurde.
*/
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
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();
return getResourceURL(source).openStream();
}
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt
* 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>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* Auftretende Exceptions
* werden als {@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,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @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 {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
URL url = getResourceURL(source);
return getOutputStream(new File(url.getPath()));
try {
return Files.newOutputStream(Path.of(getResourceURL(source).toURI()));
} catch( URISyntaxException ex ) {
throw new IOException(ex);
}
}
public static OutputStream getOutputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileOutputStream(file);
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));
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));
public static BufferedWriter getWriter( String source ) throws IOException {
return getWriter(source, StandardCharsets.UTF_8);
}
public static BufferedWriter getWriter( String source, Charset charset ) throws IOException {
return new BufferedWriter(new OutputStreamWriter(getOutputStream(source), charset.newEncoder()));
}
private ResourceStreamProvider() {

View File

@@ -5,35 +5,18 @@ import schule.ngb.zm.Zeichenmaschine;
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
public void run() {
initialize();
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
long beforeTime = System.nanoTime();
// store for deltas
long overslept = 0L;
// internal counters for tick and runtime
int _tick = 0;
double delta = 0.0;
// internal counters for tick and delta time
int _tick;
double delta;
while( running ) {
// delta in seconds
@@ -53,10 +36,7 @@ public abstract class FrameSynchronizedTask extends Task {
}
}
}
running = false;
done = true;
finish();
}

View File

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

View File

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

View File

@@ -0,0 +1,342 @@
algae green,#64e986
aliceblue,#f0f8ff
alien green,#6cc417
antiquewhite,#faebd7
aquamarine,#7fffd4
army brown,#827b60
ash gray,#666362
avocado green,#b2c248
aztech purple,#893bff
azure,#f0ffff
baby blue,#95b9c7
bashful pink,#c25283
basket ball orange,#f88158
battleship gray,#848482
bean red,#f75d59
bee yellow,#e9ab17
beer,#fbb117
beetle green,#4c787e
beige,#f5f5dc
black cat,#413839
black cow,#4c4646
black eel,#463e3f
black,#000000
blanchedalmond,#ffebcd
blonde,#fbf6d9
blood red,#7e3517
blossom pink,#f9b7ff
blue angel,#b7ceec
blue diamond,#4ee2ec
blue dress,#157dec
blue eyes,#1569c7
blue gray,#98afc7
blue green,#7bccb5
blue hosta,#77bfc7
blue ivy,#3090c7
blue jay,#2b547e
blue koi,#659ec7
blue lagoon,#8eebec
blue lotus,#6960ec
blue orchid,#1f45fc
blue ribbon,#306eff
blue whale,#342d7e
blue zircon,#57feff
blueberry blue,#0041c2
blush pink,#e6a9ec
blush red,#e56e94
brass,#b5a642
bright gold,#fdd017
bright neon pink,#f433ff
bronze,#cd7f32
brown bear,#835c3b
brown sugar,#e2a76f
bullet shell,#af9b60
burgundy,#8c001a
burlywood,#deb887
burnt pink,#c12267
butterfly blue,#38acec
cadillac pink,#e38aae
camel brown,#c19a6b
camouflage green,#78866b
cantaloupe,#ffa62f
caramel,#c68e17
carbon gray,#625d5d
carnation pink,#f778a1
celeste,#50ebec
champagne,#f7e7ce
charcoal,#34282c
chartreuse,#8afb17
cherry red,#c24641
chestnut red,#c34a2c
chestnut,#954535
chilli pepper,#c11b17
chocolate,#c85a17
cinnamon,#c58917
cloudy gray,#6d6968
clover green,#3ea055
cobalt blue,#0020c2
coffee,#6f4e37
columbia blue,#87afc7
construction cone orange,#f87431
cookie brown,#c7a317
copper,#b87333
coral blue,#afdcec
coral,#ff7f50
corn yellow,#fff380
cornflower blue,#6495ed
cornsilk,#fff8dc
cotton candy,#fcdfff
cranberry,#9f000f
cream,#ffffcc
crimson,#e238ec
crocus purple,#9172ec
crystal blue,#5cb3ff
cyan opaque,#92c7c7
cyan or aqua,#00ffff
dark carnation pink,#c12283
dark forest green,#254117
dark goldenrod,#af7817
dark orange,#f88017
dark orchid,#7d1b7e
dark salmon,#e18b6b
dark sea green,#8bb381
dark slate blue,#2b3856
dark slate grey,#25383c
dark turquoise,#3b9c9c
dark violet,#842dce
day sky blue,#82caff
deep peach,#ffcba4
deep pink,#f52887
deep sky blue,#3bb9ff
denim blue,#79baec
denim dark blue,#151b8d
desert sand,#edc9af
dimorphotheca magenta,#e3319d
dodger blue,#1589ff
dollar bill green,#85bb65
dragon green,#6afb92
dull purple,#7f525d
earth blue,#0000a0
eggplant,#614051
electric blue,#9afeff
emerald green,#5ffb17
fall leaf brown,#c8b560
fern green,#667c26
ferrari red,#f70d1a
fire engine red,#f62817
firebrick,#800517
flamingo pink,#f9a7b0
forest green,#4e9258
frog green,#99c68e
ginger brown,#c9be62
glacial blue ice,#368bc1
golden brown,#eac117
goldenrod,#edda74
granite,#837e7c
grape,#5e5a80
grapefruit,#dc381f
gray cloud,#b6b6b4
gray dolphin,#5c5858
gray goose,#d1d0ce
gray wolf,#504a4b
gray,#736f6e
grayish turquoise,#5e7d7e
green apple,#4cc417
green onion,#6aa121
green peas,#89c35c
green snake,#6cbb3c
green thumb,#b5eaaa
green yellow,#b1fb17
green,#00ff00
greenish blue,#307d7e
gunmetal,#2c3539
halloween orange,#e66c2c
harvest gold,#ede275
hazel green,#617c58
heliotrope purple,#d462ff
hot pink,#f660ab
hummingbird green,#7fe817
iceberg,#56a5ec
iguana green,#9cb071
indigo,#4b0082
iridium,#3d3c3a
jade green,#5efb6e
jasmine purple,#a23bec
jeans blue,#a0cfec
jellyfish,#46c7c7
jet gray,#616d7e
jungle green,#347c2c
kelly green,#4cc552
khaki rose,#c5908e
khaki,#ada96e
lapis blue,#15317e
lava red,#e42217
lavender blue,#e3e4fa
lavender pinocchio,#ebdde2
lawn green,#87f717
lemon chiffon,#fff8c6
light aquamarine,#93ffe8
light blue,#addfff
light coral,#e77471
light cyan,#e0ffff
light jade,#c3fdb8
light pink,#faafba
light salmon,#f9966b
light sea green,#3ea99f
light sky blue,#82cafa
light slate blue,#736aff
light slate gray,#6d7b8d
light slate,#ccffff
light steel blue,#728fce
lilac,#c8a2c8
lime green,#41a317
lipstick pink,#c48793
love red,#e41b17
lovely purple,#7f38ec
macaroni and cheese,#f2bb66
macaw blue green,#43bfc7
magenta,#ff00ff
mahogany,#c04000
mango orange,#ff8040
marble blue,#566d7e
maroon,#810541
mauve,#e0b0ff
medium aquamarine,#348781
medium forest green,#347235
medium orchid,#b048b5
medium purple,#8467d7
medium sea green,#306754
medium spring green,#348017
medium turquoise,#48cccd
medium violet red,#ca226b
metallic silver,#bcc6cc
midnight blue,#151b54
midnight,#2b1b17
milk white,#fefcff
mint green,#98ff98
mist blue,#646d7e
misty rose,#fbbbb9
moccasin,#827839
mocha,#493d26
mustard,#ffdb58
navy blue,#000080
nebula green,#59e817
neon pink,#f535aa
night,#0c090a
northern lights blue,#78c7c7
oak brown,#806517
ocean blue,#2b65ec
oil,#3b3131
orange gold,#d4a017
orange salmon,#c47451
pale blue lily,#cfecec
pale violet red,#d16587
papaya orange,#e56717
parchment,#ffffc2
pastel blue,#b4cfec
peach,#ffe5b4
pearl,#fdeef4
periwinkle,#e9cfec
pig pink,#fdd7e4
pine green,#387c44
pink bow,#c48189
pink bubblegum,#ffdfdd
pink cupcake,#e45e9d
pink daisy,#e799a3
pink lemonade,#e4287c
pink rose,#e7a1b0
pink,#faafbe
pistachio green,#9dc209
platinum,#e5e4e2
plum pie,#7d0541
plum purple,#583759
plum velvet,#7d0552
plum,#b93b8f
powder blue,#c6deff
puce,#7f5a58
pumpkin orange,#f87217
purple amethyst,#6c2dc7
purple daffodil,#b041ff
purple dragon,#c38ec7
purple flower,#a74ac7
purple haze,#4e387e
purple iris,#571b7e
purple jam,#6a287e
purple mimosa,#9e7bff
purple monster,#461b7e
purple sage bush,#7a5dc7
purple,#8e35ef
red dirt,#7f5217
red fox,#c35817
red wine,#990012
red,#ff0000
robin egg blue,#bdedff
rogue pink,#c12869
rose gold,#ecc5c0
rose,#e8adaa
rosy brown,#b38481
rosy finch,#7f4e52
royal blue,#2b60de
rubber ducky yellow,#ffd801
ruby red,#f62217
rust,#c36241
saffron,#fbb917
sage green,#848b79
salad green,#a1c935
sand,#c2b280
sandstone,#786d5f
sandy brown,#ee9a4d
sangria,#7e3817
sapphire blue,#2554c7
scarlet,#ff2400
school bus yellow,#e8a317
sea blue,#c2dfff
sea green,#4e8975
sea turtle green,#438d80
seashell,#fff5ee
seaweed green,#437c17
sedona,#cc6600
sepia,#7f462c
shamrock green,#347c17
shocking orange,#e55b3c
sienna,#8a4117
silk blue,#488ac7
sky blue,#6698ff
slate blue,#737ca1
slate gray,#657383
slime green,#bce954
smokey gray,#726e6d
spring green,#4aa02c
steel blue,#4863a0
stoplight go green,#57e964
sun yellow,#ffe87c
sunrise orange,#e67451
tan brown,#ece5b6
tangerine,#e78a61
taupe,#483c32
tea green,#ccfb5d
teal,#008080
thistle,#d2b9d3
tiffany blue,#81d8d0
tiger orange,#c88141
tron blue,#7dfdfe
tulip pink,#c25a7c
turquoise,#43c6db
tyrian purple,#c45aec
valentine red,#e55451
vampire gray,#565051
vanilla,#f3e5ab
velvet maroon,#7e354d
venom green,#728c00
viola purple,#7e587e
violet red,#f6358a
violet,#8d38c9
water,#ebf4fa
watermelon pink,#fc6c85
white,#ffffff
windows blue,#357ec7
wisteria purple,#c6aec7
wood,#966f33
yellow green,#52d017
yellow,#ffff00
zombie green,#54c571
1 algae green #64e986
2 aliceblue #f0f8ff
3 alien green #6cc417
4 antiquewhite #faebd7
5 aquamarine #7fffd4
6 army brown #827b60
7 ash gray #666362
8 avocado green #b2c248
9 aztech purple #893bff
10 azure #f0ffff
11 baby blue #95b9c7
12 bashful pink #c25283
13 basket ball orange #f88158
14 battleship gray #848482
15 bean red #f75d59
16 bee yellow #e9ab17
17 beer #fbb117
18 beetle green #4c787e
19 beige #f5f5dc
20 black cat #413839
21 black cow #4c4646
22 black eel #463e3f
23 black #000000
24 blanchedalmond #ffebcd
25 blonde #fbf6d9
26 blood red #7e3517
27 blossom pink #f9b7ff
28 blue angel #b7ceec
29 blue diamond #4ee2ec
30 blue dress #157dec
31 blue eyes #1569c7
32 blue gray #98afc7
33 blue green #7bccb5
34 blue hosta #77bfc7
35 blue ivy #3090c7
36 blue jay #2b547e
37 blue koi #659ec7
38 blue lagoon #8eebec
39 blue lotus #6960ec
40 blue orchid #1f45fc
41 blue ribbon #306eff
42 blue whale #342d7e
43 blue zircon #57feff
44 blueberry blue #0041c2
45 blush pink #e6a9ec
46 blush red #e56e94
47 brass #b5a642
48 bright gold #fdd017
49 bright neon pink #f433ff
50 bronze #cd7f32
51 brown bear #835c3b
52 brown sugar #e2a76f
53 bullet shell #af9b60
54 burgundy #8c001a
55 burlywood #deb887
56 burnt pink #c12267
57 butterfly blue #38acec
58 cadillac pink #e38aae
59 camel brown #c19a6b
60 camouflage green #78866b
61 cantaloupe #ffa62f
62 caramel #c68e17
63 carbon gray #625d5d
64 carnation pink #f778a1
65 celeste #50ebec
66 champagne #f7e7ce
67 charcoal #34282c
68 chartreuse #8afb17
69 cherry red #c24641
70 chestnut red #c34a2c
71 chestnut #954535
72 chilli pepper #c11b17
73 chocolate #c85a17
74 cinnamon #c58917
75 cloudy gray #6d6968
76 clover green #3ea055
77 cobalt blue #0020c2
78 coffee #6f4e37
79 columbia blue #87afc7
80 construction cone orange #f87431
81 cookie brown #c7a317
82 copper #b87333
83 coral blue #afdcec
84 coral #ff7f50
85 corn yellow #fff380
86 cornflower blue #6495ed
87 cornsilk #fff8dc
88 cotton candy #fcdfff
89 cranberry #9f000f
90 cream #ffffcc
91 crimson #e238ec
92 crocus purple #9172ec
93 crystal blue #5cb3ff
94 cyan opaque #92c7c7
95 cyan or aqua #00ffff
96 dark carnation pink #c12283
97 dark forest green #254117
98 dark goldenrod #af7817
99 dark orange #f88017
100 dark orchid #7d1b7e
101 dark salmon #e18b6b
102 dark sea green #8bb381
103 dark slate blue #2b3856
104 dark slate grey #25383c
105 dark turquoise #3b9c9c
106 dark violet #842dce
107 day sky blue #82caff
108 deep peach #ffcba4
109 deep pink #f52887
110 deep sky blue #3bb9ff
111 denim blue #79baec
112 denim dark blue #151b8d
113 desert sand #edc9af
114 dimorphotheca magenta #e3319d
115 dodger blue #1589ff
116 dollar bill green #85bb65
117 dragon green #6afb92
118 dull purple #7f525d
119 earth blue #0000a0
120 eggplant #614051
121 electric blue #9afeff
122 emerald green #5ffb17
123 fall leaf brown #c8b560
124 fern green #667c26
125 ferrari red #f70d1a
126 fire engine red #f62817
127 firebrick #800517
128 flamingo pink #f9a7b0
129 forest green #4e9258
130 frog green #99c68e
131 ginger brown #c9be62
132 glacial blue ice #368bc1
133 golden brown #eac117
134 goldenrod #edda74
135 granite #837e7c
136 grape #5e5a80
137 grapefruit #dc381f
138 gray cloud #b6b6b4
139 gray dolphin #5c5858
140 gray goose #d1d0ce
141 gray wolf #504a4b
142 gray #736f6e
143 grayish turquoise #5e7d7e
144 green apple #4cc417
145 green onion #6aa121
146 green peas #89c35c
147 green snake #6cbb3c
148 green thumb #b5eaaa
149 green yellow #b1fb17
150 green #00ff00
151 greenish blue #307d7e
152 gunmetal #2c3539
153 halloween orange #e66c2c
154 harvest gold #ede275
155 hazel green #617c58
156 heliotrope purple #d462ff
157 hot pink #f660ab
158 hummingbird green #7fe817
159 iceberg #56a5ec
160 iguana green #9cb071
161 indigo #4b0082
162 iridium #3d3c3a
163 jade green #5efb6e
164 jasmine purple #a23bec
165 jeans blue #a0cfec
166 jellyfish #46c7c7
167 jet gray #616d7e
168 jungle green #347c2c
169 kelly green #4cc552
170 khaki rose #c5908e
171 khaki #ada96e
172 lapis blue #15317e
173 lava red #e42217
174 lavender blue #e3e4fa
175 lavender pinocchio #ebdde2
176 lawn green #87f717
177 lemon chiffon #fff8c6
178 light aquamarine #93ffe8
179 light blue #addfff
180 light coral #e77471
181 light cyan #e0ffff
182 light jade #c3fdb8
183 light pink #faafba
184 light salmon #f9966b
185 light sea green #3ea99f
186 light sky blue #82cafa
187 light slate blue #736aff
188 light slate gray #6d7b8d
189 light slate #ccffff
190 light steel blue #728fce
191 lilac #c8a2c8
192 lime green #41a317
193 lipstick pink #c48793
194 love red #e41b17
195 lovely purple #7f38ec
196 macaroni and cheese #f2bb66
197 macaw blue green #43bfc7
198 magenta #ff00ff
199 mahogany #c04000
200 mango orange #ff8040
201 marble blue #566d7e
202 maroon #810541
203 mauve #e0b0ff
204 medium aquamarine #348781
205 medium forest green #347235
206 medium orchid #b048b5
207 medium purple #8467d7
208 medium sea green #306754
209 medium spring green #348017
210 medium turquoise #48cccd
211 medium violet red #ca226b
212 metallic silver #bcc6cc
213 midnight blue #151b54
214 midnight #2b1b17
215 milk white #fefcff
216 mint green #98ff98
217 mist blue #646d7e
218 misty rose #fbbbb9
219 moccasin #827839
220 mocha #493d26
221 mustard #ffdb58
222 navy blue #000080
223 nebula green #59e817
224 neon pink #f535aa
225 night #0c090a
226 northern lights blue #78c7c7
227 oak brown #806517
228 ocean blue #2b65ec
229 oil #3b3131
230 orange gold #d4a017
231 orange salmon #c47451
232 pale blue lily #cfecec
233 pale violet red #d16587
234 papaya orange #e56717
235 parchment #ffffc2
236 pastel blue #b4cfec
237 peach #ffe5b4
238 pearl #fdeef4
239 periwinkle #e9cfec
240 pig pink #fdd7e4
241 pine green #387c44
242 pink bow #c48189
243 pink bubblegum #ffdfdd
244 pink cupcake #e45e9d
245 pink daisy #e799a3
246 pink lemonade #e4287c
247 pink rose #e7a1b0
248 pink #faafbe
249 pistachio green #9dc209
250 platinum #e5e4e2
251 plum pie #7d0541
252 plum purple #583759
253 plum velvet #7d0552
254 plum #b93b8f
255 powder blue #c6deff
256 puce #7f5a58
257 pumpkin orange #f87217
258 purple amethyst #6c2dc7
259 purple daffodil #b041ff
260 purple dragon #c38ec7
261 purple flower #a74ac7
262 purple haze #4e387e
263 purple iris #571b7e
264 purple jam #6a287e
265 purple mimosa #9e7bff
266 purple monster #461b7e
267 purple sage bush #7a5dc7
268 purple #8e35ef
269 red dirt #7f5217
270 red fox #c35817
271 red wine #990012
272 red #ff0000
273 robin egg blue #bdedff
274 rogue pink #c12869
275 rose gold #ecc5c0
276 rose #e8adaa
277 rosy brown #b38481
278 rosy finch #7f4e52
279 royal blue #2b60de
280 rubber ducky yellow #ffd801
281 ruby red #f62217
282 rust #c36241
283 saffron #fbb917
284 sage green #848b79
285 salad green #a1c935
286 sand #c2b280
287 sandstone #786d5f
288 sandy brown #ee9a4d
289 sangria #7e3817
290 sapphire blue #2554c7
291 scarlet #ff2400
292 school bus yellow #e8a317
293 sea blue #c2dfff
294 sea green #4e8975
295 sea turtle green #438d80
296 seashell #fff5ee
297 seaweed green #437c17
298 sedona #cc6600
299 sepia #7f462c
300 shamrock green #347c17
301 shocking orange #e55b3c
302 sienna #8a4117
303 silk blue #488ac7
304 sky blue #6698ff
305 slate blue #737ca1
306 slate gray #657383
307 slime green #bce954
308 smokey gray #726e6d
309 spring green #4aa02c
310 steel blue #4863a0
311 stoplight go green #57e964
312 sun yellow #ffe87c
313 sunrise orange #e67451
314 tan brown #ece5b6
315 tangerine #e78a61
316 taupe #483c32
317 tea green #ccfb5d
318 teal #008080
319 thistle #d2b9d3
320 tiffany blue #81d8d0
321 tiger orange #c88141
322 tron blue #7dfdfe
323 tulip pink #c25a7c
324 turquoise #43c6db
325 tyrian purple #c45aec
326 valentine red #e55451
327 vampire gray #565051
328 vanilla #f3e5ab
329 velvet maroon #7e354d
330 venom green #728c00
331 viola purple #7e587e
332 violet red #f6358a
333 violet #8d38c9
334 water #ebf4fa
335 watermelon pink #fc6c85
336 white #ffffff
337 windows blue #357ec7
338 wisteria purple #c6aec7
339 wood #966f33
340 yellow green #52d017
341 yellow #ffff00
342 zombie green #54c571

File diff suppressed because it is too large Load Diff

View File

@@ -147,6 +147,36 @@ class ColorTest {
@Test
void parseHexcode() {
Color c;
c = Color.parseHexcode("#f05025");
assertEquals(Color.RED, c);
c = Color.parseHexcode("#F8EF22");
assertEquals(Color.YELLOW, c);
c = Color.parseHexcode("#FF40ff");
assertEquals(new Color(255, 64, 255), c);
// TODO: Test hexcodes of length 3, 6 and 9
// TODO: Test malformed hexcodes
}
@Test
void parseString() {
Color c;
c = Color.parseString("red");
assertEquals(new Color(255, 0, 0), c);
c = Color.parseString("zombie green");
assertEquals(Color.parseHexcode("#54c571"), c);
c = Color.parseString("algae green");
assertEquals(new Color(100, 233, 134), c);
c = Color.parseString("light slate gray");
assertEquals(Color.parseHexcode("#6d7b8d"), c);
}
@Test
@@ -173,10 +203,6 @@ class ColorTest {
void HSLtoRGB() {
}
@Test
void testHSLtoRGB() {
}
@Test
void copy() {
}

View File

@@ -2,6 +2,9 @@ package schule.ngb.zm;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
class ConstantsTest {
@@ -169,4 +172,30 @@ class ConstantsTest {
assertTrue(0.0 <= d && 1.0 >= d, "Noise should be in Range 0 to 1. Was <" + d + ">.");
}
@Test
void choice() {
int[] values = IntStream.range(0,1000).toArray();
int[] choosen = Constants.choice(values, 1, false);
assertEquals(1, choosen.length);
assertTrue(Arrays.stream(values).anyMatch((i) -> i == choosen[0]));
int[] choosen4 = Constants.choice(values, 2000, false);
assertEquals(2000, choosen4.length);
int[] choosen2 = Constants.choice(values, 400, false);
assertEquals(400, choosen2.length);
assertTrue(Arrays.stream(choosen2).allMatch((i) -> Arrays.binarySearch(values, i)>=0 ));
int[] choosen3 = Constants.choice(values, 999, true);
assertEquals(999, choosen3.length);
assertTrue(Arrays.stream(choosen3).allMatch((i) -> Arrays.binarySearch(values, i)>=0 ));
for( int i = 0; i < choosen3.length; i++ ) {
for( int j = i+1; j < choosen3.length; j++ ) {
assertNotEquals(choosen3[i], choosen3[j], "Item "+i+" also found at index "+j);
}
}
assertThrows(IllegalArgumentException.class, () -> Constants.choice(values, 2000, true));
}
}

View File

@@ -2,8 +2,6 @@ package schule.ngb.zm;
import schule.ngb.zm.shapes.*;
import java.awt.geom.Point2D;
public class TestInput extends Zeichenmaschine {
public static void main( String[] args ) {
@@ -23,7 +21,7 @@ public class TestInput extends Zeichenmaschine {
shapes.getShape(1).setFillColor(PINK);
text = new Text(10, 42, "");
text.setFontsize(32);
text.setFontSize(32);
text.setAnchor(NORTHWEST);
shapes.add(text);
}