Compare commits
68 Commits
b76d533739
...
particels
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ab9701629 | ||
|
|
3196914564 | ||
|
|
a7747aea70 | ||
|
|
1d41bf36c5 | ||
|
|
595bdd7556 | ||
|
|
df10d38184 | ||
| fc3039b484 | |||
| 6d1d47fed0 | |||
| 3342db6f79 | |||
| 3f07b9ee7e | |||
| 4e7adf26f7 | |||
| 02085c83fc | |||
| ef248a8580 | |||
| 39014fe82e | |||
| e451a2f087 | |||
| 936043bf85 | |||
| d75f67c0fa | |||
| adbc29dabe | |||
| b687483e6d | |||
| 19bacd15e9 | |||
| 2d4abf6f0d | |||
| cefe7c8cfa | |||
| b04e68c7bd | |||
| 25ce3a35e9 | |||
| 44d0f79c6c | |||
| 43195aa63c | |||
| 6cc23de620 | |||
| 836571ca95 | |||
| 5232057b15 | |||
| f0a3c65552 | |||
| 2c40e1ba31 | |||
| 26e3593f2c | |||
| 13cad69e1d | |||
| bd718ba27d | |||
| 1895378978 | |||
| 2f845bdcd9 | |||
| 788ed888e9 | |||
| ce3ffee4da | |||
| 74c85e0f61 | |||
| c7b2a520c4 | |||
| c5c046521b | |||
| 9834b9c389 | |||
| eaaca6b90f | |||
| 1275af55f3 | |||
| 632038030e | |||
| cec17f0d7c | |||
| b29532bf6e | |||
| d500c130ed | |||
| b4d390cd9b | |||
| 97ea610f34 | |||
| e2e1f24e3e | |||
| a09b956b48 | |||
| c92a4517b3 | |||
| 1260a38bb7 | |||
| 20772da813 | |||
| 8898d6e8ee | |||
| cb0ee9c842 | |||
| 3cf7871591 | |||
| c7e1eb11ed | |||
| 9fc58b05b6 | |||
| 8de3c41b9b | |||
| 15e47ceaa8 | |||
| 9f28786ab6 | |||
| 18b5c50016 | |||
| 03945e029a | |||
| 90e043e5f8 | |||
| 559459aef6 | |||
| 8d0bd2bc99 |
1
.gitignore
vendored
@@ -34,6 +34,7 @@ hs_err_pid*
|
||||
Thumbs.db
|
||||
|
||||
.gradle
|
||||
local.properties
|
||||
**/build/
|
||||
!src/**/build/
|
||||
|
||||
|
||||
27
CHANGELOG.md
@@ -6,6 +6,33 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## 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.
|
||||
|
||||
121
build.gradle
@@ -1,10 +1,16 @@
|
||||
plugins {
|
||||
id 'idea'
|
||||
id 'java-library'
|
||||
id 'org.hidetake.ssh' version '2.10.1'
|
||||
}
|
||||
|
||||
group 'schule.ngb'
|
||||
version '0.0.32-SNAPSHOT'
|
||||
version '0.0.35-SNAPSHOT'
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.release = 11
|
||||
@@ -14,6 +20,15 @@ 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'
|
||||
@@ -28,15 +43,111 @@ dependencies {
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'Class-Path': '.'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('jarMP3SPI', Jar) {
|
||||
archiveClassifier = 'all'
|
||||
group "build"
|
||||
description "Build jar with MP3SPI included"
|
||||
|
||||
archiveClassifier = 'mp3spi'
|
||||
duplicatesStrategy = 'exclude'
|
||||
archivesBaseName = 'zeichenmaschine-mp3spi'
|
||||
// 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()
|
||||
}
|
||||
|
||||
634
docs/assets/quickstart/AblaufMoleActive.excalidraw
Normal file
@@ -0,0 +1,634 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 152,
|
||||
"versionNonce": 1288225375,
|
||||
"isDeleted": false,
|
||||
"id": "fxk8rHocjpTteICJMa6n8",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 619.9963325816416,
|
||||
"y": 186.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fab005",
|
||||
"width": 150,
|
||||
"height": 46,
|
||||
"seed": 1339263918,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "JmEBjNProPAgJQZUvMGGa"
|
||||
},
|
||||
{
|
||||
"id": "eryKwAzIMcBMQP0Ybl1Mm",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1670307372550,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 264,
|
||||
"versionNonce": 1267158257,
|
||||
"isDeleted": false,
|
||||
"id": "wZLPkORf755_2Vp0J6I0V",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 566.9963325816416,
|
||||
"y": 289.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fab005",
|
||||
"width": 248,
|
||||
"height": 50,
|
||||
"seed": 359801906,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "9TujIdwDvtinylO3z50y6"
|
||||
},
|
||||
{
|
||||
"id": "eryKwAzIMcBMQP0Ybl1Mm",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1670307379131,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 376,
|
||||
"versionNonce": 1807861489,
|
||||
"isDeleted": false,
|
||||
"id": "290mWFx31fA5smc5FqPUr",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 607.9963325816416,
|
||||
"y": 392.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"width": 143,
|
||||
"height": 50,
|
||||
"seed": 1948766318,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "_ISR-LCZm2Hu2G57R_uxN"
|
||||
},
|
||||
{
|
||||
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "hUUqTCXva-vZjnBeM-PR3",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1670307888001,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 518,
|
||||
"versionNonce": 2136415391,
|
||||
"isDeleted": false,
|
||||
"id": "kbEG_cCZadugfCPxYedhf",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 614.9963325816416,
|
||||
"y": 589.9758066195944,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"width": 131,
|
||||
"height": 50,
|
||||
"seed": 97599794,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "6KjVrNy_dxGXJtntdqrjY"
|
||||
},
|
||||
{
|
||||
"id": "hUUqTCXva-vZjnBeM-PR3",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "bplSSGA4kyy-Av7nKNK1B",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "qukSk_W6enSdwERPEPkhZ",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1670307888001,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 110,
|
||||
"versionNonce": 1350901746,
|
||||
"isDeleted": false,
|
||||
"id": "JmEBjNProPAgJQZUvMGGa",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 643.9963325816416,
|
||||
"y": 199.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 102,
|
||||
"height": 20,
|
||||
"seed": 1889147762,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1670164406970,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"text": "new Shapes()",
|
||||
"baseline": 14,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "fxk8rHocjpTteICJMa6n8",
|
||||
"originalText": "new Shapes()"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 245,
|
||||
"versionNonce": 1536101230,
|
||||
"isDeleted": false,
|
||||
"id": "9TujIdwDvtinylO3z50y6",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 584.9963325816416,
|
||||
"y": 304.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 212,
|
||||
"height": 20,
|
||||
"seed": 1297543150,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1670164433291,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"text": "super(800, 800, \"Shapes\")",
|
||||
"baseline": 14,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "wZLPkORf755_2Vp0J6I0V",
|
||||
"originalText": "super(800, 800, \"Shapes\")"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 362,
|
||||
"versionNonce": 2069822514,
|
||||
"isDeleted": false,
|
||||
"id": "_ISR-LCZm2Hu2G57R_uxN",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 650.4963325816416,
|
||||
"y": 407.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 58,
|
||||
"height": 20,
|
||||
"seed": 525219186,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1670164509204,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"text": "setup()",
|
||||
"baseline": 14,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "290mWFx31fA5smc5FqPUr",
|
||||
"originalText": "setup()"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 502,
|
||||
"versionNonce": 747611665,
|
||||
"isDeleted": false,
|
||||
"id": "6KjVrNy_dxGXJtntdqrjY",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 655.4963325816416,
|
||||
"y": 604.9758066195944,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 50,
|
||||
"height": 20,
|
||||
"seed": 1808016110,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [],
|
||||
"updated": 1670307245844,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"text": "draw()",
|
||||
"baseline": 14,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "kbEG_cCZadugfCPxYedhf",
|
||||
"originalText": "draw()"
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 31,
|
||||
"versionNonce": 101778865,
|
||||
"isDeleted": false,
|
||||
"id": "eryKwAzIMcBMQP0Ybl1Mm",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 694.9963325816416,
|
||||
"y": 242.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#12b886",
|
||||
"width": 0,
|
||||
"height": 38,
|
||||
"seed": 1130444978,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1670307364549,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "fxk8rHocjpTteICJMa6n8",
|
||||
"focus": 0,
|
||||
"gap": 10
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "wZLPkORf755_2Vp0J6I0V",
|
||||
"focus": 0.03225806451612903,
|
||||
"gap": 9
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
38
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 160,
|
||||
"versionNonce": 1672489439,
|
||||
"isDeleted": false,
|
||||
"id": "Zc9GOJ8DsIQYo4WaGvvHy",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 686.7253597961584,
|
||||
"y": 347.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#12b886",
|
||||
"width": 0.3256260532492661,
|
||||
"height": 36,
|
||||
"seed": 1138446962,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1670307364549,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "wZLPkORf755_2Vp0J6I0V",
|
||||
"gap": 8,
|
||||
"focus": 0.03595554587056003
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "290mWFx31fA5smc5FqPUr",
|
||||
"gap": 9,
|
||||
"focus": 0.09195903246894747
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-0.3256260532492661,
|
||||
36
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "arrow",
|
||||
"version": 616,
|
||||
"versionNonce": 447594705,
|
||||
"isDeleted": false,
|
||||
"id": "hUUqTCXva-vZjnBeM-PR3",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 684.6524492354905,
|
||||
"y": 452.97580661959455,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"width": 1.5815106831711319,
|
||||
"height": 29.000000000000114,
|
||||
"seed": 1169930546,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"boundElements": [],
|
||||
"updated": 1670307888001,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"startBinding": {
|
||||
"elementId": "290mWFx31fA5smc5FqPUr",
|
||||
"focus": -0.044553538542618336,
|
||||
"gap": 10
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
|
||||
"focus": 0.08947713014192164,
|
||||
"gap": 6.999999999999915
|
||||
},
|
||||
"lastCommittedPoint": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
1.5815106831711319,
|
||||
29.000000000000114
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "rectangle",
|
||||
"version": 560,
|
||||
"versionNonce": 11022527,
|
||||
"isDeleted": false,
|
||||
"id": "5cG3FjQlYuIGPZMsIMgJU",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 616.4963325816416,
|
||||
"y": 488.9758066195945,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"width": 131,
|
||||
"height": 50,
|
||||
"seed": 609895569,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "-Fl205ZvyGaxHAHt7C3r3"
|
||||
},
|
||||
{
|
||||
"id": "hUUqTCXva-vZjnBeM-PR3",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "bplSSGA4kyy-Av7nKNK1B",
|
||||
"type": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "qukSk_W6enSdwERPEPkhZ",
|
||||
"type": "arrow"
|
||||
}
|
||||
],
|
||||
"updated": 1670307888001,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"version": 551,
|
||||
"versionNonce": 685087263,
|
||||
"isDeleted": false,
|
||||
"id": "-Fl205ZvyGaxHAHt7C3r3",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"x": 647.4963325816416,
|
||||
"y": 503.97580661959455,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"width": 69,
|
||||
"height": 20,
|
||||
"seed": 415680767,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"boundElements": null,
|
||||
"updated": 1670307260196,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"text": "update()",
|
||||
"baseline": 14,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "5cG3FjQlYuIGPZMsIMgJU",
|
||||
"originalText": "update()"
|
||||
},
|
||||
{
|
||||
"id": "bplSSGA4kyy-Av7nKNK1B",
|
||||
"type": "arrow",
|
||||
"x": 683.9963325816416,
|
||||
"y": 546.9758066195944,
|
||||
"width": 1,
|
||||
"height": 34,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1937189809,
|
||||
"version": 33,
|
||||
"versionNonce": 1639939761,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670307888001,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-1,
|
||||
34
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
|
||||
"focus": -0.044849023090586096,
|
||||
"gap": 7.999999999999858
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "kbEG_cCZadugfCPxYedhf",
|
||||
"focus": 0.022646536412078155,
|
||||
"gap": 9
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
},
|
||||
{
|
||||
"id": "qukSk_W6enSdwERPEPkhZ",
|
||||
"type": "arrow",
|
||||
"x": 758.9963325816416,
|
||||
"y": 615.9758066195944,
|
||||
"width": 87,
|
||||
"height": 107.99999999999994,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 677436529,
|
||||
"version": 170,
|
||||
"versionNonce": 377207327,
|
||||
"isDeleted": false,
|
||||
"boundElements": [],
|
||||
"updated": 1670307315516,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
86,
|
||||
0
|
||||
],
|
||||
[
|
||||
83,
|
||||
-106.99999999999994
|
||||
],
|
||||
[
|
||||
-1,
|
||||
-107.99999999999994
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": {
|
||||
"elementId": "kbEG_cCZadugfCPxYedhf",
|
||||
"focus": 0.04,
|
||||
"gap": 13
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "5cG3FjQlYuIGPZMsIMgJU",
|
||||
"focus": -0.26783652736088875,
|
||||
"gap": 10.5
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/assets/quickstart/AblaufMoleActive.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
1030
docs/assets/quickstart/AblaufMoleStatic.excalidraw
Normal file
BIN
docs/assets/quickstart/AblaufMoleStatic.png
Normal file
|
After Width: | Height: | Size: 411 KiB |
375
docs/assets/quickstart/CircleMouseCollision.excalidraw
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "Mhp-wQ2wZxCI4BYWvTvV2",
|
||||
"type": "ellipse",
|
||||
"x": 420,
|
||||
"y": 233,
|
||||
"width": 385.99999999999994,
|
||||
"height": 385.99999999999994,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#ffdf22",
|
||||
"fillStyle": "cross-hatch",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 474299186,
|
||||
"version": 149,
|
||||
"versionNonce": 2072559726,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158627399,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "SqZRA75ACKHo0lB799wpS",
|
||||
"type": "line",
|
||||
"x": 605,
|
||||
"y": 426,
|
||||
"width": 173.02018127597637,
|
||||
"height": 99.89324823492274,
|
||||
"angle": 0,
|
||||
"strokeColor": "#087f5b",
|
||||
"backgroundColor": "#ffdf22",
|
||||
"fillStyle": "cross-hatch",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 484805038,
|
||||
"version": 286,
|
||||
"versionNonce": 1603007154,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670159044301,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
173.02018127597637,
|
||||
-99.89324823492274
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "7XVC5Wqiy62pN9Vc92bgl",
|
||||
"type": "text",
|
||||
"x": 645.7358370304399,
|
||||
"y": 356.14958623820286,
|
||||
"width": 86,
|
||||
"height": 20,
|
||||
"angle": 5.766085793504818,
|
||||
"strokeColor": "#087f5b",
|
||||
"backgroundColor": "#ffdf22",
|
||||
"fillStyle": "cross-hatch",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1883808110,
|
||||
"version": 228,
|
||||
"versionNonce": 627525998,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670159044301,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "moleRadius",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "moleRadius"
|
||||
},
|
||||
{
|
||||
"id": "zp9JMFpfOA6ZEWCCiqwW0",
|
||||
"type": "line",
|
||||
"x": 606,
|
||||
"y": 427,
|
||||
"width": 128.012747010704,
|
||||
"height": 208.02230726873202,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fa5252",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 224372978,
|
||||
"version": 81,
|
||||
"versionNonce": 563971630,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158772553,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-128.012747010704,
|
||||
-208.02230726873202
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "LIDFlKBUhkbZdzTUFT1qp",
|
||||
"type": "line",
|
||||
"x": 604.8240237554426,
|
||||
"y": 425.10629955596494,
|
||||
"width": 127.71248741178238,
|
||||
"height": 4.628286654564533,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fa5252",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 34525358,
|
||||
"version": 124,
|
||||
"versionNonce": 1131380590,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158787752,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
-127.71248741178238,
|
||||
-4.628286654564533
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "-Of3RfHAvJDq0qHIwjm2U",
|
||||
"type": "ellipse",
|
||||
"x": 471.96813247323996,
|
||||
"y": 413.60866976111646,
|
||||
"width": 12,
|
||||
"height": 12,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#4c6ef5",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1050702514,
|
||||
"version": 384,
|
||||
"versionNonce": 1957432558,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158970885,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "NB4WGe-lT7XqLrsKX8jN8",
|
||||
"type": "ellipse",
|
||||
"x": 472,
|
||||
"y": 212,
|
||||
"width": 12,
|
||||
"height": 12,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fa5252",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 2059751730,
|
||||
"version": 277,
|
||||
"versionNonce": 756441010,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158769939,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "T1vEWEHV-1FM4A7Q7dTzy",
|
||||
"type": "ellipse",
|
||||
"x": 601.6117120882632,
|
||||
"y": 422.28007605476614,
|
||||
"width": 6.436664554034337,
|
||||
"height": 6.436664554034337,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#000000",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1983845742,
|
||||
"version": 376,
|
||||
"versionNonce": 1202556466,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158984089,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "eEMIGJ2Ag5HqWZLPmjXMM",
|
||||
"type": "text",
|
||||
"x": 551.4695946462369,
|
||||
"y": 432.8553279772536,
|
||||
"width": 108,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#000000",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 817794290,
|
||||
"version": 97,
|
||||
"versionNonce": 1438641266,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158980419,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "(moleX, moleY)",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "(moleX, moleY)"
|
||||
},
|
||||
{
|
||||
"id": "6cMmrU3lcJbub8sEKnVWe",
|
||||
"type": "text",
|
||||
"x": 409.80838527586974,
|
||||
"y": 186.48331271464983,
|
||||
"width": 135,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#c92a2a",
|
||||
"backgroundColor": "#000000",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 399694958,
|
||||
"version": 166,
|
||||
"versionNonce": 1571185842,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158939920,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "(mouseX, mouseY)",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "(mouseX, mouseY)"
|
||||
},
|
||||
{
|
||||
"id": "0idmv_zLtEYyctHB4lSWV",
|
||||
"type": "text",
|
||||
"x": 406.7568976895149,
|
||||
"y": 390.3911282833501,
|
||||
"width": 135,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#364fc7",
|
||||
"backgroundColor": "#000000",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "sharp",
|
||||
"seed": 1008210350,
|
||||
"version": 224,
|
||||
"versionNonce": 968376110,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670158975919,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "(mouseX, mouseY)",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "(mouseX, mouseY)"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/assets/quickstart/CircleMouseCollision.png
Normal file
|
After Width: | Height: | Size: 821 KiB |
375
docs/assets/quickstart/Layers.excalidraw
Normal file
@@ -0,0 +1,375 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{
|
||||
"id": "HPGP8tbAPy0aGI3_MCE3D",
|
||||
"type": "diamond",
|
||||
"x": 491.42779164474496,
|
||||
"y": 479.56091158550765,
|
||||
"width": 432.03244941244884,
|
||||
"height": 55.325188818463005,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#868e96",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1103578030,
|
||||
"version": 138,
|
||||
"versionNonce": 1150893294,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "vhIFJN3-91oJXV-RfFsBf",
|
||||
"type": "diamond",
|
||||
"x": 491.42779164474496,
|
||||
"y": 435.8942449188411,
|
||||
"width": 432.03244941244884,
|
||||
"height": 55.325188818463005,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#fab005",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 618886834,
|
||||
"version": 198,
|
||||
"versionNonce": 1515704562,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "B2pM-4m_3Dk2w_6dY-hVJ",
|
||||
"type": "diamond",
|
||||
"x": 491.42779164474496,
|
||||
"y": 392.2275782521744,
|
||||
"width": 432.03244941244884,
|
||||
"height": 55.325188818463005,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#228be6",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 844345582,
|
||||
"version": 231,
|
||||
"versionNonce": 1954508590,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "Ge28XC9PqD26hknNaFXKP",
|
||||
"type": "diamond",
|
||||
"x": 491.42779164474496,
|
||||
"y": 348.5609115855077,
|
||||
"width": 432.03244941244884,
|
||||
"height": 55.325188818463005,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1326657902,
|
||||
"version": 249,
|
||||
"versionNonce": 685040306,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "PLU80s2TkyMEAOVgolwJx",
|
||||
"type": "line",
|
||||
"x": 506.9963325816415,
|
||||
"y": 507.9758066195945,
|
||||
"width": 0,
|
||||
"height": 132.00000000000006,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1246804466,
|
||||
"version": 39,
|
||||
"versionNonce": 627399022,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-132.00000000000006
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "xDdJ5FnbMLIbN95FE3Iyc",
|
||||
"type": "line",
|
||||
"x": 910.9963325816416,
|
||||
"y": 506.9758066195945,
|
||||
"width": 0,
|
||||
"height": 132,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1034568050,
|
||||
"version": 77,
|
||||
"versionNonce": 1363786866,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-132
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "clYu5Q9GzKbAfohBEsGwn",
|
||||
"type": "line",
|
||||
"x": 709.9963325816416,
|
||||
"y": 532.9758066195944,
|
||||
"width": 0,
|
||||
"height": 129.99999999999994,
|
||||
"angle": 0,
|
||||
"strokeColor": "#000000",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1798035246,
|
||||
"version": 42,
|
||||
"versionNonce": 244579246,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
0,
|
||||
-129.99999999999994
|
||||
]
|
||||
],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": null
|
||||
},
|
||||
{
|
||||
"id": "v2nD7_bvWJYHqs82XDWO2",
|
||||
"type": "text",
|
||||
"x": 935.9963325816416,
|
||||
"y": 497.2235059947391,
|
||||
"width": 159,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#343a40",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1323260978,
|
||||
"version": 111,
|
||||
"versionNonce": 372528622,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Ebene 0: ColorLayer",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "Ebene 0: ColorLayer"
|
||||
},
|
||||
{
|
||||
"id": "c6T2j8hozL3RZcNYGMgRl",
|
||||
"type": "text",
|
||||
"x": 935.9963325816416,
|
||||
"y": 453.55683932807256,
|
||||
"width": 172,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#e67700",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 1949746930,
|
||||
"version": 111,
|
||||
"versionNonce": 1448527858,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Ebene 1: DrawingLayer",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "Ebene 1: DrawingLayer"
|
||||
},
|
||||
{
|
||||
"id": "bAmGMg2i4Hvv7t_obXSKn",
|
||||
"type": "text",
|
||||
"x": 935.9963325816416,
|
||||
"y": 409.8901726614059,
|
||||
"width": 174,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1864ab",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 706529710,
|
||||
"version": 144,
|
||||
"versionNonce": 1968138286,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163691142,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Ebene 2: ShapesLayer",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "Ebene 2: ShapesLayer"
|
||||
},
|
||||
{
|
||||
"id": "9-QieQt-nVqGCe5D4r15j",
|
||||
"type": "text",
|
||||
"x": 935.9963325816416,
|
||||
"y": 366.2235059947392,
|
||||
"width": 65,
|
||||
"height": 20,
|
||||
"angle": 0,
|
||||
"strokeColor": "#2b8a3e",
|
||||
"backgroundColor": "#40c057",
|
||||
"fillStyle": "hachure",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "dotted",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"strokeSharpness": "round",
|
||||
"seed": 2100742126,
|
||||
"version": 179,
|
||||
"versionNonce": 289074606,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1670163697684,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Ebene 3",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"baseline": 14,
|
||||
"containerId": null,
|
||||
"originalText": "Ebene 3"
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": null,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/assets/quickstart/Layers.png
Normal file
|
After Width: | Height: | Size: 289 KiB |
BIN
docs/assets/quickstart/shapes_5.1.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/quickstart/shapes_5.3.gif
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
docs/assets/quickstart/shapes_6.1.gif
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
docs/assets/quickstart/shapes_6.2.png
Normal file
|
After Width: | Height: | Size: 457 KiB |
@@ -1,4 +1,3 @@
|
||||
|
||||
<figure markdown>
|
||||
{ width=128 }
|
||||
</figure>
|
||||
@@ -9,10 +8,11 @@ Informatikunterricht.</h2>
|
||||
|
||||
## Projektidee
|
||||
|
||||
Die **Zeichenmaschine** ist eine für den Informatikunterricht entwickelte Bibliothek,
|
||||
die unter anderem an [Processing](https://processing.org/) angelehnt ist. Die
|
||||
Bibliothek soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen
|
||||
und für Schülerinnen und Schüler im Unterricht nutzbar machen.
|
||||
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
|
||||
|
||||
@@ -29,15 +29,43 @@ und für Schülerinnen und Schüler im Unterricht nutzbar machen.
|
||||
(Gleiches gilt für diese Webseite zum Projekt.)
|
||||
|
||||
## Dokumentation
|
||||
* [Schnellstart](quickstart.md)
|
||||
* [Installation](install.md)
|
||||
* {{ javadoc_link() }}
|
||||
|
||||
* [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`.
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
Um ein einfaches Projekt mit der **Zeichenmaschine** aufzusetzen ist nicht mehr
|
||||
nötig, als
|
||||
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
|
||||
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.
|
||||
für verschiedene Entwicklungsumgebungen sind hier aufgelistet.
|
||||
|
||||
## Integration in Entwicklungsumgebungen
|
||||
|
||||
@@ -42,3 +41,5 @@ Projekt in den Classpath aufgenommen werden:
|
||||
### NetBeans
|
||||
|
||||
> Coming soon
|
||||
|
||||
## Unterstützung für MP3
|
||||
@@ -14,14 +14,33 @@ def define_env(env):
|
||||
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 = clazz.replace(".", "/") + ".html"
|
||||
|
||||
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, title: str = None
|
||||
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:
|
||||
@@ -30,6 +49,17 @@ def define_env(env):
|
||||
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:
|
||||
@@ -37,7 +67,33 @@ def define_env(env):
|
||||
if title:
|
||||
name = title
|
||||
|
||||
return f"[{name}]({javadoc(clazz, target)})"
|
||||
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(
|
||||
@@ -57,6 +113,15 @@ def define_env(env):
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
# Schnellstart mit der Zeichenmaschine
|
||||
|
||||
Um ein einfaches Projekt mit der **Zeichenmaschine** aufzusetzen ist nicht mehr
|
||||
nötig, als die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
|
||||
herunterzuladen und dem *Classpath* des Projekts hinzuzufügen. Eine Beschreibung
|
||||
für verschiedene Entwicklungsumgebungen findet sich im Abschnitt [Installation](install.md).
|
||||
|
||||
## Die Basisklasse
|
||||
|
||||
Um eine Zeichenmaschine zu erstellen, muss eine Unterklasse von {{ javadoc_link("schule.ngb.zm.Zeichenmaschine") }}
|
||||
erstellt werden.
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Die gezeigte Klasse ist schon eine lauffähige Zeichenmaschine und kann gestartet
|
||||
werden.
|
||||
|
||||
!!! note
|
||||
|
||||
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 Größe und
|
||||
den Titel des Fensters zu ändern, legen wir einen Konstruktor an.
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
public Shapes() {
|
||||
super(800, 800, "Shapes");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Starten wir das Projekt nun, wird das Zeichenfenster in der Größe 800x800 mit
|
||||
dem Titel "Shapes" angezeigt.
|
||||
|
||||
<figure markdown>
|
||||
{ width=400 }
|
||||
</figure>
|
||||
|
||||
### Formen zeichnen
|
||||
|
||||
Eine Zeichenmaschine hat verschiedene Möglichkeiten, Inhalte in das Zeichenfenster
|
||||
zu zeichnen. Um einfach nur ein statisches Bild zu erzeugen, überschreiben wir die
|
||||
{{ javadoc_link("schule.ngb.zm.Zeichenmaschine", "draw()") }} Methode.
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
public Shapes() {
|
||||
super(800, 800, "Shapes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw() {
|
||||
background.setColor(43, 128, 243);
|
||||
|
||||
drawing.setFillColor(255, 223, 34);
|
||||
drawing.noStroke();
|
||||
drawing.circle(400, 400, 100);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nun haben wir einen gelben Kreis (ohne Konturlinie) auf einem blauen Hintergrund
|
||||
gezeichnet.
|
||||
|
||||
<figure markdown>
|
||||
{ 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 sind, können wir sie statt in `draw()` auch in die `setup()` Methode schreiben.
|
||||
Diese bereitet die Zeichenfläche vor dem ersten Zeichnen vor.
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
public Shapes() {
|
||||
super(800, 800, "Shapes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setup() {
|
||||
background.setColor(43, 128, 243);
|
||||
|
||||
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>
|
||||
{ width=400 }
|
||||
</figure>
|
||||
|
||||
!!! tip ""
|
||||
|
||||
Mit {{ javadoc_link("Constants", "canvasWidth", title="canvasWidth") }} und {{ javadoc_link("Constants", "canvasHeight", strip_clazz=True) }} kannst du in der Zeichenmaschine auf
|
||||
die aktuelle Größe der Zeichenfläche zugreifen. {{ javadoc_link("Constants", "random(int,int)") }}
|
||||
erzeugt eine Zufallszahl innerhalb der angegebenen Grenzen.
|
||||
606
docs/schnellstart.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# Schnellstart mit der Zeichenmaschine
|
||||
|
||||
Um die **Zeichenmaschine** in einem Projekt zu nutzen ist nicht mehr nötig, als
|
||||
die [JAR-Datei der aktuellen Version](https://github.com/jneug/zeichenmaschine/release/latest)
|
||||
herunterzuladen und
|
||||
dem [Classpath](https://www.delftstack.com/de/howto/java/java-classpath-/)
|
||||
hinzuzufügen. Eine Beschreibung für verschiedene Entwicklungsumgebungen findet
|
||||
sich im Abschnitt [Installation](installation.md).
|
||||
|
||||
## Die Basisklasse
|
||||
|
||||
Eine _Zeichenmaschine_ wird immer als Unterklasse von {{ javadoc_link("
|
||||
schule.ngb.zm.Zeichenmaschine") }} erstellt.
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Die gezeigte Klasse ist schon eine lauffähige Zeichenmaschine und kann gestartet
|
||||
werden.
|
||||
|
||||
!!! note "main Methode"
|
||||
|
||||
Bei einigen Entwicklungsumgebungen muss noch eine `main` Methode erstellt
|
||||
werden, um die Zeichenmaschine zu starten:
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
new Shapes();
|
||||
}
|
||||
```
|
||||
|
||||
Es öffnet sich ein Zeichenfenster in einer vordefinierten Größe. Um die
|
||||
Abmessungen und den Titel des Fensters zu ändern, legen wir einen Konstruktor
|
||||
an.
|
||||
|
||||
???+ example "Quelltext"
|
||||
|
||||
```java
|
||||
public class Shapes extends Zeichenmaschine {
|
||||
|
||||
public Shapes() {
|
||||
super(800, 800, "Shapes");
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Starten wir das Projekt, wird eine Zeichenfläche in der Größe 800-mal-800 Pixel
|
||||
erstellt und in einem Fenster mit dem Titel „Shapes“ angezeigt.
|
||||
|
||||
<figure markdown>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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
|
||||
|
||||
{ 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>
|
||||
{ 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>
|
||||
{ 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).
|
||||
19
docs/tutorials/aquarium/aquarium1.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Tutorial: Aquarium
|
||||
|
||||
In diesem Tutorial wollen wir mithilfe der _Zeichenmaschine_ ein (bonbonbuntes)
|
||||
interaktives Aquarium entwickeln. Dabei werden wir in verschiedenen Ausbaustufen
|
||||
zunächst das System Modellieren und dann implementieren.
|
||||
|
||||
!!! info "Mein bonbonbuntes Aquarium"
|
||||
|
||||
Das Projekt [Mein bonbonbuntes Aquarium](http://blog.schockwellenreiter.de/2021/02/2021021201.html)
|
||||
stammt ursprünglich aus dem Blog [Schockwellenreiter](http://blog.schockwellenreiter.de)
|
||||
von [Jörg Kantel](http://cognitiones.kantel-chaos-team.de/cv.html).
|
||||
|
||||
Das Endprodukt soll folgendes umfassen:
|
||||
|
||||
- Darstellung eines hübschen Aquariums mit Fischen, die hin und her schwimmen.
|
||||
- Zur Darstellung wollen wir wie im Original die Sprites aus dem [Fish Pack von Kenny.nl](https://www.kenney.nl/assets/fish-pack) nutzen.
|
||||
- Das Aquarium soll durch passende Geräusche untermalt werden.
|
||||
- Bei einem Klick in das Aquarium soll ein zufälliger Fisch erscheinen.
|
||||
- Bei einem Druck auf die Leertaste soll ein Hai durch das Aquarium schwimmen und alle Fische auf seinem Weg auffressen.
|
||||
19
mkdocs.yml
@@ -8,7 +8,7 @@ site_dir: build/docs/site
|
||||
|
||||
theme:
|
||||
name: material
|
||||
custom_dir: docs/home_override/
|
||||
# custom_dir: docs/home_override/
|
||||
language: de
|
||||
logo: assets/icon_64.png
|
||||
favicon: assets/icon_32.png
|
||||
@@ -37,9 +37,13 @@ extra_css:
|
||||
- assets/zmstyles.css
|
||||
|
||||
nav:
|
||||
- Einführung: index.md
|
||||
- Schnellstart: quickstart.md
|
||||
- Installation: install.md
|
||||
- Einführung: index.md
|
||||
- Schnellstart: schnellstart.md
|
||||
- Installation: installation.md
|
||||
- Tutorials:
|
||||
- Aquarium: tutorials/aquarium/aquarium1.md
|
||||
- Beispiele:
|
||||
- Mondrian: beispiele/mondrian.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
@@ -62,8 +66,11 @@ markdown_extensions:
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
plugins:
|
||||
- search:
|
||||
lang: de
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.MultipleGradientPaint;
|
||||
import java.awt.Paint;
|
||||
import java.awt.Stroke;
|
||||
|
||||
/**
|
||||
* Basisimplementierung der {@link Strokeable} und {@link Fillable} APIs.
|
||||
@@ -31,6 +34,11 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
*/
|
||||
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
|
||||
@@ -50,6 +58,7 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
*/
|
||||
protected MultipleGradientPaint fill = null;
|
||||
|
||||
// TODO: Add TexturePaint fill (https://docs.oracle.com/javase/8/docs//api/java/awt/TexturePaint.html)
|
||||
|
||||
// Implementierung Drawable Interface
|
||||
|
||||
@@ -90,9 +99,6 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
|
||||
// Implementierung Fillable Interface
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setFill( Paint fill ) {
|
||||
if( fill == null ) {
|
||||
@@ -106,9 +112,6 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Paint getFill() {
|
||||
if( fill != null ) {
|
||||
@@ -120,42 +123,27 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean hasFillColor() {
|
||||
return fillColor != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean hasGradient() {
|
||||
return fill != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Color getFillColor() {
|
||||
return fillColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setFillColor( Color color ) {
|
||||
fillColor = color;
|
||||
fill = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MultipleGradientPaint getGradient() {
|
||||
return fill;
|
||||
@@ -164,73 +152,60 @@ public abstract class BasicDrawable extends Constants implements Strokeable, Fil
|
||||
|
||||
// Implementierung Strokeable Interface
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setStroke( Stroke stroke ) {
|
||||
this.stroke = stroke;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Stroke getStroke() {
|
||||
if( stroke == null ) {
|
||||
stroke = Strokeable.createStroke(strokeType, strokeWeight);
|
||||
stroke = Strokeable.createStroke(strokeType, strokeWeight, strokeJoin);
|
||||
}
|
||||
return stroke;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Color getStrokeColor() {
|
||||
return strokeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setStrokeColor( Color color ) {
|
||||
strokeColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getStrokeWeight() {
|
||||
return strokeWeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setStrokeWeight( double weight ) {
|
||||
strokeWeight = weight;
|
||||
this.stroke = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Options.StrokeType getStrokeType() {
|
||||
return strokeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setStrokeType( Options.StrokeType type ) {
|
||||
strokeType = type;
|
||||
this.stroke = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Options.StrokeJoin getStrokeJoin() {
|
||||
return strokeJoin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStrokeJoin( Options.StrokeJoin join ) {
|
||||
strokeJoin = join;
|
||||
this.stroke = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,38 +52,67 @@ public class Color implements Paint {
|
||||
* Die Farbe Zeichenmaschinen-Rot.
|
||||
*/
|
||||
public static final Color RED = new Color(240, 80, 37);
|
||||
|
||||
/**
|
||||
* Die Farbe Rot.
|
||||
*/
|
||||
public static final Color PURE_RED = new Color(255, 0, 0);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Grün.
|
||||
*/
|
||||
public static final Color GREEN = new Color(98, 199, 119);
|
||||
|
||||
/**
|
||||
* Die Farbe Grün.
|
||||
*/
|
||||
public static final Color PURE_GREEN = new Color(0, 255, 0);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Blau.
|
||||
*/
|
||||
public static final Color BLUE = new Color(49, 197, 244);
|
||||
public static final Color BLUE = new Color(43, 128, 243); // 49, 197, 244
|
||||
|
||||
/**
|
||||
* Die Farbe Blau.
|
||||
*/
|
||||
public static final Color PURE_BLUE = new Color(0, 0, 255);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Gelb.
|
||||
*/
|
||||
public static final Color YELLOW = new Color(248, 239, 34);
|
||||
|
||||
/**
|
||||
* Die Farbe Gelb.
|
||||
*/
|
||||
public static final Color PURE_YELLOW = new Color(255, 255, 0);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Orange.
|
||||
*/
|
||||
public static final Color ORANGE = new Color(248, 158, 80);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Türkis.
|
||||
*/
|
||||
public static final Color CYAN = new Color(java.awt.Color.CYAN);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Magenta.
|
||||
*/
|
||||
public static final Color MAGENTA = new Color(java.awt.Color.MAGENTA);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Rosa.
|
||||
*/
|
||||
public static final Color PINK = new Color(240, 99, 164);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Lila.
|
||||
*/
|
||||
public static final Color PURPLE = new Color(101, 0, 191);
|
||||
|
||||
/**
|
||||
* Die Farbe Zeichenmaschinen-Braun.
|
||||
*/
|
||||
@@ -93,6 +122,7 @@ public class Color implements Paint {
|
||||
* Die Farbe Helmholtz-Grün.
|
||||
*/
|
||||
public static final Color HGGREEN = new Color(0, 165, 81);
|
||||
|
||||
/**
|
||||
* Die Farbe Helmholtz-Rot.
|
||||
*/
|
||||
@@ -272,10 +302,7 @@ public class Color implements Paint {
|
||||
color = color.toLowerCase().strip();
|
||||
|
||||
// Parse colornames file and return first match
|
||||
try(
|
||||
InputStream in = Color.class.getResourceAsStream("colornames.csv");
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in))
|
||||
) {
|
||||
try( InputStream in = Color.class.getResourceAsStream("colornames.csv"); BufferedReader reader = new BufferedReader(new InputStreamReader(in)) ) {
|
||||
String line;
|
||||
while( (line = reader.readLine()) != null ) {
|
||||
String[] parts = line.split(",");
|
||||
@@ -549,6 +576,18 @@ public class Color implements Paint {
|
||||
}
|
||||
}
|
||||
|
||||
public double compare( Color color ) {
|
||||
double maxDist = 764.8333151739665;
|
||||
|
||||
// see: https://www.compuphase.com/cmetric.htm
|
||||
long rmean = (getRed() + color.getRed()) / 2;
|
||||
long r = getRed() - color.getRed();
|
||||
long g = getGreen() - color.getGreen();
|
||||
long b = getBlue() - color.getBlue();
|
||||
|
||||
return 1.0 - (Math.sqrt((((512+rmean)*r*r)>>8) + 4*g*g + (((767-rmean)*b*b)>>8)) / maxDist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob ein anderes Objekt zu diesem gleich ist.
|
||||
* <p>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import schule.ngb.zm.anim.Easing;
|
||||
import schule.ngb.zm.util.io.ImageLoader;
|
||||
import schule.ngb.zm.util.Noise;
|
||||
import schule.ngb.zm.util.io.ImageLoader;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Font;
|
||||
@@ -11,6 +11,7 @@ import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
@@ -23,7 +24,7 @@ import java.util.function.DoubleUnaryOperator;
|
||||
* aktuell gehalten werden (beispielsweise {@link #runtime}).
|
||||
* <p>
|
||||
* Für die Implementierung eigener Klassen ist es oft hilfreich von
|
||||
* {@code Constants} zu erben, um die Methoden und Konstanten einfach im
|
||||
* {@code Constants} zu erben, um die Methoden und Konstanten einfacher im
|
||||
* Programm nutzen zu können.
|
||||
* <pre><code>
|
||||
* class MyClass extends Constants {
|
||||
@@ -67,7 +68,7 @@ public class Constants {
|
||||
/**
|
||||
* Patchversion der Zeichenmaschine.
|
||||
*/
|
||||
public static final int APP_VERSION_REV = 32;
|
||||
public static final int APP_VERSION_REV = 35;
|
||||
|
||||
/**
|
||||
* Version der Zeichenmaschine als Text-String.
|
||||
@@ -170,6 +171,21 @@ public class Constants {
|
||||
*/
|
||||
public static final Options.StrokeType DOTTED = Options.StrokeType.DOTTED;
|
||||
|
||||
/**
|
||||
* Option für abgerundete Kantenverbindungen von Konturen und Linien.
|
||||
*/
|
||||
public static final Options.StrokeJoin ROUND = Options.StrokeJoin.ROUND;
|
||||
|
||||
/**
|
||||
* Option für abgeschnittene Kantenverbindungen von Konturen und Linien.
|
||||
*/
|
||||
public static final Options.StrokeJoin BEVEL = Options.StrokeJoin.BEVEL;
|
||||
|
||||
/**
|
||||
* Option für eckige Kantenverbindungen von Konturen und Linien.
|
||||
*/
|
||||
public static final Options.StrokeJoin MITER = Options.StrokeJoin.MITER;
|
||||
|
||||
/**
|
||||
* Option für Pfeile mit Strichen als Kopf.
|
||||
*/
|
||||
@@ -785,6 +801,30 @@ public class Constants {
|
||||
|
||||
// Mathematische Funktionen
|
||||
|
||||
/**
|
||||
* Berechnet das Minimum aller angegebenen Werte.
|
||||
*
|
||||
* <pre><code>
|
||||
* int minimum = min(1, 5, 3); // 1
|
||||
* </code></pre>
|
||||
*
|
||||
* @param numbers Die Werte, aus denen das Minimum ermittelt werden soll.
|
||||
* @return Das Minimum der Werte.
|
||||
* @throws IllegalArgumentException Wenn die Eingabe {@code null} oder leer
|
||||
* ist.
|
||||
*/
|
||||
public static final int min( int... numbers ) {
|
||||
if( numbers == null || numbers.length == 0 ) {
|
||||
throw new IllegalArgumentException("Array may not be <null> or empty.");
|
||||
}
|
||||
|
||||
int min = numbers[0];
|
||||
for( int i = 1; i < numbers.length; i++ ) {
|
||||
min = Math.min(min, numbers[i]);
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das Minimum aller angegebenen Werte.
|
||||
*
|
||||
@@ -809,6 +849,30 @@ public class Constants {
|
||||
return min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das Maximum aller angegebenen Werte.
|
||||
*
|
||||
* <pre><code>
|
||||
* double maximum = max(1, 5, 3); // 5
|
||||
* </code></pre>
|
||||
*
|
||||
* @param numbers Die Werte, aus denen das Maximum ermittelt werden soll.
|
||||
* @return Das Maximum der Werte.
|
||||
* @throws IllegalArgumentException Wenn die Eingabe {@code null} oder leer
|
||||
* ist.
|
||||
*/
|
||||
public static final int max( int... numbers ) {
|
||||
if( numbers == null || numbers.length == 0 ) {
|
||||
throw new IllegalArgumentException("Array may not be <null> or empty.");
|
||||
}
|
||||
|
||||
int max = numbers[0];
|
||||
for( int i = 1; i < numbers.length; i++ ) {
|
||||
max = Math.max(max, numbers[i]);
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet das Maximum aller angegebenen Werte.
|
||||
*
|
||||
@@ -1204,6 +1268,12 @@ public class Constants {
|
||||
return interpolate(toMin, toMax, (value - fromMin) / (fromMax - fromMin));
|
||||
}
|
||||
|
||||
public static final double distance( double fromX, double fromY, double toX, double toY ) {
|
||||
double diffX = toX - fromX;
|
||||
double diffY = toY - fromY;
|
||||
return sqrt(diffX * diffX + diffY * diffY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Geteilte {@code Random}-Instanz für einheitliche Zufallszahlen.
|
||||
*/
|
||||
@@ -1214,7 +1284,7 @@ public class Constants {
|
||||
*
|
||||
* @return Die {@code Random}-Instanz.
|
||||
*/
|
||||
private static Random getRandom() {
|
||||
public static Random getRandom() {
|
||||
if( random == null ) {
|
||||
random = new Random();
|
||||
}
|
||||
@@ -1266,7 +1336,8 @@ public class Constants {
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt eine ganze Pseudozufallszahl zwischen {@code 0} und {@code max}.
|
||||
* Erzeugt eine ganze Pseudozufallszahl zwischen {@code 0} und {@code max}
|
||||
* (einschließlich der Grenzen).
|
||||
*
|
||||
* @param max Obere Grenze.
|
||||
* @return Eine Zufallszahl.
|
||||
@@ -1277,7 +1348,7 @@ public class Constants {
|
||||
|
||||
/**
|
||||
* Erzeugt eine ganze Pseudozufallsganzzahl zwischen {@code min} und
|
||||
* {@code max}.
|
||||
* {@code max} (einschließlich der Grenzen).
|
||||
*
|
||||
* @param min Untere Grenze.
|
||||
* @param max Obere Grenze.
|
||||
@@ -1334,26 +1405,6 @@ public class Constants {
|
||||
return getRandom().nextGaussian();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wählt ein zufälliges Element aus dem Array aus.
|
||||
*
|
||||
* @param values Ein Array mit Werten, die zur Auswahl stehen.
|
||||
* @return Ein zufälliges Element aus dem Array.
|
||||
*/
|
||||
public static final int choice( int[] values ) {
|
||||
return values[random(0, values.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wählt ein zufälliges Element aus dem Array aus.
|
||||
*
|
||||
* @param values Ein Array mit Werten, die zur Auswahl stehen.
|
||||
* @return Ein zufälliges Element aus dem Array.
|
||||
*/
|
||||
public static final double choice( double[] values ) {
|
||||
return values[random(0, values.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wählt ein zufälliges Element aus dem Array aus.
|
||||
*
|
||||
@@ -1361,7 +1412,8 @@ public class Constants {
|
||||
* @param <T> Datentyp des Elements.
|
||||
* @return Ein zufälliges Element aus dem Array.
|
||||
*/
|
||||
public static final <T> T choice( T[] values ) {
|
||||
@SafeVarargs
|
||||
public static final <T> T choice( T... values ) {
|
||||
return values[random(0, values.length - 1)];
|
||||
}
|
||||
|
||||
@@ -1515,6 +1567,18 @@ public class Constants {
|
||||
return valueList.toArray(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bringt die Werte im Array in eine zufällige Reihenfolge.
|
||||
*
|
||||
* @param values Ein Array mit Werte, die gemischt werden sollen.
|
||||
* @param <T> Datentyp der Elemente.
|
||||
* @return Das Array in zufälliger Reihenfolge.
|
||||
*/
|
||||
public static final <T> List<T> shuffle( List<T> values ) {
|
||||
Collections.shuffle(values, random);
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geteilte {@code Noise}-Instanz zur Erzeugung von Perlin-Noise.
|
||||
*/
|
||||
|
||||
@@ -88,7 +88,11 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
|
||||
} else {
|
||||
createCanvas(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: prevent access to graphics context?
|
||||
public Graphics2D getGraphics() {
|
||||
return this.drawing;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +184,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
|
||||
/**
|
||||
* Zeigt die Ebene an, falls sie versteckt war.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings( "unused" )
|
||||
public void show() {
|
||||
visible = true;
|
||||
}
|
||||
@@ -189,7 +193,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
|
||||
* Versteckt oder zeigt die Ebene, je nachdem, welchen Zustand sie derzeit
|
||||
* hat.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressWarnings( "unused" )
|
||||
public void toggle() {
|
||||
visible = !visible;
|
||||
}
|
||||
@@ -203,4 +207,22 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.geom.Arc2D;
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,36 @@ public final class Options {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -174,7 +174,7 @@ public interface Strokeable extends Drawable {
|
||||
* @param weight Die Dicke der Konturlinie.
|
||||
*/
|
||||
default void setStrokeWeight( double weight ) {
|
||||
setStroke(createStroke(getStrokeType(), weight));
|
||||
setStroke(createStroke(getStrokeType(), weight, getStrokeJoin()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +193,26 @@ public interface Strokeable extends Drawable {
|
||||
* @see Options.StrokeType
|
||||
*/
|
||||
default void setStrokeType( Options.StrokeType type ) {
|
||||
setStroke(createStroke(type, getStrokeWeight()));
|
||||
setStroke(createStroke(type, getStrokeWeight(), getStrokeJoin()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Art der Konturverbindungen zurück.
|
||||
*
|
||||
* @return Die aktuelle Art der Konturverbindungen.
|
||||
* @see Options.StrokeJoin
|
||||
*/
|
||||
Options.StrokeJoin getStrokeJoin();
|
||||
|
||||
/**
|
||||
* Setzt den Typ der Konturverbindungen. Erlaubte Werte sind {@link Constants#ROUND},
|
||||
* {@link Constants#MITER} und {@link Constants#BEVEL}.
|
||||
*
|
||||
* @param join Eine der möglichen Konturverbindungen.
|
||||
* @see Options.StrokeJoin
|
||||
*/
|
||||
default void setStrokeJoin( Options.StrokeJoin join ) {
|
||||
setStroke(createStroke(getStrokeType(), getStrokeWeight(), join));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,28 +220,30 @@ public interface Strokeable extends Drawable {
|
||||
* 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 ) {
|
||||
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight, Options.StrokeJoin strokeJoin ) {
|
||||
switch( strokeType ) {
|
||||
case DOTTED:
|
||||
return new BasicStroke(
|
||||
(float) strokeWeight,
|
||||
BasicStroke.CAP_ROUND,
|
||||
BasicStroke.JOIN_ROUND,
|
||||
strokeJoin.awt_type,
|
||||
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
|
||||
case DASHED:
|
||||
return new BasicStroke(
|
||||
(float) strokeWeight,
|
||||
BasicStroke.CAP_ROUND,
|
||||
BasicStroke.JOIN_ROUND,
|
||||
strokeJoin.awt_type,
|
||||
10.0f, new float[]{5.0f}, 0.0f);
|
||||
case SOLID:
|
||||
default:
|
||||
return new BasicStroke(
|
||||
(float) strokeWeight,
|
||||
BasicStroke.CAP_ROUND,
|
||||
BasicStroke.JOIN_ROUND);
|
||||
strokeJoin.awt_type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -191,6 +191,22 @@ public class Vector extends Point2D.Double {
|
||||
return new Vector(x, y);
|
||||
}
|
||||
|
||||
public int getIntX() {
|
||||
return (int)this.x;
|
||||
}
|
||||
|
||||
public int getRoundedX() {
|
||||
return (int)Math.round(this.x);
|
||||
}
|
||||
|
||||
public int getIntY() {
|
||||
return (int)this.y;
|
||||
}
|
||||
|
||||
public int getRoundedY() {
|
||||
return (int)Math.round(this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Komponenten dieses Vektors neu.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.*;
|
||||
@@ -10,6 +11,7 @@ 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;
|
||||
|
||||
@@ -26,7 +28,7 @@ public class Zeichenfenster extends JFrame {
|
||||
/**
|
||||
* Setzt das Look and Feel auf den Standard des Systems.
|
||||
* <p>
|
||||
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen
|
||||
* Sollte einmalig vor Erstellen des ersten Programmfensters aufgerufen
|
||||
* werden.
|
||||
*/
|
||||
public static final void setLookAndFeel() {
|
||||
@@ -156,7 +158,7 @@ public class Zeichenfenster extends JFrame {
|
||||
* @param displayDevice Das Anzeigegerät für das Fenster.
|
||||
*/
|
||||
public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
|
||||
super(Validator.requireNotNull(displayDevice).getDefaultConfiguration());
|
||||
super(Validator.requireNotNull(displayDevice, "displayDevice").getDefaultConfiguration());
|
||||
this.displayDevice = displayDevice;
|
||||
|
||||
Validator.requireNotNull(canvas, "Every Zeichenfenster needs a Zeichenleinwand, but got <null>.");
|
||||
@@ -174,12 +176,14 @@ public class Zeichenfenster extends JFrame {
|
||||
// Das Icon des Fensters ändern
|
||||
try {
|
||||
if( Zeichenmaschine.MACOS ) {
|
||||
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png");
|
||||
if( iconUrl != null ) {
|
||||
Image icon = ImageIO.read(iconUrl);
|
||||
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);
|
||||
@@ -190,7 +194,11 @@ public class Zeichenfenster extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
this.setIconImages(icons);
|
||||
if( icons.isEmpty() ) {
|
||||
LOG.warn("Could not load dock-icon");
|
||||
} else {
|
||||
this.setIconImages(icons);
|
||||
}
|
||||
}
|
||||
} catch( IllegalArgumentException | IOException e ) {
|
||||
LOG.warn("Could not load image icons: %s", e.getMessage());
|
||||
|
||||
@@ -94,7 +94,7 @@ public class Zeichenmaschine extends Constants {
|
||||
*/
|
||||
private boolean running;
|
||||
|
||||
private boolean terminateImediately = false;
|
||||
private boolean terminateImmediately = false;
|
||||
|
||||
/**
|
||||
* Ob die ZM nach dem nächsten Frame pausiert werden soll.
|
||||
@@ -545,7 +545,7 @@ public class Zeichenmaschine extends Constants {
|
||||
|
||||
if( running ) {
|
||||
running = false;
|
||||
terminateImediately = true;
|
||||
terminateImmediately = true;
|
||||
quitAfterShutdown = true;
|
||||
mainThread.interrupt();
|
||||
} else {
|
||||
@@ -769,6 +769,23 @@ public class Zeichenmaschine extends Constants {
|
||||
framesPerSecond = framesPerSecondInternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt aus dem aktuellen Inhalt der {@link Zeichenleinwand} ein neues
|
||||
* {@link BufferedImage}.
|
||||
*/
|
||||
public final BufferedImage getImage() {
|
||||
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
Graphics2D g = img.createGraphics();
|
||||
// TODO: Transparente Hintergründe beim Speichern von png erlauben
|
||||
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
|
||||
g.fillRect(0, 0, img.getWidth(), img.getHeight());
|
||||
canvas.draw(g);
|
||||
g.dispose();
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert den aktuellen Inhalt der {@link Zeichenleinwand} in einer
|
||||
* Bilddatei auf der Festplatte. Zur Auswahl der Zieldatei wird dem Nutzer
|
||||
@@ -794,23 +811,15 @@ public class Zeichenmaschine extends Constants {
|
||||
* Bilddatei im angegebenen Dateipfad auf der Festplatte.
|
||||
*/
|
||||
public final void saveImage( String filepath ) {
|
||||
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
Graphics2D g = img.createGraphics();
|
||||
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
|
||||
g.fillRect(0, 0, img.getWidth(), img.getHeight());
|
||||
canvas.draw(g);
|
||||
g.dispose();
|
||||
|
||||
try {
|
||||
ImageLoader.saveImage(img, new File(filepath), true);
|
||||
ImageLoader.saveImage(getImage(), new File(filepath), true);
|
||||
} catch( IOException ex ) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine Momentanaufnahme des aktuellen Inhalts der
|
||||
* Erstellt eine Momentaufnahme des aktuellen Inhalts der
|
||||
* {@link Zeichenleinwand} und erstellt daraus eine
|
||||
* {@link ImageLayer Bildebene}. Die Ebene wird automatisch der
|
||||
* {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt.
|
||||
@@ -1177,6 +1186,9 @@ public class Zeichenmaschine extends Constants {
|
||||
//saveMousePosition(evt);
|
||||
mouseMoved(evt);
|
||||
break;
|
||||
case MouseEvent.MOUSE_WHEEL:
|
||||
mouseWheelMoved(evt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1220,6 +1232,14 @@ public class Zeichenmaschine extends Constants {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
public void mouseWheelMoved( MouseEvent e ) {
|
||||
mouseMoved();
|
||||
}
|
||||
|
||||
public void mouseWheelMoved() {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
private void saveMousePosition( MouseEvent event ) {
|
||||
if( mouseEvent != null && event.getComponent() == canvas ) {
|
||||
pmouseX = mouseX;
|
||||
@@ -1387,7 +1407,7 @@ public class Zeichenmaschine extends Constants {
|
||||
|
||||
if( Thread.interrupted() ) {
|
||||
running = false;
|
||||
terminateImediately = true;
|
||||
terminateImmediately = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1443,18 +1463,23 @@ public class Zeichenmaschine extends Constants {
|
||||
}
|
||||
state = Options.AppState.STOPPED;
|
||||
// Shutdown the updateThread
|
||||
while( !terminateImediately && updateThreadExecutor.isRunning() ) {
|
||||
while( !terminateImmediately && updateThreadExecutor.isRunning() ) {
|
||||
Thread.yield();
|
||||
}
|
||||
updateThreadExecutor.shutdownNow();
|
||||
try {
|
||||
updateThreadExecutor.awaitTermination(500, TimeUnit.MILLISECONDS);
|
||||
} catch( InterruptedException ex ) {
|
||||
|
||||
// Cleanup
|
||||
shutdown();
|
||||
cleanup();
|
||||
state = Options.AppState.TERMINATED;
|
||||
} finally {
|
||||
// Cleanup
|
||||
shutdown();
|
||||
cleanup();
|
||||
state = Options.AppState.TERMINATED;
|
||||
|
||||
if( quitAfterShutdown ) {
|
||||
quit();
|
||||
if( quitAfterShutdown ) {
|
||||
quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
|
||||
public Animation( DoubleUnaryOperator easing ) {
|
||||
this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
|
||||
this.easing = Validator.requireNotNull(easing);
|
||||
this.easing = Validator.requireNotNull(easing, "easing");
|
||||
}
|
||||
|
||||
public Animation( int runtime ) {
|
||||
@@ -34,7 +34,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
|
||||
public Animation( int runtime, DoubleUnaryOperator easing ) {
|
||||
this.runtime = runtime;
|
||||
this.easing = Validator.requireNotNull(easing);
|
||||
this.easing = Validator.requireNotNull(easing, "easing");
|
||||
}
|
||||
|
||||
public int getRuntime() {
|
||||
@@ -50,7 +50,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
}
|
||||
|
||||
public void setEasing( DoubleUnaryOperator pEasing ) {
|
||||
this.easing = pEasing;
|
||||
this.easing = Validator.requireNotNull(pEasing, "easing");
|
||||
}
|
||||
|
||||
public abstract T getAnimationTarget();
|
||||
@@ -61,7 +61,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
running = true;
|
||||
finished = false;
|
||||
animate(easing.applyAsDouble(0.0));
|
||||
initializeEventDispatcher().dispatchEvent("start", this);
|
||||
dispatchEvent("start");
|
||||
}
|
||||
|
||||
public final void stop() {
|
||||
@@ -70,7 +70,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
animate(easing.applyAsDouble((double) elapsedTime / (double) runtime));
|
||||
this.finish();
|
||||
finished = true;
|
||||
initializeEventDispatcher().dispatchEvent("stop", this);
|
||||
dispatchEvent("stop");
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@@ -100,10 +100,9 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
|
||||
double t = (double) elapsedTime / (double) runtime;
|
||||
if( t >= 1.0 ) {
|
||||
running = false;
|
||||
stop();
|
||||
} else {
|
||||
animate(easing.applyAsDouble(t));
|
||||
animate(getEasing().applyAsDouble(t));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +117,7 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
* e = Constants.limit(e, 0, 1);
|
||||
* </code></pre>
|
||||
*
|
||||
* @param e Fortschritt der Animation nachdem die Easingfunktion angewandt
|
||||
* @param e Fortschritt der Animation, nachdem die Easing-Funktion angewandt
|
||||
* wurde.
|
||||
*/
|
||||
public abstract void animate( double e );
|
||||
@@ -134,6 +133,12 @@ public abstract class Animation<T> extends Constants implements Updatable {
|
||||
return eventDispatcher;
|
||||
}
|
||||
|
||||
private void dispatchEvent( String type ) {
|
||||
if( eventDispatcher != null ) {
|
||||
eventDispatcher.dispatchEvent(type, this);
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( AnimationListener listener ) {
|
||||
initializeEventDispatcher().addListener(listener);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,19 @@ 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 Animation<S> anim;
|
||||
private final Animation<S> anim;
|
||||
|
||||
public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
|
||||
super(runtime, easing);
|
||||
this.anim = Validator.requireNotNull(anim);
|
||||
this.anim = Validator.requireNotNull(anim, "anim");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
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> {
|
||||
|
||||
List<Animation<T>> anims;
|
||||
private final List<Animation<T>> anims;
|
||||
|
||||
|
||||
private boolean overrideEasing = false;
|
||||
private final boolean overrideEasing;
|
||||
|
||||
private int overrideRuntime = -1;
|
||||
|
||||
private int lag = 0;
|
||||
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);
|
||||
}
|
||||
@@ -43,6 +48,8 @@ public class AnimationGroup<T> extends Animation<T> {
|
||||
if( easing != null ) {
|
||||
this.easing = easing;
|
||||
overrideEasing = true;
|
||||
} else {
|
||||
overrideEasing = false;
|
||||
}
|
||||
|
||||
if( runtime > 0 ) {
|
||||
@@ -65,52 +72,110 @@ public class AnimationGroup<T> extends Animation<T> {
|
||||
return anim.getAnimationTarget();
|
||||
}
|
||||
}
|
||||
return anims.get(anims.size() - 1).getAnimationTarget();
|
||||
if( this.finished ) {
|
||||
return anims.get(anims.size() - 1).getAnimationTarget();
|
||||
} else {
|
||||
return anims.get(0).getAnimationTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
public DoubleUnaryOperator getEasing() {
|
||||
for( Animation<T> anim : anims ) {
|
||||
if( anim.isActive() ) {
|
||||
return anim.getEasing();
|
||||
}
|
||||
running = false;
|
||||
this.stop();
|
||||
}
|
||||
|
||||
while( active < anims.size() && elapsedTime >= active * lag ) {
|
||||
anims.get(active).start();
|
||||
active += 1;
|
||||
if( this.finished ) {
|
||||
return anims.get(anims.size() - 1).getEasing();
|
||||
} else {
|
||||
return anims.get(0).getEasing();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
// @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);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
144
src/main/java/schule/ngb/zm/anim/AnimationSequence.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
/**
|
||||
* Führt eine Liste von Animationen nacheinander aus. Jede Animation startet direkt nachdem die
|
||||
* davor geendet ist. Optional kann zwischen dem Ende einer und dem Start der nächsten Animation
|
||||
* ein
|
||||
* <var>lag</var> eingefügt werden.
|
||||
*
|
||||
* @param <T> Die Art des animierten Objektes.
|
||||
*/
|
||||
@SuppressWarnings( "unused" )
|
||||
public class AnimationSequence<T> extends Animation<T> {
|
||||
|
||||
private final List<Animation<T>> anims;
|
||||
|
||||
private final int lag;
|
||||
|
||||
private int currentAnimationIndex = -1, currentStart = -1, nextStart = -1;
|
||||
|
||||
@SafeVarargs
|
||||
public AnimationSequence( Animation<T>... anims ) {
|
||||
this(0, Arrays.asList(anims));
|
||||
}
|
||||
|
||||
public AnimationSequence( Collection<Animation<T>> anims ) {
|
||||
this(0, anims);
|
||||
}
|
||||
|
||||
public AnimationSequence( int lag, Collection<Animation<T>> anims ) {
|
||||
super(Easing::linear);
|
||||
|
||||
this.anims = List.copyOf(anims);
|
||||
this.lag = lag;
|
||||
|
||||
this.runtime = (anims.size() - 1) * lag + anims.stream().mapToInt(Animation::getRuntime).sum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getAnimationTarget() {
|
||||
for( Animation<T> anim : anims ) {
|
||||
if( anim.isActive() ) {
|
||||
return anim.getAnimationTarget();
|
||||
}
|
||||
}
|
||||
if( this.finished ) {
|
||||
return anims.get(anims.size() - 1).getAnimationTarget();
|
||||
} else {
|
||||
return anims.get(0).getAnimationTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleUnaryOperator getEasing() {
|
||||
for( Animation<T> anim : anims ) {
|
||||
if( anim.isActive() ) {
|
||||
return anim.getEasing();
|
||||
}
|
||||
}
|
||||
if( this.finished ) {
|
||||
return anims.get(anims.size() - 1).getEasing();
|
||||
} else {
|
||||
return anims.get(0).getEasing();
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void update( double delta ) {
|
||||
// elapsedTime += (int) (delta * 1000);
|
||||
//
|
||||
// // Animation is done. Stop all Animations.
|
||||
// if( elapsedTime > runtime ) {
|
||||
// for( int i = 0; i < anims.size(); i++ ) {
|
||||
// if( anims.get(i).isActive() ) {
|
||||
// anims.get(i).elapsedTime = anims.get(i).runtime;
|
||||
// anims.get(i).stop();
|
||||
// }
|
||||
// }
|
||||
// elapsedTime = runtime;
|
||||
// running = false;
|
||||
// this.stop();
|
||||
// }
|
||||
//
|
||||
// Animation<T> curAnim = null;
|
||||
// if( elapsedTime > nextStart ) {
|
||||
// currentAnimation += 1;
|
||||
// curAnim = anims.get(currentAnimation);
|
||||
// currentStart = nextStart;
|
||||
// nextStart += lag + curAnim.getRuntime();
|
||||
// curAnim.start();
|
||||
// } else {
|
||||
// curAnim = anims.get(currentAnimation);
|
||||
// }
|
||||
//
|
||||
// // Calculate delta for current animation
|
||||
// double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
|
||||
// if( t >= 1.0 ) {
|
||||
// curAnim.elapsedTime = curAnim.runtime;
|
||||
// curAnim.stop();
|
||||
// } else {
|
||||
// curAnim.animate(curAnim.easing.applyAsDouble(t));
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
for( Animation<T> anim : anims ) {
|
||||
if( anim.isActive() ) {
|
||||
anim.elapsedTime = anim.runtime;
|
||||
anim.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animate( double e ) {
|
||||
Animation<T> curAnim = null;
|
||||
if( running && elapsedTime > nextStart ) {
|
||||
currentAnimationIndex += 1;
|
||||
curAnim = anims.get(currentAnimationIndex);
|
||||
currentStart = nextStart;
|
||||
nextStart += lag + curAnim.getRuntime();
|
||||
curAnim.start();
|
||||
} else {
|
||||
curAnim = anims.get(currentAnimationIndex);
|
||||
}
|
||||
|
||||
// Calculate delta for current animation
|
||||
double t = (double) (elapsedTime - currentStart) / (double) curAnim.getRuntime();
|
||||
if( t >= 1.0 ) {
|
||||
curAnim.elapsedTime = curAnim.runtime;
|
||||
curAnim.stop();
|
||||
} else {
|
||||
curAnim.elapsedTime = (elapsedTime - currentStart);
|
||||
curAnim.animate(curAnim.easing.applyAsDouble(t));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -123,8 +123,8 @@ public class Animations {
|
||||
}
|
||||
|
||||
public static final <T> Future<T> animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) {
|
||||
Validator.requireNotNull(target);
|
||||
Validator.requireNotNull(propSetter);
|
||||
Validator.requireNotNull(target, "target");
|
||||
Validator.requireNotNull(propSetter, "propSetter");
|
||||
return play(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,33 +7,87 @@ 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 Shape object;
|
||||
private final Shape target;
|
||||
|
||||
private double centerx, centery, radius, startangle;
|
||||
private final double centerX, centerY, rotateTo;
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, int runtime, DoubleUnaryOperator easing ) {
|
||||
private double rotationRadius, startAngle;
|
||||
|
||||
private final boolean rotateRight;
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy ) {
|
||||
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, double rotateTo ) {
|
||||
this(target, cx, cy, rotateTo, true, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight ) {
|
||||
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight ) {
|
||||
this(target, cx, cy, rotateTo, rotateRight, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, int runtime ) {
|
||||
this(target, cx, cy, 360, true, runtime, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, int runtime ) {
|
||||
this(target, cx, cy, 360, rotateRight, runtime, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, DoubleUnaryOperator easing ) {
|
||||
this(target, cx, cy, 360, true, DEFAULT_ANIM_RUNTIME, easing);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, boolean rotateRight, DoubleUnaryOperator easing ) {
|
||||
this(target, cx, cy, 360, rotateRight, DEFAULT_ANIM_RUNTIME, easing);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, int runtime, DoubleUnaryOperator easing ) {
|
||||
this(target, cx, cy, rotateTo, true, runtime, easing);
|
||||
}
|
||||
|
||||
public CircleAnimation( Shape target, double cx, double cy, double rotateTo, boolean rotateRight, int runtime, DoubleUnaryOperator easing ) {
|
||||
super(runtime, easing);
|
||||
object = target;
|
||||
centerx = cx;
|
||||
centery = cy;
|
||||
Vector vec = new Vector(target.getX(), target.getY()).sub(cx, cy);
|
||||
startangle = vec.heading();
|
||||
radius = vec.length();
|
||||
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 object;
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animate( double e ) {
|
||||
double angle = startangle + Constants.radians(Constants.interpolate(0, 360, e));
|
||||
double x = centerx + radius * Constants.cos(angle);
|
||||
double y = centery + radius * Constants.sin(angle);
|
||||
object.moveTo(x, y);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ public class ContinousAnimation<T> extends Animation<T> {
|
||||
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.
|
||||
* 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;
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ContinousAnimation<T> extends Animation<T> {
|
||||
}
|
||||
|
||||
private ContinousAnimation( Animation<T> baseAnimation, int lag, boolean easeInOnly ) {
|
||||
super(baseAnimation.getRuntime(), baseAnimation.getEasing());
|
||||
super(baseAnimation.getRuntime() + lag, baseAnimation.getEasing());
|
||||
this.baseAnimation = baseAnimation;
|
||||
this.lag = lag;
|
||||
this.easeInOnly = easeInOnly;
|
||||
@@ -40,35 +40,80 @@ public class ContinousAnimation<T> extends Animation<T> {
|
||||
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 ) {
|
||||
elapsedTime += (int) (delta * 1000);
|
||||
if( elapsedTime >= runtime + lag ) {
|
||||
elapsedTime %= (runtime + lag);
|
||||
int currentRuntime = elapsedTime + (int) (delta * 1000);
|
||||
if( currentRuntime >= runtime + lag ) {
|
||||
elapsedTime = currentRuntime % (runtime + lag);
|
||||
|
||||
if( easeInOnly && easing != null ) {
|
||||
easing = null;
|
||||
easing = Easing.linear();
|
||||
// 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,32 +13,51 @@ public class FadeAnimation extends Animation<Shape> {
|
||||
|
||||
public static final int FADE_OUT = 0;
|
||||
|
||||
private Shape object;
|
||||
private final Shape target;
|
||||
|
||||
private final int targetAlpha;
|
||||
|
||||
private Color fill, stroke;
|
||||
|
||||
private int fillAlpha, strokeAlpha, tAlpha;
|
||||
private int fillAlpha, strokeAlpha;
|
||||
|
||||
public FadeAnimation( Shape object, int alpha, int runtime, DoubleUnaryOperator easing ) {
|
||||
public FadeAnimation( Shape target, int targetAlpha ) {
|
||||
this(target, targetAlpha, DEFAULT_ANIM_RUNTIME, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public FadeAnimation( Shape target, int targetAlpha, int runtime ) {
|
||||
this(target, targetAlpha, runtime, DEFAULT_EASING);
|
||||
}
|
||||
|
||||
public FadeAnimation( Shape target, int runtime, DoubleUnaryOperator easing ) {
|
||||
this(target, 0, runtime, easing);
|
||||
}
|
||||
|
||||
public FadeAnimation( Shape target, int targetAlpha, int runtime, DoubleUnaryOperator easing ) {
|
||||
super(runtime, easing);
|
||||
|
||||
this.object = object;
|
||||
fill = object.getFillColor();
|
||||
this.target = target;
|
||||
this.targetAlpha = targetAlpha;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
fill = target.getFillColor();
|
||||
fillAlpha = fill.getAlpha();
|
||||
stroke = object.getStrokeColor();
|
||||
stroke = target.getStrokeColor();
|
||||
strokeAlpha = stroke.getAlpha();
|
||||
tAlpha = alpha;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getAnimationTarget() {
|
||||
return object;
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animate( double e ) {
|
||||
object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e)));
|
||||
object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e)));
|
||||
target.setFillColor(fill, (int) Constants.interpolate(fillAlpha, targetAlpha, e));
|
||||
target.setStrokeColor(stroke, (int) Constants.interpolate(strokeAlpha, targetAlpha, e));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
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;
|
||||
|
||||
public class FillAnimation extends Animation<Shape> {
|
||||
|
||||
private Shape object;
|
||||
private final Shape object;
|
||||
|
||||
private Color oFill, tFill;
|
||||
private Color originFill;
|
||||
|
||||
public FillAnimation( Shape object, Color newFill, int runtime, DoubleUnaryOperator easing ) {
|
||||
private final Color targetFill;
|
||||
|
||||
public FillAnimation( Shape target, Color newFill, int runtime, DoubleUnaryOperator easing ) {
|
||||
super(runtime, easing);
|
||||
|
||||
this.object = object;
|
||||
oFill = object.getFillColor();
|
||||
tFill = newFill;
|
||||
this.object = target;
|
||||
targetFill = newFill;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
originFill = object.getFillColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -27,7 +32,7 @@ public class FillAnimation extends Animation<Shape> {
|
||||
|
||||
@Override
|
||||
public void animate( double e ) {
|
||||
object.setFillColor(Color.interpolate(oFill, tFill, e));
|
||||
object.setFillColor(Color.interpolate(originFill, targetFill, e));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
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 MoveAnimation extends Animation<Shape> {
|
||||
|
||||
private Shape object;
|
||||
private final Shape object;
|
||||
|
||||
private double oX, oY, tX, tY;
|
||||
private final double targetX, targetY;
|
||||
|
||||
public MoveAnimation( Shape object, double x, double y, int runtime, DoubleUnaryOperator easing ) {
|
||||
private double originX, originY;
|
||||
|
||||
|
||||
public MoveAnimation( Shape target, double targetX, double targetY, int runtime, DoubleUnaryOperator easing ) {
|
||||
super(runtime, easing);
|
||||
|
||||
this.object = object;
|
||||
oX = object.getX();
|
||||
oY = object.getY();
|
||||
tX = x;
|
||||
tY = y;
|
||||
this.object = target;
|
||||
this.targetX = targetX;
|
||||
this.targetY = targetY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
originX = object.getX();
|
||||
originY = object.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,8 +35,8 @@ public class MoveAnimation extends Animation<Shape> {
|
||||
|
||||
@Override
|
||||
public void animate( double e ) {
|
||||
object.setX(Constants.interpolate(oX, tX, e));
|
||||
object.setY(Constants.interpolate(oY, tY, e));
|
||||
object.setX(Constants.interpolate(originX, targetX, e));
|
||||
object.setY(Constants.interpolate(originY, targetY, e));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
12
src/main/java/schule/ngb/zm/anim/package-info.java
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Dieses Paket enthält Klassen zur Animation von
|
||||
* {@link schule.ngb.zm.shapes.Shape} Objekten auf einem
|
||||
* {@link schule.ngb.zm.layers.ShapesLayer}.
|
||||
* <p>
|
||||
* Mit den Animationsklassen lassen sich neben {@code Shape} Objekten aber auch
|
||||
* andere Objekte animieren.
|
||||
* <p>
|
||||
* Das Paket setzt auf den funktionalen Programmierschnittstellen von Java auf
|
||||
* und kann als Einführung in das Paradigma dienen.
|
||||
*/
|
||||
package schule.ngb.zm.anim;
|
||||
@@ -57,9 +57,6 @@ public class ColorLayer extends Layer {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setSize( int width, int height ) {
|
||||
super.setSize(width, height);
|
||||
@@ -67,16 +64,14 @@ public class ColorLayer extends Layer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Hintergrundfarbe der Ebene zurück.
|
||||
*
|
||||
* @return Die aktuelle Hintergrundfarbe.
|
||||
* @return Die aktuelle Hintergrundfarbe der Ebene.
|
||||
*/
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Farbe der Ebene neu.
|
||||
* Setzt die Farbe der Ebene auf die angegebene Farbe.
|
||||
*
|
||||
* @param color Die neue Hintergrundfarbe.
|
||||
*/
|
||||
@@ -86,22 +81,69 @@ public class ColorLayer extends Layer {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
|
||||
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
|
||||
*
|
||||
* @param gray Ein Grauwert zwischen 0 und 255.
|
||||
* @see Color#Color(int)
|
||||
*/
|
||||
public void setColor( int gray ) {
|
||||
setColor(gray, gray, gray, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
|
||||
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
|
||||
* schwarz, 255 entspricht weiß.
|
||||
*
|
||||
* @param gray Ein Grauwert zwischen 0 und 255.
|
||||
* @param alpha Ein Transparenzwert zwischen 0 und 255.
|
||||
* @see Color#Color(int, int)
|
||||
*/
|
||||
public void setColor( int gray, int alpha ) {
|
||||
setColor(gray, gray, gray, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
|
||||
* und Blauanteilen.
|
||||
*
|
||||
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
|
||||
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
|
||||
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
|
||||
* @see Color#Color(int, int, int)
|
||||
* @see <a
|
||||
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
|
||||
*/
|
||||
public void setColor( int red, int green, int blue ) {
|
||||
setColor(red, green, blue, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
|
||||
* und Blauanteilen und dem angegebenen Transparenzwert.
|
||||
*
|
||||
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
|
||||
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
|
||||
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
|
||||
* @param alpha Ein Transparenzwert zwischen 0 und 25
|
||||
* @see Color#Color(int, int, int, int)
|
||||
* @see <a
|
||||
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
|
||||
*/
|
||||
public void setColor( int red, int green, int blue, int alpha ) {
|
||||
setColor(new Color(red, green, blue, alpha));
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der in die
|
||||
* angegebene Richtung verläuft.
|
||||
*
|
||||
* @param from Farbe am Startpunkt.
|
||||
* @param to Farbe am Endpunkt.
|
||||
* @param dir Richtung des Farbverlaufs.
|
||||
*/
|
||||
public void setGradient( Color from, Color to, Options.Direction dir ) {
|
||||
double halfW = getWidth() * .5;
|
||||
double halfH = getHeight() * .5;
|
||||
@@ -116,6 +158,18 @@ public class ColorLayer extends Layer {
|
||||
setGradient(fromX, fromY, from, toX, toY, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der am Punkt
|
||||
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
|
||||
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
|
||||
*
|
||||
* @param fromX x-Koordinate des Startpunktes.
|
||||
* @param fromY y-Koordinate des Startpunktes.
|
||||
* @param from Farbe am Startpunkt.
|
||||
* @param toX x-Koordinate des Endpunktes.
|
||||
* @param toY y-Koordinate des Endpunktes.
|
||||
* @param to Farbe am Endpunkt.
|
||||
*/
|
||||
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
|
||||
this.color = from;
|
||||
background = new GradientPaint(
|
||||
@@ -125,10 +179,30 @@ public class ColorLayer extends Layer {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
|
||||
* Farbverlauf, der im Zentrum beginnt.
|
||||
*
|
||||
* @param from Farbe im Zentrum.
|
||||
* @param to Farbe am Rand.
|
||||
*/
|
||||
public void setGradient( Color from, Color to ) {
|
||||
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
|
||||
* Farbverlauf, mit dem Zentrum im Punkt ({@code centerX}, {@code centerY})
|
||||
* und dem angegebenen Radius. Der Verlauf starte im Zentrum mit der Farbe
|
||||
* {@code from} und endet am Rand des durch den Radius beschriebenen Kreises
|
||||
* mit der Farbe {@code to}.
|
||||
*
|
||||
* @param centerX x-Koordinate des Kreismittelpunktes.
|
||||
* @param centerY y-Koordinate des Kreismittelpunktes.
|
||||
* @param radius Radius des Kreises.
|
||||
* @param from Farbe im Zentrum des Kreises.
|
||||
* @param to Farbe am Rand des Kreises.
|
||||
*/
|
||||
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
|
||||
this.color = from;
|
||||
background = new RadialGradientPaint(
|
||||
@@ -138,6 +212,9 @@ public class ColorLayer extends Layer {
|
||||
clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeichnet den Hintergrund der Ebene mit der gesetzten Füllung neu.
|
||||
*/
|
||||
@Override
|
||||
public void clear() {
|
||||
drawing.setPaint(background);
|
||||
|
||||
@@ -87,9 +87,6 @@ public class DrawableLayer extends Layer {
|
||||
this.clearBeforeDraw = pClearBeforeDraw;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void draw( Graphics2D graphics ) {
|
||||
if( clearBeforeDraw ) {
|
||||
|
||||
@@ -6,53 +6,121 @@ 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();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
@@ -11,7 +12,7 @@ import java.util.*;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
/**
|
||||
* Ein Layer um {@link Shape} Objekte zu zeichnen.
|
||||
* Eine Ebene um {@link Shape} Objekte zu zeichnen.
|
||||
* <p>
|
||||
* Ein {@code ShapesLayer} ist eine der drei Standardebenen der
|
||||
* {@link schule.ngb.zm.Zeichenmaschine}.
|
||||
@@ -19,22 +20,31 @@ import java.util.function.DoubleUnaryOperator;
|
||||
@SuppressWarnings( "unused" )
|
||||
public class ShapesLayer extends Layer {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected boolean clearBeforeDraw = true;
|
||||
|
||||
private final List<Shape> shapes;
|
||||
protected boolean updateShapes = true;
|
||||
|
||||
protected final List<Shape> shapes;
|
||||
|
||||
private final List<Animation<? extends Shape>> animations;
|
||||
|
||||
private final List<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 ) {
|
||||
@@ -67,12 +77,24 @@ public class ShapesLayer extends Layer {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +158,27 @@ public class ShapesLayer extends Layer {
|
||||
|
||||
@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();
|
||||
|
||||
@@ -12,6 +12,12 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Eine Ebene, auf der eine Turtle gesteuert werden kann, die Grafiken plottet.
|
||||
* <p>
|
||||
* Die Turtle verhält sich ähnlich zu ihren Entsprechungen in Logo oder
|
||||
* TigerJython.
|
||||
*/
|
||||
@SuppressWarnings( "unused" )
|
||||
public class TurtleLayer extends Layer implements Strokeable, Fillable {
|
||||
|
||||
@@ -224,6 +230,11 @@ public class TurtleLayer extends Layer implements Strokeable, Fillable {
|
||||
return mainTurtle.getStrokeType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Options.StrokeJoin getStrokeJoin() {
|
||||
return mainTurtle.getStrokeJoin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStrokeType( Options.StrokeType type ) {
|
||||
mainTurtle.setStrokeType(type);
|
||||
|
||||
8
src/main/java/schule/ngb/zm/layers/package-info.java
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Dieses Paket enthält implementationen der abstrakten
|
||||
* {@link schule.ngb.zm.Layer} Klasse.
|
||||
* <p>
|
||||
* {@code Layer} sind Ebenen, die der {@link schule.ngb.zm.Zeichenleinwand}
|
||||
* hinzugefügt und pro Frame gerendert werden.
|
||||
*/
|
||||
package schule.ngb.zm.layers;
|
||||
@@ -1,10 +1,21 @@
|
||||
package schule.ngb.zm.media;
|
||||
|
||||
/**
|
||||
* Interface für Audio-Medien.
|
||||
* Schnittstelle für Audio-Medien.
|
||||
*
|
||||
* <h2>MP3-Dateien verwenden</h2>
|
||||
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
|
||||
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
|
||||
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
|
||||
* Details zur Verwendung können in der <a
|
||||
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
|
||||
* der Zeichenmaschine</a> nachgelesen werden.
|
||||
*/
|
||||
public interface Audio {
|
||||
|
||||
/**
|
||||
* @return Die Quelle, aus der das Medium geladen wurde.
|
||||
*/
|
||||
String getSource();
|
||||
|
||||
/**
|
||||
@@ -17,7 +28,7 @@ public interface Audio {
|
||||
|
||||
/**
|
||||
* Prüft, ob das Medium gerade in einer Schleife abgespielt wird. Wenn
|
||||
* {@code isLooping() == true}, dann muss auch immer
|
||||
* {@code isLooping() == true} gilt, dann muss auch immer
|
||||
* {@code isPlaying() == true} gelten.
|
||||
*
|
||||
* @return {@code true}, wenn das Medium in einer Schleife abgespielt wird,
|
||||
@@ -30,7 +41,7 @@ public interface Audio {
|
||||
* <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.
|
||||
* Mediums. Negative Werte setzen die Lautstärke aud 0.
|
||||
*
|
||||
* @param volume Die neue Lautstärke zwischen 0 und 1.
|
||||
* @see <a
|
||||
@@ -39,7 +50,7 @@ public interface Audio {
|
||||
void setVolume( double volume );
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Lautstärkeeinstellung dieses Mediums zurück.
|
||||
* 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
|
||||
@@ -50,8 +61,16 @@ public interface Audio {
|
||||
double getVolume();
|
||||
|
||||
/**
|
||||
* Startet die Wiedergabe des Mediums und beendet die Methode. Das
|
||||
* Audio-Medium wird einmal abgespielt und stoppt dann.
|
||||
* 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();
|
||||
|
||||
@@ -63,21 +82,36 @@ public interface Audio {
|
||||
|
||||
/**
|
||||
* Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode
|
||||
* startet die Wiedergabe und beendet dann direkt die Methode. Um die
|
||||
* Wiedergabe zu stoppen muss {@link #stop()} aufgerufen werden.
|
||||
* 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.
|
||||
* ({@code isPlaying() == false}), dann passiert nichts.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Stoppt die Wiedergabe und gibt alle Resourcen, die für das Medium
|
||||
* Stoppt die Wiedergabe und gibt alle Ressourcen, die für das Medium
|
||||
* verwendet werden, frei.
|
||||
*/
|
||||
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 );
|
||||
|
||||
}
|
||||
|
||||
@@ -2,10 +2,33 @@ 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> {
|
||||
|
||||
void start( Audio source );
|
||||
/**
|
||||
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Mediums startet, dem
|
||||
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
|
||||
* hinzugefügt wurde.
|
||||
*
|
||||
* @param source Das Audio-Medium, dessen Wiedergabe gestartet wurde.
|
||||
*/
|
||||
void playbackStarted( Audio source );
|
||||
|
||||
void stop( Audio source );
|
||||
/**
|
||||
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Mediums stoppt, dem
|
||||
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
|
||||
* hinzugefügt wurde.
|
||||
*
|
||||
* @param source Das Audio-Medium, dessen Wiedergabe gestoppt wurde.
|
||||
*/
|
||||
void playbackStopped( Audio source );
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -18,24 +20,14 @@ import java.util.List;
|
||||
* Darüber hinaus kann ein Mixer Effekte wie einen
|
||||
* {@link #fade(double, int) fadeIn} auf die Medien anwenden.
|
||||
*/
|
||||
public class Mixer implements Audio {
|
||||
@SuppressWarnings( "unused" )
|
||||
public class Mixer implements Audio, AudioListener {
|
||||
|
||||
private List<AudioWrapper> audios;
|
||||
|
||||
private float volume = 0.8f;
|
||||
|
||||
class AudioWrapper {
|
||||
|
||||
Audio audio;
|
||||
|
||||
float volumeFactor;
|
||||
|
||||
public AudioWrapper( Audio audio, float volumeFactor ) {
|
||||
this.audio = audio;
|
||||
this.volumeFactor = volumeFactor;
|
||||
}
|
||||
|
||||
}
|
||||
EventDispatcher<Audio, AudioListener> eventDispatcher;
|
||||
|
||||
public Mixer() {
|
||||
this.audios = new ArrayList<>(4);
|
||||
@@ -45,66 +37,93 @@ public class Mixer implements Audio {
|
||||
return "";
|
||||
}
|
||||
|
||||
public void add( Audio pAudio ) {
|
||||
add(pAudio, 1f);
|
||||
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 ) {
|
||||
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
|
||||
if( !contains(pAudio) ) {
|
||||
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
|
||||
} else {
|
||||
findWrapper(pAudio).volumeFactor = (float) pVolumeFactor;
|
||||
}
|
||||
pAudio.setVolume(pVolumeFactor * volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isLooping() {
|
||||
return audios.stream().anyMatch(aw -> aw.audio.isLooping());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setVolume( double pVolume ) {
|
||||
volume = (float) pVolume;
|
||||
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * pVolume));
|
||||
public void setVolume( double volume ) {
|
||||
this.volume = volume < 0 ? 0f : (float) volume;
|
||||
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * volume));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void play() {
|
||||
audios.stream().forEach(aw -> aw.audio.play());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void playAndWait() {
|
||||
audios.stream().forEach(aw -> aw.audio.play());
|
||||
@@ -117,25 +136,16 @@ public class Mixer implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void loop() {
|
||||
audios.stream().forEach(aw -> aw.audio.loop());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
audios.stream().forEach(aw -> aw.audio.stop());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void dispose() {
|
||||
if( isPlaying() ) {
|
||||
@@ -178,4 +188,58 @@ public class Mixer implements Audio {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playbackStarted( Audio source ) {
|
||||
if( eventDispatcher != null ) {
|
||||
eventDispatcher.dispatchEvent("start", Mixer.this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playbackStopped( Audio source ) {
|
||||
if( !isPlaying() ) {
|
||||
if( eventDispatcher != null ) {
|
||||
eventDispatcher.dispatchEvent("stop", Mixer.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAudioListener( AudioListener listener ) {
|
||||
initializeEventDispatcher().addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAudioListener( AudioListener listener ) {
|
||||
initializeEventDispatcher().removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
|
||||
* aufgerufen, sobald sich auch ein Listener registrieren möchte.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
|
||||
if( eventDispatcher == null ) {
|
||||
eventDispatcher = new EventDispatcher<>();
|
||||
eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
|
||||
eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
|
||||
}
|
||||
return eventDispatcher;
|
||||
}
|
||||
|
||||
class AudioWrapper {
|
||||
|
||||
Audio audio;
|
||||
|
||||
float volumeFactor;
|
||||
|
||||
public AudioWrapper( Audio audio, float volumeFactor ) {
|
||||
this.audio = audio;
|
||||
this.volumeFactor = volumeFactor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,83 +1,103 @@
|
||||
package schule.ngb.zm.media;
|
||||
|
||||
import schule.ngb.zm.util.events.EventDispatcher;
|
||||
import schule.ngb.zm.util.tasks.TaskRunner;
|
||||
import schule.ngb.zm.util.Log;
|
||||
import schule.ngb.zm.util.io.ResourceStreamProvider;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Ein Musikstück, dass im Projekt abgespielt werden soll.
|
||||
* Eine Musik, die abgespielt werden kann.
|
||||
* <p>
|
||||
* Im gegensatz zu einem {@link Sound} sind Musikstücke längere Audiodateien,
|
||||
* die zum Beispiel als Hintergrundmusik ablaufen sollen. Die Musik wird daher
|
||||
* nicht komplett in den Speicher geladen, sondern direkt aus der Audioquelle
|
||||
* gestreamt und wiedergegeben.
|
||||
* 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 {
|
||||
|
||||
// size of the byte buffer used to read/write the audio stream
|
||||
/**
|
||||
* Größe des verwendeten Input-Puffers für die Audiodaten.
|
||||
*/
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
|
||||
/**
|
||||
* Ob der Sound gerade abgespielt wird.
|
||||
* Ob der Sound aktuell abgespielt wird.
|
||||
*/
|
||||
private boolean playing = false;
|
||||
|
||||
/**
|
||||
* Ob der Sound gerade in einer Schleife abgespielt wird.
|
||||
* Ob der Sound aktuell in einer Schleife abgespielt wird.
|
||||
*/
|
||||
private boolean looping = false;
|
||||
|
||||
/**
|
||||
* Die Quelle des Musikstücks.
|
||||
* Die Quelle der Audiodaten.
|
||||
*/
|
||||
private String audioSource;
|
||||
|
||||
/**
|
||||
* Der AudioStream, um die AUdiosdaten zulsen, falls dieser schon geöffnet
|
||||
* wurde. Sonst {@code null}.
|
||||
* Der {@link AudioInputStream}, um die Audiosdaten zu lesen. {@code null},
|
||||
* falls noch kein Stream geöffnet wurde.
|
||||
*/
|
||||
private AudioInputStream audioStream;
|
||||
|
||||
/**
|
||||
* Die Line für die Ausgabe, falls diese schon geöffnet wurde. Sonst
|
||||
* {@code null}.
|
||||
* Die {@link SourceDataLine} für die Ausgabe. {@code null}, falls die
|
||||
* Audiodatei noch nicht geöffnet wurde.
|
||||
*/
|
||||
private SourceDataLine audioLine;
|
||||
|
||||
/**
|
||||
* Die Lautstärke der Musik.
|
||||
* Die aktuelle Lautstärke des Mediums.
|
||||
*/
|
||||
private float volume = 0.8f;
|
||||
|
||||
/**
|
||||
* Dispatcher für Audio-Events (start und stop).
|
||||
*/
|
||||
EventDispatcher<Audio, AudioListener> eventDispatcher;
|
||||
|
||||
public Music( String source ) {
|
||||
Validator.requireNotNull(source);
|
||||
this.audioSource = source;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isLooping() {
|
||||
if( !playing ) {
|
||||
@@ -86,28 +106,21 @@ public class Music implements Audio {
|
||||
return looping;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setVolume( double volume ) {
|
||||
this.volume = (float) volume;
|
||||
this.volume = volume < 0 ? 0f : (float) volume;
|
||||
if( audioLine != null ) {
|
||||
applyVolume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen
|
||||
* anzuwenden.
|
||||
* Wendet die Lautstärke vor dem Abspielen auf den Audiostream an.
|
||||
*/
|
||||
private void applyVolume() {
|
||||
FloatControl gainControl =
|
||||
@@ -118,24 +131,13 @@ public class Music implements Audio {
|
||||
gainControl.setValue(vol);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void play() {
|
||||
if( openLine() ) {
|
||||
TaskRunner.run(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
stream();
|
||||
}
|
||||
});
|
||||
TaskRunner.run(this::stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void playAndWait() {
|
||||
if( openLine() ) {
|
||||
@@ -143,18 +145,12 @@ public class Music implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void loop() {
|
||||
looping = true;
|
||||
play();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
playing = false;
|
||||
@@ -162,9 +158,6 @@ public class Music implements Audio {
|
||||
dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
if( audioLine != null ) {
|
||||
@@ -188,7 +181,17 @@ public class Music implements Audio {
|
||||
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 ) {
|
||||
@@ -225,6 +228,14 @@ public class Music implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Öffnet eine {@link SourceDataLine} für die
|
||||
* {@link #audioSource Audioquelle} und bereitet die Wiedergabe vor. Es wird
|
||||
* noch nichts abgespielt.
|
||||
*
|
||||
* @return {@code true}, wenn die Line geöffnet werden konnte, {@code false}
|
||||
* sonst.
|
||||
*/
|
||||
private boolean openLine() {
|
||||
if( audioLine != null ) {
|
||||
return true;
|
||||
@@ -261,6 +272,15 @@ public class Music implements Audio {
|
||||
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();
|
||||
|
||||
@@ -276,24 +296,27 @@ public class Music implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener( AudioListener listener ) {
|
||||
@Override
|
||||
public void addAudioListener( AudioListener listener ) {
|
||||
initializeEventDispatcher().addListener(listener);
|
||||
}
|
||||
|
||||
public void removeListener( AudioListener listener ) {
|
||||
@Override
|
||||
public void removeAudioListener( AudioListener listener ) {
|
||||
initializeEventDispatcher().removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
|
||||
* aufgerufen, soblad sich auch ein Listener registrieren möchte.
|
||||
* @return
|
||||
* 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.start(a));
|
||||
eventDispatcher.registerEventType("stop", (a,l) -> l.stop(a));
|
||||
eventDispatcher.registerEventType("start", ( a, l ) -> l.playbackStarted(a));
|
||||
eventDispatcher.registerEventType("stop", ( a, l ) -> l.playbackStopped(a));
|
||||
}
|
||||
return eventDispatcher;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.*;
|
||||
@@ -9,78 +10,87 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Wiedergabe kurzer Soundclips, die mehrmals wiederverwendet werden.
|
||||
* Ein kurzer Soundclip, der mehrmals wiederverwendet werden kann.
|
||||
* <p>
|
||||
* In Spielen und anderen Projekten gibt es oftmals eine Reihe kurzer Sounds,
|
||||
* die zusammen mit bestimmten Aktionen wiedergegeben werden (zum Beispiel, wenn
|
||||
* die Spielfigur springt, wenn zwei Objekte kollidieren, usw.). Sounds werden
|
||||
* komplett in den Speicher geladen und können dadurch immer wieder, als
|
||||
* Schleife oder auch nur Abschnittsweise abgespielt werden.
|
||||
* 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ängre Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher die
|
||||
* KLasse {@link Music} an.
|
||||
* 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 gerade abgespielt wird.
|
||||
* Ob der Sound aktuell abgespielt wird.
|
||||
*/
|
||||
private boolean playing = false;
|
||||
|
||||
/**
|
||||
* Ob der Sound gerade in einer Schleife abgespielt wird.
|
||||
* Ob der Sound aktuell in einer Schleife abgespielt wird.
|
||||
*/
|
||||
private boolean looping = false;
|
||||
|
||||
/**
|
||||
* Die Quelle des Musikstücks.
|
||||
* Die Quelle der Audiodaten.
|
||||
*/
|
||||
private String audioSource;
|
||||
|
||||
/**
|
||||
* Der Clip, falls er schon geladen wurde, sonst {@code null}.
|
||||
* Der Clip, falls er schon geladen wurde. Ansonsten {@code null}.
|
||||
*/
|
||||
private Clip audioClip;
|
||||
|
||||
/**
|
||||
* Ob die Resourcen des Clips im Speicher nach dem nächsten Abspielen
|
||||
* Ob die Ressourcen des Clips im Speicher nach dem nächsten Abspielen
|
||||
* freigegeben werden sollen.
|
||||
*/
|
||||
private boolean disposeAfterPlay = false;
|
||||
|
||||
/**
|
||||
* Die Lautstärke des Clips.
|
||||
* 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 Ein Dateipfad oder eine Webadresse.
|
||||
* @param source Quelle der Audiodaten.
|
||||
* @throws NullPointerException Falls die Quelle {@code null} ist.
|
||||
* @see ResourceStreamProvider#getResourceURL(String)
|
||||
*/
|
||||
public Sound( String source ) {
|
||||
Validator.requireNotNull(source);
|
||||
Validator.requireNotNull(source, "source");
|
||||
this.audioSource = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSource() {
|
||||
return audioSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
// return audioClip != null && audioClip.isRunning();
|
||||
return playing;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean isLooping() {
|
||||
if( !playing ) {
|
||||
@@ -89,28 +99,21 @@ public class Sound implements Audio {
|
||||
return looping;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setVolume( double volume ) {
|
||||
this.volume = (float) volume;
|
||||
this.volume = volume < 0 ? 0f : (float) volume;
|
||||
if( audioClip != null ) {
|
||||
applyVolume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen
|
||||
* anzuwenden.
|
||||
* Wendet die Lautstärke vor dem Abspielen auf den Clip an.
|
||||
*/
|
||||
private void applyVolume() {
|
||||
FloatControl gainControl =
|
||||
@@ -121,9 +124,6 @@ public class Sound implements Audio {
|
||||
gainControl.setValue(vol);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
looping = false;
|
||||
@@ -133,9 +133,6 @@ public class Sound implements Audio {
|
||||
playing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void play() {
|
||||
if( this.openClip() ) {
|
||||
@@ -144,9 +141,6 @@ public class Sound implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void playAndWait() {
|
||||
this.play();
|
||||
@@ -165,7 +159,7 @@ public class Sound implements Audio {
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips
|
||||
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
|
||||
* frei.
|
||||
* <p>
|
||||
* Der Aufruf ist effektiv gleich zu
|
||||
@@ -182,7 +176,7 @@ public class Sound implements Audio {
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips
|
||||
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
|
||||
* frei.
|
||||
* <p>
|
||||
* Der Aufruf entspricht
|
||||
@@ -196,17 +190,18 @@ public class Sound implements Audio {
|
||||
playAndWait();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void loop() {
|
||||
loop(Clip.LOOP_CONTINUOUSLY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und
|
||||
* stoppt die Wiedergabe dann.
|
||||
* 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.
|
||||
*/
|
||||
@@ -228,20 +223,24 @@ public class Sound implements Audio {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public synchronized void dispose() {
|
||||
if( audioClip != null ) {
|
||||
if( audioClip.isRunning() ) {
|
||||
audioClip.stop();
|
||||
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);
|
||||
@@ -259,7 +258,11 @@ public class Sound implements Audio {
|
||||
audioClip.addLineListener(new LineListener() {
|
||||
@Override
|
||||
public void update( LineEvent event ) {
|
||||
if( event.getType() == LineEvent.Type.STOP ) {
|
||||
if( event.getType() == LineEvent.Type.START ) {
|
||||
if( eventDispatcher != null ) {
|
||||
eventDispatcher.dispatchEvent("start", Sound.this);
|
||||
}
|
||||
} else if( event.getType() == LineEvent.Type.STOP ) {
|
||||
playbackStopped();
|
||||
}
|
||||
}
|
||||
@@ -296,18 +299,51 @@ public class Sound implements Audio {
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Interne Methode, die aufgerufen wird, wenn die Wiedergabe gestoppt wird.
|
||||
* Entweder durch einen Aufruf von {@link #stop()} oder, weil die Wiedergabe
|
||||
* nach {@link #playOnce()} beendet wurde.
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
||||
7
src/main/java/schule/ngb/zm/media/package-info.java
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Dieses Paket enthält Klassen zur Einbettung von Mediendateien.
|
||||
* <p>
|
||||
* Mit Medien sind vor allem Audio und Videodateien gemeint. Aktuell kann die
|
||||
* Zeichenmaschine Audiodateien verwenden.
|
||||
*/
|
||||
package schule.ngb.zm.media;
|
||||
@@ -93,32 +93,20 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
rows * columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int columns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int rows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
int idx( int r, int c ) {
|
||||
return c * rows + r;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double get( int row, int col ) {
|
||||
try {
|
||||
@@ -128,9 +116,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix set( int row, int col, double value ) {
|
||||
try {
|
||||
@@ -141,52 +126,34 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix initializeRandom() {
|
||||
return initializeRandom(-1.0, 1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix initializeRandom( double lower, double upper ) {
|
||||
applyInPlace(( d ) -> ((upper - lower) * Constants.random()) + lower);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix initializeOne() {
|
||||
applyInPlace(( d ) -> 1.0);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix initializeZero() {
|
||||
applyInPlace(( d ) -> 0.0);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix duplicate() {
|
||||
return new DoubleMatrix(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix multiplyTransposed( MLMatrix B ) {
|
||||
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
|
||||
@@ -208,9 +175,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix multiplyAddBias( final MLMatrix B, final MLMatrix C ) {
|
||||
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
|
||||
@@ -233,9 +197,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix transposedMultiplyAndScale( final MLMatrix B, final double scalar ) {
|
||||
/*return new DoubleMatrix(IntStream.range(0, columns).parallel().mapToObj(
|
||||
@@ -258,9 +219,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix add( MLMatrix B ) {
|
||||
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
|
||||
@@ -277,9 +235,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix addInPlace( MLMatrix B ) {
|
||||
for( int j = 0; j < columns; j++ ) {
|
||||
@@ -290,9 +245,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix sub( MLMatrix B ) {
|
||||
/*return new DoubleMatrix(IntStream.range(0, rows).parallel().mapToObj(
|
||||
@@ -309,9 +261,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix colSums() {
|
||||
/*DoubleMatrix colSums = new DoubleMatrix(1, columns);
|
||||
@@ -331,9 +280,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return colSums;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix scaleInPlace( final double scalar ) {
|
||||
for( int i = 0; i < coefficients.length; i++ ) {
|
||||
@@ -342,9 +288,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix scaleInPlace( final MLMatrix S ) {
|
||||
for( int j = 0; j < columns; j++ ) {
|
||||
@@ -355,9 +298,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix apply( DoubleUnaryOperator op ) {
|
||||
DoubleMatrix result = new DoubleMatrix(rows, columns);
|
||||
@@ -367,9 +307,6 @@ public final class DoubleMatrix implements MLMatrix {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public MLMatrix applyInPlace( DoubleUnaryOperator op ) {
|
||||
for( int i = 0; i < coefficients.length; i++ ) {
|
||||
|
||||
16
src/main/java/schule/ngb/zm/ml/package-info.java
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Dieses Paekt enthält Klassen für Experimente mit Verfahren des maschinellen
|
||||
* Lernens (ML).
|
||||
* <p>
|
||||
* Die hier implementierten Klassen sind eine prototypische Umsetzung von
|
||||
* einfachen neuronalen Netzwerken, mit denen an kleinen Problemstellungen
|
||||
* experimentell Modelle trainiert und angewandt werden können.
|
||||
* <p>
|
||||
* Die Implementierungen sind nicht optimiert und setzen auf native
|
||||
* Java-Methoden. Daher sind sie nur für die Anwendung auf extrem kleine Modelle
|
||||
* in Bildungskontexten gedacht.
|
||||
* <p>
|
||||
* Durch Einbettung wissenschaftlicher Bibliotheken mit optimierten Operationen
|
||||
* lassen sich bessere Ergebnisse erreichen.
|
||||
*/
|
||||
package schule.ngb.zm.ml;
|
||||
13
src/main/java/schule/ngb/zm/package-info.java
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* <h2>Die Zeichenmaschine</h2>
|
||||
* <p>
|
||||
* Die <b>Zeichenmaschine</b> ist eine für den Informatikunterricht entwickelte
|
||||
* Bibliothek, die unter anderem an <a
|
||||
* href="https://processing.org/">Processing</a> angelehnt ist. Die Bibliothek
|
||||
* soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen und
|
||||
* für Schülerinnen und Schüler im Unterricht nutzbar machen.
|
||||
* <p>
|
||||
* Eine umfassende Dokumentation ist unter <a
|
||||
* href="https://zeichenmaschine.xyz">zeichenmaschine.xyz</a> verfügbar.
|
||||
*/
|
||||
package schule.ngb.zm;
|
||||
77
src/main/java/schule/ngb/zm/particles/BasicParticle.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.Color;
|
||||
import schule.ngb.zm.Vector;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
|
||||
public class BasicParticle extends Particle {
|
||||
|
||||
protected Color color, startColor, finalColor;
|
||||
|
||||
|
||||
public BasicParticle() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BasicParticle( Color startColor ) {
|
||||
this(startColor, null);
|
||||
}
|
||||
|
||||
public BasicParticle( Color startColor, Color finalColor ) {
|
||||
super();
|
||||
|
||||
this.color = startColor;
|
||||
this.startColor = startColor;
|
||||
this.finalColor = finalColor;
|
||||
}
|
||||
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor( Color pColor ) {
|
||||
this.color = pColor;
|
||||
}
|
||||
|
||||
public Color getStartColor() {
|
||||
return startColor;
|
||||
}
|
||||
|
||||
public void setStartColor( Color pStartColor ) {
|
||||
this.startColor = pStartColor;
|
||||
}
|
||||
|
||||
public Color getFinalColor() {
|
||||
return finalColor;
|
||||
}
|
||||
|
||||
public void setFinalColor( Color pFinalColor ) {
|
||||
this.finalColor = pFinalColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawn( int pLifetime, Vector pPosition, Vector pVelocity ) {
|
||||
super.spawn(pLifetime, pPosition, pVelocity);
|
||||
this.color = this.startColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
super.update(delta);
|
||||
|
||||
if( isActive() && startColor != null && finalColor != null ) {
|
||||
double t = 1.0 - lifetime / maxLifetime;
|
||||
this.color = Color.interpolate(startColor, finalColor, t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw( Graphics2D graphics ) {
|
||||
if( isActive() && this.color != null ) {
|
||||
graphics.setColor(this.color.getJavaColor());
|
||||
graphics.fillOval(position.getIntX() - 3, position.getIntY() - 3, 6, 6);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.Color;
|
||||
|
||||
public class BasicParticleFactory implements ParticleFactory {
|
||||
|
||||
private final Color startColor;
|
||||
|
||||
private final Color finalColor;
|
||||
|
||||
private boolean fadeOut = true;
|
||||
|
||||
public BasicParticleFactory() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public BasicParticleFactory( Color startColor ) {
|
||||
this(startColor, null);
|
||||
}
|
||||
|
||||
public BasicParticleFactory( Color startColor, Color finalColor ) {
|
||||
this.startColor = startColor;
|
||||
this.finalColor = finalColor;
|
||||
}
|
||||
|
||||
public void setFadeOut( boolean pFadeOut ) {
|
||||
this.fadeOut = pFadeOut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Particle createParticle() {
|
||||
Color finalClr = finalColor;
|
||||
if( fadeOut ) {
|
||||
if( finalColor != null ) {
|
||||
finalClr = new Color(finalColor, 0);
|
||||
} else if( startColor != null ) {
|
||||
finalClr = new Color(startColor, 0);
|
||||
}
|
||||
}
|
||||
return new BasicParticle(startColor, finalClr);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class GenericParticleFactory<T extends Particle> implements ParticleFactory {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
private final Supplier<T> supplier;
|
||||
|
||||
public GenericParticleFactory( Class<T> type, Object... params ) {
|
||||
this.type = type;
|
||||
|
||||
// Create paramTypes array once
|
||||
Class<?>[] paramTypes = new Class<?>[params.length];
|
||||
for( int i = 0; i < params.length; i++ ) {
|
||||
paramTypes[i] = params[i].getClass();
|
||||
}
|
||||
|
||||
this.supplier = () -> {
|
||||
T p = null;
|
||||
try {
|
||||
p = GenericParticleFactory.this.type.getDeclaredConstructor(paramTypes).newInstance(params);
|
||||
} catch( Exception ex ) {
|
||||
LOG.error( ex,
|
||||
"Unable to create new Particle of type %s",
|
||||
GenericParticleFactory.this.type.getCanonicalName()
|
||||
);
|
||||
}
|
||||
return p;
|
||||
};
|
||||
}
|
||||
|
||||
public GenericParticleFactory( Supplier<T> supplier ) {
|
||||
this.supplier = supplier;
|
||||
this.type = (Class<T>)supplier.get().getClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Particle createParticle() {
|
||||
return this.supplier.get();
|
||||
}
|
||||
|
||||
private static final Log LOG = Log.getLogger(GenericParticleFactory.class);
|
||||
|
||||
}
|
||||
60
src/main/java/schule/ngb/zm/particles/Particle.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.Drawable;
|
||||
import schule.ngb.zm.Updatable;
|
||||
import schule.ngb.zm.Vector;
|
||||
|
||||
|
||||
public abstract class Particle extends PhysicsObject implements Updatable, Drawable {
|
||||
|
||||
protected double maxLifetime = 0, lifetime = 0;
|
||||
|
||||
|
||||
public Particle() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void spawn( int pLifetime, Vector pPosition, Vector pVelocity ) {
|
||||
this.maxLifetime = pLifetime;
|
||||
this.lifetime = pLifetime;
|
||||
this.position = pPosition.copy();
|
||||
this.velocity = pVelocity.copy();
|
||||
this.acceleration = new Vector();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return lifetime > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return isActive();
|
||||
}
|
||||
|
||||
public double getLifetime() {
|
||||
return lifetime;
|
||||
}
|
||||
|
||||
public void setLifetime( double pLifetime ) {
|
||||
this.lifetime = pLifetime;
|
||||
}
|
||||
|
||||
public double getMaxLifetime() {
|
||||
return maxLifetime;
|
||||
}
|
||||
|
||||
public void setMaxLifetime( double pMaxLifetime ) {
|
||||
this.maxLifetime = pMaxLifetime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
super.update(delta);
|
||||
// lifetime -= delta;
|
||||
lifetime -= 1;
|
||||
|
||||
// TODO: (ngb) calculate delta based on lifetime?
|
||||
}
|
||||
|
||||
}
|
||||
174
src/main/java/schule/ngb/zm/particles/ParticleEmitter.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
|
||||
import schule.ngb.zm.Drawable;
|
||||
import schule.ngb.zm.Updatable;
|
||||
import schule.ngb.zm.Vector;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
|
||||
public class ParticleEmitter implements Updatable, Drawable {
|
||||
|
||||
protected ParticleFactory particleFactory;
|
||||
|
||||
private int particlesPerFrame;
|
||||
|
||||
private int particleLifetime = 180;
|
||||
|
||||
private Particle[] particles;
|
||||
|
||||
private boolean active = false;
|
||||
|
||||
private Particle nextParticle;
|
||||
|
||||
public Vector position;
|
||||
|
||||
public Vector direction = new Vector();
|
||||
|
||||
public double strength = 100.0;
|
||||
|
||||
public int angle = 0;
|
||||
|
||||
public double randomness = 0.0;
|
||||
|
||||
// private Vortex vortex = null;
|
||||
|
||||
public ParticleEmitter( double pX, double pY, int pParticleLifetime, int pParticlesPerFrame, ParticleFactory pFactory ) {
|
||||
this.position = new Vector(pX, pY);
|
||||
this.particlesPerFrame = pParticlesPerFrame;
|
||||
this.particleLifetime = pParticleLifetime;
|
||||
this.particleFactory = pFactory;
|
||||
|
||||
// Create particle pool
|
||||
this.particles = new Particle[particlesPerFrame * pParticleLifetime];
|
||||
this.direction = Vector.random(8, 16).normalize();
|
||||
|
||||
// vortex = new Vortex(position.copy().add(-10, -10), -.2, 8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.direction.normalize();
|
||||
|
||||
// Partikel initialisieren
|
||||
for( int i = 0; i < particles.length; i++ ) {
|
||||
particles[i] = particleFactory.createParticle();
|
||||
}
|
||||
|
||||
active = true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for( int i = 0; i < particles.length; i++ ) {
|
||||
particles[i].setLifetime(0);
|
||||
particles[i] = null;
|
||||
}
|
||||
active = false;
|
||||
}
|
||||
|
||||
private Particle getNextParticle() {
|
||||
// TODO: improve by caching next particle
|
||||
for( Particle p : particles ) {
|
||||
if( p != null && !p.isActive() ) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void emitParticle() {
|
||||
int ppf = particlesPerFrame;
|
||||
Particle nextParticle = getNextParticle();
|
||||
while( ppf > 0 && nextParticle != null ) {
|
||||
int lifetime = (int) random(particleLifetime);
|
||||
|
||||
double rotation = (angle / 2.0) - (int) (Math.random() * angle);
|
||||
Vector velocity = direction.copy().scale(strength).rotate(rotation);
|
||||
velocity.scale(random());
|
||||
|
||||
nextParticle.spawn(lifetime, this.position, velocity);
|
||||
nextParticle = getNextParticle();
|
||||
ppf -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
emitParticle();
|
||||
|
||||
boolean _active = false;
|
||||
for( Particle particle : particles ) {
|
||||
if( particle != null ) {
|
||||
if( particle.isActive() ) {
|
||||
// if( vortex != null ) {
|
||||
// vortex.attract(particle);
|
||||
// }
|
||||
particle.update(delta);
|
||||
_active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.active = _active;
|
||||
}
|
||||
|
||||
private double random() {
|
||||
return 1.0 - (Math.random() * randomness);
|
||||
}
|
||||
|
||||
private double random( double pZahl ) {
|
||||
return pZahl * random();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw( Graphics2D graphics ) {
|
||||
java.awt.Color current = graphics.getColor();
|
||||
for( Particle particle : particles ) {
|
||||
if( particle != null && particle.isVisible() ) {
|
||||
particle.draw(graphics);
|
||||
}
|
||||
}
|
||||
|
||||
// if( vortex != null ) {
|
||||
// graphics.setColor(java.awt.Color.BLACK);
|
||||
// double vscale = (4 * vortex.scale);
|
||||
// graphics.fillOval((int) (vortex.position.x - vscale * .5), (int) (vortex.position.y - vscale * .5), (int) vscale, (int) vscale);
|
||||
// }
|
||||
graphics.setColor(current);
|
||||
}
|
||||
|
||||
|
||||
class Vortex {
|
||||
|
||||
Vector position;
|
||||
|
||||
double speed = 1.0, scale = 1.0;
|
||||
|
||||
public Vortex( Vector pPosition, double pSpeed, double pScale ) {
|
||||
this.position = pPosition.copy();
|
||||
this.scale = pScale;
|
||||
this.speed = pSpeed;
|
||||
}
|
||||
|
||||
public void attract( Particle pPartikel ) {
|
||||
Vector diff = Vector.sub(pPartikel.position, this.position);
|
||||
double dx = -diff.y * this.speed;
|
||||
double dy = diff.x * this.speed;
|
||||
double f = 1.0 / (1.0 + (dx * dx + dy * dy) / scale);
|
||||
|
||||
pPartikel.position.x += (diff.x - pPartikel.velocity.x) * f;
|
||||
pPartikel.position.y += (diff.y - pPartikel.velocity.y) * f;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
public interface ParticleFactory {
|
||||
|
||||
Particle createParticle();
|
||||
|
||||
}
|
||||
69
src/main/java/schule/ngb/zm/particles/PhysicsObject.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.Updatable;
|
||||
import schule.ngb.zm.Vector;
|
||||
|
||||
|
||||
public abstract class PhysicsObject implements Updatable {
|
||||
|
||||
protected Vector position, velocity, acceleration;
|
||||
|
||||
protected double mass = 1.0;
|
||||
|
||||
|
||||
public PhysicsObject() {
|
||||
position = new Vector();
|
||||
velocity = new Vector();
|
||||
acceleration = new Vector();
|
||||
}
|
||||
|
||||
public PhysicsObject( Vector pPosition ) {
|
||||
position = pPosition.copy();
|
||||
velocity = new Vector();
|
||||
acceleration = new Vector();
|
||||
}
|
||||
|
||||
public Vector getAcceleration() {
|
||||
return acceleration;
|
||||
}
|
||||
|
||||
public void setAcceleration( Vector pAcceleration ) {
|
||||
this.acceleration = pAcceleration;
|
||||
}
|
||||
|
||||
public double getMass() {
|
||||
return mass;
|
||||
}
|
||||
|
||||
public void setMass( double pMass ) {
|
||||
this.mass = pMass;
|
||||
}
|
||||
|
||||
public Vector getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition( Vector pPosition ) {
|
||||
this.position = pPosition;
|
||||
}
|
||||
|
||||
public Vector getVelocity() {
|
||||
return velocity;
|
||||
}
|
||||
|
||||
public void setVelocity( Vector pVelocity ) {
|
||||
this.velocity = pVelocity;
|
||||
}
|
||||
|
||||
public void accelerate( Vector pAcceleration ) {
|
||||
acceleration.add(Vector.div(pAcceleration, mass));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
velocity.add(acceleration);
|
||||
position.add(Vector.scale(velocity, delta));
|
||||
acceleration.scale(0.0);
|
||||
}
|
||||
|
||||
}
|
||||
35
src/main/java/schule/ngb/zm/particles/StarParticle.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package schule.ngb.zm.particles;
|
||||
|
||||
import schule.ngb.zm.Color;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
|
||||
public class StarParticle extends BasicParticle {
|
||||
|
||||
public StarParticle() {
|
||||
super();
|
||||
this.startColor = Color.PURE_GREEN;
|
||||
}
|
||||
|
||||
public StarParticle( Color startColor ) {
|
||||
this(startColor, null);
|
||||
}
|
||||
|
||||
public StarParticle( Color startColor, Color finalColor ) {
|
||||
super();
|
||||
|
||||
this.color = startColor;
|
||||
this.startColor = startColor;
|
||||
this.finalColor = finalColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw( Graphics2D graphics ) {
|
||||
if( isActive() && this.color != null ) {
|
||||
graphics.setColor(this.color.getJavaColor());
|
||||
graphics.drawLine((int) position.x - 3, (int) position.y - 3, (int) position.x + 3, (int) position.y + 3);
|
||||
graphics.drawLine((int) position.x + 3, (int) position.y - 3, (int) position.x - 3, (int) position.y + 3);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
package schule.ngb.zm.shapes;
|
||||
|
||||
import schule.ngb.zm.Options;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.CubicCurve2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.QuadCurve2D;
|
||||
@@ -170,6 +174,30 @@ public class Curve extends Shape {
|
||||
move(dx, dy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw( Graphics2D graphics, AffineTransform transform ) {
|
||||
if( !visible ) {
|
||||
return;
|
||||
}
|
||||
|
||||
AffineTransform orig = graphics.getTransform();
|
||||
if( transform != null ) {
|
||||
//graphics.transform(transform);
|
||||
}
|
||||
|
||||
graphics.translate(x, y);
|
||||
graphics.rotate(Math.toRadians(rotation));
|
||||
|
||||
java.awt.Shape shape = getShape();
|
||||
|
||||
java.awt.Color currentColor = graphics.getColor();
|
||||
fillShape(shape, graphics);
|
||||
strokeShape(shape, graphics);
|
||||
graphics.setColor(currentColor);
|
||||
|
||||
graphics.setTransform(orig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals( Object o ) {
|
||||
if( this == o ) return true;
|
||||
|
||||
@@ -11,6 +11,7 @@ public class CustomShape extends Shape {
|
||||
public CustomShape( double x, double y ) {
|
||||
super(x, y);
|
||||
path = new Path2D.Double();
|
||||
path.moveTo(x, y);
|
||||
}
|
||||
|
||||
public CustomShape( CustomShape custom ) {
|
||||
@@ -36,7 +37,7 @@ public class CustomShape extends Shape {
|
||||
}
|
||||
|
||||
public void lineTo( double x, double y ) {
|
||||
path.lineTo(x - x, y - y);
|
||||
path.lineTo(x - this.x, y - this.y);
|
||||
calculateBounds();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package schule.ngb.zm.shapes;
|
||||
|
||||
import schule.ngb.zm.*;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Basisklasse für alle Formen in der Zeichenmaschine.
|
||||
* Dies ist die Basisklasse für alle Formen in der Zeichenmaschine.
|
||||
* <p>
|
||||
* Alle Formen sind als Unterklassen von {@code Shape} implementiert.
|
||||
* <p>
|
||||
@@ -16,7 +20,6 @@ import java.awt.geom.Point2D;
|
||||
* 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);
|
||||
@@ -36,42 +39,42 @@ import java.awt.geom.Point2D;
|
||||
public abstract class Shape extends BasicDrawable {
|
||||
|
||||
/**
|
||||
* x-Koordinate der Form.
|
||||
* Speichert die x-Koordinate der Form.
|
||||
*/
|
||||
protected double x;
|
||||
|
||||
/**
|
||||
* y-Koordinate der Form.
|
||||
* Speichert die y-Koordinate der Form.
|
||||
*/
|
||||
protected double y;
|
||||
|
||||
/**
|
||||
* Rotation in Grad um den Punkt (x, y).
|
||||
* Speichert die Rotation in Grad um den Punkt (x, y).
|
||||
*/
|
||||
protected double rotation = 0.0;
|
||||
|
||||
/**
|
||||
* Skalierungsfaktor.
|
||||
* Speichert den Skalierungsfaktor.
|
||||
*/
|
||||
protected double scale = 1.0;
|
||||
|
||||
/**
|
||||
* Ankerpunkt der Form.
|
||||
* Speichert den Ankerpunkt.
|
||||
*/
|
||||
protected Options.Direction anchor = Options.Direction.CENTER;
|
||||
|
||||
/**
|
||||
* Setzt die x- und y-Koordinate der Form auf 0.
|
||||
* Erstellt eine neue Form mit den Koordinaten {@code (0,0)}.
|
||||
*/
|
||||
public Shape() {
|
||||
this(0.0, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die x- und y-Koordinate der Form.
|
||||
* Erstellt eine Form mit den angegebenen Koordinaten.
|
||||
*
|
||||
* @param x
|
||||
* @param y
|
||||
* @param x Die x-Koordinate.
|
||||
* @param y Die y-Koordinate.
|
||||
*/
|
||||
public Shape( double x, double y ) {
|
||||
this.x = x;
|
||||
@@ -79,7 +82,7 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die x-Koordinate der Form zurück.
|
||||
* Liefert die aktuelle x-Koordinate der Form.
|
||||
*
|
||||
* @return Die x-Koordinate.
|
||||
*/
|
||||
@@ -97,7 +100,7 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die y-Koordinate der Form zurück.
|
||||
* Liefert die aktuelle y-Koordinate der Form.
|
||||
*
|
||||
* @return Die y-Koordinate.
|
||||
*/
|
||||
@@ -106,63 +109,44 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die y-Koordinate der Form.
|
||||
* Setzt die x-Koordinate der Form.
|
||||
*
|
||||
* @param y Die neue y-Koordinate.
|
||||
* @param y Die neue y-Koordinate der Form.
|
||||
*/
|
||||
public void setY( double y ) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Breite dieser Form zurück.
|
||||
* 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 kann mit
|
||||
* {@link #getBounds()} abgerufen werden.
|
||||
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
|
||||
* {@link #getBounds()} abgerufen.
|
||||
*
|
||||
* @return
|
||||
* @return Die Breite der Form.
|
||||
*/
|
||||
public abstract double getWidth();
|
||||
|
||||
/**
|
||||
* Gibt die Höhe dieser Form zurück.
|
||||
* 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 kann mit
|
||||
* {@link #getBounds()} abgerufen werden.
|
||||
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
|
||||
* {@link #getBounds()} abgerufen.
|
||||
*
|
||||
* @return
|
||||
* @return Die Höhe der Form.
|
||||
*/
|
||||
public abstract double getHeight();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
|
||||
Point2D apDir = getAbsAnchorPoint(dir);
|
||||
Point2D apInv = getAbsAnchorPoint(dir.inverse());
|
||||
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
|
||||
Point2D ap = getAbsAnchorPoint(CENTER);
|
||||
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Rotation in Grad zurück.
|
||||
* Liefert die Rotation in Grad.
|
||||
*
|
||||
* @return Rotation in Grad.
|
||||
*/
|
||||
@@ -179,14 +163,25 @@ public abstract class Shape extends BasicDrawable {
|
||||
this.rotation += angle % 360;
|
||||
}
|
||||
|
||||
public void rotateTo( double angle ) {
|
||||
this.rotation = angle % 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dreht die Form um den angegebenen Winkel um das angegebene Drehzentrum.
|
||||
*
|
||||
* @param center Das Drehzentrum der Drehung.
|
||||
* @param angle Der Drehwinkel.
|
||||
*/
|
||||
public void rotate( Point2D center, double angle ) {
|
||||
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;
|
||||
|
||||
@@ -201,6 +196,15 @@ public abstract class Shape extends BasicDrawable {
|
||||
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.
|
||||
*
|
||||
@@ -210,33 +214,91 @@ public abstract class Shape extends BasicDrawable {
|
||||
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 basierend auf der angegebenen
|
||||
* {@link Options.Direction Richtung}.
|
||||
* Setzt den Ankerpunkt der Form auf die angegebene Richtung.
|
||||
* <p>
|
||||
* Für das Setzen des Ankers muss das
|
||||
* {@link #getBounds() begrenzende Rechteck} berechnet werden. Unterklassen
|
||||
* sollten die Methode überschreiben, wenn der Anker auch direkt gesetzt
|
||||
* werden kann.
|
||||
* 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
|
||||
* @param anchor Der Ankerpunkt.
|
||||
*/
|
||||
public void setAnchor( Options.Direction anchor ) {
|
||||
if( anchor != null ) {
|
||||
this.anchor = anchor;
|
||||
}
|
||||
Validator.requireNotNull(anchor, "anchor");
|
||||
this.anchor = anchor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +307,19 @@ public abstract class Shape extends BasicDrawable {
|
||||
* <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.
|
||||
* 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.
|
||||
@@ -262,11 +336,12 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt den Ankerpunkt der Form relativ zum gesetzten
|
||||
* {@link #setAnchor(Options.Direction) Ankerpunkt}.
|
||||
* Bestimmt die Koordinaten des angegebenen Ankers der Form relativ zum
|
||||
* aktuellen {@link #setAnchor(Options.Direction) Ankerpunkt}.
|
||||
*
|
||||
* @param anchor Die Richtung des Ankerpunktes.
|
||||
* @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;
|
||||
@@ -279,11 +354,20 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die absoluten Koordinaten eines angegebenen
|
||||
* 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.
|
||||
*
|
||||
* @param anchor Die Richtung des Ankerpunktes.
|
||||
* <b>Wichtig:</b> Die Berechnung berücksichtigt derzeit keine Rotationen
|
||||
* und Transformationen der Form.
|
||||
*
|
||||
* @param anchor Die Richtung des Ankers.
|
||||
* @return Der absolute Ankerpunkt.
|
||||
* @see #getAnchorPoint(double, double, Options.Direction)
|
||||
*/
|
||||
public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) {
|
||||
// TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen.
|
||||
@@ -296,20 +380,22 @@ public abstract class Shape extends BasicDrawable {
|
||||
/**
|
||||
* Kopiert die Eigenschaften der angegebenen Form in diese.
|
||||
* <p>
|
||||
* Unterklassen sollten diese Methode überschreiben, um weitere
|
||||
* Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises).
|
||||
* Unterklassen sollten immer mit dem Aufruf {@code super.copyFrom(shape)}
|
||||
* die Basiseigenschaften kopieren.
|
||||
* 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 sollte so viele Eigenschaften wie möglich von der anderen
|
||||
* Form in diese kopieren. Wenn die andere Form einen anderen Typ hat, dann
|
||||
* werden trotzdem die Basiseigenschaften (Konturlinie, Füllung, Position,
|
||||
* Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese Form kopiert.
|
||||
* Implementierende Unterklassen können soweit sinnvoll auch andere Werte
|
||||
* übernehmen. Eine {@link Ellipse} kann beispielsweise auch die Breite und
|
||||
* Höhe eines {@link Rectangle} übernehmen.
|
||||
* 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 werden soll.
|
||||
* @param shape Die Originalform, von der kopiert wird.
|
||||
*/
|
||||
public void copyFrom( Shape shape ) {
|
||||
if( shape != null ) {
|
||||
@@ -318,6 +404,7 @@ public abstract class Shape extends BasicDrawable {
|
||||
setStrokeColor(shape.getStrokeColor());
|
||||
setStrokeWeight(shape.getStrokeWeight());
|
||||
setStrokeType(shape.getStrokeType());
|
||||
setStrokeJoin(shape.getStrokeJoin());
|
||||
visible = shape.isVisible();
|
||||
rotation = shape.getRotation();
|
||||
scale(shape.getScale());
|
||||
@@ -338,9 +425,9 @@ public abstract class Shape extends BasicDrawable {
|
||||
* </code></pre>
|
||||
* <p>
|
||||
* Die Methode kann beliebig umgesetzt werden, um eine 1-zu-1-Kopie dieser
|
||||
* Form zu erhalten. In der Regel sollte aber jede Form einen Konstruktor
|
||||
* besitzen, die alle Werte einer andern Form übernimmt. Die gezeigte
|
||||
* Implementierung dürfte daher im Regelfall ausreichend sein.
|
||||
* 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.
|
||||
*/
|
||||
@@ -351,8 +438,8 @@ public abstract class Shape extends BasicDrawable {
|
||||
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
|
||||
* {@link Graphics2D Grafikkontext} zu zeichnen.
|
||||
* <p>
|
||||
* Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, kann die
|
||||
* Methode {@code null} zurückgeben.
|
||||
* 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}.
|
||||
@@ -372,34 +459,85 @@ public abstract class Shape extends BasicDrawable {
|
||||
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.
|
||||
* Mit {@code buff} kann ein zusätzlicher Abstand angegeben werden, um den
|
||||
* die Form entlang des Ankerpunktes {@code anchor} verschoben werden soll.
|
||||
* 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} nach oben verschoben.
|
||||
* {@code buff} oberhalb der oberen Kante der zweiten Form verschoben.
|
||||
* <p>
|
||||
* Befinden sich die Formen zuvor in folgender Ausrichtung:
|
||||
* <pre>
|
||||
* ┌─────────┐
|
||||
* │ │
|
||||
* W B │
|
||||
* ┌─────┐ │ │
|
||||
* │ │ └─────────┘
|
||||
* W A │
|
||||
* │ │
|
||||
* └─────┘
|
||||
* </pre>
|
||||
* <p>
|
||||
* bringt sie der Aufruf {@code B.moveTo(A, DOWN, 0)} in diese Ausrichtung:
|
||||
* <pre>
|
||||
* B.moveTo(A, WEST, 0) │ B.moveTo(A, WEST, 10)
|
||||
* │
|
||||
* ┌─────┬───┐ │ ┌┬────┬────┐
|
||||
* │ │ │ │ ││ │ │
|
||||
* │ A B│ │ │ ││ A B │
|
||||
* │ │ │ │ ││ │ │
|
||||
* └─────┴───┘ │ └┴────┴────┘
|
||||
* </pre>
|
||||
*
|
||||
* @param shape
|
||||
* @param dir
|
||||
* @param buff
|
||||
* @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);
|
||||
@@ -409,15 +547,22 @@ public abstract class Shape extends BasicDrawable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Richtet die Form entlang der angegebenen Richtung am Rand der
|
||||
* Zeichenfläche aus.
|
||||
* Bewegt die Form an den Rand der Zeichenfläche in der angegebenen
|
||||
* Richtung.
|
||||
*
|
||||
* @param dir Die Richtung der Ausrichtung.
|
||||
* @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);
|
||||
@@ -426,20 +571,58 @@ public abstract class Shape extends BasicDrawable {
|
||||
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. Für {@code DOWN} wird beispielsweise die y-Koordinate der unteren
|
||||
* Kante dieser Form an der unteren Kante der angegebenen Form {@code shape}
|
||||
* ausgerichtet. Die x-Koordinate wird nicht verändert. {@code buff} gibt
|
||||
* einen Abstand ab, um den diese From versetzt ausgerichtet werden soll.
|
||||
* 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
|
||||
* @param dir
|
||||
* @param buff
|
||||
* @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);
|
||||
@@ -449,16 +632,46 @@ public abstract class Shape extends BasicDrawable {
|
||||
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. 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.
|
||||
* 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
|
||||
@@ -472,6 +685,19 @@ public abstract class Shape extends BasicDrawable {
|
||||
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);
|
||||
}*/
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BarChart extends Rectangle {
|
||||
|
||||
public static String DEFAULT_LABEL = "%.2f";
|
||||
@@ -105,10 +106,10 @@ public class BarChart extends Rectangle {
|
||||
}
|
||||
|
||||
public void addValue( double pValue, Color pColor ) {
|
||||
addValue(pValue, pColor, String.format(DEFAULT_LABEL, pValue));
|
||||
addValue(pValue, String.format(DEFAULT_LABEL, pValue), pColor);
|
||||
}
|
||||
|
||||
public void addValue( double pValue, Color pColor, String pLabel ) {
|
||||
public void addValue( double pValue, String pLabel, Color pColor ) {
|
||||
addValue(new BasicChartValue(pValue, pLabel, pColor));
|
||||
}
|
||||
|
||||
|
||||
@@ -136,57 +136,36 @@ public class BasicChartValue implements ChartValue {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getX() {
|
||||
return xValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setValue( double pValue ) {
|
||||
this.value = pValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setLabel( String pLabel ) {
|
||||
this.label = pLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Color getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setColor( Color pColor ) {
|
||||
this.color = pColor;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Diese Paket enthält Formen, die Diagramme darstellen.
|
||||
*/
|
||||
package schule.ngb.zm.shapes.charts;
|
||||
13
src/main/java/schule/ngb/zm/shapes/package-info.java
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Dieses Paket enthält Implementationen der abstrakten
|
||||
* {@link schule.ngb.zm.shapes.Shape} Klasse.
|
||||
*
|
||||
* Jede Unterklasse von {@code Shape} stellt eine konkrete Form wie ein
|
||||
* {@link schule.ngb.zm.shapes.Rectangle Rechteck}, ein
|
||||
* {@link schule.ngb.zm.shapes.Circle Kreis} oder ein
|
||||
* {@link schule.ngb.zm.shapes.Picture Bild} dar.
|
||||
*
|
||||
* Mit {@link schule.ngb.zm.shapes.ShapeGroup} können Formen gruppiert
|
||||
* und gemeinsam transformiert werden.
|
||||
*/
|
||||
package schule.ngb.zm.shapes;
|
||||
204
src/main/java/schule/ngb/zm/util/Cache.java
Normal file
@@ -0,0 +1,204 @@
|
||||
package schule.ngb.zm.util;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ein Cache ist ein {@link Map} Implementation, die Inhaltsobjekte in einer
|
||||
* {@link Reference} speichert und als Zwischenspeicher für Objekte dienen kann,
|
||||
* deren Erstellung aufwendig ist.
|
||||
* <p>
|
||||
* Für einen Cache ist nicht garantiert, dass ein eingefügtes Objekt beim
|
||||
* nächsten Aufruf noch vorhanden ist, da die Referenz inzwischen vom Garbage
|
||||
* Collector gelöscht worden sein kann.
|
||||
* <p>
|
||||
* Als interne Map wird einen {@link ConcurrentHashMap} verwendet.
|
||||
* <p>
|
||||
* Ein passender Cache wird mittels der Fabrikmethoden {@link #newSoftCache()}
|
||||
* und {@link #newWeakCache()} erstellt.
|
||||
* <pre><code>
|
||||
* Cache<String, Image> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Eine Hilfsklasse um Dinge zu zählen.
|
||||
* 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
|
||||
@@ -13,11 +13,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
* <p>
|
||||
* Der Zähler kann aber auch Objekte zählen, indem die Instanzen an
|
||||
* {@link #count(Object)} übergeben werden. Am Ende kann mit {@link #getCount()}
|
||||
* die Anzahl der Objkete abgerufen werden.
|
||||
* die Anzahl der Objekte abgerufen werden.
|
||||
* <p>
|
||||
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein Zähler auch
|
||||
* das Maximum, das Minimum, die Summe und den Durchschnitt der gezählten
|
||||
* Werte.
|
||||
* 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.
|
||||
@@ -96,31 +96,42 @@ public final class Counter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die aktuelle Anzahl zurück.
|
||||
* @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;
|
||||
@@ -129,13 +140,27 @@ public final class Counter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setzt den Zähler auf den angegebenen Wert.
|
||||
* <p>
|
||||
* Die anderen Statistiken werden nicht verändert.
|
||||
*
|
||||
* @param count Der neue Wert des Zählers.
|
||||
* @return Dieser Zähler selbst (method chaining).
|
||||
*/
|
||||
@SuppressWarnings( "UnusedReturnValue" )
|
||||
public Counter setCount( int count ) {
|
||||
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);
|
||||
@@ -147,18 +172,40 @@ public final class Counter {
|
||||
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();
|
||||
@@ -173,11 +220,27 @@ public final class Counter {
|
||||
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 ) {
|
||||
@@ -188,6 +251,13 @@ public final class Counter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zöhlt alle Werte im angegebenen Array.
|
||||
*
|
||||
* @param values Das Array der neuen Werte.
|
||||
* @return Dieser Zähler selbst (method chaining).
|
||||
* @see #count(double)
|
||||
*/
|
||||
@SuppressWarnings( "UnusedReturnValue" )
|
||||
public synchronized Counter countAll( double[] values ) {
|
||||
for( double value : values ) {
|
||||
@@ -196,6 +266,13 @@ public final class Counter {
|
||||
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 ) {
|
||||
@@ -204,6 +281,13 @@ public final class Counter {
|
||||
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 ) {
|
||||
@@ -212,6 +296,13 @@ public final class Counter {
|
||||
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 ) {
|
||||
|
||||
@@ -2,8 +2,6 @@ package schule.ngb.zm.util;
|
||||
|
||||
import schule.ngb.zm.Color;
|
||||
import schule.ngb.zm.Constants;
|
||||
import schule.ngb.zm.Zeichenmaschine;
|
||||
import schule.ngb.zm.util.io.FileLoader;
|
||||
import schule.ngb.zm.util.io.ImageLoader;
|
||||
|
||||
import java.awt.Font;
|
||||
@@ -11,112 +9,330 @@ 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;
|
||||
|
||||
/**
|
||||
* Hilfsklasse, um zufällige Beispieldaten zu erzeugen.
|
||||
* 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 Platzhalterbilder.
|
||||
* 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 Nutzernamen.
|
||||
* 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 n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[][] fakeUsers( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[][] result = new String[n][];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
result[i] = Constants.choice(data);
|
||||
}
|
||||
return result;
|
||||
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 n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[] fakeNames( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[] result = new String[n];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
String[] row = Constants.choice(data);
|
||||
result[i] = row[0];
|
||||
}
|
||||
return result;
|
||||
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 (Vor- und
|
||||
* Nachname).
|
||||
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Namen im Format
|
||||
* "Vorname Nachname".
|
||||
*
|
||||
* @param n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[] fakeFullnames( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[] result = new String[n];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
String[] row = Constants.choice(data);
|
||||
result[i] = row[0] + " " + row[1];
|
||||
}
|
||||
return result;
|
||||
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 n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[] fakeUsernames( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[] result = new String[n];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
String[] row = Constants.choice(data);
|
||||
result[i] = row[3];
|
||||
}
|
||||
return result;
|
||||
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 n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[] fakePasswords( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[] result = new String[n];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
String[] row = Constants.choice(data);
|
||||
result[i] = row[4];
|
||||
}
|
||||
return result;
|
||||
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 n Anzahl der Beispieldaten.
|
||||
* @param count Anzahl der Beispieldaten.
|
||||
* @return Ein Array mit den Beispieldaten.
|
||||
*/
|
||||
public static String[] fakeEmails( int n ) {
|
||||
String[][] data = loadMockfile("users");
|
||||
String[] result = new String[n];
|
||||
for( int i = 0; i < n; i++ ) {
|
||||
String[] row = Constants.choice(data);
|
||||
result[i] = row[5];
|
||||
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<Integer> l = new LinkedList<>();
|
||||
* 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;
|
||||
}
|
||||
|
||||
@@ -165,7 +381,7 @@ public final class Faker {
|
||||
FontMetrics fontMerics = graphics.getFontMetrics();
|
||||
LineMetrics lineMetrics = fontMerics.getLineMetrics(text, graphics);
|
||||
graphics.drawString(text,
|
||||
(int) ((width - fontMerics.stringWidth(text)) / 2),
|
||||
(width - fontMerics.stringWidth(text)) / 2,
|
||||
(int) (height / 2 - lineMetrics.getDescent() + lineMetrics.getAscent() / 2)
|
||||
);
|
||||
graphics.dispose();
|
||||
@@ -176,15 +392,9 @@ public final class Faker {
|
||||
}
|
||||
}
|
||||
|
||||
public static String fakeText( int words, int paragraphs ) {
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String[][] loadMockfile( String name ) {
|
||||
return FileLoader.loadCsv("schule/ngb/zm/util/mock-" + name + ".csv", true);
|
||||
}
|
||||
|
||||
private Faker() {
|
||||
}
|
||||
|
||||
private static final Log LOG = Log.getLogger(Faker.class);
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class Log {
|
||||
* mindestens herabgesenkt werden sollen.
|
||||
*/
|
||||
public static void enableGlobalLevel( Level level ) {
|
||||
int lvl = Validator.requireNotNull(level).intValue();
|
||||
int lvl = Validator.requireNotNull(level, "level").intValue();
|
||||
ensureRootLoggerInitialized();
|
||||
|
||||
// Decrease level of root level ConsoleHandlers for output
|
||||
|
||||
@@ -3,7 +3,7 @@ package schule.ngb.zm.util;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Zufallsgenerator für Perlin-Noise.
|
||||
* Generator für Perlin-Noise.
|
||||
* <p>
|
||||
* Die Implementierung basiert auf dem von Ken Perlin entwickelten Algorithmus
|
||||
* und wurde anhand der <a
|
||||
@@ -18,7 +18,7 @@ public class Noise {
|
||||
private static final int M = N - 1;
|
||||
|
||||
/**
|
||||
* Interne Permutationstabelle für diesen Generator
|
||||
* Interne Permutationstabelle für diesen Generator.
|
||||
*/
|
||||
private int[] p;
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package schule.ngb.zm.util;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Eine Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
|
||||
* Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
|
||||
* <p>
|
||||
* Die Klasse kann zum Beispiel genutzt werden, um die Laufzeit eines
|
||||
* Quelltextes zu messen. Wie eine echte Stoppuhr läuft der {@code Timer}
|
||||
* weiter, wenn nach einem {@link #stop()} wieder {@link #start()} aufgerufen
|
||||
* wird. Soll die Zeitmessung wieder bei null beginnen, muss vorher
|
||||
* {@link #reset()} genutzt werden.
|
||||
* 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
|
||||
@@ -100,7 +100,7 @@ public final class Timer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Zeit wird
|
||||
* 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).
|
||||
@@ -116,8 +116,8 @@ public final class Timer {
|
||||
|
||||
/**
|
||||
* Setzt den {@code Timer} auf den Startzustand und löscht alle bisher
|
||||
* gemessenen Zeiten. Falls die Zeitmessung gerade läuft, wird sie nicht
|
||||
* gestoppt, sondern läuft vom Zeitpunkt des Aufrufs weiter.
|
||||
* 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).
|
||||
*/
|
||||
|
||||
@@ -3,35 +3,58 @@ package schule.ngb.zm.util;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@SuppressWarnings( "UnusedReturnValue" )
|
||||
/**
|
||||
* Statische Methoden, um Methodenparameter auf Gültigkeit zu prüfen.
|
||||
*/
|
||||
@SuppressWarnings( {"unused", "UnusedReturnValue"} )
|
||||
public class Validator {
|
||||
|
||||
public static final <T> T requireNotNull( T obj ) {
|
||||
return Objects.requireNonNull(obj);
|
||||
public static final <T> T requireNotNull( T obj, CharSequence paramName ) {
|
||||
return requireNotNull(obj, () -> "Parameter <%s> may not be null.".format(paramName.toString()));
|
||||
}
|
||||
|
||||
public static final <T> T requireNotNull( T obj, CharSequence msg ) {
|
||||
return Objects.requireNonNull(obj, msg::toString);
|
||||
public static final <T> T requireNotNull( T obj, CharSequence paramName, CharSequence msg ) {
|
||||
return requireNotNull(obj, () -> msg.toString().format(paramName.toString()));
|
||||
}
|
||||
|
||||
public static final <T> T requireNotNull( T obj, Supplier<String> msg ) {
|
||||
return Objects.requireNonNull(obj, msg);
|
||||
if( obj == null ) {
|
||||
throw new NullPointerException(msg == null ? "Parameter may not be null." : msg.get());
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
public static final String requireNotEmpty( String str ) {
|
||||
return requireNotEmpty(str, (Supplier<String>) null);
|
||||
public static final String requireNotEmpty( String str, CharSequence paramName ) {
|
||||
return requireNotEmpty(str, () -> String.format("Parameter <%s> may not be empty string (<%s> provided)", paramName, str));
|
||||
}
|
||||
|
||||
public static final String requireNotEmpty( String str, CharSequence msg ) {
|
||||
return requireNotEmpty(str, msg::toString);
|
||||
public static final String requireNotEmpty( String str, CharSequence paramName, CharSequence msg ) {
|
||||
return requireNotEmpty(str, () -> msg.toString().format(paramName.toString()));
|
||||
}
|
||||
|
||||
public static final String requireNotEmpty( String str, Supplier<String> msg ) {
|
||||
if( str.isEmpty() )
|
||||
if( str.isEmpty() ) {
|
||||
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be empty string (<%s> provided)", str) : msg.get());
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName ) {
|
||||
return requireNotEmpty(arr, () -> String.format("Parameter <%s> may not be empty", paramName));
|
||||
}
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName, CharSequence msg ) {
|
||||
return requireNotEmpty(arr, () -> msg.toString().format(paramName.toString()));
|
||||
}
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
|
||||
if( arr.length == 0 )
|
||||
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
|
||||
return arr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static final int requirePositive( int i ) {
|
||||
return requirePositive(i, (Supplier<String>) null);
|
||||
}
|
||||
@@ -136,21 +159,6 @@ public class Validator {
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr ) {
|
||||
return requireNotEmpty(arr, (Supplier<String>) null);
|
||||
}
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence msg ) {
|
||||
return requireNotEmpty(arr, msg::toString);
|
||||
}
|
||||
|
||||
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
|
||||
if( arr.length == 0 )
|
||||
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static final <T> T[] requireSize( T[] arr, int size ) {
|
||||
return requireSize(arr, size, (Supplier<String>) null);
|
||||
}
|
||||
@@ -165,20 +173,6 @@ public class Validator {
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static final <T> T[] requireValid( T[] arr ) {
|
||||
return requireValid(arr, (Supplier<String>) null);
|
||||
}
|
||||
|
||||
public static final <T> T[] requireValid( T[] arr, CharSequence msg ) {
|
||||
return requireValid(arr, msg::toString);
|
||||
}
|
||||
|
||||
public static final <T> T[] requireValid( T[] arr, Supplier<String> msg ) {
|
||||
if( arr == null || arr.length > 0 )
|
||||
throw new IllegalArgumentException(msg == null ? "Parameter array may not be null or empty" : msg.get());
|
||||
return arr;
|
||||
}
|
||||
|
||||
private Validator() {
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,63 @@ 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<MyEvent, MyEventListener> dispatcher = new EventDispatcher<>();
|
||||
* 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;
|
||||
@@ -19,8 +76,8 @@ public class EventDispatcher<E, L extends Listener<E>> {
|
||||
}
|
||||
|
||||
public void registerEventType( String eventKey, BiConsumer<E, L> dispatcher ) {
|
||||
Validator.requireNotNull(eventKey);
|
||||
Validator.requireNotNull(dispatcher);
|
||||
Validator.requireNotNull(eventKey, "eventKey");
|
||||
Validator.requireNotNull(dispatcher, "dispatcher");
|
||||
|
||||
if( !eventRegistered(eventKey) ) {
|
||||
eventRegistry.put(eventKey, dispatcher);
|
||||
@@ -35,6 +92,7 @@ public class EventDispatcher<E, L extends Listener<E>> {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@SuppressWarnings( "unused" )
|
||||
public boolean hasListeners() {
|
||||
return !listeners.isEmpty();
|
||||
}
|
||||
@@ -44,8 +102,8 @@ public class EventDispatcher<E, L extends Listener<E>> {
|
||||
}
|
||||
|
||||
public void dispatchEvent( String eventKey, final E event ) {
|
||||
Validator.requireNotNull(eventKey);
|
||||
Validator.requireNotNull(event);
|
||||
Validator.requireNotNull(eventKey, "eventKey");
|
||||
Validator.requireNotNull(event, "event");
|
||||
|
||||
if( eventRegistered(eventKey) ) {
|
||||
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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> {
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Dieses Paket enthält Hilfsklassen, die das Listener-Entwurfsmuster
|
||||
* umsetzen.
|
||||
*/
|
||||
package schule.ngb.zm.util.events;
|
||||
@@ -1,42 +1,74 @@
|
||||
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.URISyntaxException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
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 Datei Zeilenweise in eine Liste.
|
||||
* 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
|
||||
* @param charset
|
||||
* @return
|
||||
* @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<>();
|
||||
|
||||
@@ -46,17 +78,41 @@ public final class FileLoader {
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch( MalformedURLException muex ) {
|
||||
LOG.warn("Could not find resource for <%s>", source);
|
||||
return Collections.emptyList();
|
||||
} catch( IOException ex ) {
|
||||
LOG.error(ex, "Error while loading lines from source <%s>", source);
|
||||
LOG.warn(ex, "Error while loading lines from source <%s>", source);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
@@ -66,22 +122,26 @@ public final class FileLoader {
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
} catch( MalformedURLException muex ) {
|
||||
LOG.warn("Could not find resource for <%s>", source);
|
||||
return "";
|
||||
} catch( IOException ex ) {
|
||||
LOG.error(ex, "Error while loading string from source <%s>", source);
|
||||
LOG.warn(ex, "Error while loading string from source <%s>", source);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Daten aus einer CSV Datei in ein zweidimensionales
|
||||
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
|
||||
* String-Array.
|
||||
* <p>
|
||||
* Der Aufruf entspricht dem Aufruf von
|
||||
* Der Aufruf entspricht
|
||||
* <pre><code>
|
||||
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
|
||||
* </code></pre>
|
||||
*
|
||||
* @param source Die Quelle der CSV-Daten.
|
||||
* @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)
|
||||
@@ -91,12 +151,12 @@ public final class FileLoader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Daten aus einer CSV Datei in ein zweidimensionales
|
||||
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
|
||||
* String-Array.
|
||||
* <p>
|
||||
* Die Methode ist nicht in der Lage, komplexe CSV-Dateien zu verarbeiten.
|
||||
* Insbesondere können Inhalte, die das Trennzeichen {@code separator}
|
||||
* enthalten, nicht korrekt erkannt werden. Das Trennzeichen wird unabhängig
|
||||
* 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.)
|
||||
@@ -121,34 +181,39 @@ public final class FileLoader {
|
||||
).toArray(String[][]::new);
|
||||
}
|
||||
|
||||
public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
|
||||
public static double[][] loadValues( String source, String separator, boolean skipFirst ) {
|
||||
return loadValues(source, separator, skipFirst, UTF8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Double-Werte aus einer CSV Datei in ein zweidimensionales Array.
|
||||
* Lädt Double-Werte aus einer Text-Datei in ein zweidimensionales Array.
|
||||
* <p>
|
||||
* Die gelesenen Strings werden mit {@link Double#parseDouble(String)} in
|
||||
* {@code double} umgeformt. Es leigt in der Verantwortung des Nutzers
|
||||
* sicherzustellen, dass die CSV-Datei auch nur Zahlen enthält, die korrekt
|
||||
* in {@code double} umgewandelt werden können. Zellen für die die
|
||||
* Umwandlung fehlschlägt werden mit 0.0 befüllt.
|
||||
* 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 Das verwendete Trennzeichen.
|
||||
* @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, char separator, boolean skipFirst, Charset charset ) {
|
||||
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(Character.toString(separator)))
|
||||
.stream(line.split(separator))
|
||||
.mapToDouble(
|
||||
( value ) -> {
|
||||
try {
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
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.Objects;
|
||||
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 Map<String, Font> fontCache = new ConcurrentHashMap<>();
|
||||
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}.
|
||||
* 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.
|
||||
* 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
|
||||
@@ -53,8 +63,8 @@ public class FontLoader {
|
||||
}
|
||||
|
||||
public static Font loadFont( String name, String source ) {
|
||||
Validator.requireNotNull(source,"Font source may not be null");
|
||||
Validator.requireNotEmpty(source, "Font source may not be empty.");
|
||||
Validator.requireNotNull(source, "source");
|
||||
Validator.requireNotEmpty(source, "source");
|
||||
|
||||
if( fontCache.containsKey(name) ) {
|
||||
LOG.trace("Retrieved font <%s> from font cache.", name);
|
||||
@@ -82,10 +92,12 @@ public class FontLoader {
|
||||
//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.error(ioex, "Error loading custom font file from source <%s>.", source);
|
||||
LOG.warn(ioex, "Error loading custom font file from source <%s>.", source);
|
||||
} catch( FontFormatException ffex ) {
|
||||
LOG.error(ffex, "Error creating custom font from source <%s>.", source);
|
||||
LOG.warn(ffex, "Error creating custom font from source <%s>.", source);
|
||||
}
|
||||
|
||||
return font;
|
||||
@@ -96,11 +108,12 @@ public class FontLoader {
|
||||
* <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}.
|
||||
* 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.
|
||||
* 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
|
||||
@@ -113,7 +126,7 @@ public class FontLoader {
|
||||
* @see #loadFont(String, String)
|
||||
*/
|
||||
public static Font loadFonts( String name, String... sources ) {
|
||||
for( String fontSource: sources ) {
|
||||
for( String fontSource : sources ) {
|
||||
// TODO: Ignore exceptions in this case and throw own at end?
|
||||
Font font = loadFont(name, fontSource);
|
||||
if( font != null ) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package schule.ngb.zm.util.io;
|
||||
|
||||
import schule.ngb.zm.util.Log;
|
||||
import schule.ngb.zm.util.Cache;
|
||||
import schule.ngb.zm.util.Validator;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -15,16 +16,20 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
/**
|
||||
* Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
|
||||
* <p>
|
||||
* Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die
|
||||
* Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche,
|
||||
* dieselbe Quelle zu laden, werden aus dem Cache bedient.
|
||||
*/
|
||||
public final class ImageLoader {
|
||||
|
||||
public static boolean caching = true;
|
||||
|
||||
private static final Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private static final SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
|
||||
private static final Cache<String, BufferedImage> imageCache = Cache.newSoftCache();
|
||||
|
||||
/**
|
||||
* Lädt ein Bild von der angegebenen Quelle {@code source}.
|
||||
@@ -63,11 +68,11 @@ public final class ImageLoader {
|
||||
* @return
|
||||
*/
|
||||
public static BufferedImage loadImage( String source, boolean caching ) {
|
||||
Validator.requireNotNull(source, "Image source may not be null");
|
||||
Validator.requireNotEmpty(source, "Image source may not be empty.");
|
||||
Validator.requireNotNull(source, "source");
|
||||
Validator.requireNotEmpty(source, "source");
|
||||
|
||||
if( caching && isCached(source) ) {
|
||||
return getCache(source);
|
||||
if( caching && imageCache.containsKey(source) ) {
|
||||
return imageCache.get(source);
|
||||
}
|
||||
|
||||
BufferedImage img = null;
|
||||
@@ -78,10 +83,12 @@ public final class ImageLoader {
|
||||
img = ImageIO.read(in);
|
||||
|
||||
if( caching && img != null ) {
|
||||
putCache(source, img);
|
||||
imageCache.put(source, img);
|
||||
}
|
||||
} catch( MalformedURLException muex ) {
|
||||
LOG.warn("Could not find image resource for <%s>", source);
|
||||
} catch( IOException ioex ) {
|
||||
LOG.error(ioex, "Error loading image file from source <%s>.", source);
|
||||
LOG.warn(ioex, "Error loading image file from source <%s>.", source);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
@@ -106,7 +113,7 @@ public final class ImageLoader {
|
||||
public static boolean preloadImage( String name, String source ) {
|
||||
BufferedImage img = loadImage(source, true);
|
||||
if( img != null ) {
|
||||
putCache(name, img);
|
||||
imageCache.put(name, img);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -127,7 +134,7 @@ public final class ImageLoader {
|
||||
* @param img ZU speicherndes Bild.
|
||||
*/
|
||||
public static void preloadImage( String name, BufferedImage img ) {
|
||||
putCache(name, img);
|
||||
imageCache.put(name, img);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,8 +145,7 @@ public final class ImageLoader {
|
||||
* {@code false}.
|
||||
*/
|
||||
public static boolean isCached( String name ) {
|
||||
SoftReference<BufferedImage> imgRef = imageCache.get(name);
|
||||
return imgRef != null && imgRef != NOCACHE && imgRef.get() != null;
|
||||
return imageCache.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,30 +158,6 @@ public final class ImageLoader {
|
||||
imageCache.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert ein Bild als {@link SoftReference} im Cache.
|
||||
*
|
||||
* @param name Name des Bildes im Zwischenspeicher.
|
||||
* @param img Das zu speichernde Bild.
|
||||
*/
|
||||
private static void putCache( final String name, final BufferedImage img ) {
|
||||
imageCache.put(name, new SoftReference<>(img));
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt ein Bild aus dem Cache.
|
||||
* <p>
|
||||
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
|
||||
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
|
||||
* werden kann.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
private static BufferedImage getCache( final String name ) {
|
||||
return imageCache.get(name).get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deaktiviert den Cache für die angegebene Quelle.
|
||||
* <p>
|
||||
@@ -185,14 +167,14 @@ public final class ImageLoader {
|
||||
*
|
||||
* @param name Die Quelle des Bildes.
|
||||
*/
|
||||
public static void preventCache( final String name ) {
|
||||
imageCache.put(name, NOCACHE);
|
||||
public static void disableCache( final String name ) {
|
||||
imageCache.disableCache(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leer den Cache und löschte alle bisher gespeicherten Bilder.
|
||||
* <p>
|
||||
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden
|
||||
* Auch vorher mit {@link #disableCache(String)} verhinderte Caches werden
|
||||
* gelöscht und müssen neu gesetzt werden.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
@@ -339,12 +321,8 @@ public final class ImageLoader {
|
||||
* @throws IOException Falls es einen Fehler beim Speichern gab.
|
||||
*/
|
||||
public static void saveImage( BufferedImage image, File file, boolean overwriteIfExists ) throws IOException {
|
||||
if( image == null ) {
|
||||
throw new NullPointerException("Image may not be <null>.");
|
||||
}
|
||||
if( file == null ) {
|
||||
throw new NullPointerException("File may not be <null>.");
|
||||
}
|
||||
Validator.requireNotNull(image, "image");
|
||||
Validator.requireNotNull(file, "file");
|
||||
|
||||
if( file.isFile() ) {
|
||||
// Datei existiert schon
|
||||
|
||||
@@ -13,43 +13,42 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.
|
||||
* Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ResourceStreamProvider {
|
||||
|
||||
|
||||
/**
|
||||
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
|
||||
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt
|
||||
* 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} ein relativer Pfad im Projekt ({@code getResource(source) != null})?</li>
|
||||
* <li>Ist {@code source} im Classpath enthalten ({@code getClassLoader().getResource(source) != null})?</li>
|
||||
* <li>Ansonten erstellt ein {@link URL}-Objekt.</li>
|
||||
* <li>Ansonsten erstelle ein {@link URL}-Objekt.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
|
||||
* Auftretende Exceptions
|
||||
* werden als {@link IOException} geworfen.
|
||||
* <p>
|
||||
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt.
|
||||
* 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 Resource (Absoluter Dateipfad,
|
||||
* Dateipfad im Classpath oder Netzwerkresource)
|
||||
* @return Ein {@code InputStream} für die Resource
|
||||
* @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 Resource.
|
||||
* einer bestehenden Ressource.
|
||||
*/
|
||||
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
|
||||
Validator.requireNotNull(source, "Resource source may not be null");
|
||||
Validator.requireNotEmpty(source, "Resource source may not be empty.");
|
||||
Validator.requireNotNull(source, "source");
|
||||
Validator.requireNotEmpty(source, "source");
|
||||
|
||||
// Ist source ein valider Dateipfad?
|
||||
File file = new File(source);
|
||||
@@ -66,50 +65,59 @@ public class ResourceStreamProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht eine zur angegebenen Quelle passende Resource und öffnet einen
|
||||
* 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 Resource gefunden wird:
|
||||
* ab, welche Art an Ressource gefunden wird:
|
||||
* <ol>
|
||||
* <li>Ist {@code source} eine existierende Datei
|
||||
* {@code new File(source}.isFile() == true}, dann wird ein
|
||||
* {@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
|
||||
* <li>Ist {@code source} eine gültige {@link URL}, dann wird ein
|
||||
* {@link URL#openStream() Netwerkstream} geöffnet.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Die Möglichen Resourcen werden in der gelisteten Reihenfolge durchprobiert
|
||||
* Die Möglichen Ressourcen werden in der gelisteten Reihenfolge durchprobiert
|
||||
* und der erste gefundene Stream zurückgegeben. Auftretende Exceptions
|
||||
* werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source}
|
||||
* keine gültige Resource gefunden werden kann, wird am Ende die
|
||||
* keine gültige Ressource gefunden werden kann, wird am Ende die
|
||||
* von {@link URL#openStream()} erzeugte {@code IOException} geworfen.
|
||||
* <p>
|
||||
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt.
|
||||
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
|
||||
* Eine {@link SecurityException} beim Öffnen des {@code FileInputStream}
|
||||
* (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 Resource (Absoluter Dateipfad,
|
||||
* Dateipfad im Classpath oder Netzwerkresource)
|
||||
* @return Ein {@code InputStream} für die Resource
|
||||
* @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 Resource oder falls
|
||||
* keine passende Resource gefunden wurde.
|
||||
* einer bestehenden Ressource oder falls
|
||||
* keine passende Ressource gefunden
|
||||
* wurde.
|
||||
*/
|
||||
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
|
||||
return getResourceURL(source).openStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param source
|
||||
* @return
|
||||
* @throws IOException
|
||||
* 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 {
|
||||
@@ -119,22 +127,83 @@ public class ResourceStreamProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
}
|
||||
|
||||
|
||||
5
src/main/java/schule/ngb/zm/util/io/package-info.java
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Dieses Paket enthält Hilfsklassen, um Ressourcen aus verschiedenen Quellen
|
||||
* zu laden.
|
||||
*/
|
||||
package schule.ngb.zm.util.io;
|
||||
4
src/main/java/schule/ngb/zm/util/package-info.java
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Dieses Paket enthält Hilfsklassen für verschiedene Einsatzzwecke.
|
||||
*/
|
||||
package schule.ngb.zm.util;
|
||||
5
src/main/java/schule/ngb/zm/util/tasks/package-info.java
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Dieses Paket enthält Hilfsklassen zur Ausführung paralleler Aufgaben
|
||||
* innerhalb der Zeichenmaschine.
|
||||
*/
|
||||
package schule.ngb.zm.util.tasks;
|
||||
9
src/main/resources/overview.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<HTML>
|
||||
<BODY>
|
||||
Die Zeichenmaschine
|
||||
<p>
|
||||
<h1>Die Zeichenmaschine</h1>
|
||||
|
||||
@author J. Neugebauer
|
||||
</BODY>
|
||||
</HTML>
|
||||
1
src/main/resources/schule/ngb/zm/util/mock-text.txt
Normal file
@@ -0,0 +1 @@
|
||||
Weit hinten, hinter den Wortbergen, fern der Länder Vokalien und Konsonantien leben die Blindtexte. Abgeschieden wohnen Sie in Buchstabhausen an der Küste des Semantik, eines großen Sprachozeans. Ein kleines Bächlein namens Duden fließt durch ihren Ort und versorgt sie mit den nötigen Regelialien. Es ist ein paradiesmatisches Land, in dem einem gebratene Satzteile in den Mund fliegen. Nicht einmal von der allmächtigen Interpunktion werden die Blindtexte beherrscht - ein geradezu unorthographisches Leben. Eines Tages aber beschloß eine kleine Zeile Blindtext, ihr Name war Lorem Ipsum, hinaus zu gehen in die weite Grammatik. Der große Oxmox riet ihr davon ab, da es dort wimmele von bösen Kommata, wilden Fragezeichen und hinterhältigen Semikoli, doch das Blindtextchen ließ sich nicht beirren. Es packte seine sieben Versalien, schob sich sein Initial in den Gürtel und machte sich auf den Weg. Als es die ersten Hügel des Kursivgebirges erklommen hatte, warf es einen letzten Blick zurück auf die Skyline seiner Heimatstadt Buchstabhausen, die Headline von Alphabetdorf und die Subline seiner eigenen Straße, der Zeilengasse. Wehmütig lief ihm eine rethorische Frage über die Wange, dann setzte es seinen Weg fort. Unterwegs traf es eine Copy. Die Copy warnte das Blindtextchen, da, wo sie herkäme wäre sie zigmal umgeschrieben worden und alles, was von ihrem Ursprung noch übrig wäre, sei das Wort "und" und das Blindtextchen solle umkehren und wieder in sein eigenes, sicheres Land zurückkehren. Doch alles Gutzureden konnte es nicht überzeugen und so dauerte es nicht lange, bis ihm ein paar heimtückische Werbetexter auflauerten, es mit Longe und Parole betrunken machten und es dann in ihre Agentur schleppten, wo sie es für ihre Projekte wieder und wieder mißbrauchten. Und wenn es nicht umgeschrieben wurde, dann benutzen Sie es immernoch.
|
||||
949
src/main/resources/schule/ngb/zm/util/mock-words.csv
Normal file
@@ -0,0 +1,949 @@
|
||||
Abbau
|
||||
Abend
|
||||
Abzug
|
||||
Adler
|
||||
Adolf
|
||||
Agent
|
||||
Akten
|
||||
Aktie
|
||||
Alarm
|
||||
Album
|
||||
Allee
|
||||
Alpen
|
||||
Ampel
|
||||
Amtes
|
||||
Anbau
|
||||
Angst
|
||||
Anruf
|
||||
Anton
|
||||
Anzug
|
||||
Apple
|
||||
April
|
||||
Areal
|
||||
Arena
|
||||
Armee
|
||||
Armut
|
||||
Arten
|
||||
Asien
|
||||
Athen
|
||||
Augen
|
||||
Autor
|
||||
Autos
|
||||
Baden
|
||||
Bande
|
||||
Bands
|
||||
Basel
|
||||
Basis
|
||||
Bauch
|
||||
Bauer
|
||||
Bayer
|
||||
Beate
|
||||
Beide
|
||||
Beine
|
||||
Beleg
|
||||
Bernd
|
||||
Beruf
|
||||
Beste
|
||||
Beton
|
||||
Beute
|
||||
Bevor
|
||||
Bezug
|
||||
Bibel
|
||||
Bitte
|
||||
Blatt
|
||||
Blick
|
||||
Block
|
||||
Blues
|
||||
Bogen
|
||||
Bombe
|
||||
Boris
|
||||
Brand
|
||||
Braun
|
||||
Brief
|
||||
Bruch
|
||||
Bruno
|
||||
Brust
|
||||
Buben
|
||||
Buche
|
||||
Busse
|
||||
Bäume
|
||||
Böden
|
||||
Börse
|
||||
Bühne
|
||||
Büros
|
||||
Chaos
|
||||
Chefs
|
||||
Chile
|
||||
China
|
||||
Clubs
|
||||
Coach
|
||||
Daher
|
||||
Damen
|
||||
Daran
|
||||
Darum
|
||||
Datei
|
||||
Daten
|
||||
Datum
|
||||
Dauer
|
||||
David
|
||||
Davon
|
||||
Decke
|
||||
Deren
|
||||
Diana
|
||||
Dinge
|
||||
Donau
|
||||
Doris
|
||||
Drama
|
||||
Dreck
|
||||
Druck
|
||||
Duell
|
||||
Durch
|
||||
Ecken
|
||||
Edith
|
||||
Eiern
|
||||
Eisen
|
||||
Elite
|
||||
Engel
|
||||
Enkel
|
||||
Erbin
|
||||
Erich
|
||||
Erika
|
||||
Erlös
|
||||
Ernst
|
||||
Erste
|
||||
Erwin
|
||||
Essen
|
||||
Ethik
|
||||
Fahne
|
||||
Fahrt
|
||||
Falls
|
||||
Farbe
|
||||
Faust
|
||||
Fazit
|
||||
Feier
|
||||
Feind
|
||||
Felix
|
||||
Ferne
|
||||
Feuer
|
||||
Figur
|
||||
Filme
|
||||
Films
|
||||
Firma
|
||||
Fisch
|
||||
Flüge
|
||||
Focus
|
||||
Folge
|
||||
Fonds
|
||||
Forum
|
||||
Fotos
|
||||
Foyer
|
||||
Franc
|
||||
Frank
|
||||
Franz
|
||||
Frist
|
||||
Fritz
|
||||
Front
|
||||
Frust
|
||||
Fuchs
|
||||
Fulda
|
||||
Fälle
|
||||
Fülle
|
||||
Füßen
|
||||
Geben
|
||||
Gebot
|
||||
Gehen
|
||||
Gehör
|
||||
Geist
|
||||
Georg
|
||||
Gerda
|
||||
Gerät
|
||||
Geste
|
||||
Glanz
|
||||
Glück
|
||||
Gnade
|
||||
Gramm
|
||||
Griff
|
||||
Group
|
||||
Grund
|
||||
Grüne
|
||||
Gunst
|
||||
Gutes
|
||||
Gäste
|
||||
Güter
|
||||
Haare
|
||||
Haars
|
||||
Hafen
|
||||
Halle
|
||||
Hanau
|
||||
Handy
|
||||
Harry
|
||||
Hauch
|
||||
Hause
|
||||
Hebel
|
||||
Heike
|
||||
Heims
|
||||
Heinz
|
||||
Helga
|
||||
Henry
|
||||
Herrn
|
||||
Hilfe
|
||||
Hitze
|
||||
Horst
|
||||
Hosen
|
||||
Hotel
|
||||
Huber
|
||||
Humor
|
||||
Hunde
|
||||
Hände
|
||||
Härte
|
||||
Hätte
|
||||
Höhen
|
||||
Hölle
|
||||
Hörer
|
||||
Hügel
|
||||
Hürde
|
||||
Hütte
|
||||
Ideen
|
||||
Image
|
||||
Index
|
||||
Indiz
|
||||
Infos
|
||||
Insel
|
||||
Intel
|
||||
Irene
|
||||
Islam
|
||||
Jahre
|
||||
Jakob
|
||||
James
|
||||
Japan
|
||||
Jeans
|
||||
Jesus
|
||||
Josef
|
||||
Jubel
|
||||
Juden
|
||||
Julia
|
||||
Jungs
|
||||
Jutta
|
||||
Jäger
|
||||
Kabel
|
||||
Kabul
|
||||
Kader
|
||||
Kairo
|
||||
Kampf
|
||||
Kanal
|
||||
Karin
|
||||
Karte
|
||||
Kasse
|
||||
Kenia
|
||||
Kette
|
||||
Kinos
|
||||
Kirch
|
||||
Klage
|
||||
Klaus
|
||||
Klima
|
||||
Klubs
|
||||
Kohle
|
||||
Kohls
|
||||
Konto
|
||||
Kopie
|
||||
Korea
|
||||
Kraft
|
||||
Krebs
|
||||
Kreis
|
||||
Kreml
|
||||
Kreuz
|
||||
Krieg
|
||||
Kripo
|
||||
Krise
|
||||
Krone
|
||||
Krupp
|
||||
Kugel
|
||||
Kunde
|
||||
Kunst
|
||||
Kurse
|
||||
Kurve
|
||||
König
|
||||
Köpfe
|
||||
Küche
|
||||
Kürze
|
||||
Küste
|
||||
Labor
|
||||
Lager
|
||||
Laien
|
||||
Lande
|
||||
Lange
|
||||
Laufe
|
||||
Laune
|
||||
Leder
|
||||
Legen
|
||||
Lehre
|
||||
Leser
|
||||
Leute
|
||||
Licht
|
||||
Linie
|
||||
Linke
|
||||
Liste
|
||||
Liter
|
||||
Logik
|
||||
Lokal
|
||||
Luxus
|
||||
Lyrik
|
||||
Löhne
|
||||
Löwen
|
||||
Lücke
|
||||
Macht
|
||||
Mafia
|
||||
Magen
|
||||
Mainz
|
||||
Major
|
||||
Maler
|
||||
Maria
|
||||
Marie
|
||||
Mario
|
||||
Marke
|
||||
Markt
|
||||
Maske
|
||||
Masse
|
||||
Match
|
||||
Mauer
|
||||
Media
|
||||
Meist
|
||||
Menge
|
||||
Meter
|
||||
Metro
|
||||
Meyer
|
||||
Miami
|
||||
Milch
|
||||
Minen
|
||||
Mitte
|
||||
Monat
|
||||
Moral
|
||||
Motiv
|
||||
Motor
|
||||
Motto
|
||||
Musik
|
||||
Möbel
|
||||
Nacht
|
||||
Nagel
|
||||
Namen
|
||||
Natur
|
||||
Nazis
|
||||
Nebel
|
||||
Netze
|
||||
Noten
|
||||
Ohren
|
||||
Onkel
|
||||
Opfer
|
||||
Orden
|
||||
Organ
|
||||
Orten
|
||||
Oskar
|
||||
Osten
|
||||
Paare
|
||||
Paket
|
||||
Panik
|
||||
Papst
|
||||
Paris
|
||||
Parks
|
||||
Party
|
||||
Pause
|
||||
Pence
|
||||
Peter
|
||||
Petra
|
||||
Pferd
|
||||
Pfund
|
||||
Phase
|
||||
Pilot
|
||||
Platz
|
||||
Polen
|
||||
Preis
|
||||
Prinz
|
||||
Probe
|
||||
Profi
|
||||
Prosa
|
||||
Punkt
|
||||
Quote
|
||||
Rache
|
||||
Radio
|
||||
Rande
|
||||
Rasen
|
||||
Ratte
|
||||
Rauch
|
||||
Regel
|
||||
Regen
|
||||
Regie
|
||||
Reihe
|
||||
Rente
|
||||
Reste
|
||||
Rhein
|
||||
Rolle
|
||||
Roman
|
||||
Route
|
||||
Rover
|
||||
Rubel
|
||||
Russe
|
||||
Räder
|
||||
Römer
|
||||
Sache
|
||||
Sauna
|
||||
Schau
|
||||
Schoß
|
||||
Seele
|
||||
Segen
|
||||
Seite
|
||||
Sekte
|
||||
Senat
|
||||
Seoul
|
||||
Serie
|
||||
Shell
|
||||
Sicht
|
||||
Simon
|
||||
Sinne
|
||||
Smith
|
||||
Songs
|
||||
Sonne
|
||||
Sound
|
||||
Spiel
|
||||
Sport
|
||||
Spree
|
||||
Staat
|
||||
Stadt
|
||||
Stamm
|
||||
Stars
|
||||
Start
|
||||
Stasi
|
||||
Staub
|
||||
Staus
|
||||
Stein
|
||||
Stern
|
||||
Stirn
|
||||
Stock
|
||||
Stoff
|
||||
Story
|
||||
Strom
|
||||
Stufe
|
||||
Stuhl
|
||||
Sturm
|
||||
Sturz
|
||||
Stück
|
||||
Summe
|
||||
Szene
|
||||
Sätze
|
||||
Söhne
|
||||
Süden
|
||||
Tagen
|
||||
Tages
|
||||
Tante
|
||||
Tarif
|
||||
Teams
|
||||
Tempo
|
||||
Tenor
|
||||
Tests
|
||||
Texas
|
||||
Texte
|
||||
Thema
|
||||
These
|
||||
Thron
|
||||
Tiefs
|
||||
Tiere
|
||||
Tiger
|
||||
Tisch
|
||||
Titel
|
||||
Todes
|
||||
Tokio
|
||||
Tonne
|
||||
Toren
|
||||
Traum
|
||||
Treff
|
||||
Trend
|
||||
Trick
|
||||
Trios
|
||||
Trost
|
||||
Typen
|
||||
Täter
|
||||
Tönen
|
||||
Türen
|
||||
Türke
|
||||
UdSSR
|
||||
Uhren
|
||||
Ulmer
|
||||
Umbau
|
||||
Umzug
|
||||
Union
|
||||
Unmut
|
||||
Vater
|
||||
Vetos
|
||||
Video
|
||||
Villa
|
||||
Viren
|
||||
Virus
|
||||
Vogts
|
||||
Votum
|
||||
Vögel
|
||||
Waffe
|
||||
Weber
|
||||
Weile
|
||||
Welle
|
||||
Werke
|
||||
Werks
|
||||
Wesen
|
||||
Wiese
|
||||
Wille
|
||||
Willi
|
||||
Wirte
|
||||
Wirts
|
||||
Witwe
|
||||
Witze
|
||||
Woche
|
||||
Wolke
|
||||
World
|
||||
Worte
|
||||
Wände
|
||||
Wüste
|
||||
Zaire
|
||||
Zeile
|
||||
Zelle
|
||||
Zeugs
|
||||
Ziels
|
||||
Zitat
|
||||
Zorns
|
||||
Zunge
|
||||
Zweck
|
||||
Zähne
|
||||
Zügen
|
||||
ahnen
|
||||
ahnst
|
||||
aktiv
|
||||
allem
|
||||
allen
|
||||
aller
|
||||
alles
|
||||
allzu
|
||||
altem
|
||||
alten
|
||||
altes
|
||||
angab
|
||||
armen
|
||||
außen
|
||||
außer
|
||||
bauen
|
||||
baute
|
||||
bekam
|
||||
berge
|
||||
berät
|
||||
besaß
|
||||
bezog
|
||||
biete
|
||||
birgt
|
||||
blaue
|
||||
blieb
|
||||
blind
|
||||
bloße
|
||||
boten
|
||||
brach
|
||||
breit
|
||||
bunte
|
||||
bösen
|
||||
dabei
|
||||
dafür
|
||||
dahin
|
||||
damit
|
||||
darin
|
||||
davor
|
||||
deine
|
||||
denen
|
||||
denke
|
||||
denkt
|
||||
derer
|
||||
desto
|
||||
dicht
|
||||
dicke
|
||||
dient
|
||||
diese
|
||||
drauf
|
||||
dreht
|
||||
drohe
|
||||
droht
|
||||
dürfe
|
||||
ebene
|
||||
echte
|
||||
ehren
|
||||
eigen
|
||||
einem
|
||||
einen
|
||||
einer
|
||||
eines
|
||||
einig
|
||||
einst
|
||||
eisig
|
||||
elend
|
||||
enden
|
||||
endet
|
||||
engem
|
||||
engen
|
||||
enger
|
||||
enges
|
||||
enorm
|
||||
erben
|
||||
ergab
|
||||
erhob
|
||||
ernte
|
||||
etwas
|
||||
ewige
|
||||
exakt
|
||||
extra
|
||||
fehle
|
||||
fehlt
|
||||
feine
|
||||
feste
|
||||
finde
|
||||
folgt
|
||||
frage
|
||||
fragt
|
||||
freie
|
||||
fremd
|
||||
freut
|
||||
frühe
|
||||
fähig
|
||||
fällt
|
||||
fängt
|
||||
fügte
|
||||
fühle
|
||||
fühlt
|
||||
führe
|
||||
führt
|
||||
füllt
|
||||
gaben
|
||||
ganze
|
||||
gegen
|
||||
gelbe
|
||||
gelte
|
||||
gemäß
|
||||
genau
|
||||
genug
|
||||
gerne
|
||||
getan
|
||||
geübt
|
||||
ginge
|
||||
gingt
|
||||
glatt
|
||||
graue
|
||||
große
|
||||
gutem
|
||||
guten
|
||||
haben
|
||||
halbe
|
||||
halte
|
||||
heben
|
||||
hebst
|
||||
hefte
|
||||
heiße
|
||||
heißt
|
||||
helle
|
||||
heran
|
||||
herum
|
||||
heuer
|
||||
heute
|
||||
hielt
|
||||
hilft
|
||||
hinzu
|
||||
hoffe
|
||||
hofft
|
||||
hohem
|
||||
hohes
|
||||
holen
|
||||
holte
|
||||
hängt
|
||||
höher
|
||||
hören
|
||||
hörte
|
||||
ideal
|
||||
ihnen
|
||||
ihrem
|
||||
ihren
|
||||
ihrer
|
||||
ihres
|
||||
immer
|
||||
indem
|
||||
indes
|
||||
innen
|
||||
jedem
|
||||
jeden
|
||||
jeder
|
||||
jedes
|
||||
jeher
|
||||
jenem
|
||||
jenen
|
||||
jener
|
||||
jenes
|
||||
jetzt
|
||||
junge
|
||||
kalte
|
||||
kauft
|
||||
kehrt
|
||||
keine
|
||||
kenne
|
||||
kennt
|
||||
klagt
|
||||
klang
|
||||
klare
|
||||
klein
|
||||
knapp
|
||||
komme
|
||||
kommt
|
||||
krank
|
||||
kämen
|
||||
könne
|
||||
lacht
|
||||
laden
|
||||
lagen
|
||||
lasse
|
||||
leben
|
||||
lebte
|
||||
leere
|
||||
legte
|
||||
lehnt
|
||||
lehrt
|
||||
leise
|
||||
lernt
|
||||
lesen
|
||||
liebe
|
||||
liebt
|
||||
liege
|
||||
liegt
|
||||
liest
|
||||
ließe
|
||||
links
|
||||
lobte
|
||||
lockt
|
||||
lohnt
|
||||
lässt
|
||||
läuft
|
||||
lösen
|
||||
löste
|
||||
mache
|
||||
manch
|
||||
meine
|
||||
meint
|
||||
merkt
|
||||
messe
|
||||
miete
|
||||
minus
|
||||
morde
|
||||
mögen
|
||||
müsse
|
||||
nahen
|
||||
neben
|
||||
nebst
|
||||
nehme
|
||||
nennt
|
||||
nette
|
||||
neuem
|
||||
neuen
|
||||
neuer
|
||||
neues
|
||||
nicht
|
||||
nimmt
|
||||
näher
|
||||
nötig
|
||||
nützt
|
||||
obere
|
||||
offen
|
||||
ordne
|
||||
packe
|
||||
packt
|
||||
parat
|
||||
passe
|
||||
passt
|
||||
plane
|
||||
plant
|
||||
prüfe
|
||||
prüft
|
||||
quasi
|
||||
rasch
|
||||
raten
|
||||
reale
|
||||
recht
|
||||
reden
|
||||
redet
|
||||
reich
|
||||
reine
|
||||
reise
|
||||
reist
|
||||
rollt
|
||||
rotem
|
||||
roten
|
||||
roter
|
||||
rotes
|
||||
rufen
|
||||
ruhen
|
||||
ruhig
|
||||
runde
|
||||
räume
|
||||
räumt
|
||||
rückt
|
||||
sagen
|
||||
sagte
|
||||
sahen
|
||||
sanft
|
||||
satte
|
||||
sauer
|
||||
saßen
|
||||
schuf
|
||||
schön
|
||||
sechs
|
||||
sehen
|
||||
seien
|
||||
seine
|
||||
sende
|
||||
setze
|
||||
setzt
|
||||
siege
|
||||
siehe
|
||||
sieht
|
||||
singt
|
||||
sinkt
|
||||
sitze
|
||||
sitzt
|
||||
sogar
|
||||
solch
|
||||
solle
|
||||
sollt
|
||||
somit
|
||||
sonst
|
||||
sorge
|
||||
sorgt
|
||||
sowie
|
||||
spare
|
||||
spart
|
||||
spitz
|
||||
späte
|
||||
spürt
|
||||
stahl
|
||||
stand
|
||||
starb
|
||||
stark
|
||||
statt
|
||||
stehe
|
||||
steht
|
||||
stets
|
||||
stich
|
||||
stieg
|
||||
stieß
|
||||
still
|
||||
stolz
|
||||
stoße
|
||||
stört
|
||||
stößt
|
||||
suche
|
||||
sucht
|
||||
tanze
|
||||
tanzt
|
||||
taten
|
||||
teile
|
||||
teils
|
||||
teilt
|
||||
teuer
|
||||
teure
|
||||
tiefe
|
||||
tolle
|
||||
total
|
||||
totem
|
||||
toter
|
||||
totes
|
||||
traue
|
||||
traut
|
||||
treue
|
||||
trieb
|
||||
trink
|
||||
tritt
|
||||
trotz
|
||||
träge
|
||||
trägt
|
||||
tätig
|
||||
töten
|
||||
tötet
|
||||
unser
|
||||
unten
|
||||
unter
|
||||
vagem
|
||||
vagen
|
||||
vager
|
||||
vages
|
||||
viele
|
||||
volle
|
||||
vorab
|
||||
voran
|
||||
vorne
|
||||
wagen
|
||||
wahre
|
||||
waren
|
||||
warnt
|
||||
warst
|
||||
warte
|
||||
warum
|
||||
wecke
|
||||
weckt
|
||||
weder
|
||||
wegen
|
||||
wehre
|
||||
wehrt
|
||||
weich
|
||||
weist
|
||||
weite
|
||||
weiße
|
||||
welch
|
||||
wende
|
||||
wenig
|
||||
werbe
|
||||
werde
|
||||
werte
|
||||
wider
|
||||
widme
|
||||
wiegt
|
||||
wilde
|
||||
wirbt
|
||||
wirft
|
||||
wirke
|
||||
wirkt
|
||||
wisse
|
||||
wobei
|
||||
wofür
|
||||
woher
|
||||
wohin
|
||||
wohne
|
||||
wohnt
|
||||
wolle
|
||||
womit
|
||||
wovon
|
||||
wuchs
|
||||
wunde
|
||||
wurde
|
||||
wähle
|
||||
wählt
|
||||
wärme
|
||||
zahle
|
||||
zeige
|
||||
zeigt
|
||||
zeuge
|
||||
zieht
|
||||
ziele
|
||||
zielt
|
||||
zivil
|
||||
zogen
|
||||
zudem
|
||||
zumal
|
||||
zuvor
|
||||
zwang
|
||||
zwölf
|
||||
zählt
|
||||
zügig
|
||||
Ämter
|
||||
Ärger
|
||||
Ärzte
|
||||
Übung
|
||||
älter
|
||||
öffne
|
||||
öfter
|
||||
übrig
|
||||
übten
|
||||
|
@@ -252,4 +252,18 @@ class ColorTest {
|
||||
void darker() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void compare() {
|
||||
assertEquals(1.0, Color.RED.compare(Color.RED), 0.0001);
|
||||
assertEquals(1.0, Color.BLUE.compare(Color.BLUE), 0.0001);
|
||||
assertEquals(1.0, Color.WHITE.compare(Color.WHITE), 0.0001);
|
||||
assertEquals(1.0, Color.BLACK.compare(Color.BLACK), 0.0001);
|
||||
|
||||
|
||||
assertEquals(0.0, Color.BLACK.compare(Color.WHITE), 0.0001);
|
||||
assertEquals(0.0, Color.WHITE.compare(Color.BLACK), 0.0001);
|
||||
|
||||
assertEquals(0.5, Color.GRAY.compare(Color.BLACK), 0.01);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,10 +25,28 @@ class ConstantsTest {
|
||||
assertEquals(5.0, Constants.max(new double[]{ 5.0, 5.0, 5.0, 5.0, 5.0 }), 0.000001);
|
||||
assertEquals(5.0, Constants.max(new double[]{ 5.0 }), 0.000001);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> Constants.max(null));
|
||||
double[] doubleArr = null;
|
||||
assertThrows(IllegalArgumentException.class, () -> Constants.max(doubleArr));
|
||||
assertThrows(IllegalArgumentException.class, () -> Constants.max(new double[]{}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxInt() {
|
||||
assertEquals(5, Constants.max(5, 0));
|
||||
assertEquals(5, Constants.max(5, 4));
|
||||
assertEquals(5, Constants.max(5, 5));
|
||||
assertEquals(100, Constants.max(100, -100));
|
||||
assertEquals(0, Constants.max(0, 0));
|
||||
|
||||
assertEquals(5, Constants.max(new double[]{ 0, 1, 2, 3, 4, 5 }));
|
||||
assertEquals(5, Constants.max(new double[]{ 5, 5, 5, 5, 5 }));
|
||||
assertEquals(5, Constants.max(new double[]{ 5 }));
|
||||
|
||||
int[] intArr = null;
|
||||
assertThrows(IllegalArgumentException.class, () -> Constants.max(intArr));
|
||||
assertThrows(IllegalArgumentException.class, () -> Constants.max(new int[]{}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void abs() {
|
||||
assertEquals(5.0, Constants.abs(5.0), 0.000001);
|
||||
|
||||
51
src/test/java/schule/ngb/zm/Testmaschine.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
|
||||
import schule.ngb.zm.layers.DrawingLayer;
|
||||
import schule.ngb.zm.util.Log;
|
||||
|
||||
public class Testmaschine extends Zeichenmaschine {
|
||||
|
||||
static {
|
||||
Log.enableGlobalDebugging();
|
||||
}
|
||||
|
||||
private DrawingLayer gridLayer;
|
||||
|
||||
public Testmaschine() {
|
||||
this(400, 400);
|
||||
}
|
||||
|
||||
public Testmaschine( int width, int height ) {
|
||||
super(width, height, "Testmaschine", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void settings() {
|
||||
gridLayer = new DrawingLayer(getWidth(), getHeight());
|
||||
this.getCanvas().addLayer(1, gridLayer);
|
||||
setGrid(50, 10);
|
||||
}
|
||||
|
||||
public void setGrid( int majorGrid, int minorGrid ) {
|
||||
gridLayer.clear();
|
||||
|
||||
gridLayer.clear(LIGHTGRAY);
|
||||
gridLayer.setStrokeColor(LIGHTGRAY.darker(20));
|
||||
for( int i = 0; i < getWidth(); i += minorGrid ) {
|
||||
gridLayer.line(i, 0, i, gridLayer.getHeight());
|
||||
}
|
||||
for( int i = 0; i < getHeight(); i += minorGrid ) {
|
||||
gridLayer.line(0, i, gridLayer.getWidth(), i);
|
||||
}
|
||||
|
||||
gridLayer.setStrokeColor(LIGHTGRAY.darker(50));
|
||||
for( int i = 0; i < getWidth(); i += majorGrid ) {
|
||||
gridLayer.line(i, 0, i, gridLayer.getHeight());
|
||||
}
|
||||
for( int i = 0; i < getHeight(); i += majorGrid ) {
|
||||
gridLayer.line(0, i, gridLayer.getWidth(), i);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
71
src/test/java/schule/ngb/zm/TestmaschineTest.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import schule.ngb.zm.layers.DrawingLayer;
|
||||
import schule.ngb.zm.util.io.ImageLoader;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static schule.ngb.zm.util.test.ImageAssertions.assertEquals;
|
||||
import static schule.ngb.zm.util.test.ImageAssertions.setSaveDiffImageOnFail;
|
||||
|
||||
public class TestmaschineTest {
|
||||
|
||||
private static Testmaschine tm;
|
||||
|
||||
private static DrawingLayer drawing;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
setSaveDiffImageOnFail(true);
|
||||
|
||||
tm = new Testmaschine();
|
||||
drawing = tm.getDrawingLayer();
|
||||
assertNotNull(drawing);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
tm.exit();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
drawing.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSaveDiffImage() {
|
||||
drawing.noStroke();
|
||||
drawing.setAnchor(Constants.NORTHWEST);
|
||||
drawing.setFillColor(Constants.BLUE);
|
||||
drawing.rect(0, 0, 400, 400);
|
||||
drawing.setFillColor(Constants.RED);
|
||||
drawing.rect(100, 100, 200, 200);
|
||||
|
||||
BufferedImage img1 = ImageLoader.createImage(400, 400);
|
||||
Graphics2D graphics = img1.createGraphics();
|
||||
|
||||
graphics.setColor(Constants.BLUE.getJavaColor());
|
||||
graphics.fillRect(0, 0, 400, 400);
|
||||
graphics.setColor(Constants.RED.getJavaColor());
|
||||
graphics.fillRect(100, 100, 200, 200);
|
||||
|
||||
assertEquals(drawing.buffer, drawing.buffer);
|
||||
assertEquals(ImageLoader.copyImage(drawing.buffer), drawing.buffer);
|
||||
assertEquals(img1, drawing.buffer);
|
||||
assertEquals(img1, tm.getImage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGrid() {
|
||||
// tm.setGrid(50, 10);
|
||||
tm.delay(2000);
|
||||
}
|
||||
|
||||
}
|
||||
96
src/test/java/schule/ngb/zm/anim/AnimationGroupsTest.java
Normal file
@@ -0,0 +1,96 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import schule.ngb.zm.Color;
|
||||
import schule.ngb.zm.Testmaschine;
|
||||
import schule.ngb.zm.Zeichenmaschine;
|
||||
import schule.ngb.zm.layers.ShapesLayer;
|
||||
import schule.ngb.zm.shapes.Circle;
|
||||
import schule.ngb.zm.shapes.Shape;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class AnimationGroupsTest {
|
||||
|
||||
private static Testmaschine zm;
|
||||
|
||||
private static ShapesLayer shapes;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
zm = new Testmaschine();
|
||||
shapes = zm.getShapesLayer();
|
||||
assertNotNull(shapes);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
zm.exit();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
shapes.removeAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void animationGroup() {
|
||||
Shape s = new Circle(0, 0, 10);
|
||||
shapes.add(s);
|
||||
|
||||
Animation<Shape> anims = new AnimationGroup<>(
|
||||
500,
|
||||
Arrays.asList(
|
||||
new MoveAnimation(s, 200, 200, 2000, Easing.DEFAULT_EASING),
|
||||
new FillAnimation(s, Color.GREEN, 1000, Easing.sineIn())
|
||||
)
|
||||
);
|
||||
Animations.playAndWait(anims);
|
||||
assertEquals(200, s.getX());
|
||||
assertEquals(200, s.getY());
|
||||
assertEquals(Color.GREEN, s.getFillColor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void animationSequence() {
|
||||
Shape s = new Circle(0, 0, 10);
|
||||
shapes.add(s);
|
||||
|
||||
Animation<Shape> anims = new AnimationSequence<>(
|
||||
Arrays.asList(
|
||||
new CircleAnimation(s, 200, 0, 90, false, 1000, Easing::rushIn),
|
||||
new CircleAnimation(s, 200, 400, 90, 1000, Easing::rushOut),
|
||||
new CircleAnimation(s, 200, 400, 90, false, 1000, Easing::rushIn),
|
||||
new CircleAnimation(s, 200, 0, 90, 1000, Easing::rushOut)
|
||||
)
|
||||
);
|
||||
Animations.playAndWait(anims);
|
||||
assertEquals(0, s.getX());
|
||||
assertEquals(0, s.getY());
|
||||
}
|
||||
|
||||
@Test
|
||||
void animationSequenceContinous() {
|
||||
Shape s = new Circle(0, 0, 10);
|
||||
shapes.add(s);
|
||||
|
||||
Animation<Shape> anims = new ContinousAnimation<>(new AnimationSequence<>(
|
||||
Arrays.asList(
|
||||
new CircleAnimation(s, 200, 0, 90, false, 1000, Easing::rushIn),
|
||||
new CircleAnimation(s, 200, 400, 90, 1000, Easing::rushOut),
|
||||
new CircleAnimation(s, 200, 400, 90, false, 1000, Easing::rushIn),
|
||||
new CircleAnimation(s, 200, 0, 90, 1000, Easing::rushOut)
|
||||
)
|
||||
), false);
|
||||
Animations.playAndWait(anims);
|
||||
zm.delay(8000);
|
||||
anims.stop();
|
||||
}
|
||||
|
||||
}
|
||||
75
src/test/java/schule/ngb/zm/anim/AnimationTest.java
Normal file
@@ -0,0 +1,75 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import schule.ngb.zm.Testmaschine;
|
||||
import schule.ngb.zm.layers.ShapesLayer;
|
||||
import schule.ngb.zm.shapes.Circle;
|
||||
import schule.ngb.zm.shapes.Shape;
|
||||
import schule.ngb.zm.util.test.TestEnv;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class AnimationTest {
|
||||
|
||||
private static Testmaschine zm;
|
||||
|
||||
private static ShapesLayer shapes;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeAll() {
|
||||
zm = new Testmaschine();
|
||||
shapes = zm.getShapesLayer();
|
||||
assertNotNull(shapes);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterAll() {
|
||||
zm.exit();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
shapes.removeAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void circleAnimation() {
|
||||
Shape s = new Circle(zm.getWidth()/4.0, zm.getHeight()/2.0, 10);
|
||||
shapes.add(s);
|
||||
|
||||
CircleAnimation anim = new CircleAnimation(s, zm.getWidth()/2.0, zm.getHeight()/2.0, 360, true, 3000, Easing::linear);
|
||||
Animations.playAndWait(anim);
|
||||
assertEquals(zm.getWidth()/4.0, s.getX());
|
||||
assertEquals(zm.getHeight()/2.0, s.getY());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fadeAnimation() {
|
||||
Shape s = new Circle(zm.getWidth()/4.0, zm.getHeight()/2.0, 10);
|
||||
s.setFillColor(s.getFillColor(), 0);
|
||||
s.setStrokeColor(s.getStrokeColor(), 0);
|
||||
shapes.add(s);
|
||||
|
||||
Animation<Shape> anim = new FadeAnimation(s, 255, 1000);
|
||||
Animations.playAndWait(anim);
|
||||
assertEquals(s.getFillColor().getAlpha(), 255);
|
||||
}
|
||||
|
||||
@Test
|
||||
void continousAnimation() {
|
||||
Shape s = new Circle(zm.getWidth()/4, zm.getHeight()/2, 10);
|
||||
shapes.add(s);
|
||||
|
||||
ContinousAnimation anim = new ContinousAnimation(
|
||||
new CircleAnimation(s, zm.getWidth()/2, zm.getHeight()/2, 360, true, 1000, Easing::linear)
|
||||
);
|
||||
Animations.play(anim);
|
||||
zm.delay(3000);
|
||||
anim.stop();
|
||||
}
|
||||
|
||||
}
|
||||