1 Commits

Author SHA1 Message Date
97f8eaeb49 Base classes for InputContexts 2022-07-07 08:10:53 +02:00
230 changed files with 6017 additions and 29090 deletions

25
.gitignore vendored
View File

@@ -32,28 +32,3 @@ hs_err_pid*
.DS_Store .DS_Store
._* ._*
Thumbs.db Thumbs.db
.gradle
local.properties
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Python mkdocs
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

View File

@@ -6,64 +6,14 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased] ## [Unreleased]
## Added ## Added
- Dokumentation erweitert.
- Caching-Mechanismen in Klasse `util.Cache` ausgelagert.
- `util.io.ImageLoader` und `util.io.FontLoader` verwenden `Cache`.
- `mouseWheelMoved` Eventhandler für Mausrad.
- `DrawingLayer.imageRotate(...)` Methoden, um Bilder um ihr Zentrum gedreht zu zeichnen.
## Changed
- Die Methoden in `Validator` erwarten nun als zweiten Parameter den Namen des Parameters, der geprüft wird.
- `DrawingLayer.image(...)` mit Größenänderung umbenannt zu `imageScale(...)`.
- Klassen in `schule.ngb.zm.util.io` werfen nun nur eine Warnung ohne Stack-Trace, wenn die Ressource nicht gefunden werden konnte.
## Fixed
- `Constants.choice(int...)` und `Constants.choice(double...)` wiederhergestellt.
- Timing-Problem beim Aufruf von `AudioListener.playbackStopped()` in `Sound` behoben.
## Removed
- `layers.Shape2DLayer` ist nur noch im Test-Paket verfügbar.
## Version 0.0.34
### Added
- `Faker`-Klasse zur Erzeugung von Fake-Daten hinzugefügt.
- Dokumentation unter [zeichenmaschine.xyz](https://zeichenmaschine.xyz) mit
[MkDocs](https://www.mkdocs.org) und [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).
- Neue `image` methoden im `DrawingLayer`.
### Changed
- `FilledShape` und `StrokedShape` durch `Fillable` und `Strokeable` Interfaces ersetzt.
- `Shape` erweitert nun `BasisDrawable` als abstrakte Grundlage.
- `io` Klassen nutzen nun mehr der `java.nio` Funktionen.
- Package-Struktur angepasst.
## Version 0.0.23
### Added
- System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.
- Pakete für Animationen und Maschinelles-Lernen.
- Farbverläufe als Füllung.
### Changed
- `update(double)` und `draw()` werden nun in einem eigenen Thread aufgerufen.
- Die Standardwerte in `Constants` wurden mit dem Prefix `DEFAULT_` benannt (vorher `STD_`).
- Die Standardwerte sind nun nicht mehr `final` und können vom Nutzer manuell gesetzt werden.
## Version 0.0.22
### Added
- Interface `Audio` extrahiert, mit Basisfunktionen von `Sound` und `Music`. - Interface `Audio` extrahiert, mit Basisfunktionen von `Sound` und `Music`.
- Klasse `Mixer` steuert mehrere Audio-Objekte gleichzeitig. - Klasse `Mixer` steuert mehrere Audio-Objekte gleichzeitig.
- Klasse `tasks.RateLimitedTask`, `tasks.FramerateLimitedTask`, `tasks.FrameSynchronizedTask` und `tasks.DelayedTask`.
### Changed ## Changed
- Neue Package-Struktur: - Neue Package-Struktur:
- `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video). - `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video).
- `schule.ngb.zm.util.tasks` für alles Rund um Parallelität. - `schule.ngb.zm.tasks` für alles Rund um Parallelität.
- `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging.
### Removed ### Removed
- Beispielprojekte in [eigenes Repository](https://github.com/jneug/zeichenmaschine-examples) verschoben. - Beispielprojekte in [eigenes Repository](https://github.com/jneug/zeichenmaschine-examples) verschoben.

View File

@@ -1,153 +0,0 @@
plugins {
id 'idea'
id 'java-library'
id 'org.hidetake.ssh' version '2.10.1'
}
group 'schule.ngb'
version '0.0.35-SNAPSHOT'
java {
withSourcesJar()
withJavadocJar()
}
compileJava {
options.release = 11
}
repositories {
mavenCentral()
}
remotes {
uberspace {
host = 'westphal.uberspace.de'
user = 'ngb'
identity = file("${System.properties['user.home']}/.ssh/uberspace_rsa")
knownHosts = allowAnyHosts
}
}
dependencies {
runtimeOnly 'com.googlecode.soundlibs:jlayer:1.0.1.4'
runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4'
runtimeOnly 'com.googlecode.soundlibs:mp3spi:1.9.5.4'
compileOnlyApi 'colt:colt:1.2.0'
//api 'colt:colt:1.2.0'
//api 'net.sourceforge.parallelcolt:parallelcolt:0.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
jar {
manifest {
attributes 'Class-Path': '.'
}
}
tasks.register('jarMP3SPI', Jar) {
group "build"
description "Build jar with MP3SPI included"
archiveClassifier = 'mp3spi'
duplicatesStrategy = 'exclude'
// archivesBaseName = 'zeichenmaschine-mp3spi'
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
with jar
}
task buildAll {
group "build"
description "Build all jar packages"
dependsOn 'jar'
dependsOn 'jarMP3SPI'
dependsOn 'sourcesJar'
dependsOn 'javadocJar'
}
javadoc {
options {
encoding = "UTF-8"
overview = "src/resources/java/overview.html"
// title = "Die Zeichenmaschine"
// options.links 'https://docs.oracle.com/javase/8/docs/api/'
// options.links 'https://docs.oracle.com/javaee/7/api'
options.links 'https://docs.oracle.com/en/java/javase/11/docs/api'
}
options.addStringOption("charset", "UTF-8")
}
task mkdocs(type: Exec) {
group "documentation"
description "Build MKDocs site"
workingDir "${projectDir}"
commandLine ".venv/bin/python", "-m", "mkdocs", "build"
}
task buildDocs {
group "documentation"
description "Run all documentation tasks"
dependsOn 'javadoc'
dependsOn 'javadocJar'
dependsOn 'mkdocs'
}
task zipSite(type: Zip) {
group "documentation"
description "Create zip archives for documentations"
dependsOn 'mkdocs'
from fileTree("${buildDir}/docs/site")
exclude '*.py'
exclude '__pycache__'
archiveName 'site.zip'
destinationDir(file("${buildDir}/docs"))
}
task zipJavadoc(type: Zip) {
group "documentation"
description "Create zip archives for javadoc"
dependsOn 'javadoc'
from fileTree("${buildDir}/docs/javadoc")
archiveName 'javadoc.zip'
destinationDir(file("${buildDir}/docs"))
}
task uploadDocs {
group "documentation"
description "Run all documentation tasks and upload artifacts to zeichenmaschine.xyz"
dependsOn 'zipSite'
dependsOn 'zipJavadoc'
doLast {
ssh.run {
session(remotes.uberspace) {
execute 'rm -rf /var/www/virtual/ngb/zeichenmaschine.xyz/*', ignoreError: true
put from: "${buildDir}/docs/site.zip", into: '/var/www/virtual/ngb/zeichenmaschine.xyz', ignoreError: true
execute 'unzip -o -q /var/www/virtual/ngb/zeichenmaschine.xyz/site.zip -d /var/www/virtual/ngb/zeichenmaschine.xyz'
put from: "${buildDir}/docs/javadoc.zip", into: '/var/www/virtual/ngb/zeichenmaschine.xyz', ignoreError: true
execute 'unzip -o -q /var/www/virtual/ngb/zeichenmaschine.xyz/javadoc.zip -d /var/www/virtual/ngb/zeichenmaschine.xyz/docs'
}
}
}
}
test {
useJUnitPlatform()
}

View File

@@ -1,634 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 258 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 KiB

View File

@@ -1,375 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 821 KiB

View File

@@ -1,375 +0,0 @@
{
"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.

Before

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

View File

@@ -1,12 +0,0 @@
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

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

View File

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

View File

@@ -1,45 +0,0 @@
# 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/releases/latest)
herunterzuladen und dem *Classpath* des Projekts hinzuzufügen. Beschreibungen
für verschiedene Entwicklungsumgebungen sind hier aufgelistet.
## Integration in Entwicklungsumgebungen
### BlueJ
[BlueJ](https://bluej.org) sucht an drei Orten nach Bibliotheken, die für ein
Projekt in den Classpath aufgenommen werden:
- Für ein einzelnes Projekt im Projektordner im Unterordner `+libs`.
- Im Reiter "Bibliotheken" der BlueJ-Einstellungen.
Hier können Programmbibliotheken hinzugefügt werden, die dann allen Projekten
zur Verfügung stehen.
- Für alle Projekte und alle Nutzer dieser BlueJ-Version im
Unterordner `userlib` des Programmordners.
Auf Windows-Systemen ist dieser im Order `lib` des Installationsordners von BlueJ zu finden.
Auf macos-Systemen muss via Rechtsklick auf die Programmdatei `BlueJ.app` über den Menüpunkt "Paketinhalt zeigen" in den Ordner `Contents/Resources/Java/` navigiert werden.
### VSCode / VSCodium
> Coming soon
### IntelliJ
> Coming soon
### Eclipse
> Coming soon
### NetBeans
> Coming soon
## Unterstützung für MP3

View File

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

View File

@@ -1,606 +0,0 @@
# Schnellstart mit der Zeichenmaschine
Um die **Zeichenmaschine** in einem Projekt zu nutzen ist nicht mehr nötig, als
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
herunterzuladen und
dem [Classpath](https://www.delftstack.com/de/howto/java/java-classpath-/)
hinzuzufügen. Eine Beschreibung für verschiedene Entwicklungsumgebungen findet
sich im Abschnitt [Installation](installation.md).
## Die Basisklasse
Eine _Zeichenmaschine_ wird immer als Unterklasse von {{ javadoc_link("
schule.ngb.zm.Zeichenmaschine") }} erstellt.
```java
public class Shapes extends Zeichenmaschine {
}
```
Die gezeigte Klasse ist schon eine lauffähige Zeichenmaschine und kann gestartet
werden.
!!! note "main Methode"
Bei einigen Entwicklungsumgebungen muss noch eine `main` Methode erstellt
werden, um die Zeichenmaschine zu starten:
```java
public static void main(String[] args) {
new Shapes();
}
```
Es öffnet sich ein Zeichenfenster in einer vordefinierten Größe. Um die
Abmessungen und den Titel des Fensters zu ändern, legen wir einen Konstruktor
an.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
}
```
Starten wir das Projekt, wird eine Zeichenfläche in der Größe 800-mal-800 Pixel
erstellt und in einem Fenster mit dem Titel „Shapes“ angezeigt.
<figure markdown>
![Shapes 2](assets/quickstart/shapes_2.png){ width=400 }
</figure>
### Formen zeichnen
Eine Zeichenmaschine hat verschiedene Möglichkeiten Inhalte in das
Zeichenfenster zu zeichnen. Um ein einfaches statisches Bild zu erzeugen,
überschreiben wir die {{ jdl("schule.ngb.zm.Zeichenmaschine", "draw()",
c=False) }} Methode.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void draw() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
drawing.circle(400, 400, 100);
}
}
```
Wir sehen einen gelben Kreis (ohne Konturlinie) auf einem blauen Hintergrund.
<figure markdown>
![Shapes 3](assets/quickstart/shapes_3.png){ width=400 }
</figure>
### Vorbereitung der Zeichenfläche
Im Beispiel oben setzen wir die Hintergrundfarbe auf Blau, die Füllfarbe auf
Gelb und deaktivieren die Konturlinie. Wenn diese Einstellungen für alle
Zeichenobjekte gleich bleiben, können wir sie statt in `draw()` auch in die {{
jdl('Zeichenmaschine', 'setup()', c=False) }} Methode schreiben. Diese bereitet
die Zeichenfläche vor dem ersten Zeichnen vor.
???+ example "Quelltext"
```java
public class Shapes extends Zeichenmaschine {
public Shapes() {
super(800, 800, "Shapes");
}
@Override
public void setup() {
background.setColor(BLUE);
drawing.setFillColor(255, 223, 34);
drawing.noStroke();
}
@Override
public void draw() {
for( int i = 0; i < 10; i++ ) {
drawing.circle(
random(0, canvasWidth),
random(0, canvasHeight),
random(50, 200)
);
}
}
}
```
Im Beispiel setzen wir nun die Grundeinstellungen in der `setup()` Methode. In
`draw()` werden zehn gelbe Kreise an Zufallskoordinaten gezeichnet.
<figure markdown>
![Shapes 4](assets/quickstart/shapes_4.1.png){ width=400 }
</figure>
!!! tip ""
Mit {{ jdm("Constants", "canvasWidth") }} und
{{ jdm("Constants", "canvasHeight") }} kannst du in der Zeichenmaschine
auf die aktuelle Größe der Zeichenfläche zugreifen.
{{ jdm("Constants", "random(int,int)") }} erzeugt eine Zufallszahl
innerhalb der angegebenen Grenzen.
## Interaktionen mit der Maus: Whack-a-mole
Mit der Zeichenmaschine lassen sich Interaktionen mit der Maus leicht umsetzen.
Wir 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:
```java
@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 Sekunden 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

@@ -1,19 +0,0 @@
# 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.

Binary file not shown.

View File

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

234
gradlew vendored
View File

@@ -1,234 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored
View File

@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -1,83 +0,0 @@
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: schnellstart.md
- Installation: installation.md
- Tutorials:
- Aquarium: tutorials/aquarium/aquarium1.md
- Beispiele:
- Mondrian: beispiele/mondrian.md
markdown_extensions:
- admonition
- attr_list
- def_list
- footnotes
- md_in_html
- toc:
permalink: true
- pymdownx.magiclink
- pymdownx.betterem:
smart_enable: all
- pymdownx.caret
- pymdownx.smartsymbols
- pymdownx.emoji:
emoji_index: !!python/name:materialx.emoji.twemoji
emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
plugins:
- search:
lang: de
separator: '[\s\-\.]'
- macros:
module_name: docs/macros
extra:
javadoc_url: https://zeichenmaschine.xyz/docs/
javadoc_default_package: schule.ngb.zm

View File

@@ -1,25 +0,0 @@
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

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,2 +0,0 @@
rootProject.name = 'zeichenmaschine'

View File

@@ -1,211 +0,0 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.Stroke;
/**
* Basisimplementierung der {@link Strokeable} und {@link Fillable} APIs.
*
* Die Klasse bietet eine Grundlage zur Implementierung eigener Zeichenobjekte,
* die eine Füllung und eine Konturlinie haben können.
*/
public abstract class BasicDrawable extends Constants implements Strokeable, Fillable {
/**
* Ob das Objekt gezeichnet werden soll.
*/
protected boolean visible = true;
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn das Objekt ohne
* Kontur dargestellt werden soll.
*/
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
/**
* Die Art der Konturlinie.
*/
protected Options.StrokeType strokeType = SOLID;
/**
* Die Art der Kantenverbindungen von Linien.
*/
protected Options.StrokeJoin strokeJoin = MITER;
/**
* 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;
// TODO: Add TexturePaint fill (https://docs.oracle.com/javase/8/docs//api/java/awt/TexturePaint.html)
// Implementierung Drawable Interface
/**
* Ob das Objekt angezeigt bzw. gezeichnet werden soll.
*
* @return {@code true}, wenn das Objekt angezeigt werden soll,
* {@code false} sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt das Objekt.
*/
public void hide() {
visible = false;
}
/**
* Zeigt das Objekt an.
*/
public void show() {
visible = true;
}
/**
* Versteckt da Objekt, wenn es derzeit angezeigt wird und zeigt es
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
public abstract void draw( Graphics2D graphics );
// Implementierung Fillable Interface
@Override
public void setFill( Paint fill ) {
if( fill == null ) {
this.fill = null;
} else if( fill instanceof Color ) {
this.fillColor = ((Color) fill);
this.fill = null;
} else if( fill instanceof MultipleGradientPaint ) {
this.fillColor = null;
this.fill = (MultipleGradientPaint) fill;
}
}
@Override
public Paint getFill() {
if( fill != null ) {
return fill;
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
return fillColor;
} else {
return null;
}
}
@Override
public boolean hasFillColor() {
return fillColor != null;
}
@Override
public boolean hasGradient() {
return fill != null;
}
@Override
public Color getFillColor() {
return fillColor;
}
@Override
public void setFillColor( Color color ) {
fillColor = color;
fill = null;
}
@Override
public MultipleGradientPaint getGradient() {
return fill;
}
// Implementierung Strokeable Interface
@Override
public void setStroke( Stroke stroke ) {
this.stroke = stroke;
}
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight, strokeJoin);
}
return stroke;
}
@Override
public Color getStrokeColor() {
return strokeColor;
}
@Override
public void setStrokeColor( Color color ) {
strokeColor = color;
}
@Override
public double getStrokeWeight() {
return strokeWeight;
}
@Override
public void setStrokeWeight( double weight ) {
strokeWeight = weight;
this.stroke = null;
}
@Override
public Options.StrokeType getStrokeType() {
return strokeType;
}
@Override
public void setStrokeType( Options.StrokeType type ) {
strokeType = type;
this.stroke = null;
}
@Override
public Options.StrokeJoin getStrokeJoin() {
return strokeJoin;
}
@Override
public void setStrokeJoin( Options.StrokeJoin join ) {
strokeJoin = join;
this.stroke = null;
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,228 +0,0 @@
package schule.ngb.zm;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
/**
* Basisklasse für Ebenen der {@link Zeichenleinwand}.
* <p>
* Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
* <p>
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer
* {@link Zeichenmaschine}. Sie erben von {@code Constants}, da neue Ebenentypen
* von Nutzern implementiert werden können.
*/
public abstract class Layer extends Constants implements Drawable, Updatable {
/**
* Interner Puffer für die Ebene, der einmal pro Frame auf die
* Zeichenleinwand übertragen wird.
*/
protected BufferedImage buffer;
/**
* Der Grafikkontext der Ebene, der zum Zeichnen der Inhalte verwendet
* wird.
*/
protected Graphics2D drawing;
/**
* Ob die Ebene derzeit sichtbar ist.
*/
protected boolean visible = true;
/**
* Ob die Ebene aktiv ist, also {@link #update(double) Updates} empfangen
* soll.
*/
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 angegebenen Maße.
*
* @param width Die neue Breite.
* @param height Die neue Höhe.
*/
public void setSize( int width, int height ) {
if( buffer != null ) {
if( buffer.getWidth() != width || buffer.getHeight() != height ) {
recreateCanvas(width, height);
}
} else {
createCanvas(width, height);
}
}
// TODO: prevent access to graphics context?
public Graphics2D getGraphics() {
return this.drawing;
}
/**
* Gibt die Ressourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
drawing = null;
buffer = null;
}
/**
* Erstellt einen neuen Puffer für die Ebene und konfiguriert diesen.
*
* @param width Breite des neuen Puffers.
* @param height Höhe des neuen Puffers.
*/
private void createCanvas( int width, int height ) {
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
drawing = buffer.createGraphics();
// add antialiasing
RenderingHints hints = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
hints.put(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON
);
hints.put(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY
);
drawing.addRenderingHints(hints);
}
/**
* 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 Breite des neuen Puffers.
* @param height Höhe des neuen Puffers.
*/
private void recreateCanvas( int width, int height ) {
BufferedImage oldCanvas = buffer;
createCanvas(width, height);
drawing.drawImage(oldCanvas, 0, 0, null);
}
/**
* Leert die Ebene und löscht alles bisher gezeichnete. Alle Pixel der Ebene
* werden transparent, damit unterliegende Ebenen durchscheinen können.
*/
public void clear() {
// https://stackoverflow.com/questions/31149206/set-pixels-of-bufferedimage-as-transparent
Color current = drawing.getColor();
drawing.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
drawing.setColor(new Color(255, 255, 255, 255));
drawing.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
drawing.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
drawing.setColor(current);
}
/**
* Zeichnet den Puffer auf den Grafikkontext.
*
* @param graphics Der Grafikkontext, auf den gezeichnet wird.
*/
@Override
public void draw( Graphics2D graphics ) {
if( visible ) {
graphics.drawImage(buffer, 0, 0, null);
}
}
@Override
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Ebene.
*/
public void hide() {
visible = false;
}
/**
* 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;
}
@Override
public void update( double delta ) {
}
@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

@@ -1,365 +0,0 @@
package schule.ngb.zm;
import java.awt.BasicStroke;
import java.awt.geom.Arc2D;
/**
* Diese Klasse sammelt Enumerationen, die verschiedene Eigenschaften der zu
* zeichnenden Formen darstellen.
*/
public final class Options {
private Options() {
}
/**
* Linienstile für Konturlinien.
*/
public enum StrokeType {
/**
* Durchgezogene Linien.
*/
SOLID,
/**
* Getrichelte Linien.
*/
DASHED,
/**
* Gepunktete Linien.
*/
DOTTED
}
/**
* Linienstile für Konturlinien.
*/
public enum StrokeJoin {
/**
* Abgerundete Verbindungen.
*/
ROUND(BasicStroke.JOIN_ROUND),
/**
* Abgeschnittene Verbindungen.
*/
BEVEL(BasicStroke.JOIN_BEVEL),
/**
* Eckige Verbindungen.
*/
MITER(BasicStroke.JOIN_MITER);
/**
* Der entsprechende Wert der Konstanten in {@link java.awt}
*/
public final int awt_type;
StrokeJoin( int type ) {
awt_type = type;
}
}
/**
* Stile für Pfeilspitzen.
*/
public enum ArrowHead {
/**
* 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 {
/**
* 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 ) {
awt_type = type;
}
}
/**
* 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,
/**
* 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 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),
NORTH(0, -1),
EAST(1, 0),
SOUTH(0, 1),
WEST(-1, 0),
NORTHEAST(1, -1),
SOUTHEAST(1, 1),
SOUTHWEST(-1, 1),
NORTHWEST(-1, -1),
MIDDLE(CENTER),
UP(NORTH),
RIGHT(EAST),
DOWN(SOUTH),
LEFT(WEST),
UPLEFT(NORTHWEST),
DOWNLEFT(SOUTHWEST),
DOWNRIGHT(SOUTHEAST),
UPRIGHT(NORTHEAST);
public final byte x, y;
Direction( int x, int y ) {
this.x = (byte) x;
this.y = (byte) y;
}
Direction( Direction original ) {
this.x = original.x;
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);
}
/**
* 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 Die entgegengesetzte Richtung zu dieser.
*/
public Direction inverse() {
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;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,355 +0,0 @@
package schule.ngb.zm;
import schule.ngb.zm.util.Log;
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;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
* mehreren Ebenen, auf denen auf verschiedene Arten gezeichnet werden kann. Die
* Ebenen lassen sich beliebig übereinander legen, ausblenden oder wieder
* löschen.
* <p>
* Jede Ebene besitzt eine Zeichenfläche, auf der ihre Zeichnung liegt. Diese
* Zeichenflächen werden pro Frame einmal von "unten" nach "oben" auf diese
* Leinwand gezeichnet.
*/
public class Zeichenleinwand extends Canvas {
// 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.
*
* @param width Breite der Zeichenleinwand.
* @param height Höhe der Zeichenleinwand.
*/
public Zeichenleinwand( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
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>
* Bei einer Größenänderung wird auch die Größe aller bisher hinzugefügter
* {@link Layer Ebenen} angepasst, sodass sie die gesamte Leinwand füllen.
*
* @param width Neue Width der Leinwand in Pixeln.
* @param height Neue Höhe der Leinwand in Pixeln.
*/
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
synchronized( layers ) {
for( Layer layer : layers ) {
layer.setSize(width, height);
}
}
}
/**
* Fügt der Zeichenleinwand eine Ebene hinzu, die oberhalb aller bisherigen
* Ebenen eingefügt wird.
*
* @param layer Die neue Ebene.
*/
public void addLayer( Layer layer ) {
if( layer != null ) {
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
* 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 layer Die neue Ebene.
*/
public void addLayer( int i, Layer layer ) {
if( layer != null ) {
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
}
}
/**
* Gibt die Anzahl der {@link Layer Ebenen} in dieser Leinwand zurück.
*
* @return Die Anzahl der Ebenen.
*/
public int getLayerCount() {
return layers.size();
}
/**
* Gibt eine Kopie der Liste der bisher hinzugefügten Ebenen zurück.
*
* @return Liste der Ebenen.
*/
public List<Layer> getLayers() {
return List.copyOf(layers);
}
/**
* Holt die Ebene am Index {@code i} (beginnend bei 0).
*
* @param i Index der Ebene (beginnend bei 0).
* @return Die Ebene am Index {@code i} oder {@code null}.
* @throws IndexOutOfBoundsException Falls der Index nicht existiert.
*/
public Layer getLayer( int i ) {
if( layers.size() > i ) {
return layers.get(i);
} else {
throw new IndexOutOfBoundsException("No layer at index " + i + " (max: " + (layers.size() - 1) + ").");
}
}
/**
* Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben.
*
* @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> type ) {
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
return type.cast(layer);
}
}
}
return null;
}
/**
* Sucht alle Ebenen von einem bestimmten Typ aus der Liste der Ebenen und
* gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der
* Reihenfolge der Ebenen in der Leinwand (von unten nach oben).
*
* @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.
*/
@SuppressWarnings( "unused" )
public <L extends Layer> List<L> getLayers( Class<L> type ) {
ArrayList<L> result = new ArrayList<>(layers.size());
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 ) {
return layers.remove(pLayer);
}
/**
* 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 : removeLayers ) {
layers.remove(layer);
}
}
}
/**
* Entfernt alle vorhandenen Ebenen von dieser Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public void clearLayers() {
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 ) {
for( Layer layer : List.copyOf(layers) ) {
layer.update(delta);
}
}
}
/**
* Erstellt eine passende {@link BufferStrategy} für diese Ebene.
*/
public void allocateBuffer() {
this.createBufferStrategy(2);
}
/**
* Löscht alle Ebenen der Zeichenebene und gibt deren Ressourcen frei.
*/
public void dispose() {
synchronized( layers ) {
for( Layer layer : layers ) {
layer.dispose();
}
}
}
@Override
public void paint( Graphics g ) {
render();
}
/**
* Zeichnet den Inhalt aller {@link Layer Ebenen} in den Grafikkontext.
*
* @param graphics Der Grafikkontext.
*/
public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
for( Layer layer : layers ) {
layer.draw(g2d);
}
}
g2d.dispose();
}
/**
* Zeigt den aktuellen Inhalt der Zeichenleinwand an.
*/
public void render() {
if( !suspended && isDisplayable() ) {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
synchronized( renderLock ) {
rendering = true;
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
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();
}
// Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() );
}
rendering = false;
renderLock.notifyAll();
}
}
}
private static final Log LOG = Log.getLogger(Zeichenleinwand.class);
}

View File

@@ -1,150 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import java.util.function.DoubleUnaryOperator;
public abstract class Animation<T> extends Constants implements Updatable {
protected int runtime;
protected int elapsedTime = 0;
protected boolean running = false, finished = false;
protected DoubleUnaryOperator easing;
public Animation() {
this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
this.easing = Constants.DEFAULT_EASING;
}
public Animation( DoubleUnaryOperator easing ) {
this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
this.easing = Validator.requireNotNull(easing, "easing");
}
public Animation( int runtime ) {
this.runtime = runtime;
this.easing = Constants.DEFAULT_EASING;
}
public Animation( int runtime, DoubleUnaryOperator easing ) {
this.runtime = runtime;
this.easing = Validator.requireNotNull(easing, "easing");
}
public int getRuntime() {
return runtime;
}
public void setRuntime( int pRuntime ) {
this.runtime = pRuntime;
}
public DoubleUnaryOperator getEasing() {
return easing;
}
public void setEasing( DoubleUnaryOperator pEasing ) {
this.easing = Validator.requireNotNull(pEasing, "easing");
}
public abstract T getAnimationTarget();
public final void start() {
this.initialize();
elapsedTime = 0;
running = true;
finished = false;
animate(easing.applyAsDouble(0.0));
dispatchEvent("start");
}
public final void stop() {
running = false;
// Make sure the last animation frame was interpolated correctly
animate(easing.applyAsDouble((double) elapsedTime / (double) runtime));
this.finish();
finished = true;
dispatchEvent("stop");
}
public void initialize() {
// Intentionally left blank
}
public void finish() {
// Intentionally left blank
}
public final void await() {
while( !finished ) {
Thread.yield();
}
}
@Override
public boolean isActive() {
return running;
}
@Override
public void update( double delta ) {
elapsedTime += (int) (delta * 1000);
if( elapsedTime > runtime )
elapsedTime = runtime;
double t = (double) elapsedTime / (double) runtime;
if( t >= 1.0 ) {
stop();
} else {
animate(getEasing().applyAsDouble(t));
}
}
/**
* Setzt den Fortschritt der Animation auf den angegebenen Wert.
* <p>
* {@code e} liegt in der Regel zwischen 0 und 1. Je nach verwendeten
* {@link Easing} Funktion kann der Wert aber in Ausnahmefällen unter 0 oder
* über 1 liegen. Die {@code step()} Methode muss dem nicht Rechnung tragen
* und kann wenn sinnvoll den {@code e} Wert auf [0, 1] limitieren:
* <pre><code>
* e = Constants.limit(e, 0, 1);
* </code></pre>
*
* @param e Fortschritt der Animation, nachdem die Easing-Funktion angewandt
* wurde.
*/
public abstract void animate( double e );
EventDispatcher<Animation, AnimationListener> eventDispatcher;
private EventDispatcher<Animation, AnimationListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", ( a, l ) -> l.animationStarted(a));
eventDispatcher.registerEventType("stop", ( a, l ) -> l.animationStopped(a));
}
return eventDispatcher;
}
private void dispatchEvent( String type ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent(type, this);
}
}
public void addListener( AnimationListener listener ) {
initializeEventDispatcher().addListener(listener);
}
public void removeListener( AnimationListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
}

View File

@@ -1,42 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.util.Validator;
import java.util.function.DoubleUnaryOperator;
/**
* Eine Wrapper Animation, um die Werte einer anderen Animation (Laufzeit, Easing) zu überschrieben,
* ohne die Werte der Originalanimation zu verändern.
*
* @param <S> Art des Animierten Objektes.
*/
public class AnimationFacade<S> extends Animation<S> {
private final Animation<S> anim;
public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.anim = Validator.requireNotNull(anim, "anim");
}
@Override
public S getAnimationTarget() {
return anim.getAnimationTarget();
}
@Override
public void animate( double e ) {
anim.animate(e);
}
@Override
public void initialize() {
anim.initialize();
}
@Override
public void finish() {
anim.finish();
}
}

View File

@@ -1,181 +0,0 @@
package schule.ngb.zm.anim;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
// TODO: (ngb) Maybe use AnimationFacade to override runtime?
@SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> {
private final List<Animation<T>> anims;
private final boolean overrideEasing;
private int overrideRuntime = -1;
private final int lag;
private int active = 0;
public AnimationGroup( Animation<T>... anims ) {
this(0, -1, null, Arrays.asList(anims));
}
public AnimationGroup( Collection<Animation<T>> anims ) {
this(0, -1, null, anims);
}
public AnimationGroup( int lag, Collection<Animation<T>> anims ) {
this(lag, -1, null, anims);
}
public AnimationGroup( DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(0, -1, easing, anims);
}
public AnimationGroup( int lag, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(lag, -1, easing, anims);
}
public AnimationGroup( int lag, int runtime, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
super();
this.anims = List.copyOf(anims);
this.lag = lag;
if( easing != null ) {
this.easing = easing;
overrideEasing = true;
} else {
overrideEasing = false;
}
if( runtime > 0 ) {
this.runtime = anims.size() * lag + runtime;
this.overrideRuntime = runtime;
} else {
this.runtime = 0;
for( int i = 0; i < this.anims.size(); i++ ) {
if( i * lag + this.anims.get(i).getRuntime() > this.runtime ) {
this.runtime = i * lag + this.anims.get(i).getRuntime();
}
}
}
}
@Override
public T getAnimationTarget() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getAnimationTarget();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getAnimationTarget();
} else {
return anims.get(0).getAnimationTarget();
}
}
@Override
public DoubleUnaryOperator getEasing() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getEasing();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getEasing();
} else {
return anims.get(0).getEasing();
}
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
//
// // Animation is done. Stop all Animations.
// if( elapsedTime > runtime ) {
// for( int i = 0; i < anims.size(); i++ ) {
// if( anims.get(i).isActive() ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// }
// }
// elapsedTime = runtime;
// running = false;
// this.stop();
// }
//
// while( active < anims.size() && elapsedTime >= active * lag ) {
// anims.get(active).start();
// active += 1;
// }
//
// for( int i = 0; i < active; i++ ) {
// double t = 0.0;
// if( overrideRuntime > 0 ) {
// t = (double) (elapsedTime - i*lag) / (double) overrideRuntime;
// } else {
// t = (double) (elapsedTime - i*lag) / (double) anims.get(i).getRuntime();
// }
//
// if( t >= 1.0 ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// } else {
// double e = overrideEasing ?
// easing.applyAsDouble(t) :
// anims.get(i).easing.applyAsDouble(t);
//
// anims.get(i).animate(e);
// }
// }
// }
@Override
public void finish() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
anim.elapsedTime = anim.runtime;
anim.stop();
}
}
}
@Override
public void animate( double e ) {
while( active < anims.size() && elapsedTime >= active * lag ) {
anims.get(active).start();
active += 1;
}
for( int i = 0; i < active; i++ ) {
Animation<T> curAnim = anims.get(i);
double curRuntime = curAnim.getRuntime();
if( overrideRuntime > 0 ) {
curRuntime = overrideRuntime;
}
double t = (double) (elapsedTime - i * lag) / (double) curRuntime;
if( t >= 1.0 ) {
curAnim.elapsedTime = curAnim.getRuntime();
curAnim.stop();
} else {
e = overrideEasing ?
easing.applyAsDouble(t) :
curAnim.easing.applyAsDouble(t);
curAnim.elapsedTime = (elapsedTime - i * lag);
curAnim.animate(e);
}
}
}
}

View File

@@ -1,11 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.util.events.Listener;
public interface AnimationListener extends Listener<Animation> {
void animationStarted( Animation anim );
void animationStopped( Animation anim );
}

View File

@@ -1,144 +0,0 @@
package schule.ngb.zm.anim;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
/**
* Führt eine Liste von Animationen nacheinander aus. Jede Animation startet direkt nachdem die
* davor geendet ist. Optional kann zwischen dem Ende einer und dem Start der nächsten Animation
* ein
* <var>lag</var> eingefügt werden.
*
* @param <T> Die Art des animierten Objektes.
*/
@SuppressWarnings( "unused" )
public class AnimationSequence<T> extends Animation<T> {
private final List<Animation<T>> anims;
private final int lag;
private int currentAnimationIndex = -1, currentStart = -1, nextStart = -1;
@SafeVarargs
public AnimationSequence( Animation<T>... anims ) {
this(0, Arrays.asList(anims));
}
public AnimationSequence( Collection<Animation<T>> anims ) {
this(0, anims);
}
public AnimationSequence( int lag, Collection<Animation<T>> anims ) {
super(Easing::linear);
this.anims = List.copyOf(anims);
this.lag = lag;
this.runtime = (anims.size() - 1) * lag + anims.stream().mapToInt(Animation::getRuntime).sum();
}
@Override
public T getAnimationTarget() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getAnimationTarget();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getAnimationTarget();
} else {
return anims.get(0).getAnimationTarget();
}
}
@Override
public DoubleUnaryOperator getEasing() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getEasing();
}
}
if( this.finished ) {
return anims.get(anims.size() - 1).getEasing();
} else {
return anims.get(0).getEasing();
}
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
//
// // Animation is done. Stop all Animations.
// if( elapsedTime > runtime ) {
// for( int i = 0; i < anims.size(); i++ ) {
// if( anims.get(i).isActive() ) {
// anims.get(i).elapsedTime = anims.get(i).runtime;
// anims.get(i).stop();
// }
// }
// elapsedTime = runtime;
// running = false;
// this.stop();
// }
//
// Animation<T> curAnim = null;
// if( elapsedTime > nextStart ) {
// currentAnimation += 1;
// curAnim = anims.get(currentAnimation);
// currentStart = nextStart;
// nextStart += lag + curAnim.getRuntime();
// curAnim.start();
// } else {
// curAnim = anims.get(currentAnimation);
// }
//
// // Calculate delta for current animation
// double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
// if( t >= 1.0 ) {
// curAnim.elapsedTime = curAnim.runtime;
// curAnim.stop();
// } else {
// curAnim.animate(curAnim.easing.applyAsDouble(t));
// }
// }
@Override
public void finish() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
anim.elapsedTime = anim.runtime;
anim.stop();
}
}
}
@Override
public void animate( double e ) {
Animation<T> curAnim = null;
if( running && elapsedTime > nextStart ) {
currentAnimationIndex += 1;
curAnim = anims.get(currentAnimationIndex);
currentStart = nextStart;
nextStart += lag + curAnim.getRuntime();
curAnim.start();
} else {
curAnim = anims.get(currentAnimationIndex);
}
// Calculate delta for current animation
double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
if( t >= 1.0 ) {
curAnim.elapsedTime = curAnim.runtime;
curAnim.stop();
} else {
curAnim.elapsedTime = (elapsedTime - currentStart);
curAnim.animate(curAnim.easing.applyAsDouble(t));
}
}
}

View File

@@ -1,234 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector;
import schule.ngb.zm.util.tasks.FramerateLimitedTask;
import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.*;
@SuppressWarnings( "unused" )
public class Animations {
public static final <T> Future<T> animateProperty( String propName, T target, double to, int runtime, DoubleUnaryOperator easing ) {
double from;
try {
from = callGetter(target, propName, double.class);
} catch( InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex ) {
throw new RuntimeException("Can't access property getter for animation.", ex);
}
Method propSetter;
try {
propSetter = findSetter(target, propName, double.class);
} catch( NoSuchMethodException ex ) {
throw new RuntimeException("Can't find property setter for animation.", ex);
}
return animateProperty(target, from, to, runtime, easing, ( d ) -> {
try {
propSetter.invoke(target, d);
} catch( IllegalAccessException | InvocationTargetException e ) {
throw new RuntimeException("Can't access property setter for animation.", e);
}
});
}
public static final <T> Future<T> animateProperty( String propName, T target, Color to, int runtime, DoubleUnaryOperator easing ) {
Color from;
try {
from = callGetter(target, propName, Color.class);
} catch( InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex ) {
throw new RuntimeException("Can't access property getter for animation.", ex);
}
Method propSetter;
try {
propSetter = findSetter(target, propName, Color.class);
} catch( NoSuchMethodException ex ) {
throw new RuntimeException("Can't find property setter for animation.", ex);
}
return animateProperty(target, from, to, runtime, easing, ( d ) -> {
try {
propSetter.invoke(target, d);
} catch( IllegalAccessException | InvocationTargetException e ) {
throw new RuntimeException("Can't access property setter for animation.", e);
}
});
}
public static final <T> Future<T> animateProperty( String propName, T target, Vector to, int runtime, DoubleUnaryOperator easing ) {
Vector from;
try {
from = callGetter(target, propName, Vector.class);
} catch( InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex ) {
throw new RuntimeException("Can't access property getter for animation.", ex);
}
Method propSetter;
try {
propSetter = findSetter(target, propName, Vector.class);
} catch( NoSuchMethodException ex ) {
throw new RuntimeException("Can't find property setter for animation.", ex);
}
return animateProperty(target, from, to, runtime, easing, ( d ) -> {
try {
propSetter.invoke(target, d);
} catch( IllegalAccessException | InvocationTargetException e ) {
throw new RuntimeException("Can't access property setter for animation.", e);
}
});
}
@SuppressWarnings( "unchecked" )
private static <T, R> R callGetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String getterName = makeMethodName("get", propName);
Method getter = target.getClass().getMethod(getterName);
if( getter.getReturnType().equals(propType) ) {
return (R) getter.invoke(target);
} else {
throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName));
}
}
private static <T, R> Method findSetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException {
String setterName = makeMethodName("set", propName);
Method setter = target.getClass().getMethod(setterName, propType);
if( setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) {
return setter;
} else {
throw new NoSuchMethodException(String.format("No setter for property <%s> found.", propName));
}
}
private static String makeMethodName( String prefix, String propName ) {
String firstChar = propName.substring(0, 1).toUpperCase();
String tail = "";
if( propName.length() > 1 ) {
tail = propName.substring(1);
}
return prefix + firstChar + tail;
}
public static final <T> Future<T> animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) {
Validator.requireNotNull(target, "target");
Validator.requireNotNull(propSetter, "propSetter");
return play(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
}
public static final <T> Future<T> animateProperty( T target, final Color from, final Color to, int runtime, DoubleUnaryOperator easing, Consumer<Color> propSetter ) {
return play(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e)));
}
public static final <T> Future<T> animateProperty( T target, final Vector from, final Vector to, int runtime, DoubleUnaryOperator easing, Consumer<Vector> propSetter ) {
return play(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e)));
}
public static final <T, R> Future<T> animateProperty( T target, R from, R to, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, Consumer<R> propSetter ) {
return play(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
}
public static final <T, R> Future<T> play( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, BiConsumer<T, R> applicator ) {
return play(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e)));
}
public static final <T> Future<T> play( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
return TaskRunner.run(new FramerateLimitedTask() {
double t = 0.0;
final long starttime = System.currentTimeMillis();
@Override
public void update( double delta ) {
// One animation step for t in [0,1]
stepper.accept(easing.applyAsDouble(t));
t = (double) (System.currentTimeMillis() - starttime) / (double) runtime;
running = (t <= 1.0);
}
@Override
protected void finish() {
stepper.accept(easing.applyAsDouble(1.0));
}
}, target);
}
public static final <T> T playAndWait( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
Future<T> future = play(target, runtime, easing, stepper);
while( !future.isDone() ) {
try {
return future.get();
} catch( InterruptedException iex ) {
// Keep waiting
} catch( ExecutionException eex ) {
LOG.error(eex.getCause(), "Animation task terminated with exception");
return target;
}
}
return target;
}
/*public static final <T, R> Future<T> animate( T target, int runtime, Animator<T, R> animator ) {
return animate(
target, runtime,
animator::easing,
animator::interpolator,
animator::applicator
);
}*/
public static <T> Future<Animation<T>> play( Animation<T> animation ) {
// TODO: (ngb) Don't start when running
return TaskRunner.run(new FramerateLimitedTask() {
@Override
protected void initialize() {
animation.start();
}
@Override
public void update( double delta ) {
animation.update(delta);
running = animation.isActive();
}
}, animation);
}
public static <T> Animation<T> playAndWait( Animation<T> animation ) {
Future<Animation<T>> future = play(animation);
animation.await();
return animation;
}
public static <T> Future<Animation<T>> play( Animation<T> animation, DoubleUnaryOperator easing ) {
final AnimationFacade<T> facade = new AnimationFacade<>(animation, animation.getRuntime(), easing);
return TaskRunner.run(new FramerateLimitedTask() {
@Override
protected void initialize() {
facade.start();
}
@Override
public void update( double delta ) {
facade.update(delta);
running = facade.isActive();
}
}, animation);
}
public static final Log LOG = Log.getLogger(Animations.class);
}

View File

@@ -1,93 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
/**
* Animates the {@code target} in a circular motion centered at (<var>cx</var>, <var>cy</var>).
*/
public class CircleAnimation extends Animation<Shape> {
private final Shape target;
private final double centerX, centerY, rotateTo;
private double rotationRadius, startAngle;
private final boolean rotateRight;
public CircleAnimation( Shape target, double cx, double cy ) {
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo ) {
this(target, cx, cy, rotateTo, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight ) {
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight ) {
this(target, cx, cy, rotateTo, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, int runtime ) {
this(target, cx, cy, 360, true, runtime, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, int runtime ) {
this(target, cx, cy, 360, rotateRight, runtime, DEFAULT_EASING);
}
public CircleAnimation( Shape target, double cx, double cy, DoubleUnaryOperator easing ) {
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, easing);
}
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, DoubleUnaryOperator easing ) {
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, easing);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, int runtime, DoubleUnaryOperator easing ) {
this(target, cx, cy, rotateTo, true, runtime, easing);
}
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.target = target;
this.centerX = cx;
this.centerY = cy;
this.rotateTo = Constants.radians(Constants.limit(rotateTo, 0, 360));
this.rotateRight = rotateRight;
}
@Override
public void initialize() {
Vector vec = new Vector(target.getX(), target.getY()).sub(centerX, centerY);
startAngle = vec.heading();
rotationRadius = vec.length();
}
@Override
public Shape getAnimationTarget() {
return target;
}
@Override
public void animate( double e ) {
double angle = startAngle;
if( rotateRight ) {
angle += Constants.interpolate(0, rotateTo, e);
} else {
angle -= Constants.interpolate(0, rotateTo, e);
}
double x = centerX + rotationRadius * Constants.cos(angle);
double y = centerY + rotationRadius * Constants.sin(angle);
target.moveTo(x, y);
}
}

View File

@@ -1,119 +0,0 @@
package schule.ngb.zm.anim;
@SuppressWarnings( "unused" )
public class ContinousAnimation<T> extends Animation<T> {
private final Animation<T> baseAnimation;
private int lag = 0;
/**
* Speichert eine Approximation der aktuellen Steigung der Easing-Funktion, um im Fall
* {@code easeInOnly == true} nach dem ersten Durchlauf die passende Geschwindigkeit
* beizubehalten.
*/
private double m = 1.0, lastEase = 0.0;
private boolean easeInOnly = false;
public ContinousAnimation( Animation<T> baseAnimation ) {
this(baseAnimation, 0, false);
}
public ContinousAnimation( Animation<T> baseAnimation, int lag ) {
this(baseAnimation, lag, false);
}
public ContinousAnimation( Animation<T> baseAnimation, boolean easeInOnly ) {
this(baseAnimation, 0, easeInOnly);
}
private ContinousAnimation( Animation<T> baseAnimation, int lag, boolean easeInOnly ) {
super(baseAnimation.getRuntime() + lag, baseAnimation.getEasing());
this.baseAnimation = baseAnimation;
this.lag = lag;
this.easeInOnly = easeInOnly;
}
@Override
public T getAnimationTarget() {
return baseAnimation.getAnimationTarget();
}
@Override
public int getRuntime() {
return Integer.MAX_VALUE;
}
// @Override
// public void update( double delta ) {
// elapsedTime += (int) (delta * 1000);
// if( elapsedTime >= runtime + lag ) {
// elapsedTime %= (runtime + lag);
//
// if( easeInOnly && easing != null ) {
// easing = null;
// // runtime = (int)((1.0/m)*(runtime + lag));
// }
// }
//
// double t = (double) elapsedTime / (double) runtime;
// if( t >= 1.0 ) {
// t = 1.0;
// }
// if( easing != null ) {
// double e = easing.applyAsDouble(t);
// animate(e);
// m = (e-lastEase)/(delta*1000/(asDouble(runtime)));
// lastEase = e;
// } else {
// animate(t);
// }
// }
@Override
public void finish() {
baseAnimation.elapsedTime = baseAnimation.getRuntime();
baseAnimation.stop();
}
@Override
public void initialize() {
baseAnimation.start();
}
@Override
public void setRuntime( int pRuntime ) {
baseAnimation.setRuntime(pRuntime);
runtime = pRuntime + lag;
}
@Override
public void update( double delta ) {
int currentRuntime = elapsedTime + (int) (delta * 1000);
if( currentRuntime >= runtime + lag ) {
elapsedTime = currentRuntime % (runtime + lag);
if( easeInOnly && easing != null ) {
easing = Easing.linear();
// runtime = (int)((1.0/m)*(runtime + lag));
}
}
super.update(delta);
}
@Override
public void animate( double e ) {
// double t = (double) elapsedTime / (double) runtime;
// if( t >= 1.0 ) {
// t = 1.0;
// }
baseAnimation.elapsedTime = elapsedTime;
baseAnimation.animate(e);
m = (e - lastEase) / (delta * 1000 / (asDouble(runtime)));
lastEase = e;
}
}

View File

@@ -1,320 +0,0 @@
package schule.ngb.zm.anim;
import java.util.function.DoubleUnaryOperator;
/**
* @see <a href="https://easings.net/de">Cheat Sheet für Easing-Funktionen</a>
*/
public class Easing {
public static final DoubleUnaryOperator DEFAULT_EASING = Easing::smooth;
public static final DoubleUnaryOperator thereAndBack() {
return Easing::thereAndBack;
}
public static final DoubleUnaryOperator thereAndBack( final DoubleUnaryOperator baseEasing ) {
return ( t ) -> thereAndBack(t, baseEasing);
}
public static final double thereAndBack( double t ) {
return thereAndBack(t, DEFAULT_EASING);
}
public static final double thereAndBack( double t, DoubleUnaryOperator baseEasing ) {
if( t < 0.5 ) {
return baseEasing.applyAsDouble(2 * t);
} else {
return baseEasing.applyAsDouble(2 - 2 * t);
}
}
public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing ) {
return ( t ) -> halfAndHalf(t, firstEasing, secondEasing);
}
public static final DoubleUnaryOperator halfAndHalf( final DoubleUnaryOperator firstEasing, final DoubleUnaryOperator secondEasing, final double split ) {
return ( t ) -> halfAndHalf(t, firstEasing, secondEasing, split);
}
public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing ) {
return halfAndHalf(t, firstEasing, secondEasing, 0.5);
}
public static final double halfAndHalf( double t, DoubleUnaryOperator firstEasing, DoubleUnaryOperator secondEasing, double split ) {
if( t < split ) {
return firstEasing.applyAsDouble(2 * t);
} else {
return secondEasing.applyAsDouble(1 - 2 * t);
}
}
/*
* Functions taken from easings.net
*/
public static final DoubleUnaryOperator linear() {
return Easing::linear;
}
public static final double linear( double t ) {
return t;
}
public static final DoubleUnaryOperator quadIn() {
return Easing::quadIn;
}
public static final double quadIn( double t ) {
return t * t;
}
public static final DoubleUnaryOperator quadOut() {
return Easing::quadOut;
}
public static final double quadOut( double t ) {
return 1 - (1 - t) * (1 - t);
}
public static final DoubleUnaryOperator quadInOut() {
return Easing::quadInOut;
}
public static final double quadInOut( double t ) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
public static final DoubleUnaryOperator cubicIn() {
return Easing::cubicIn;
}
public static final double cubicIn( double t ) {
return t * t * t;
}
public static final DoubleUnaryOperator cubicOut() {
return Easing::cubicOut;
}
public static final double cubicOut( double t ) {
return 1 - Math.pow(1 - t, 3);
}
public static final DoubleUnaryOperator cubicInOut() {
return Easing::cubicInOut;
}
public static final double cubicInOut( double t ) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
public static final DoubleUnaryOperator sineIn() {
return Easing::sineIn;
}
public static final double sineIn( double t ) {
return 1 - Math.cos((t * Math.PI) / 2);
}
public static final DoubleUnaryOperator sineOut() {
return Easing::sineOut;
}
public static final double sineOut( double t ) {
return Math.sin((t * Math.PI) / 2);
}
public static final DoubleUnaryOperator sineInOut() {
return Easing::sineInOut;
}
public static final double sineInOut( double t ) {
return -(Math.cos(Math.PI * t) - 1) / 2;
}
public static final DoubleUnaryOperator elasticIn() {
return Easing::elasticIn;
}
public static final double elasticIn( double t ) {
double c4 = (2 * Math.PI) / 3;
return t == 0
? 0
: t == 1
? 1
: -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4);
}
public static final DoubleUnaryOperator elasticOut() {
return Easing::elasticOut;
}
public static final double elasticOut( double t ) {
double c4 = (2 * Math.PI) / 3;
return t == 0
? 0
: t == 1
? 1
: Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1;
}
public static final DoubleUnaryOperator elasticInOut() {
return Easing::elasticInOut;
}
public static final double elasticInOut( double t ) {
double c5 = (2 * Math.PI) / 4.5;
return t == 0
? 0
: t == 1
? 1
: t < 0.5
? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
: (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1;
}
public static final DoubleUnaryOperator bounceIn() {
return Easing::bounceIn;
}
public static final double bounceIn( double t ) {
return 1 - bounceOut(1 - t);
}
public static final DoubleUnaryOperator bounceOut() {
return Easing::bounceOut;
}
public static final double bounceOut( double t ) {
double n1 = 7.5625;
double d1 = 2.75;
if( t < 1.0 / d1 ) {
return n1 * t * t;
} else if( t < 2.0 / d1 ) {
return n1 * (t -= 1.5 / d1) * t + 0.75;
} else if( t < 2.5 / d1 ) {
return n1 * (t -= 2.25 / d1) * t + 0.9375;
} else {
return n1 * (t -= 2.625 / d1) * t + 0.984375;
}
}
public static final DoubleUnaryOperator bounceInOut() {
return Easing::bounceInOut;
}
public static final double bounceInOut( double t ) {
return t < 0.5
? (1 - bounceOut(1 - 2 * t)) / 2
: (1 + bounceOut(2 * t - 1)) / 2;
}
public static final DoubleUnaryOperator backIn() {
return Easing::backIn;
}
public static final double backIn( double t ) {
double c1 = 1.70158;
double c3 = c1 + 1;
return c3 * t * t * t - c1 * t * t;
}
public static final DoubleUnaryOperator backOut() {
return Easing::backOut;
}
public static final double backOut( double t ) {
double c1 = 1.70158;
double c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
public static final DoubleUnaryOperator backInOut() {
return Easing::backInOut;
}
public static final double backInOut( double t ) {
double c1 = 1.70158;
double c2 = c1 * 1.525;
return t < 0.5
? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
: (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2;
}
/*
* Functions from manim community
*/
public static final DoubleUnaryOperator smooth() {
return Easing::smooth;
}
public static final double smooth( double t ) {
double error = sigmoid(-INFLECTION / 2.0);
return Math.min(
Math.max(
(sigmoid(INFLECTION * (t - 0.5)) - error) / (1 - 2 * error),
0
),
1.0
);
}
public static final double rushIn( double t ) {
return 2 * smooth(t / 2.0);
}
public static final double rushOut( double t ) {
return 2 * smooth(t / 2.0 + 0.5) - 1;
}
public static final double doubleSmooth( double t ) {
if( t < 0.5 )
return 0.5 * smooth(2 * t);
else
return 0.5 * (1 + smooth(2 * t - 1));
}
public static final double hobbit( double t ) {
double new_t = t < 0.5 ? 2 * t : 2 * (1 - t);
return smooth(new_t);
}
public static final DoubleUnaryOperator wiggle() {
return Easing::wiggle;
}
public static final DoubleUnaryOperator wiggle( final int wiggles ) {
return (t) -> Easing.wiggle(t, wiggles);
}
public static final double wiggle( double t ) {
return wiggle(t, 2);
}
public static final double wiggle( double t, int wiggles ) {
return hobbit(t) * Math.sin(wiggles * Math.PI * t);
}
public static double INFLECTION = 10.0;
public static final double sigmoid( double x ) {
return 1.0 / (1 + Math.exp(-x));
}
private Easing() {
}
}

View File

@@ -1,63 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
@SuppressWarnings( "unused" )
public class FadeAnimation extends Animation<Shape> {
public static final int FADE_IN = 255;
public static final int FADE_OUT = 0;
private final Shape target;
private final int targetAlpha;
private Color fill, stroke;
private int fillAlpha, strokeAlpha;
public FadeAnimation( Shape target, int targetAlpha ) {
this(target, targetAlpha, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
}
public FadeAnimation( Shape target, int targetAlpha, int runtime ) {
this(target, targetAlpha, runtime, DEFAULT_EASING);
}
public FadeAnimation( Shape target, int runtime, DoubleUnaryOperator easing ) {
this(target, 0, runtime, easing);
}
public FadeAnimation( Shape target, int targetAlpha, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.target = target;
this.targetAlpha = targetAlpha;
}
@Override
public void initialize() {
fill = target.getFillColor();
fillAlpha = fill.getAlpha();
stroke = target.getStrokeColor();
strokeAlpha = stroke.getAlpha();
}
@Override
public Shape getAnimationTarget() {
return target;
}
@Override
public void animate( double e ) {
target.setFillColor(fill, (int) Constants.interpolate(fillAlpha, targetAlpha, e));
target.setStrokeColor(stroke, (int) Constants.interpolate(strokeAlpha, targetAlpha, e));
}
}

View File

@@ -1,38 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class FillAnimation extends Animation<Shape> {
private final Shape object;
private Color originFill;
private final Color targetFill;
public FillAnimation( Shape target, Color newFill, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = target;
targetFill = newFill;
}
@Override
public void initialize() {
originFill = object.getFillColor();
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setFillColor(Color.interpolate(originFill, targetFill, e));
}
}

View File

@@ -1,53 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Circle;
import schule.ngb.zm.shapes.Ellipse;
import schule.ngb.zm.shapes.Rectangle;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class MorphAnimation extends Animation<Shape> {
private Shape object, original, target;
public MorphAnimation( Shape object, Shape target, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.original = object.copy();
this.object = object;
this.target = target;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setX(Constants.interpolate(original.getX(), target.getX(), e));
object.setY(Constants.interpolate(original.getY(), target.getY(), e));
object.setFillColor(Color.interpolate(original.getFillColor(), target.getFillColor(), e));
object.setStrokeColor(Color.interpolate(original.getStrokeColor(), target.getStrokeColor(), e));
object.rotateTo(Constants.interpolate(original.getRotation(), target.getRotation(), e));
object.scale(Constants.interpolate(original.getScale(), target.getScale(), e));
object.setStrokeWeight(Constants.interpolate(original.getStrokeWeight(), target.getStrokeWeight(), e));
if( object instanceof Rectangle ) {
Rectangle r = (Rectangle)object;
r.setWidth(Constants.interpolate(original.getWidth(), target.getWidth(), e));
r.setHeight(Constants.interpolate(original.getHeight(), target.getHeight(), e));
} else if( object instanceof Circle ) {
Circle r = (Circle)object;
r.setRadius(Constants.interpolate(original.getWidth()*.5, target.getWidth()*.5, e));
} else if( object instanceof Ellipse ) {
Ellipse r = (Ellipse)object;
r.setWidth(Constants.interpolate(original.getWidth(), target.getWidth(), e));
r.setHeight(Constants.interpolate(original.getHeight(), target.getHeight(), e));
}
}
}

View File

@@ -1,42 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class MoveAnimation extends Animation<Shape> {
private final Shape object;
private final double targetX, targetY;
private double originX, originY;
public MoveAnimation( Shape target, double targetX, double targetY, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = target;
this.targetX = targetX;
this.targetY = targetY;
}
@Override
public void initialize() {
originX = object.getX();
originY = object.getY();
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setX(Constants.interpolate(originX, targetX, e));
object.setY(Constants.interpolate(originY, targetY, e));
}
}

View File

@@ -1,32 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class RotateAnimation extends Animation<Shape> {
private Shape object;
private double oA, tA;
public RotateAnimation( Shape object, double angle, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oA = object.getRotation();
tA = angle;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.rotateTo(Constants.interpolate(oA, tA, e));
}
}

View File

@@ -1,32 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class StrokeAnimation extends Animation<Shape> {
private Shape object;
private Color oFill, tFill;
public StrokeAnimation( Shape object, Color newStroke, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oFill = object.getFillColor();
tFill = newStroke;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setStrokeColor(Color.interpolate(oFill, tFill, e));
}
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,211 +0,0 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.anim.AnimationFacade;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.shapes.Shape;
import java.awt.Graphics2D;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
/**
* Eine Ebene um {@link Shape} Objekte zu zeichnen.
* <p>
* Ein {@code ShapesLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
*/
@SuppressWarnings( "unused" )
public class ShapesLayer extends Layer {
/**
*
*/
protected boolean clearBeforeDraw = true;
protected boolean updateShapes = true;
protected final List<Shape> shapes;
private final List<Animation<? extends Shape>> animations;
private final List<Updatable> updatables;
public ShapesLayer() {
super();
shapes = new LinkedList<>();
animations = new LinkedList<>();
updatables = new LinkedList<>();
}
public ShapesLayer( int width, int height ) {
super(width, height);
shapes = new LinkedList<>();
animations = new LinkedList<>();
updatables = new LinkedList<>();
}
public Shape getShape( int index ) {
return shapes.get(index);
}
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
return shapeClass.cast(s);
}
}
return null;
}
public List<Shape> getShapes() {
return shapes;
}
public <ST extends Shape> List<ST> getShapes( Class<ST> shapeClass ) {
List<ST> result = new LinkedList<>();
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
result.add(shapeClass.cast(s));
}
}
return result;
}
public void add( Shape... shapes ) {
synchronized( this.shapes ) {
Collections.addAll(this.shapes, shapes);
for( Shape s : shapes ) {
if( Updatable.class.isInstance(s) ) {
updatables.add((Updatable) s);
}
}
}
}
public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
this.shapes.addAll(shapes);
for( Shape s : shapes ) {
if( Updatable.class.isInstance(s) ) {
updatables.add((Updatable) s);
}
}
}
}
public void remove( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
}
public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
this.shapes.removeAll(shapes);
}
}
public void removeAll() {
synchronized( shapes ) {
shapes.clear();
}
}
public void showAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.show();
}
}
}
public void hideAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.hide();
}
}
}
public <S extends Shape> void play( Animation<S> anim ) {
this.animations.add(anim);
anim.start();
}
@SafeVarargs
public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim);
anim.start();
}
}
public <S extends Shape> void play( Animation<S> anim, int runtime ) {
play(anim, runtime, Easing.DEFAULT_EASING);
}
public <S extends Shape> void play( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
AnimationFacade<S> facade = new AnimationFacade<>(anim, runtime, easing);
play(facade);
}
@Override
public void update( double delta ) {
if( updateShapes ) {
synchronized( shapes ) {
List<Updatable> uit = List.copyOf(updatables);
for( Updatable u : uit ) {
if( u.isActive() ) {
u.update(delta);
}
}
}
/*
Iterator<Updatable> uit = updatables.iterator();
while( uit.hasNext() ) {
Updatable u = uit.next();
if( u.isActive() ) {
u.update(delta);
}
}
*/
}
Iterator<Animation<? extends Shape>> it = animations.iterator();
while( it.hasNext() ) {
Animation<? extends Shape> anim = it.next();
anim.update(delta);
if( !anim.isActive() ) {
it.remove();
}
}
}
@Override
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
synchronized( shapes ) {
List<Shape> it = List.copyOf(shapes);
for( Shape s : it ) {
if( s.isVisible() ) {
s.draw(drawing);
}
}
}
super.draw(graphics);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,245 +0,0 @@
package schule.ngb.zm.media;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Ein Mixer ist eine Sammlung mehrerer {@link Audio Audio-Medien}, die
* gemeinsam kontrolliert werden können.
* <p>
* Im einfachsten Fall kann die Audio-Gruppe gemeinsam gestartet und gestoppt
* werden. Ein Mixer kann die Lautstärke der Medien in Relation zueinander
* setzen. Dazu wird jedem Medium ein Faktor mitgegeben. Ein Medium mit dem
* Faktor 0.5 ist dann halb so laut wie eines, mit dem Faktor 1.0.
* <p>
* Darüber hinaus kann ein Mixer Effekte wie einen
* {@link #fade(double, int) fadeIn} auf die Medien anwenden.
*/
@SuppressWarnings( "unused" )
public class Mixer implements Audio, AudioListener {
private List<AudioWrapper> audios;
private float volume = 0.8f;
EventDispatcher<Audio, AudioListener> eventDispatcher;
public Mixer() {
this.audios = new ArrayList<>(4);
}
public String getSource() {
return "";
}
private AudioWrapper findWrapper( Audio pAudio ) {
for( AudioWrapper aw: audios ) {
if( aw.audio == pAudio ) {
return aw;
}
}
return null;
}
public boolean contains( Audio pAudio ) {
return findWrapper(pAudio) != null;
}
public void add( Audio pAudio ) {
add(pAudio, 1);
}
/**
* Fügt ein Audio-Objekt dem Mixer mit dem angegebenen Lautstärke-Faktor
* hinzu.
* <p>
* Der Lautstärke-Faktor setzt die Lautstärke des Audio-Objektes relativ zur
* Lautstärke des Mixers. Bei einem Faktor von 1.0 wird die Lautstärke des
* Mixers übernommen. Bei einem Wert von 0.5 wird das Objekt halb so laut
* abgespielt. Auf diese Weise lässt sich die Lautstärke aller Audio-Objekte
* des Mixers gleichzeitig anpassen, während ihre relative Lautstärke
* zueinander gleich bleibt.
*
* @param pAudio Ein Audio-Objekt.
* @param pVolumeFactor Der Lautstärke-Faktor.
*/
public void add( Audio pAudio, double pVolumeFactor ) {
if( !contains(pAudio) ) {
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
} else {
findWrapper(pAudio).volumeFactor = (float) pVolumeFactor;
}
pAudio.setVolume(pVolumeFactor * volume);
}
/**
* Entfernt die das angegebene Audio-Objekt aus dem Mixer. Ist das Objekt
* nicht Teil des Mixers, passiert nichts.
*
* @param pAudio Ein Audio-Objekt.
*/
public void remove( Audio pAudio ) {
Iterator<AudioWrapper> it = audios.listIterator();
while( it.hasNext() ) {
AudioWrapper aw = it.next();
if( aw.audio == pAudio ) {
it.remove();
break;
}
}
}
public void removeAll() {
audios.clear();
}
@Override
public boolean isPlaying() {
return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
}
@Override
public boolean isLooping() {
return audios.stream().anyMatch(aw -> aw.audio.isLooping());
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * volume));
}
@Override
public double getVolume() {
return volume;
}
@Override
public void play() {
audios.stream().forEach(aw -> aw.audio.play());
}
@Override
public void playAndWait() {
audios.stream().forEach(aw -> aw.audio.play());
while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) {
try {
Thread.sleep(10);
} catch( InterruptedException e ) {
// Just keep waiting
}
}
}
@Override
public void loop() {
audios.stream().forEach(aw -> aw.audio.loop());
}
@Override
public void stop() {
audios.stream().forEach(aw -> aw.audio.stop());
}
@Override
public void dispose() {
if( isPlaying() ) {
stop();
}
audios.stream().forEach(aw -> aw.audio.dispose());
}
/**
* Ändert die Lautstärke aller hinzugefügten Audiomedien in der angegebenen
* Zeit schrittweise, bis die angegebene Lautstärke erreicht ist.
* <p>
* Zu beachten ist, dass die Lautstärke des Mixers angepasst wird. Das
* bedeutet, dass die Lautstärke der hinzugefügten Medien mit ihrem
* Lautstärkefaktor multipliziert werden. Die Medien haben am Ende also
* nicht unbedingt die Lautstärke {@code to}.
*
* @param to Der Zielwert für die Lautstärke.
* @param time Die Zeit, nach der die Änderung abgeschlossen sein soll.
*/
public void fade( final double to, final int time ) {
TaskRunner.run(new Runnable() {
@Override
public void run() {
final long start = System.currentTimeMillis();
double t = 0.0;
double from = volume;
do {
setVolume(Constants.interpolate(from, to, t));
t = (double) (System.currentTimeMillis() - start) / (double) time;
try {
Thread.sleep(1000 / Constants.framesPerSecond);
} catch( InterruptedException e ) {
// Keep waiting
}
} while( t < 1.0 );
setVolume(to);
}
});
}
@Override
public void playbackStarted( Audio source ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Mixer.this);
}
}
@Override
public void playbackStopped( Audio source ) {
if( !isPlaying() ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Mixer.this);
}
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich auch ein Listener registrieren möchte.
*
* @return
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
}
return eventDispatcher;
}
class AudioWrapper {
Audio audio;
float volumeFactor;
public AudioWrapper( Audio audio, float volumeFactor ) {
this.audio = audio;
this.volumeFactor = volumeFactor;
}
}
}

View File

@@ -1,326 +0,0 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import schule.ngb.zm.util.tasks.TaskRunner;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Eine Musik, die abgespielt werden kann.
* <p>
* Im Gegensatz zu einem {@link Sound} wird {@code Music} für längere
* Audiodateien benutzt, die zum Beispiel als Hintergrundmusik gespielt werden.
* Die Audiodaten werden daher nicht vollständig in den Speicher geladen,
* sondern direkt aus der Quelle gestreamt und direkt wiedergegeben.
* <p>
* Daher ist es nicht möglich, die länge der Musik im Vorfeld abzufragen oder zu
* einer bestimmten Stelle im Stream zu springen.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/
// TODO: Wann sollten Listener beim Loopen informiert werden? Nach jedem Loop oder erst ganz am Ende?
@SuppressWarnings( "unused" )
public class Music implements Audio {
/**
* Größe des verwendeten Input-Puffers für die Audiodaten.
*/
private static final int BUFFER_SIZE = 4096;
/**
* Ob der Sound aktuell abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound aktuell in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle der Audiodaten.
*/
private String audioSource;
/**
* Der {@link AudioInputStream}, um die Audiosdaten zu lesen. {@code null},
* falls noch kein Stream geöffnet wurde.
*/
private AudioInputStream audioStream;
/**
* Die {@link SourceDataLine} für die Ausgabe. {@code null}, falls die
* Audiodatei noch nicht geöffnet wurde.
*/
private SourceDataLine audioLine;
/**
* Die aktuelle Lautstärke des Mediums.
*/
private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher;
/**
* Erstellt eine Musik aus der angegebenen Audioquelle.
*
* @param audioSource Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/
public Music( String audioSource ) {
Validator.requireNotNull(audioSource, "audioSource");
this.audioSource = audioSource;
}
@Override
public String getSource() {
return audioSource;
}
@Override
public boolean isPlaying() {
return playing;
}
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
if( audioLine != null ) {
applyVolume();
}
}
@Override
public double getVolume() {
return volume;
}
/**
* Wendet die Lautstärke vor dem Abspielen auf den Audiostream an.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
@Override
public void play() {
if( openLine() ) {
TaskRunner.run(this::stream);
}
}
@Override
public void playAndWait() {
if( openLine() ) {
stream();
}
}
@Override
public void loop() {
looping = true;
play();
}
@Override
public void stop() {
playing = false;
looping = false;
dispose();
}
@Override
public synchronized void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
playing = false;
audioLine.stop();
}
if( audioLine.isOpen() ) {
audioLine.drain();
audioLine.close();
}
}
try {
if( audioStream != null ) {
audioStream.close();
}
} catch( IOException ex ) {
}
audioLine = null;
audioStream = null;
}
/**
* Startet den Stream der Audiodaten und damit die Wiedergabe.
* <p>
* Die {@link #audioLine} muss vorher mit {@link #openLine()} geöffnet
* werden, ansonsten passiert nichts.
*/
private synchronized void stream() {
if( audioLine == null ) {
return;
}
audioLine.start();
playing = true;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Music.this);
}
byte[] bytesBuffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
try {
while( playing && (bytesRead = audioStream.read(bytesBuffer)) != -1 ) {
audioLine.write(bytesBuffer, 0, bytesRead);
}
audioLine.drain();
audioLine.stop();
} catch( IOException ex ) {
LOG.warn(ex, "Error while playing Music source <%s>", audioSource);
}
// Wait for the remaining audio to play
/*while( audioLine.isRunning() ) {
try {
Thread.sleep(10);
} catch( InterruptedException ex ) {
// Just keep waiting ...
}
}*/
playing = false;
streamingStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Music.this);
}
}
/**
* Öffnet eine {@link SourceDataLine} für die
* {@link #audioSource Audioquelle} und bereitet die Wiedergabe vor. Es wird
* noch nichts abgespielt.
*
* @return {@code true}, wenn die Line geöffnet werden konnte, {@code false}
* sonst.
*/
private boolean openLine() {
if( audioLine != null ) {
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream inStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = inStream.getFormat();
final int ch = format.getChannels();
final float rate = format.getSampleRate();
AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, outFormat);
audioLine = (SourceDataLine) AudioSystem.getLine(info);
audioLine.open(outFormat);
applyVolume();
audioStream = AudioSystem.getAudioInputStream(outFormat, inStream);
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
/**
* Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Aufruf von {@link #stop()} oder weil keine Audiodaten mehr vorhanden
* sind.
* <p>
* Nach dem Ende des Streams wird {@link #dispose()} aufgerufen und, falls
* das Musikstück in einer Schleife abgespielt wird, der Stream direkt
* wieder gestartet.
*/
private void streamingStopped() {
dispose();
if( looping ) {
if( openLine() ) {
stream();
} else {
playing = false;
looping = false;
}
} else {
playing = false;
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich der erste Listener anmelden möchte.
*
* @return Der {@code EventDispatcher} für dieses Objekt.
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", ( a, l ) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", ( a, l ) -> l.playbackStopped(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Music.class);
}

View File

@@ -1,349 +0,0 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Ein kurzer Soundclip, der mehrmals wiederverwendet werden kann.
* <p>
* In Spielen und anderen Projekten gibt es oftmals eine Reihe kurzer
* Soundclips, die zusammen mit bestimmten Aktionen wiedergegeben werden (zum
* Beispiel, wenn die Spielfigur springt, wenn zwei Objekte kollidieren, usw.).
* Sounds werden vollständig in den Speicher geladen und können immer wieder,
* als Schleife oder auch nur Abschnittsweise, abgespielt werden.
* <p>
* Für längere Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher
* die Klasse {@link Music} an.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/
@SuppressWarnings( "unused" )
public class Sound implements Audio {
/**
* Ob der Sound aktuell abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound aktuell in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle der Audiodaten.
*/
private String audioSource;
/**
* Der Clip, falls er schon geladen wurde. Ansonsten {@code null}.
*/
private Clip audioClip;
/**
* Ob die Ressourcen des Clips im Speicher nach dem nächsten Abspielen
* freigegeben werden sollen.
*/
private boolean disposeAfterPlay = false;
/**
* Die aktuelle Lautstärke des Clips.
*/
private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher;
/**
* Erstellt einen Sound aus der angegebene Quelle.
*
* @param source Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/
public Sound( String source ) {
Validator.requireNotNull(source, "source");
this.audioSource = source;
}
@Override
public String getSource() {
return audioSource;
}
@Override
public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning();
return playing;
}
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
if( audioClip != null ) {
applyVolume();
}
}
@Override
public double getVolume() {
return volume;
}
/**
* Wendet die Lautstärke vor dem Abspielen auf den Clip an.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioClip.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
@Override
public void stop() {
looping = false;
if( audioClip.isRunning() ) {
audioClip.stop();
}
playing = false;
}
@Override
public void play() {
if( this.openClip() ) {
audioClip.start();
playing = true;
}
}
@Override
public void playAndWait() {
this.play();
long audioLen = audioClip.getMicrosecondLength();
while( playing ) {
try {
long ms = (audioLen - audioClip.getMicrosecondPosition()) / 1000L;
Thread.sleep(ms);
} catch( InterruptedException ex ) {
// Ignore
}
}
audioClip.close();
}
/**
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei.
* <p>
* Der Aufruf ist effektiv gleich zu
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
* allerdings wird der aufrufende Thread nicht blockiert und
* {@link #dispose()} automatisch am Ende aufgerufen.
*/
public void playOnce() {
disposeAfterPlay = true;
play();
}
/**
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei.
* <p>
* Der Aufruf entspricht
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
*/
public void playOnceAndWait() {
disposeAfterPlay = true;
playAndWait();
}
@Override
public void loop() {
loop(Clip.LOOP_CONTINUOUSLY);
}
/**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen und stoppt
* die Wiedergabe dann.
* <p>
* Wird {@code count} auf {@link Clip#LOOP_CONTINUOUSLY} gesetzt (-1), wird
* der Clip unendlich oft wiederholt. Der Aufruf entspricht dann
* {@link #loop()}.
*
* @param count Anzahl der Wiederholungen.
*/
public void loop( int count ) {
if( count > 0 ) {
int loopCount = count;
if( loopCount != Clip.LOOP_CONTINUOUSLY ) {
if( loopCount <= 0 ) {
return;
}
// Adjust Number of loops
loopCount -= 1;
}
if( openClip() ) {
looping = true;
audioClip.loop(loopCount);
playing = true;
}
}
}
@Override
public synchronized void dispose() {
if( audioClip != null ) {
if( audioClip.isRunning() ) {
stop();
}
audioClip.close();
audioClip = null;
}
}
/**
* Lädt, falls nötig, den {@link Clip} für die
* {@link #audioSource Audioquelle}.
*
* @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false}
* sonst.
*/
private synchronized boolean openClip() {
if( audioClip != null ) {
audioClip.setFramePosition(0);
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream audioStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = audioStream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, format);
audioClip = (Clip) AudioSystem.getLine(info);
audioClip.addLineListener(new LineListener() {
@Override
public void update( LineEvent event ) {
if( event.getType() == LineEvent.Type.START ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Sound.this);
}
} else if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped();
}
}
});
audioClip.open(audioStream);
applyVolume();
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
/*@Override
public void update( LineEvent event ) {
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START ) {
playing = true;
} else if( type == LineEvent.Type.STOP ) {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
}*/
/**
* Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Aufruf von {@link #stop()} oder, weil die Wiedergabe nach
* {@link #playOnce()} beendet wurde.
* <p>
* Falls {@link #disposeAfterPlay} gesetzt ist, wird nach dem Ende der
* Wiedergabe {@link #dispose()} aufgerufen.
*/
private void playbackStopped() {
playing = false;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich der erste Listener anmelden möchte.
*
* @return Der {@code EventDispatcher} für dieses Objekt.
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", ( a, l ) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", ( a, l ) -> l.playbackStopped(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Sound.class);
}

View File

@@ -1,7 +0,0 @@
/**
* Dieses Paket enthält Klassen zur Einbettung von Mediendateien.
* <p>
* Mit Medien sind vor allem Audio und Videodateien gemeint. Aktuell kann die
* Zeichenmaschine Audiodateien verwenden.
*/
package schule.ngb.zm.media;

View File

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

View File

@@ -1,183 +0,0 @@
package schule.ngb.zm.ml;
import java.util.Arrays;
import java.util.function.DoubleUnaryOperator;
import java.util.stream.IntStream;
// See https://github.com/wheresvic/neuralnet
public final class MLMath {
public static double sigmoid( double x ) {
return 1 / (1 + Math.exp(-x));
}
public static double sigmoidDerivative( double x ) {
return x * (1 - x);
}
public static double tanh( double x ) {
return Math.tanh(x);
}
public static double tanhDerivative( double x ) {
return 1 - Math.tanh(x) * Math.tanh(x);
}
public static double[] normalize( double[] vector ) {
final double sum = Arrays.stream(vector).sum();
return Arrays.stream(vector).map(( d ) -> d / sum).toArray();
}
public static double[][] matrixMultiply( double[][] A, double[][] B ) {
int a = A.length, b = A[0].length, c = B[0].length;
if( B.length != b ) {
throw new IllegalArgumentException(
String.format("Matrix A needs equal columns to matrix B rows. (Currently <%d> vs <%d>)", a, B.length)
);
}
return IntStream.range(0, a).parallel().mapToObj(
( i ) -> IntStream.range(0, c).mapToDouble(
( j ) -> IntStream.range(0, b).mapToDouble(
( k ) -> A[i][k] * B[k][j]
).sum()
).toArray()
).toArray(double[][]::new);
}
public static double[][] matrixScale( final double[][] A, final double[][] S ) {
if( A.length != S.length || A[0].length != S[0].length ) {
throw new IllegalArgumentException("Matrices need to be same size.");
}
return IntStream.range(0, A.length).parallel().mapToObj(
( i ) -> IntStream.range(0, A[i].length).mapToDouble(
( j ) -> A[i][j] * S[i][j]
).toArray()
).toArray(double[][]::new);
}
public static double[][] matrixSub( double[][] A, double[][] B ) {
if( A.length != B.length || A[0].length != B[0].length ) {
throw new IllegalArgumentException("Cannot subtract unequal matrices");
}
return IntStream.range(0, A.length).parallel().mapToObj(
( i ) -> IntStream.range(0, A[i].length).mapToDouble(
( j ) -> A[i][j] - B[i][j]
).toArray()
).toArray(double[][]::new);
}
public static double[][] matrixAdd( double[][] A, double[][] B ) {
if( A.length != B.length || A[0].length != B[0].length ) {
throw new IllegalArgumentException("Cannot add unequal matrices");
}
return IntStream.range(0, A.length).parallel().mapToObj(
( i ) -> IntStream.range(0, A[i].length).mapToDouble(
( j ) -> A[i][j] + B[i][j]
).toArray()
).toArray(double[][]::new);
}
public static double[][] matrixTranspose( double[][] matrix ) {
int a = matrix.length, b = matrix[0].length;
double[][] result = new double[matrix[0].length][matrix.length];
for( int i = 0; i < a; i++ ) {
for( int j = 0; j < b; ++j ) {
result[j][i] = matrix[i][j];
}
}
return result;
}
public static double[][] matrixApply( double[][] A, DoubleUnaryOperator op ) {
return Arrays.stream(A).parallel().map(
( arr ) -> Arrays.stream(arr).map(op).toArray()
).toArray(double[][]::new);
}
public static double[][] copyMatrix( double[][] matrix ) {
/*return Arrays.stream(matrix).map(
(arr) -> Arrays.copyOf(arr, arr.length)
).toArray(double[][]::new);*/
double[][] result = new double[matrix.length][matrix[0].length];
for( int i = 0; i < matrix.length; i++ ) {
result[i] = Arrays.copyOf(matrix[i], matrix[i].length);
}
return result;
}
public static double[] toVector( double[][] matrix ) {
return Arrays.stream(matrix).mapToDouble(
( arr ) -> arr[0]
).toArray();
}
public static double[][] toMatrix( double[] vector ) {
return Arrays.stream(vector).mapToObj(
( d ) -> new double[]{d}
).toArray(double[][]::new);
}
public static double entropy(double[][] A, double[][] Y, int batch_size) {
int m = A.length;
int n = A[0].length;
double[][] z = new double[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
z[i][j] = (Y[i][j] * Math.log(A[i][j])) + ((1 - Y[i][j]) * Math.log(1 - A[i][j]));
}
}
double sum = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sum += z[i][j];
}
}
return -sum / batch_size;
}
public static double[][] biasAdd( double[][] A, double[] V ) {
if( A[0].length != V.length ) {
throw new IllegalArgumentException("Can't add bias vector to matrix with wrong column count");
}
double[][] result = new double[A.length][A[0].length];
for( int j = 0; j < A[0].length; j++ ) {
for( int i = 0; i < A.length; i++ ) {
result[i][j] = A[i][j] + V[j];
}
}
return result;
}
public static double[] biasAdjust( double[] biases, double[][] delta ) {
if( biases.length != delta[0].length ) {
throw new IllegalArgumentException("Can't add adjust bias vector by delta with wrong column count");
}
double[] result = new double[biases.length];
for( int j = 0; j < delta[0].length; j++ ) {
for( int i = 0; i < delta.length; i++ ) {
result[j] += biases[j] - delta[i][j];
}
result[j] /= delta.length;
}
return result;
}
private MLMath() {
}
}

View File

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

View File

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

View File

@@ -1,231 +0,0 @@
package schule.ngb.zm.ml;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
public class NeuralNetwork {
public static void saveToFile( String source, NeuralNetwork network ) {
try(
Writer writer = ResourceStreamProvider.getWriter(source);
PrintWriter out = new PrintWriter(writer)
) {
for( NeuronLayer layer : network.layers ) {
out.print(layer.getNeuronCount());
out.print(' ');
out.print(layer.getInputCount());
out.println();
for( int i = 0; i < layer.getInputCount(); i++ ) {
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.weights.get(i, j));
out.print(' ');
}
out.println();
}
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.biases.get(0, j));
out.print(' ');
}
out.println();
}
out.flush();
} catch( IOException ex ) {
LOG.error(ex, "");
}
}
public static void saveToDataFile( String source, NeuralNetwork network ) {
try(
OutputStream stream = ResourceStreamProvider.getOutputStream(source);
DataOutputStream out = new DataOutputStream(stream)
) {
for( NeuronLayer layer : network.layers ) {
out.writeInt(layer.getNeuronCount());
out.writeInt(layer.getInputCount());
for( int i = 0; i < layer.getInputCount(); i++ ) {
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.writeDouble(layer.weights.get(i, j));
}
}
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.writeDouble(layer.biases.get(0, j));
}
}
out.flush();
} catch( IOException ex ) {
LOG.error(ex, "");
}
}
public static NeuralNetwork loadFromFile( String source ) {
try(
Reader reader = ResourceStreamProvider.getReader(source);
BufferedReader in = new BufferedReader(reader)
) {
List<NeuronLayer> layers = new LinkedList<>();
String line;
while( (line = in.readLine()) != null ) {
String[] split = line.split(" ");
int neurons = Integer.parseInt(split[0]);
int inputs = Integer.parseInt(split[1]);
NeuronLayer layer = new NeuronLayer(neurons, inputs);
for( int i = 0; i < inputs; i++ ) {
split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) {
layer.weights.set(i, j, Double.parseDouble(split[j]));
}
}
// Load Biases
split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) {
layer.biases.set(0, j, Double.parseDouble(split[j]));
}
layers.add(layer);
}
return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) {
LOG.error(ex, "Could not load neural network from source <%s>", source);
}
return null;
}
public static NeuralNetwork loadFromDataFile( String source ) {
try(
InputStream stream = ResourceStreamProvider.getInputStream(source);
DataInputStream in = new DataInputStream(stream)
) {
List<NeuronLayer> layers = new LinkedList<>();
while( in.available() > 0 ) {
int neurons = in.readInt();
int inputs = in.readInt();
NeuronLayer layer = new NeuronLayer(neurons, inputs);
for( int i = 0; i < inputs; i++ ) {
for( int j = 0; j < neurons; j++ ) {
layer.weights.set(i, j, in.readDouble());
}
}
// Load Biases
for( int j = 0; j < neurons; j++ ) {
layer.biases.set(0, j, in.readDouble());
}
layers.add(layer);
}
return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) {
LOG.error(ex, "Could not load neural network from source <%s>", source);
}
return null;
}
private NeuronLayer[] layers;
private MLMatrix output;
private double learningRate = 0.1;
public NeuralNetwork( int inputs, int layer1, int outputs ) {
this(new NeuronLayer(layer1, inputs), new NeuronLayer(outputs, layer1));
}
public NeuralNetwork( int inputs, int layer1, int layer2, int outputs ) {
this(new NeuronLayer(layer1, inputs), new NeuronLayer(layer2, layer1), new NeuronLayer(outputs, layer2));
}
public NeuralNetwork( int inputs, int layer1, int layer2, int layer3, int outputs ) {
this(new NeuronLayer(layer1, inputs), new NeuronLayer(layer2, layer1), new NeuronLayer(layer3, layer2), new NeuronLayer(outputs, layer3));
}
public NeuralNetwork( List<NeuronLayer> layers ) {
this.layers = new NeuronLayer[layers.size()];
for( int i = 0; i < layers.size(); i++ ) {
this.layers[i] = layers.get(i);
if( i > 0 ) {
this.layers[i - 1].setNextLayer(this.layers[i]);
}
}
}
public NeuralNetwork( NeuronLayer... layers ) {
this.layers = new NeuronLayer[layers.length];
for( int i = 0; i < layers.length; i++ ) {
this.layers[i] = layers[i];
if( i > 0 ) {
this.layers[i - 1].setNextLayer(this.layers[i]);
}
}
}
public int getLayerCount() {
return layers.length;
}
public NeuronLayer[] getLayers() {
return layers;
}
public NeuronLayer getLayer( int i ) {
return layers[i - 1];
}
public double getLearningRate() {
return learningRate;
}
public void setLearningRate( double pLearningRate ) {
this.learningRate = pLearningRate;
}
public MLMatrix getOutput() {
return output;
}
public MLMatrix predict( double[] inputs ) {
return predict(MatrixFactory.create(new double[][]{inputs}));
}
public MLMatrix predict( double[][] inputs ) {
return predict(MatrixFactory.create(inputs));
}
public MLMatrix predict( MLMatrix inputs ) {
this.output = layers[0].apply(inputs);
return this.output;
}
public void learn( double[][] expected ) {
learn(MatrixFactory.create(expected));
}
public void learn( MLMatrix expected ) {
layers[layers.length - 1].backprop(expected, learningRate);
}
public void train( double[][] inputs, double[][] expected, int iterations/*, double minChange, int timeout */ ) {
for( int i = 0; i < iterations; i++ ) {
// pass the training set through the network
predict(inputs);
// start backpropagation through all layers
learn(expected);
if( i % 10000 == 0 ) {
LOG.trace("Training iteration %d of %d", i, iterations);
}
}
}
private static final Log LOG = Log.getLogger(NeuralNetwork.class);
}

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
/**
* <h2>Die Zeichenmaschine</h2>
* <p>
* Die <b>Zeichenmaschine</b> ist eine für den Informatikunterricht entwickelte
* Bibliothek, die unter anderem an <a
* href="https://processing.org/">Processing</a> angelehnt ist. Die Bibliothek
* soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen und
* für Schülerinnen und Schüler im Unterricht nutzbar machen.
* <p>
* Eine umfassende Dokumentation ist unter <a
* href="https://zeichenmaschine.xyz">zeichenmaschine.xyz</a> verfügbar.
*/
package schule.ngb.zm;

View File

@@ -1,816 +0,0 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.BasicDrawable;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Options;
import schule.ngb.zm.Strokeable;
import schule.ngb.zm.util.Validator;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
/**
* Dies ist die Basisklasse für alle Formen in der Zeichenmaschine.
* <p>
* Alle Formen sind als Unterklassen von {@code Shape} implementiert.
* <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die Werte einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert:
* <pre><code>
* public Circle( double x, double y, double radius ) {
* super(x, y);
* this.radius = radius;
* }
*
* public Circle( Circle circle ) {
* super(circle.x, circle.y);
* copyFrom(circle);
* }
* </code></pre>
* <p>
* Außerdem implementieren Unterklassen eine passende {@link #toString()} und
* eine {@link #equals(Object)} Methode.
*/
@SuppressWarnings( "unused" )
public abstract class Shape extends BasicDrawable {
/**
* Speichert die x-Koordinate der Form.
*/
protected double x;
/**
* Speichert die y-Koordinate der Form.
*/
protected double y;
/**
* Speichert die Rotation in Grad um den Punkt (x, y).
*/
protected double rotation = 0.0;
/**
* Speichert den Skalierungsfaktor.
*/
protected double scale = 1.0;
/**
* Speichert den Ankerpunkt.
*/
protected Options.Direction anchor = Options.Direction.CENTER;
/**
* Erstellt eine neue Form mit den Koordinaten {@code (0,0)}.
*/
public Shape() {
this(0.0, 0.0);
}
/**
* Erstellt eine Form mit den angegebenen Koordinaten.
*
* @param x Die x-Koordinate.
* @param y Die y-Koordinate.
*/
public Shape( double x, double y ) {
this.x = x;
this.y = y;
}
/**
* Liefert die aktuelle x-Koordinate der Form.
*
* @return Die x-Koordinate.
*/
public double getX() {
return x;
}
/**
* Setzt die x-Koordinate der Form.
*
* @param x Die neue x-Koordinate.
*/
public void setX( double x ) {
this.x = x;
}
/**
* Liefert die aktuelle y-Koordinate der Form.
*
* @return Die y-Koordinate.
*/
public double getY() {
return y;
}
/**
* Setzt die x-Koordinate der Form.
*
* @param y Die neue y-Koordinate der Form.
*/
public void setY( double y ) {
this.y = y;
}
/**
* Liefert die aktuelle Breite dieser Form.
* <p>
* Die Breite einer Form ist immer die Breite ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen.
*
* @return Die Breite der Form.
*/
public abstract double getWidth();
/**
* Liefert die aktuelle Höhe dieser Form.
* <p>
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen.
*
* @return Die Höhe der Form.
*/
public abstract double getHeight();
/**
* Liefert die Rotation in Grad.
*
* @return Rotation in Grad.
*/
public double getRotation() {
return rotation;
}
/**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
*
* @param angle Drehwinkel in Grad.
*/
public void rotate( double angle ) {
this.rotation += angle % 360;
}
/**
* Dreht die Form um den angegebenen Winkel um das angegebene Drehzentrum.
*
* @param center Das Drehzentrum der Drehung.
* @param angle Der Drehwinkel.
*/
public void rotate( Point2D center, double angle ) {
Validator.requireNotNull(center, "center");
rotate(center.getX(), center.getY(), angle);
}
/**
* Dreht die Form um den angegebenen Drehwinkel um die angegbenen
* Koordinaten als Drehzentrum.
*
* @param x x-Koordiante des Drehzentrums.
* @param y y-Koordiante des Drehzentrums.
* @param angle Drehwinkel in Grad.
*/
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
this.x = x2 + x;
this.y = y2 + y;
}
/**
* Setzt die Drehung der Form auf den angegebenen Winkel.
*
* @param angle Drehwinkel in Grad.
*/
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
/**
* Gibt den aktuellen Skalierungsfaktor zurück.
*
* @return Der Skalierungsfaktor.
*/
public double getScale() {
return scale;
}
/**
* Setzt den Skalierungsfaktor auf den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Das Seitenverhältnis wird immer beibehalten.
*
* @param factor Der Skalierungsfaktor.
*/
public void scale( double factor ) {
scale = factor;
}
/**
* Skaliert die Form um den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Die Skalierung wird zusätzlich zur aktuellen Skalierung angewandt. Wurde
* die Form zuvor um den Faktor 0.5 verkleinert und wird dann um 1.5
* vergrößert, dann ist die Form im Anschluss ein Drittel kleiner als zu
* Beginn ({@code 0.5 * 1.5 = 0.75}).
*
* @param factor Der Skalierungsfaktor.
*/
public void scaleBy( double factor ) {
scale(scale * factor);
}
/**
* Liefert den aktuellen Ankerpunkt der Form.
*
* @return Der Ankerpunkt.
*/
public Options.Direction getAnchor() {
return anchor;
}
/**
* Setzt den Ankerpunkt der Form auf die angegebene Richtung.
* <p>
* Jede Form hat einen Ankerpunkt, von dem aus sie gezeichnet wird. Jede
* {@link schule.ngb.zm.Options.Direction Richtung} beschreibt einen der
* Neun Ankerpunkte:
* <pre>
* NW────N────NE
* │ │
* │ │
* W C E
* │ │
* │ │
* SW────S────SE
* </pre>
* <p>
* Für den Ankerpunkt {@link #CENTER} wird die Form also ausgehend von den
* Koordinaten {@link #x} und {@link #y} um die Hälfte der Breite nach links
* und rechts, sowie um die Hälfte der Höhe nach oben und unten gezeichnet.
* Fpr den Ankerpunkt {@link #NORTHWEST} dagegen um die gesamte Breite nach
* rechts und die Höhe nach unten.
* <pre>
* setAnchor(CENTER) │ setAnchor(NORTHWEST)
* ┌───────────┐ │
* │ │ │
* │ │ │
* │ (x,y) │ │ (x,y)─────────┐
* │ │ │ │ │
* │ │ │ │ │
* └───────────┘ │ │ │
* │ │ │
* │ │ │
* │ └───────────┘
* </pre>
* <p>
* Der Ankerpunkt der Form bestimmt bei Transformationen auch die Position
* des Drehzentrums und anderer relativer Koordinaten bezüglich der Form.
*
* @param anchor Der Ankerpunkt.
*/
public void setAnchor( Options.Direction anchor ) {
Validator.requireNotNull(anchor, "anchor");
this.anchor = anchor;
}
/**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkts basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p>
* Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt. Der Ankerpunkt {@link #NORTHWEST} hat daher immer das Ergebnis
* {@code (0,0)} und {@link #SOUTHEAST} {@code (width, height)}.
* <pre>
* (0,0)───(w/2,0)───(w,0)
* │ │
* │ │
* │ │
* (0,h/2) (w/2,h/2) (w,h/2)
* │ │
* │ │
* │ │
* (0,h)───(w/2,h)───(w,h)
* </pre>
*
* @param width Breite des umschließenden Rechtecks.
* @param height Höhe des umschließenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten.
*/
public static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5;
return new Point2D.Double(
wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y
);
}
/**
* Bestimmt die Koordinaten des angegebenen Ankers der Form relativ zum
* aktuellen {@link #setAnchor(Options.Direction) Ankerpunkt}.
*
* @param anchor Die Richtung des Ankers.
* @return Der relative Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/
public Point2D.Double getAnchorPoint( Options.Direction anchor ) {
double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER
return new Point2D.Double(
wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y)
);
}
/**
* Ermittelt die absoluten Koordinaten des angegebenen
* {@link #setAnchor(Options.Direction) Ankers}.
* <p>
* Die absoluten Koordinaten werden bestimmt durch die Position der Form
* {@code (x,y)} plus die
* {@link #getAnchorPoint(Options.Direction) relativen Koordinaten} des
* Ankers.
*
* <b>Wichtig:</b> Die Berechnung berücksichtigt derzeit keine Rotationen
* und Transformationen der Form.
*
* @param anchor Die Richtung des Ankers.
* @return Der absolute Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/
public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) {
// TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen.
Point2D.Double ap = getAnchorPoint(anchor);
ap.x += getX();
ap.y += getY();
return ap;
}
/**
* Kopiert die Eigenschaften der angegebenen Form in diese.
* <p>
* Unterklassen überschreiben diese Methode, um weitere Eigenschaften zu
* kopieren (zum Beispiel den Radius eines Kreises). Überschreibende
* Methoden sollten immer mit dem Aufruf {@code super.copyFrom(shape)} die
* Basiseigenschaften kopieren.
* <p>
* Die Methode kopiert so viele Eigenschaften wie möglich von der
* angegebenen Form in diese. Wenn die andere Form einen anderen Typ hat,
* dann werden trotzdem die Basiseigenschaften (Konturlinie, Füllung,
* Position, Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese
* Form kopiert. Soweit sinnvoll übernehmen implementierende Unterklassen
* auch andere Werte. Eine {@link Ellipse} kopiert beispielsweise auch die
* Breite und Höhe eines {@link Rectangle}.
* <p>
* Wird {@code null} übergeben, dann passiert nichts.
*
* @param shape Die Originalform, von der kopiert wird.
*/
public void copyFrom( Shape shape ) {
if( shape != null ) {
moveTo(shape.x, shape.y);
setFillColor(shape.getFillColor());
setStrokeColor(shape.getStrokeColor());
setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType());
setStrokeJoin(shape.getStrokeJoin());
visible = shape.isVisible();
rotation = shape.getRotation();
scale(shape.getScale());
setAnchor(shape.getAnchor());
}
}
/**
* Erzeugt eine Kopie dieser Form mit denselben Eigenschaften.
* <p>
* Unterklassen implementieren diese Methode mit dem genauen Typ der
* Unterklasse. In {@link Rectangle} sieht die Umsetzung beispielsweise so
* aus:
* <pre><code>
* public Rectangle copy() {
* return new Rectangle(this);
* }
* </code></pre>
* <p>
* Die Methode kann beliebig umgesetzt werden, um eine 1-zu-1-Kopie dieser
* Form zu erhalten. In der Regel besitzt aber jede Form einen Konstruktor,
* der alle Werte einer andern Form übernimmt. Die gezeigte Implementierung
* ist daher im Regelfall ausreichend.
*
* @return Eine genaue Kopie dieser Form.
*/
public abstract Shape copy();
/**
* Gibt eine {@link java.awt.Shape Java-AWT Shape} Version dieser Form
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen.
* <p>
* Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, liefert die
* Methode {@code null}.
*
* @return Eine Java-AWT {@code Shape} die diese Form repräsentiert oder
* {@code null}.
*/
public abstract java.awt.Shape getShape();
/**
* Gibt die Begrenzungen der Form zurück.
* <p>
* Ein {@code Bounds}-Objekt beschreibt eine "<a
* href="https://gdbooks.gitbooks.io/3dcollisions/content/Chapter1/aabb.html">Axis
* Aligned Bounding Box</a>" (AABB).
*
* @return Die Abgrenzungen der Form nach Anwendung der Transformationen.
*/
public Bounds getBounds() {
return new Bounds(this);
}
/**
* Verschiebt die Form um die angegebenen Werte entlang der
* Koordinatenachsen.
*
* @param dx Verschiebung entlang der x-Achse.
* @param dy Verschiebung entlang der y-Achse.
*/
public void move( double dx, double dy ) {
x += dx;
y += dy;
}
/**
* Bewegt die Form an die angegebenen Koordinaten.
*
* @param x Die neue x-Koordinate.
* @param y Die neue y-Koordinate.
*/
public void moveTo( double x, double y ) {
this.x = x;
this.y = y;
}
/**
* Bewegt die Form an dieselben Koordinaten wie die angegebene Form.
*
* @param shape Eine andere Form.
*/
public void moveTo( Shape shape ) {
moveTo(shape.getX(), shape.getY());
}
/**
* Bewegt die Form zum angegebenen Ankerpunkt der angegebenen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #moveTo(Shape, Options.Direction, double)
*/
public void moveTo( Shape shape, Options.Direction dir ) {
moveTo(shape, dir, 0.0);
}
/**
* Bewegt den Ankerpunkt dieser Form zu einem Ankerpunkt einer anderen
* Form.
* <p>
* Mit {@code buff} wird ein zusätzlicher Abstand angegeben, um den die Form
* entlang des Ankerpunktes {@code anchor} verschoben wird.
* <p>
* Ist der Anker zum Beispiel {@code NORTH}, dann wird die Form um
* {@code buff} oberhalb der oberen Kante der zweiten Form verschoben.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────────┐
* │ │
* W B │
* ┌─────┐ │ │
* │ │ └─────────┘
* W A │
* │ │
* └─────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.moveTo(A, DOWN, 0)} in diese Ausrichtung:
* <pre>
* B.moveTo(A, WEST, 0) │ B.moveTo(A, WEST, 10)
* │
* ┌─────┬───┐ │ ┌┬────┬────┐
* │ │ │ │ ││ │ │
* │ A B│ │ │ ││ A B │
* │ │ │ │ ││ │ │
* └─────┴───┘ │ └┴────┴────┘
* </pre>
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @param buff Der Abstand zum angegebenen Ankerpunkt.
*/
public void moveTo( Shape shape, Options.Direction dir, double buff ) {
Point2D ap = shape.getAbsAnchorPoint(dir);
this.x = ap.getX() + dir.x * buff;
this.y = ap.getY() + dir.y * buff;
}
/**
* Bewegt die Form an den Rand der Zeichenfläche in der angegebenen
* Richtung.
*
* @param dir Die Richtung.
*/
public void alignTo( Options.Direction dir ) {
alignTo(dir, 0.0);
}
/**
* Bewegt die Form mit dem angegebenen Abstand an den Rand der Zeichenfläche
* in der angegebenen Richtung aus.
*
* @param dir Die Richtung.
* @param buff Der Abstand zum Rand.
*/
public void alignTo( Options.Direction dir, double buff ) {
Point2D anchorShape = Shape.getAnchorPoint(canvasWidth, canvasHeight, dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
/**
* Bewegt den Ankerpunkt dieser Form in der angegebenen Richtung zum
* Gleichen Ankerpunkt der anderen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #alignTo(Shape, Options.Direction, double)
*/
public void alignTo( Shape shape, Options.Direction dir ) {
alignTo(shape, dir, 0.0);
}
/**
* Richtet die Form entlang der angegebenen Richtung an einer anderen Form
* aus.
* <p>
* {@code buff} gibt einen Abstand ab, um den diese From versetzt
* ausgerichtet wird.
* <p>
* Für {@link #DOWN} wird beispielsweise die y-Koordinate der unteren Kante
* dieser Form an der unteren Kante von {@code shape} ausgerichtet. Die
* x-Koordinate wird in dem Fall nicht verändert.
* <p>
* Befinden sich die Formen beispielsweise in folgender Position:
* <pre>
* ┌─────┐
* │ │
* │ B │
* ┌─────┐ │ │
* │ │ └──D──┘
* │ A │
* │ │
* └──D──┘
* </pre>
* <p>
* <p>
* werden sie durch {@code alignTo} so positioniert:
* <pre>
* B.alignTo(A, EAST, 0) │ B.alignTo(A, EAST, 10)
* │
* ┌─────┐ ┌─────┐ │ ┌─────┐
* │ │ │ │ │ │ │ ┌─────┐
* │ A │ │ B │ │ │ A │ │ │
* │ │ │ │ │ │ │ │ B │
* └──D──┘ └──D──┘ │ └──D──┘ │ │
* │ └──D──┘
* │
* </pre>
*
* @param shape Die andere Form.
* @param dir Die Richtung.
* @param buff Der Abstand.
*/
public void alignTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
/**
* @param shape
* @param dir
* @see #nextTo(Shape, Options.Direction, double)
*/
public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, DEFAULT_BUFFER);
}
/**
* Bewegt die Form neben eine andere in Richtung des angegebenen
* Ankerpunktes.
* <p>
* Im Gegensatz zu {@link #moveTo(Shape, Options.Direction, double)} wird
* die Breite bzw. Höhe der Formen berücksichtigt und die Formen so
* platziert, dass keine Überlappungen vorhanden sind.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────┐
* │ │
* W B │
* ┌──────┐ │ │
* │ │ └─────┘
* │ A E
* │ │
* └──────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.nextTo(A, EAST, 0)} in diese Ausrichtung:
* <pre>
* B.nextTo(A, EAST, 0) │ B.nextTo(A, EAST, 10)
* │
* ┌─────┬─────┐ │ ┌─────┐ ┌─────┐
* │ │ │ │ │ │ │ │
* │ A │ B │ │ │ A │ │ B │
* │ │ │ │ │ │ │ │
* └─────┴─────┘ │ └─────┘ └─────┘
* │
* </pre>
*
* @param shape
* @param dir
* @param buff
*/
public void nextTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir.inverse());
this.x += (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
/*public void shear( double dx, double dy ) {
verzerrung.shear(dx, dy);
}*/
public AffineTransform getTransform() {
// Point2D.Double anchorPoint = getAnchorPoint();
Point2D.Double basePoint = getAnchorPoint(Options.Direction.NORTHWEST);
AffineTransform transform = new AffineTransform();
transform.translate(x, y);
transform.rotate(Math.toRadians(rotation));
//transform.scale(scale, scale);
//transform.translate(-anchorPoint.x, -anchorPoint.y);
transform.translate(basePoint.x, basePoint.y);
return transform;
}
/**
* Zeichnet die Form.
*
* @param graphics
*/
@Override
public final void draw( Graphics2D graphics ) {
draw(graphics, getTransform());
}
/**
* Zeichnet die Form, aber wendet zuvor noch eine zusätzliche
* Transformations- matrix an. Wird u.A. von der {@link ShapeGroup}
* verwendet.
*
* @param graphics
* @param transform
*/
public void draw( Graphics2D graphics, AffineTransform transform ) {
if( !visible ) {
return;
}
java.awt.Shape shape = getShape();
if( shape != null ) {
if( transform != null ) {
shape = transform.createTransformedShape(shape);
}
java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
}
}
/**
* Vergleicht die Form mit einem anderen Objekt. Handelt es sich bei dem
* Objekt um eine andere Form, werden Position, Drehwinkel und Skalierung
* verglichen. Unterklassen überschreiben die Methode, um weitere
* Eigenschaften zu berücksichtigen.
* <p>
* Die Eigenschaften, die durch {@link Fillable} und {@link Strokeable}
* impliziert werden, werden nicht verglichen.
*
* @param o Ein anderes Objekt.
* @return
*/
@Override
public boolean equals( Object o ) {
if( this == o ) return true;
if( o == null || getClass() != o.getClass() ) return false;
Shape pShape = (Shape) o;
return Double.compare(pShape.x, x) == 0 &&
Double.compare(pShape.y, y) == 0 &&
Double.compare(pShape.rotation, rotation) == 0 &&
Double.compare(pShape.scale, scale) == 0;
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit den aktuellen
* Kontureigenschaften auf den Grafik-Kontext zu zeichnen. Die Methode
* verändert gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt
* sie nicht auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void strokeShape( java.awt.Shape shape, Graphics2D graphics ) {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(getStroke());
graphics.draw(shape);
}
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit der aktuellen
* Füllung auf den Grafik-Kontext zu zeichnen. Die Methode verändert
* gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt sie nicht
* auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void fillShape( java.awt.Shape shape, Graphics2D graphics ) {
if( fill != null ) {
graphics.setPaint(fill);
graphics.fill(shape);
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fill(shape);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,314 +0,0 @@
package schule.ngb.zm.util;
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 Objekte abgerufen werden.
* <p>
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein
* {@code Counter} auch das Maximum, das Minimum, die Summe und den Durchschnitt
* der gezählten Werte.
* <p>
* Ein Zähler kann auch komplette Arrays oder Listen von Zahlen zählen und die
* obigen Statistiken auswerten.
*/
@SuppressWarnings( "unused" )
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);
}
/**
* 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;
/**
* Erstellt einen neuen, leeren {@code Counter}.
*/
public Counter() {
}
/**
* 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);
}
/**
* @return Die aktuelle Anzahl.
*/
public int getCount() {
return count.get();
}
/**
* @return Das Maxium der bisher gezählten Werte.
*/
public double getMax() {
synchronized( count ) {
return max;
}
}
/**
* @return Das Minimum der bisher gezählten Werte.
*/
public double getMin() {
synchronized( count ) {
return min;
}
}
/**
* @return Die Summe der bisher gezählten Werte.
*/
public double getSum() {
synchronized( count ) {
return sum;
}
}
/**
* @return Der Mittelwert der bisher gezählten Werte.
*/
public double getAvg() {
if( Double.isNaN(sum) ) {
return Double.NaN;
} else {
return sum / count.doubleValue();
}
}
/**
* Setzt den Zähler auf den angegebenen Wert.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @param count Der neue Wert des Zählers.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter setCount( int count ) {
this.count.set(count);
return this;
}
/**
* Setzt den Zähler auf Null.
* <p>
* Die Statistiken werden auf {@link Double#NaN} gesetzt.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter reset() {
count.set(0);
synchronized( count ) {
min = Double.NaN;
max = Double.NaN;
sum = Double.NaN;
}
return this;
}
/**
* Erhöht den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter inc() {
this.count.incrementAndGet();
return this;
}
/**
* Verringert den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter dec() {
this.count.decrementAndGet();
return this;
}
/**
* Zählt den angegebenen Wert.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param value Der neue Wert.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( double value ) {
inc();
// Update stats
synchronized( count ) {
sum = Double.isNaN(sum) ? value : sum + value;
if( Double.isNaN(max) || max < value )
max = value;
if( Double.isNaN(min) || min > value )
min = value;
}
return this;
}
/**
* Zählt die angegebene Zahl.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param num Die neue Zahl.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Number num ) {
return count(num.doubleValue());
}
/**
* Zählt das angegebenen Objekt.
* <p>
* Erhöht den Zähler um Eins.
*
* @param obj Ein beliebiges Objekt.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Object obj ) {
if( obj instanceof Number ) {
return count((Number) obj);
} else {
inc();
return this;
}
}
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) {
for( double value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( int[] values ) {
for( double value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Zahlen im angegebenen Array.
*
* @param values Das Array der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) {
for( Number value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Zahlen in der angegebenen Sammlung.
*
* @param values Die Sammlung der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) {
for( Number value : values ) {
count(value);
}
return this;
}
}

View File

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

View File

@@ -1,367 +0,0 @@
package schule.ngb.zm.util;
import java.util.Random;
/**
* Generator für Perlin-Noise.
* <p>
* Die Implementierung basiert auf dem von Ken Perlin entwickelten Algorithmus
* und wurde anhand der <a
* 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;
private static final int M = N - 1;
/**
* Interne Permutationstabelle für diesen Generator.
*/
private int[] p;
private double octaves = 1, persistence = .5;
private double frequency = 1;
private double amplitude = 1;
private int repeat = -1;
private double rangeMin = 0.0, rangeMax = 1.0;
public Noise() {
this(null);
}
public Noise( long seed ) {
init(new Random(seed));
}
/**
* Initialisiert diesen Perlin-Noise mit dem angegebenen Zufallsgenerator.
*
* @param rand Ein Zufallsgenerator-Objekt.
*/
public Noise( Random rand ) {
init(rand);
}
public double getOctaves() {
return octaves;
}
public void setOctaves( double pOctaves ) {
this.octaves = pOctaves;
}
public double getPersistence() {
return persistence;
}
public void setPersistence( double pPersistence ) {
this.persistence = pPersistence;
}
public double getFrequency() {
return frequency;
}
public void setFrequency( double pFrequency ) {
this.frequency = pFrequency;
}
public double getAmplitude() {
return amplitude;
}
public void setAmplitude( double pAmplitude ) {
this.amplitude = pAmplitude;
}
public void setRange( double pRangeMin, double pRangeMax ) {
this.rangeMin = pRangeMin;
this.rangeMax = pRangeMax;
}
public double getRangeMin() {
return rangeMin;
}
public double getRangeMax() {
return rangeMax;
}
public int getRepeat() {
return repeat;
}
public void setRepeat( int pRepeat ) {
this.repeat = pRepeat;
}
public double noise( double x ) {
double total = 0;
double freq = this.frequency;
double amp = this.amplitude;
double maxValue = 0;
for( int i = 0; i < octaves; i++ ) {
total += perlin(x * freq) * amp;
maxValue += amp;
amp *= persistence;
freq *= 2;
}
return lerp(rangeMin, rangeMax, (total / maxValue));
}
public double noise( double x, double y ) {
double total = 0;
double freq = this.frequency;
double amp = this.amplitude;
double maxValue = 0;
for( int i = 0; i < octaves; i++ ) {
total += perlin(x * freq, y * freq) * amp;
maxValue += amp;
amp *= persistence;
freq *= 2;
}
return lerp(rangeMin, rangeMax, (total / maxValue));
}
public double noise( double x, double y, double z ) {
double total = 0;
double freq = this.frequency;
double amp = this.amplitude;
double maxValue = 0;
for( int i = 0; i < octaves; i++ ) {
total += perlin(x * freq, y * freq, z * freq) * amp;
maxValue += amp;
amp *= persistence;
freq *= 2;
}
return lerp(rangeMin, rangeMax, (total / maxValue));
}
private double perlin( double x ) {
// @formatter:off
if( repeat > 0 ) {
x %= repeat;
}
int xi = (int)x & M;
double xf = x - (int)x;
double u = fade(xf);
int a, b;
a = p[ xi ];
b = p[inc(xi)];
return (lerp(grad(a,xf), grad(b,xf-1), u) + 1) / 2;
// @formatter:on
}
private double perlin( double x, double y ) {
// @formatter:off
if( repeat > 0 ) {
x %= repeat;
y %= repeat;
}
int xi = (int) x & M;
int yi = (int) y & M;
double xf = x - (int) x;
double yf = y - (int) y;
double u = fade(xf);
double v = fade(yf);
int aa, ab, ba, bb;
aa = p[p[ xi ] + yi ];
ab = p[p[ xi ] + inc(yi)];
ba = p[p[inc(xi)] + yi ];
bb = p[p[inc(xi)] + inc(yi)];
double x1, x2;
x1 = lerp(
grad(aa, xf , yf),
grad(ba, xf-1, yf),
u);
x2 = lerp(
grad(ab, xf , yf-1),
grad(bb, xf-1, yf-1),
u);
return (lerp(x1, x2, v) + 1) / 2;
// @formatter:on
}
private double perlin( double x, double y, double z ) {
// @formatter:off
if( repeat > 0 ) {
x %= repeat;
y %= repeat;
z %= repeat;
}
int xi = (int)x & M;
int yi = (int)y & M;
int zi = (int)z & M;
double xf = x - (int)x;
double yf = y - (int)y;
double zf = z - (int)z;
double u = fade(xf);
double v = fade(yf);
double w = fade(zf);
int aaa, aba, aab, abb, baa, bba, bab, bbb;
aaa = p[p[p[ xi ] + yi ] + zi ];
aba = p[p[p[ xi ] + inc(yi)] + zi ];
aab = p[p[p[ xi ] + yi ] + inc(zi)];
abb = p[p[p[ xi ] + inc(yi)] + inc(zi)];
baa = p[p[p[inc(xi)] + yi ] + zi ];
bba = p[p[p[inc(xi)] + inc(yi)] + zi ];
bab = p[p[p[inc(xi)] + yi ] + inc(zi)];
bbb = p[p[p[inc(xi)] + inc(yi)] + inc(zi)];
double x1, x2, y1, y2;
x1 = lerp(
grad(aaa, xf , yf, zf),
grad(baa, xf-1, yf, zf),
u);
x2 = lerp(
grad(aba, xf , yf-1, zf),
grad(bba, xf-1, yf-1, zf),
u);
y1 = lerp(x1, x2, v);
x1 = lerp(
grad(aab, xf , yf, zf-1),
grad(bab, xf-1, yf, zf-1),
u);
x2 = lerp(
grad(abb, xf , yf-1, zf-1),
grad(bbb, xf-1, yf-1, zf-1),
u);
y2 = lerp(x1, x2, v);
return (lerp(y1, y2, w) + 1) / 2;
// @formatter:on
}
public void init( Random rand ) {
p = new int[N * 2];
if( rand == null ) {
System.arraycopy(PERLIN_PERMUTATION, 0, p, 0, N);
} else {
// Generate random permutation
for( int i = 0; i < N; i++ ) {
int n = rand.nextInt(N);
if( p[n] == 0 )
p[n] = i;
else
i--;
}
}
// Duplicate permutation array to prevent overflow errors
System.arraycopy(p, 0, p, N, N);
}
private double fade( double t ) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
private double lerp( double a, double b, double t ) {
return a + t * (b - a);
}
private int inc( int i ) {
++i;
if( repeat > 0 )
i = i % repeat;
return i;
}
private double grad( int hash, double x ) {
switch( hash & 0x1 ) {
// @formatter:off
case 0x0: return x;
case 0x1: return -x;
default: return 0;
// @formatter:on
}
}
private double grad( int hash, double x, double y ) {
switch( hash & 0x3 ) {
// @formatter:off
case 0x0: return x;
case 0x1: return -x;
case 0x2: return y;
case 0x3: return -y;
default: return 0;
// @formatter:on
}
}
private double grad( int hash, double x, double y, double z ) {
switch( hash & 0xF ) {
// @formatter:off
case 0x0: return x + y;
case 0x1: return -x + y;
case 0x2: return x - y;
case 0x3: return -x - y;
case 0x4: return x + z;
case 0x5: return -x + z;
case 0x6: return x - z;
case 0x7: return -x - z;
case 0x8: return y + z;
case 0x9: return -y + z;
case 0xA: return y - z;
case 0xB: return -y - z;
case 0xC: return y + x;
case 0xD: return -y + z;
case 0xE: return y - x;
case 0xF: return -y - z;
default: return 0; // never happens
// @formatter:on
}
}
private static final int[] PERLIN_PERMUTATION = new int[]{
151, 160, 137, 91, 90, 15,
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
};
}

View File

@@ -1,193 +0,0 @@
package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit;
/**
* Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p>
* Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus
* gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn
* nach einem {@link #stop()} wieder {@link #start()} aufgerufen wird. Soll die
* Zeitmessung wieder bei null beginnen, muss vorher {@link #reset()} genutzt
* werden.
* <p>
* Die gemessene Zeit kann in {@link #getMillis() Millisekunden} oder
* {@link #getSeconds() Sekunden} abgerufen werden. Wird eine noch größere
* Genauigkeit benötigt, können mit {@link #getTime(TimeUnit)} beliebige
* Zeiteinheiten (zum Beispiel {@link TimeUnit#NANOSECONDS Nanosekunden})
* abgerufen werden.
* <p>
* Die Zeit kann auch bei laufender Uhr abgefragt werden. In dem Fall wird die
* bis zu diesem Zeitpunkt gemessene Zeit zurückgegeben.
*/
@SuppressWarnings( "unused" )
public final class Timer {
/**
* Die Basiseinheit für die Zeitmessung.
*/
private final TimeUnit baseUnit;
/**
* Ob die Zeitmessung gerade läuft.
*/
private boolean running = false;
/**
* 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() {
start = System.nanoTime();
running = true;
return this;
}
/**
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Dauer wird
* zur Summe aller gemessenen Zeiten hinzuaddiert.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer stop() {
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, stoppt sie nicht,
* sondern startet vom Zeitpunkt des Aufrufs neu.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer reset() {
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() - 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() - 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() - start + elapsed) / 1000000000.0;
} else {
return elapsed / 1000000000.0;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,234 +0,0 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;
/**
* Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
*/
@SuppressWarnings( "unused" )
public final class FileLoader {
/**
* Charset: ASCII
*/
public static final Charset ASCII = StandardCharsets.US_ASCII;
/**
* Charset: UTF-8
*/
public static final Charset UTF8 = StandardCharsets.UTF_8;
/**
* Charset: UTF-16
*/
public static final Charset UTF16 = StandardCharsets.UTF_16;
/**
* Charset: ISO-8859-1
*/
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
/**
* Lädt die angegebene Textdatei zeilenweise in eine Liste.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Die Quelle der Textdatei.
* @return Eine Liste mit den Zeilen der Textdatei.
* @see #loadLines(String, Charset)
*/
public static List<String> loadLines( String source ) {
return loadLines(source, UTF8);
}
/**
* Lädt die angegebene Textdatei mit dem angegebenen Charset zeilenweise in
* eine Liste.
* <p>
* Am Ende jeder Zeile wird das Symbol für einen Zeilenumbruch ({@code \n})
* entfernt.
*
* @param source Die Quelle der Textdatei.
* @param charset Das zu verwendene {@code Charset}.
* @return Eine Liste mit den Zeilen der Textdatei.
*/
public static List<String> loadLines( String source, Charset charset ) {
Validator.requireNotNull(source, "source");
Validator.requireNotNull(charset, "charset");
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
List<String> result = new ArrayList<>();
String line;
while( (line = reader.readLine()) != null ) {
result.add(line);
}
return result;
} catch( MalformedURLException muex ) {
LOG.warn("Could not find resource for <%s>", source);
return Collections.emptyList();
} catch( IOException ex ) {
LOG.warn(ex, "Error while loading lines from source <%s>", source);
return Collections.emptyList();
}
}
/**
* Lädt den Inhalt der angegebenen Textdatei vollständig in einen String.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source ) {
return loadText(source, UTF8);
}
/**
* Lädt den Inhalt der angegebenen Textdatei mit dem angegebenen
* {@code Charset} vollständig in einen String.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @param charset Der {@code Charset} der Textdatei.
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source, Charset charset ) {
Validator.requireNotNull(source, "source");
Validator.requireNotNull(charset, "charset");
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
StringBuilder result = new StringBuilder();
String line;
while( (line = reader.readLine()) != null ) {
result.append(line).append('\n');
}
return result.toString();
} catch( MalformedURLException muex ) {
LOG.warn("Could not find resource for <%s>", source);
return "";
} catch( IOException ex ) {
LOG.warn(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
* <pre><code>
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
* </code></pre>
*
* @param source Eine Quelle für die CSV-Datei (Absoluter Dateipfad,
* * Dateipfad im Classpath oder Netzwerkressource)
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @return Ein Array mit den Daten als {@code String}s.
* @see #loadCsv(String, char, boolean, Charset)
*/
public static String[][] loadCsv( String source, boolean skipFirst ) {
return loadCsv(source, ',', skipFirst, UTF8);
}
/**
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
* String-Array.
* <p>
* Die Methode ist nicht in der Lage, komplexe CSV-Dateien zu verarbeiten.
* Insbesondere werden Inhalte, die das Trennzeichen {@code separator}
* enthalten, nicht korrekt erkannt. Das Trennzeichen wird unabhängig
* vom Kontext immer als Zelltrenner erkannt. (Im Normalfall kann das
* Trennzeichen durch die Verwendung doppelter Anführungszeichen in der Art
* {@code Inhalt,"Inhalt, der Komma enthält",Inhalt} maskiert werden.)
* <p>
* Es wird auch keine erweiterte Inhaltserkennung ausgeführt, sondern alle
* Inhalte als {@code String} gelesen. Die weitere Verarbeitung mit den
* passenden Parser-Methoden (beispielsweise
* {@link Double#parseDouble(String)}) obligt dem Nutzer.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static String[][] loadCsv( String source, char separator, boolean skipFirst, Charset charset ) {
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, String separator, boolean skipFirst ) {
return loadValues(source, separator, skipFirst, UTF8);
}
/**
* Lädt Double-Werte aus einer Text-Datei in ein zweidimensionales Array.
* <p>
* Die Zeilen der Eingabedatei werden anhand der Zeichenkette {@code separator}
* in einzelne Teile aufgetrennt. {@code separator} wird als regulärer Ausdruck
* interpretiert (siehe {@link String#split(String)}).
* <p>
* Jeder Teilstring wird mit {@link Double#parseDouble(String)} in
* {@code double} umgeformt. Es liegt in der Verantwortung des Nutzers,
* sicherzustellen, dass die Eingabedatei nur Zahlen enthält, die korrekt
* in {@code double} umgewandelt werden können. Zellen, für die die
* Umwandlung fehlschlägt, werden mit 0.0 befüllt.
* <p>
* Die Methode unterliegt denselben Einschränkungen wie
* {@link #loadCsv(String, char, boolean, Charset)}.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Ein Trennzeichen oder ein regulärer Ausdruck.
* @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, String separator, boolean skipFirst, Charset charset ) {
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)))
.stream(line.split(separator))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).toArray(double[][]::new);
}
private FileLoader() {
}
private static final Log LOG = Log.getLogger(FileLoader.class);
}

View File

@@ -1,144 +0,0 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Image;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Eine Hilfsklasse mit Klassenmethoden, um Schriftarten zu laden.
* <p>
* Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten,
* die aus Dateien geladen wurden, werden in einem internen Cache gespeichert
* und nachfolgende Versuche, dieselbe Schriftart zu laden, werden aus dem Cache
* bedient.
*/
public class FontLoader {
private static final Cache<String, Font> fontCache = Cache.newSoftCache();
/**
* 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 source
* @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 = source.substring(lastIndex + 1);
}
// Dateiendung entfernen
lastIndex = name.lastIndexOf('.');
if( lastIndex > -1 ) {
name = name.substring(0, lastIndex);
}
return loadFont(name, source);
}
public static Font loadFont( String name, String source ) {
Validator.requireNotNull(source, "source");
Validator.requireNotEmpty(source, "source");
if( fontCache.containsKey(name) ) {
LOG.trace("Retrieved font <%s> from font cache.", name);
return fontCache.get(name);
}
// Look for System fonts
Font font = Font.decode(source);
if( source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(name, font);
fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source);
return font;
} else {
font = null;
}
// Load userfonts
try( InputStream in = ResourceStreamProvider.getInputStream(source) ) {
font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(Font.PLAIN);
if( font != null ) {
fontCache.put(name, font);
fontCache.put(source, font);
//ge.registerFont(font);
}
LOG.debug("Loaded custom font from source <%s>.", source);
} catch( MalformedURLException muex ) {
LOG.warn("Could not find font resource for <%s>", source);
} catch( IOException ioex ) {
LOG.warn(ioex, "Error loading custom font file from source <%s>.", source);
} catch( FontFormatException ffex ) {
LOG.warn(ffex, "Error creating custom font from source <%s>.", source);
}
return font;
}
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Methode kann eine Liste von Schriften bekommen und probiert diese
* nacheinander zu laden. Die erste Schrift, die Fehlerfrei geladen werden
* kann, wird zurückgegeben. Kann keine der Schriften geladen werden, ist
* das Ergebnis {@code null}.
* <p>
* Die gefundene Schrift wird unter ihrem Dateinamen in den
* Schriftenspeicher geladen und kann danach in der Zeichenmaschine benutzt
* werden.
* <p>
* Eine Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden.
*
* @param name
* @param sources
* @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/
public static Font loadFonts( String name, String... sources ) {
for( String fontSource : sources ) {
// TODO: Ignore exceptions in this case and throw own at end?
Font font = loadFont(name, fontSource);
if( font != null ) {
return font;
}
}
return null;
}
private FontLoader() {
}
private static final Log LOG = Log.getLogger(FontLoader.class);
}

View File

@@ -1,213 +0,0 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.Zeichenmaschine;
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;
/**
* Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
*/
@SuppressWarnings("unused")
public class ResourceStreamProvider {
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Ressource 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>Ansonsten erstelle ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* <p>
* Auftretende Exceptions werden als {@link IOException} geworfen.
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link java.net.MalformedURLException} beim Konstruieren des {@code URL}
* zu einer Datei verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Ein {@code InputStream} für die Ressource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim Erzeugen einer URL zu
* einer bestehenden Ressource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "source");
Validator.requireNotEmpty(source, "source");
// Ist source ein valider Dateipfad?
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
// Ist source im Classpath vorhanden?
URL url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
// Dann versuchen aus source direkt eine URL zu machen.
return new URL(source);
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
* ab, welche Art an Ressource gefunden wird:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* {@code new File(source).isFile() == true}, dann wird ein
* {@link FileInputStream} erstellt.</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt, wird ein passender
* Stream mit {@link Class#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} im Classpath enthalten, wird ein passender Stream
* mit {@link ClassLoader#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} eine gültige {@link URL}, dann wird ein
* {@link URL#openStream() Netwerkstream} geöffnet.</li>
* </ol>
* <p>
* Die Möglichen Ressourcen werden in der gelisteten Reihenfolge durchprobiert
* und der erste gefundene Stream zurückgegeben. Auftretende Exceptions
* werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source}
* keine gültige Ressource gefunden werden kann, wird am Ende die
* von {@link URL#openStream()} erzeugte {@code IOException} geworfen.
* <p>
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link SecurityException} beim Öffnen des {@code FileInputStream}
* (zum Beispiel, weil auf eine existierende Datei keine Leserechte bestehen)
* verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code InputStream} für die Ressource.
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim Öffnen des Streams zu
* einer bestehenden Ressource oder falls
* keine passende Ressource gefunden
* wurde.
*/
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
return getResourceURL(source).openStream();
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* passenden {@link OutputStream}.
* <p>
* Es wird davon ausgegangen, dass die Quelle auf eine Datei verweist, auf
* die Schreibrechte bestehen. Ist dies nicht der Fall, wird eine
* {@link IOException} geworfen.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath).
* @return {@code OutputStream} für die Ressource.
* @throws IOException Falls die Quelle kein gültiges Ziel beschreibt.
*/
public static OutputStream getOutputStream( String source ) throws IOException {
try {
return Files.newOutputStream(Path.of(getResourceURL(source).toURI()));
} catch( URISyntaxException ex ) {
throw new IOException(ex);
}
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* {@link BufferedReader}.
* <p>
* Die Suche erfolgt, wie bei {@link #getInputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source ) throws IOException {
return getReader(source, StandardCharsets.UTF_8);
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* {@link BufferedReader} mit dem angegebenen Charset.
* <p>
* Die Suche erfolgt wie bei {@link #getInputStream(String)} beschrieben.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @param charset Der zu verwendende Charset.
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source, Charset charset ) throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(source), charset.newDecoder()));
}
/**
* Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source ) throws IOException {
return getWriter(source, StandardCharsets.UTF_8);
}
/**
* Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @param charset Der zu verwendende Charset.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source, Charset charset ) throws IOException {
return new BufferedWriter(new OutputStreamWriter(getOutputStream(source), charset.newEncoder()));
}
// Privater C'tor
private ResourceStreamProvider() {
}
private static final Log LOG = Log.getLogger(ResourceStreamProvider.class);
}

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