120 Commits

Author SHA1 Message Date
e2e6f8c291 Bug: Synchronized Methoden verschoben 2022-07-26 18:15:23 +02:00
916a581768 Refactorings 2022-07-26 18:14:59 +02:00
5bb2f75193 Bug: getShapes in ShapeGroup war immer leer 2022-07-26 18:14:50 +02:00
f0e4cd6c80 Refactoring des Beendens der ZM 2022-07-26 18:14:23 +02:00
a228b21c84 Verantwortlichkeiten für Layout und Aufgaben klarer getrennt 2022-07-26 08:59:30 +02:00
68c88ec9ca Merge branch 'main' into zeichenfenster
# Conflicts:
#	src/main/java/schule/ngb/zm/media/Sound.java
2022-07-25 19:07:51 +02:00
0d1dd771dd Logger eingefügt 2022-07-25 19:06:01 +02:00
e995bfc4fe Bug: Spielemaschine blockt nicht mehr nebenläufige Threads 2022-07-25 19:05:54 +02:00
97ff03990a Shape caching entfernt
In Tests konnten keine Geschwindigkeitsvorteile festgestellt werden.
2022-07-25 19:05:28 +02:00
bd2364a8df Laden von Schriftarten mit eigenem Namen möglich 2022-07-25 19:05:04 +02:00
617b915874 Refactorings zur Nebenläufigkeit 2022-07-25 17:45:39 +02:00
7772793e8d Kommentar 2022-07-25 17:44:31 +02:00
bd8c0e37a7 Audio-Methoden synchronisiert 2022-07-25 17:44:22 +02:00
ecbe2b4f6b interpolate zu animate umbenannt
Animation erbt zur Vereinfachung nun auch von Constants und dort gibt es schon eine interpolate Methode.
2022-07-25 17:42:06 +02:00
20fe700756 Rechtschreibung und standard Log-Format 2022-07-25 17:41:18 +02:00
0100a3f574 Methode um mehrere Animationen im ShapesLayer zu starten 2022-07-25 17:41:01 +02:00
aceb79c44f Animate Methode zu play umbenannt 2022-07-25 17:40:42 +02:00
a4e29ccdba Loader KLassen in io Paket verschoben 2022-07-25 17:38:53 +02:00
55014c8eec Klasse Zeichenfenster ausgelagert 2022-07-25 17:35:46 +02:00
4f958cd57c ImageLoaders in io Paket verschoben 2022-07-21 22:01:54 +02:00
04506f6e9c JFrame in eine eigene Klasse ausgelagert 2022-07-21 22:01:38 +02:00
5a27e18634 Javadoc und kleine Refactorings 2022-07-21 21:02:50 +02:00
8b23c658e8 Animator Interface entfernt 2022-07-21 21:02:30 +02:00
1ca13c977a Javadoc 2022-07-21 21:02:10 +02:00
78c93666d0 Javadoc 2022-07-21 21:01:46 +02:00
917eb805c6 Bug: Threadsafety 2022-07-21 21:01:33 +02:00
fddd8d621b flush() nach jeder Log-Nachricht
Der Logger sendet nun nach jedem Log die Nachricht zum OutputStream.
2022-07-21 21:00:55 +02:00
4bf0068051 Icons werden nun in allen Größen geladen
Alle vorhandenen Icons werden geladen und mit Jframe.setIconImages() dem Fenster hinzugefügt. Unter macOS wird nur die Größe 512 geladen und als Dock-Icon gesetzt.
2022-07-21 20:59:28 +02:00
371a962432 Syncronisation des Zeichenthreads mit update/draw über eigenen Zustand
delay() setzt den Zustand auf DELAYED und der Zeichenthread läuft weiter, wenn der update/draw Thread in diesen Zustand wechselt (also delay() aufgerufen wurde). Es wird nicht mehr Thread.getState() geprüft, dies zu unzuverlässi gwar.
2022-07-21 10:54:08 +02:00
99848e47f8 colt abhängigkeit nur für’s kompilieren 2022-07-21 10:52:47 +02:00
f75aaf4b7e Predict-Methode für eine Eingabe 2022-07-21 10:52:19 +02:00
e5c6fa634a Anpassung der Package-Struktur 2022-07-20 17:15:29 +02:00
ccc83414c7 Merge branch 'optional-ml' 2022-07-20 17:09:24 +02:00
16477463d4 java doc und refactorings 2022-07-20 17:09:09 +02:00
d3997561fc Streams durch Schleifen ersetzt
Der Overhead durch die parallelen Streams war zu hoch. Jedenfalls bei den relativ kleinen Matrizen im Test. Bei größeren Matrizen könnte die Parallelität einen Vorteil bringen. Ggf. sollte dies getesett werden und abhängig von der Größe die bestte Methode gewählt werden.
2022-07-19 22:53:46 +02:00
b6b4ffe6a5 Weitere Tests eingefügt und verbessert 2022-07-19 22:52:23 +02:00
bf261b5e9b Colt als optionale Abhängigkeit
DAs Anlernen des NN geht um den Faktor 20 schneller, wenn Colt benutzt wird.
2022-07-19 20:05:37 +02:00
b79f26f51e Matric interface umbenannt 2022-07-19 09:14:00 +02:00
538a8215e6 Userinput wird nach Stopp der ZM weiterverarbeitet 2022-07-19 08:56:00 +02:00
cbda5c3077 Bug: Linearer Farbverlauf wurde nicht korrekt berechnet 2022-07-19 08:55:06 +02:00
2caa528a5e Listeniterationen Threadsafe gemacht 2022-07-18 22:48:28 +02:00
bb50abb7bd Javadoc 2022-07-18 22:48:08 +02:00
38d5f22fb6 Bug: UpdateThreadExecutor blockt nun korrekt den Zeichenthread 2022-07-18 22:47:37 +02:00
d34c60505e Bug: mousePressed wurde nicht ausgelöst 2022-07-18 22:46:48 +02:00
4c8e5c8939 USing Colt library as optional dependency 2022-07-18 11:06:08 +02:00
9a9a714050 Javadoc 2022-07-17 16:38:42 +02:00
f0b064a3d5 Changelog 2022-07-17 15:57:34 +02:00
c922357ab7 Bug behoben: Flackern bei Farbverläufen 2022-07-17 15:57:24 +02:00
17c31a1a03 Bug behoben: delay() funktioniert nun auch nach Stopp der ZM 2022-07-17 15:57:02 +02:00
6551bb75c9 Farbverläufe für Formen und neue Konstantennamen 2022-07-17 15:45:05 +02:00
3931e610c6 Changelog und Versionsnummer 2022-07-17 09:28:11 +02:00
bf14bf14dd Fixewd javadoc 2022-07-17 09:27:56 +02:00
222bc0ff7d Merge branch 'concurrent-frames' 2022-07-17 08:55:13 +02:00
33fb503ab8 animateAndWait methoden 2022-07-17 08:54:47 +02:00
aad53d51d1 FadeAnimation erstellt 2022-07-17 08:54:34 +02:00
d87e455e9d Bug beim Laden von AudioStreams behoben 2022-07-17 08:54:17 +02:00
4fd4aa9a94 update/draw nun in eigenem Thread
update/draw wird nun einmal pro Frame als separater Thread ausgeführt. Falls dabei delay oder eine andere wartende Methode aufgerufen wird, läuft die ZM aber weiter, bis der update/draw Thread wieder aufwacht. Dadurch werden Animationen und andere parallele Prozesse nicht auch geblockt.
2022-07-17 08:53:43 +02:00
7031aa40cc Animationssystem erweitert 2022-07-16 17:01:39 +02:00
c295821d85 Timer und Counter Hilfsklassen 2022-07-16 13:32:03 +02:00
91805f7794 Refactorings FileLoader 2022-07-15 22:27:44 +02:00
22c0547caa Merge branch 'main' into tasks 2022-07-15 21:45:37 +02:00
d443d4d11d Merge branch 'main' into ml 2022-07-15 21:44:58 +02:00
4366726671 Merge branch 'main' into events 2022-07-15 21:40:55 +02:00
6040545274 Tests zum Laden und SPeichern 2022-07-15 19:42:48 +02:00
7b84570d18 Umbennung getResourceStream->getinputStream 2022-07-15 19:42:35 +02:00
b24eec5063 Laden und speichern von Netzen ermöglicht 2022-07-15 19:42:13 +02:00
d5abd4ef68 Biases im NeuronLayer eingeführt 2022-07-15 19:42:01 +02:00
9cd37fdce0 Merge branch 'main' into ml 2022-07-14 23:03:08 +02:00
7e023026ce Klasse FileLoader um Textdateien zu laden 2022-07-14 23:02:26 +02:00
005f8299ac Testklasse für Perlin Noise 2022-07-14 19:27:47 +02:00
763d9eff22 Bugfix beim setzen des StrokeType 2022-07-14 19:27:36 +02:00
b244a6e094 arrange und align Funktionen 2022-07-14 18:04:01 +02:00
4597fb411a DrawingLayer.pixel zeichnet nun direkt in den Puffer 2022-07-14 18:03:23 +02:00
9634d11842 Reihenfolge der Richtungs-Enum geändert 2022-07-14 18:02:48 +02:00
e6882fcbf7 WindowAdapter beendet die ZM nun direkt, und wartet nicht auf den Thread
Bisher hat ein Schließen des Fensters dazu geführt, dass `running = false` gestzt wurde und die Maschine den aktuellen Frame noch beenden konnte. Da ein Frame durch die Verwendung von `delay(int)` aber auch lange dauern kann, hatte man als Nutzer dann den Eindruck, dass der Klick nicht registriert wurde.
Nun beendet das Programm direkt, ruft aber trotzdem vorher `teardown()` und `cleanup()` auf.
2022-07-14 18:02:30 +02:00
c0831688ba Test für Noise-Klasse 2022-07-14 17:59:48 +02:00
1e4b865492 Color refactored 2022-07-14 17:59:32 +02:00
0098621ebe Klasse für Perlin Noise implementiert
Die Klasse erzeugt Zuffalswerte nach dem von Ken Perlin erfundenen „Improved Perlin Noise“ Algorithmus.
2022-07-14 17:59:17 +02:00
949f058b5d Fehlenden Klassenkommentar ergänzt 2022-07-12 23:33:38 +02:00
740bb37279 Klasse Constants kommentiert und refactored 2022-07-12 23:22:35 +02:00
f8550ceae5 Projekt an Gradle Struktur angepasst 2022-07-11 22:12:11 +02:00
99d0e702aa eventDispatch nur nach initialisierung aufrufen 2022-07-11 22:09:00 +02:00
5557030e0d Delayed initialization auch für Music Klasse 2022-07-11 14:41:58 +02:00
855d67c873 AnimationListener 2022-07-11 14:41:45 +02:00
bc791a9dc3 Merge branch 'anim' into events 2022-07-11 14:08:03 +02:00
f79cff18a6 Merge branch 'main' into tasks
# Conflicts:
#	src/main/java/schule/ngb/zm/tasks/DelayedTask.java
#	src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java
#	src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java
#	src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java
#	src/main/java/schule/ngb/zm/tasks/Task.java
2022-07-11 14:07:24 +02:00
921e2fb3ef An Gradle Struktur angepasst 2022-07-11 14:06:47 +02:00
944249ce82 Merge branch 'main' into anim
# Conflicts:
#	src/main/java/schule/ngb/zm/tasks/DelayedTask.java
#	src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java
#	src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java
#	src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java
#	src/main/java/schule/ngb/zm/tasks/Task.java
2022-07-11 14:05:46 +02:00
e1f01fe620 Auf Gradle Struktur angepasst 2022-07-11 14:03:21 +02:00
9f56b49fb7 Merge branch 'main' into events
# Conflicts:
#	src/main/java/schule/ngb/zm/media/AudioListener.java
#	src/main/java/schule/ngb/zm/media/Mixer.java
#	src/main/java/schule/ngb/zm/media/Music.java
#	src/schule/ngb/zm/media/Sound.java
2022-07-11 14:00:37 +02:00
ec8e5cea91 API angepasst und Javadoc verbessert 2022-07-11 13:53:15 +02:00
6a2adc9d4d Mehr Javadoc Korrekturen 2022-07-11 08:22:47 +02:00
9088e6eceb Merge branch 'main' into ml 2022-07-11 08:19:41 +02:00
2c322eb678 Javadocs angepasst und Fehler behoben 2022-07-11 08:18:52 +02:00
b60ffa9ef3 Projektstruktur an Gradle angepasst 2022-07-10 22:46:55 +02:00
0a72b97d56 Gradle initialisiert 2022-07-10 22:46:27 +02:00
4ea526b239 Erste Implementation eines einfachen neuronalen Netzes
Vorbild zur Implementation: https://github.com/wheresvic/neuralnet
2022-07-10 22:33:21 +02:00
54762cb3e6 enabelGlobalDebugging verbessert 2022-07-09 21:01:56 +02:00
2e5d5d7e83 Debug Nachricht beim shutdown 2022-07-09 21:01:39 +02:00
43c5b9f28e Fehler beim erstellen eines AudioStreams behoben 2022-07-09 21:01:01 +02:00
62f221e18e BlueJ Testprojekt entfernt 2022-07-09 21:00:11 +02:00
b3ff1a507e Tests auf Junit5 angepasst 2022-07-08 16:36:33 +02:00
600a49af64 invokeLater gibt nun auch eine Future zurück 2022-07-08 09:52:03 +02:00
b575c47ab3 Neue Easing funktionen 2022-07-08 08:52:31 +02:00
447accc567 Animationen nutzen FrameSynchronizedTasks 2022-07-08 08:01:53 +02:00
2de37e5501 Merge branch 'tasks' into anim 2022-07-08 08:01:35 +02:00
98a62f35cd initialize und finish methoden für Tasks 2022-07-08 08:01:24 +02:00
a52125aeb2 Merge branch 'tasks' into anim 2022-07-08 07:53:01 +02:00
476545f721 Changelog 2022-07-08 07:52:39 +02:00
a8bbce72a2 DelayedTask wartet selbstständig 2022-07-08 07:52:35 +02:00
ced0aa6842 Synchronisation über einen globalen Monitor 2022-07-08 07:44:49 +02:00
d48b167fb3 Renamed generator to dispatcher 2022-07-08 07:31:37 +02:00
e4818d4f3e Abstraktion für Listener API erstellt
EventGenerator soll als einheitlicher Unterbau zur Umstzung von Listener Patterns dienen. Damit sollen Listener wie AudioListener oder AnimationListener umgesetzt werden.
2022-07-07 21:45:08 +02:00
9ee7c606fe Implementierung verschiedener Task-Typen
Die Tasks erfüllen verschiedene Aufgaben und können vom TaskRunner parallel ausgeführt werden. Ob ein so komplexes Task-Management notwendig ist, bleibt offen.
2022-07-07 21:18:45 +02:00
303b667cbf Tests für Animationen 2022-07-07 15:45:25 +02:00
2f59d29d08 Animationen als eigener Prozess
Die Animations API verwendet Funktionale Aspekte der Java 8 API und erlaubt die Animation beliebiger Objekte, aber ist vor allem auf die `shape.*` Klassen ausgelegt.
2022-07-07 15:44:36 +02:00
fc7ee36367 Implementierung von Easing-Funktionen
Die Implementierungen wurden von https://easings.net übernommen.
2022-07-07 15:42:47 +02:00
3030445dcf Changelog 2022-07-07 09:21:39 +02:00
b6b8263e5e Reference auf TextBox entfernt 2022-07-07 09:20:36 +02:00
49ad98fd82 Namen der Threads gesetzt, für leichteres Debugging 2022-07-07 09:20:01 +02:00
143 changed files with 9962 additions and 2399 deletions

16
.gitignore vendored
View File

@@ -32,3 +32,19 @@ hs_err_pid*
.DS_Store
._*
Thumbs.db
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache

View File

@@ -6,14 +6,29 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
## Added
### Added
- System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.
- Pakete für Animationen und Maschinelles-Lernen.
- Farbverläufe als Füllung.
### Changed
- `update(double)` und `draw()` werden nun in einem eigenen Thread aufgerufen.
- Die Standardwerte in `Constants` wurden mit dem Prefix `DEFAULT_` benannt (vorher `STD_`).
- Die Standardwerte sind nun nicht mehr `final` und können vom Nutzer manuell gesetzt werden.
## Version 0.0.22
### Added
- Interface `Audio` extrahiert, mit Basisfunktionen von `Sound` und `Music`.
- Klasse `Mixer` steuert mehrere Audio-Objekte gleichzeitig.
- Klasse `tasks.RateLimitedTask`, `tasks.FramerateLimitedTask`, `tasks.FrameSynchronizedTask` und `tasks.DelayedTask`.
## Changed
### Changed
- Neue Package-Struktur:
- `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video).
- `schule.ngb.zm.tasks` für alles Rund um Parallelität.
- `schule.ngb.zm.util.tasks` für alles Rund um Parallelität.
- `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging.
### Removed
- Beispielprojekte in [eigenes Repository](https://github.com/jneug/zeichenmaschine-examples) verschoben.

51
build.gradle Normal file
View File

@@ -0,0 +1,51 @@
plugins {
id 'idea'
id 'java-library'
}
/*properties {
zmVersion {
major = 0;
minor = 0;
rev = 21;
}
}*/
group 'schule.ngb'
version '0.0.22-SNAPSHOT'
//version '{$zmVersion.major}.{$zmVersion.minor}.{$zmVersion.rev}-SNAPSHOT'
compileJava {
options.release = 11
}
repositories {
mavenCentral()
}
dependencies {
runtimeOnly 'com.googlecode.soundlibs:jlayer:1.0.1.4'
runtimeOnly 'com.googlecode.soundlibs:tritonus-share:0.3.7.4'
runtimeOnly 'com.googlecode.soundlibs:mp3spi:1.9.5.4'
compileOnlyApi 'colt:colt:1.2.0'
//api 'colt:colt:1.2.0'
//api 'net.sourceforge.parallelcolt:parallelcolt:0.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
tasks.register('jarMP3SPI', Jar) {
archiveClassifier = 'all'
duplicatesStrategy = 'exclude'
archivesBaseName = 'zeichenmaschine-mp3spi'
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

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

234
gradlew vendored Executable file
View File

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

89
gradlew.bat vendored Normal file
View File

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

2
settings.gradle Normal file
View File

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

View File

@@ -1,17 +1,20 @@
package schule.ngb.zm;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
/**
* Repräsentiert eine Farbe in der Zeichenmaschine.
* <p>
* Farben bestehen entweder aus einem Grauwert (zwischen <code>0</code> und
* <code>255</code>) oder einem Rot-, Grün- und Blauanteil (jeweils zwischen
* <code>0</code> und <code>255</code>).
* Farben bestehen entweder aus einem Grauwert (zwischen 0 und 255) oder einem
* Rot-, Grün- und Blauanteil (jeweils zwischen 0 und 255).
* <p>
* Eine Farbe hat außerdem einen Transparenzwert zwischen <code>0</code>
* (unsichtbar) und <code>255</code> (deckend).
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend).
*/
public class Color {
public class Color implements Paint {
//@formatter:off
@@ -106,7 +109,7 @@ public class Color {
/**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var>.
*
* @param gray Ein Grauwert zwischen <code>0</code> und <code>255</code>.
* @param gray Ein Grauwert zwischen 0 und 255.
*/
public Color( int gray ) {
this(gray, gray, gray, 255);
@@ -116,7 +119,7 @@ public class Color {
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var> und
* des Transparentwertes <var>alpha</var>.
*
* @param gray Ein Grauwert zwischen <code>0</code> und <code>255</code>.
* @param gray Ein Grauwert zwischen 0 und 255.
*/
public Color( int gray, int alpha ) {
this(gray, gray, gray, alpha);
@@ -124,13 +127,12 @@ public class Color {
/**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>.
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen 0 und 255.
*
* @param red Rotwert zwischen <code>0</code> und <code>255</code>.
* @param green Grünwert zwischen <code>0</code> und <code>255</code>.
* @param blue Blauwert zwischen <code>0</code> und <code>255</code>.
* @return Ein passendes Farbobjekt.
* @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255.
* @param blue Blauwert zwischen 0 und 255.
*/
public Color( int red, int green, int blue ) {
this(red, green, blue, 255);
@@ -138,21 +140,18 @@ public class Color {
/**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>.
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen 0 und 255.
* <var>alpha</var> gibt den den Transparentwert an (auch zwischen
* code>0</code> und <code>255</code>), wobei
* <code>0</code> komplett durchsichtig ist und <code>255</code> komplett
* deckend.
* 0 und 255), wobei 0 komplett durchsichtig ist und 255 komplett deckend.
*
* @param red Rotwert zwischen <code>0</code> und <code>255</code>.
* @param green Grünwert zwischen <code>0</code> und <code>255</code>.
* @param blue Blauwert zwischen <code>0</code> und <code>255</code>.
* @param alpha Transparentwert zwischen <code>0</code> und <code>255</code>.
* @return Ein passendes Farbobjekt.
* @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255.
* @param blue Blauwert zwischen 0 und 255.
* @param alpha Transparentwert zwischen 0 und 255.
*/
public Color( int red, int green, int blue, int alpha ) {
rgba = (alpha << 24) | (red << 16) | (green << 8) | blue;
rgba = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | ((blue & 0xFF) << 0);
}
/**
@@ -186,10 +185,11 @@ public class Color {
/**
* Interner Konstruktor für die Initialisierung einer Farbe mit einem
* RGBA-Wert.
*
* <p>
* Da der Konstruktor {@link #Color(int)} schon besetzt ist, muss hier der
* Parameter {@code isRGBA} auf {@code true} gesetzt werden, damit {@code rgba}
* korrekt interpretiert wird.
* Parameter {@code isRGBA} auf {@code true} gesetzt werden, damit
* {@code rgba} korrekt interpretiert wird.
*
* @param rgba RGBA-wert der Farbe.
* @param isRGBA Sollte immer {@code true} sein.
*/
@@ -212,12 +212,12 @@ public class Color {
return c;
}
public static Color getHSBColor(double h, double s, double b) {
return new Color(java.awt.Color.getHSBColor((float)h, (float)s, (float)b));
public static Color getHSBColor( double h, double s, double b ) {
return new Color(java.awt.Color.getHSBColor((float) h, (float) s, (float) b));
}
public static Color getHSLColor(double h, double s, double l) {
int rgb = Color.HSLtoRGB(new float[]{(float)h, (float)s, (float)l});
public static Color getHSLColor( double h, double s, double l ) {
int rgb = Color.HSLtoRGB(new float[]{(float) h, (float) s, (float) l});
return Color.getRGBColor(rgb);
}
@@ -237,9 +237,9 @@ public class Color {
}
/**
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann
* sechs- oder achtstellig sein (wenn ein Transparentwert vorhanden ist).
* Dem Code kann ein <code>#</code> Zeichen vorangestellt sein.
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann sechs-
* oder achtstellig sein (wenn ein Transparentwert vorhanden ist). Dem Code
* kann ein {@code #} Zeichen vorangestellt sein.
*
* @param hexcode
* @return
@@ -338,10 +338,11 @@ public class Color {
/**
* Konvertiert eine Farbe mit Komponenten im HSL-Farbraum in den
* RGB-Farbraum.
*
* <p>
* Die Farbkomponenten werden als Float-Array übergeben. Im Index 0 steht
* der h-Wert im Bereich 0 bis 360, Index 1 und 2 enthalten den s- und
* l-Wert im Bereich von 0 bis 1.
*
* @param hsl Die Farbkomponenten im HSL-Farbraum.
* @param alpha Ein Transparenzwert im Bereich 0 bis 255.
* @return Der RGBA-Wert der Farbe.
@@ -394,6 +395,7 @@ public class Color {
/**
* Erzeugt eine Kopie dieser Farbe.
*
* @return Ein neues Farbobjekt.
*/
public Color copy() {
@@ -402,10 +404,11 @@ public class Color {
/**
* Gibt den RGBA-Wert dieser Farbe zurück.
*
* Eine Farbe wird als 32-Bit Integer gespeichert. Bits 24-31 enthalten
* den Transparenzwert, 16-23 den Rotwert, 8-15 den Grünwert und 0-7 den
* <p>
* Eine Farbe wird als 32-Bit Integer gespeichert. Bits 24-31 enthalten den
* Transparenzwert, 16-23 den Rotwert, 8-15 den Grünwert und 0-7 den
* Blauwert der Farbe.
*
* @return Der RGBA-Wert der Farbe.
* @see #getRed()
* @see #getGreen()
@@ -418,6 +421,7 @@ public class Color {
/**
* Gibt den Rotwert dieser Farbe zurück.
*
* @return Der Rotwert der Farbe zwischen 0 und 255.
*/
public int getRed() {
@@ -426,6 +430,7 @@ public class Color {
/**
* Gibt den Grünwert dieser Farbe zurück.
*
* @return Der Grünwert der Farbe zwischen 0 und 255.
*/
public int getGreen() {
@@ -434,6 +439,7 @@ public class Color {
/**
* Gibt den Blauwert dieser Farbe zurück.
*
* @return Der Blauwert der Farbe zwischen 0 und 255.
*/
public int getBlue() {
@@ -442,6 +448,7 @@ public class Color {
/**
* Gibt den Transparenzwert dieser Farbe zurück.
*
* @return Der Transparenzwert der Farbe zwischen 0 und 255.
*/
public int getAlpha() {
@@ -450,15 +457,33 @@ public class Color {
/**
* Erzeugt ein {@link java.awt.Color}-Objekt aus dieser Farbe.
* <p>
* Das erzeugte Farbobjekt hat dieselben Rot-, Grün-, Blau- und
* Transparenzwerte wie diese Farbe.
*
* Das erzeugte Farbobjekt hat dieselben Rot-, Grün-, Blau-
* und Transparenzwerte wie diese Farbe.
* @return Ein Java-Farbobjekt.
*/
public java.awt.Color getJavaColor() {
return new java.awt.Color(rgba, true);
}
@Override
public PaintContext createContext( ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints ) {
return getJavaColor().createContext(cm, deviceBounds, userBounds, xform, hints);
}
@Override
public int getTransparency() {
int alpha = getAlpha();
if( alpha == 0xff ) {
return Transparency.OPAQUE;
} else if( alpha == 0 ) {
return Transparency.BITMASK;
} else {
return Transparency.TRANSLUCENT;
}
}
@Override
/**
* Prüft, ob ein anderes Objekt zu diesem gleich ist.
@@ -470,11 +495,20 @@ public class Color {
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/
public boolean equals( Object obj ) {
return obj instanceof Color && ((Color)obj).getRGBA() == this.rgba;
if( obj == null ) {
return false;
}
if( obj instanceof Color ) {
return ((Color) obj).getRGBA() == this.rgba;
} else if( obj instanceof java.awt.Color ) {
return ((java.awt.Color) obj).getRGB() == this.rgba;
}
return false;
}
/**
* Erzeugt einen Text-String, der diese Farbe beschreibt.
*
* @return Eine Textversion der Farbe.
*/
@Override
@@ -484,6 +518,7 @@ public class Color {
/**
* Berechnet einen Hashcode für dieses Farbobjekt.
*
* @return Ein Hashcode für diese Rabe.
*/
@Override
@@ -493,6 +528,7 @@ public class Color {
/**
* Erzeugt eine um 30% hellere Version dieser Farbe.
*
* @return Ein Farbobjekt mit einer helleren Farbe.
*/
public Color brighter() {
@@ -501,6 +537,7 @@ public class Color {
/**
* Erzeugt eine um {@code percent} hellere Version dieser Farbe.
*
* @param percent Eine Prozentzahl zwischen 0 und 100.
* @return Ein Farbobjekt mit einer helleren Farbe.
*/
@@ -512,6 +549,7 @@ public class Color {
/**
* Erzeugt eine um 30% dunklere Version dieser Farbe.
*
* @return Ein Farbobjekt mit einer dunkleren Farbe.
*/
public Color darker() {
@@ -520,6 +558,7 @@ public class Color {
/**
* Erzeugt eine um {@code percent} dunklere Version dieser Farbe.
*
* @param percent Eine Prozentzahl zwischen 0 und 100.
* @return Ein Farbobjekt mit einer dunkleren Farbe.
*/
@@ -530,11 +569,12 @@ public class Color {
}
public Color greyscale() {
return new Color((int)(getRed()*.299 + getGreen()*.587 + getBlue()*0.114));
return new Color((int) (getRed() * .299 + getGreen() * .587 + getBlue() * 0.114));
}
/**
* Erzeugt eine zu dieser invertierte Farbe.
*
* @return Ein Farbobjekt mit der invertierten Farbe.
*/
public Color inverted() {
@@ -544,6 +584,7 @@ public class Color {
/**
* Erzeugt die Komplementärfarbe zu dieser.
*
* @return Ein Farbobjekt mit der Komplementärfarbe.
*/
public Color complement() {
@@ -556,11 +597,12 @@ public class Color {
* Wählt entweder {@link #WHITE weiß} oder {@link #BLACK schwarz} aus, je
* nachdem, welche der Farben besser als Textfarbe mit dieser Farbe als
* Hintergrund funktioniert (besser lesbar ist).
*
* @return Schwarz oder weiß.
*/
public Color textcolor() {
// Basiert auf https://stackoverflow.com/questions/946544/good-text-foreground-color-for-a-given-background-color
if( (getRed()*.299 + getGreen()*.587 + getBlue()*0.114) < 186 ) {
if( (getRed() * .299 + getGreen() * .587 + getBlue() * 0.114) < 186 ) {
return WHITE;
} else {
return BLACK;

View File

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

View File

@@ -1,22 +1,50 @@
package schule.ngb.zm;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.*;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
/**
* Basisklasse für Ebenen der {@link Zeichenleinwand}.
* <p>
* Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
*
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer {@link Zeichenmaschine}.
* Es werden
* Sie erben von {@code Constants}, damit sie beim
*/
public abstract class Layer extends Constants implements Drawable, Updatable {
/**
* Interner Puffer für die Ebene, der einmal pro Frame auf die
* Zeichenleinwand übertragen wird.
*/
protected BufferedImage buffer;
/**
* Der Grafikkontext der Ebene, der zum Zeichnen der Inhalte verwendet
* wird.
*/
protected Graphics2D drawing;
/**
* Ob die Ebene derzeit sichtbar ist.
*/
protected boolean visible = true;
/**
* Ob die Ebene aktiv ist, also {@link #update(double) Updates} empfangen
* soll.
*/
protected boolean active = true;
public Layer() {
this(STD_WIDTH, STD_HEIGHT);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public Layer( int width, int height ) {
@@ -31,6 +59,12 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
return buffer.getHeight();
}
/**
* Ändert die Größe der Ebene auf die angegebene Größe.
*
* @param width Die neue Breite.
* @param height Die neue Höhe.
*/
public void setSize( int width, int height ) {
if( buffer != null ) {
if( buffer.getWidth() != width || buffer.getHeight() != height ) {
@@ -42,8 +76,13 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Gibt die Resourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
drawing = null;
buffer = null;
}
/**
@@ -73,8 +112,8 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe
* und kopiert den Inhalt des alten Puffers in den Neuen.
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe und
* kopiert den Inhalt des alten Puffers in den Neuen.
*
* @param width Width des neuen Puffers.
* @param height Höhe des neuen Puffers.
@@ -86,8 +125,8 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Leert die Ebene und löscht alles bisher gezeichnete. Alle Pixel der
* Ebene werden transparent, damit unterliegende Ebenen durchscheinen können.
* Leert die Ebene und löscht alles bisher gezeichnete. Alle Pixel der Ebene
* werden transparent, damit unterliegende Ebenen durchscheinen können.
*/
public void clear() {
// https://stackoverflow.com/questions/31149206/set-pixels-of-bufferedimage-as-transparent

View File

@@ -29,6 +29,9 @@ public final class Options {
}
}
/**
* Zustände in denen sich die Zeichenmaschine befinden kann.
*/
public enum AppState {
INITIALIZING,
INITIALIZED,
@@ -37,11 +40,21 @@ public final class Options {
DRAWING,
PAUSED,
STOPPED,
TERMINATED
TERMINATED,
IDLE,
DELAYED,
DISPATCHING
}
/**
* Richtungen für die Ausrichtung von Formen. Richtungen sind durch
* Einheitsvektoren bzw. deren Kombination repräsentiert, wodurch mit ihnen
* gerechnet werden kann. Jede Richtung ist zusätzlich als Himmelsrichtung
* definiert.
*/
public enum Direction {
CENTER(0, 0),
NORTH(0, -1),
EAST(1, 0),
SOUTH(0, 1),
@@ -49,20 +62,25 @@ public final class Options {
NORTHEAST(1, -1),
SOUTHEAST(1, 1),
NORTHWEST(-1, -1),
SOUTHWEST(-1, 1),
NORTHWEST(-1, -1),
MIDDLE(CENTER),
UP(NORTH),
RIGHT(EAST),
DOWN(SOUTH),
LEFT(WEST),
RIGHT(EAST);
UPLEFT(NORTHWEST),
DOWNLEFT(SOUTHWEST),
DOWNRIGHT(SOUTHEAST),
UPRIGHT(NORTHEAST);
public final byte x, y;
Direction( int x, int y ) {
this.x = (byte)x;
this.y = (byte)y;
this.x = (byte) x;
this.y = (byte) y;
}
Direction( Direction original ) {
@@ -84,10 +102,11 @@ public final class Options {
/**
* Gibt die entgegengesetzte Richtung zu dieser zurück.
*
* @return
*/
public Direction inverse() {
for( Direction dir: Direction.values() ) {
for( Direction dir : Direction.values() ) {
if( dir.x == -this.x && dir.y == -this.y ) {
return dir;
}

View File

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

View File

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

View File

@@ -0,0 +1,239 @@
package schule.ngb.zm;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
/**
* Ein Zeichenfenster ist das Programmfenster für die Zeichenmaschine.
* <p>
* Das Zeichenfenster implementiert hilfreiche Funktionen, um ein
* Programmfenster mit einer Zeichenleinwand als zentrales Element zu erstellen.
* Ein Zeichenfenster kann auch ohne eine Zeichenmaschine verwendet werden, um
* eigene Programmabläufe zu implementieren.
*/
public class Zeichenfenster extends JFrame {
public static final void setLookAndFeel() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch( Exception ex ) {
LOG.error(ex, "Couldn't set the look and feel: %s", ex.getMessage());
}
}
public static final GraphicsDevice getGraphicsDevice() {
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
// TODO: (ngb) Wenn wir in BlueJ sind, sollte das Fenster neben dem Editor öffnen.
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = environment.getScreenDevices();
GraphicsDevice displayDevice = null;
for( GraphicsDevice gd : devices ) {
if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) {
displayDevice = gd;
break;
}
}
// Keinen passenden Bildschirm gefunden. Wir nutzen den Standard.
if( displayDevice == null ) {
displayDevice = environment.getDefaultScreenDevice();
}
return displayDevice;
}
/**
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich dem
* Aktuellen sein, wenn das Fenster verschoben wurde).
*/
private GraphicsDevice displayDevice;
/**
* Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat
* es Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* Missachtung anderer Größenvorgaben. Allerdings kann das Fenster keine
* Garantie für die Größe der Leinwand übernehmen.
*/
private int canvasPreferredWidth, canvasPreferredHeight;
/**
* Speichert, ob die Zeichenmaschine mit {@link #setFullscreen(boolean)} in
* den Vollbildmodus versetzt wurde.
*/
private boolean fullscreen = false;
/**
* {@code KeyListener}, um den Vollbild-Modus mit der Escape-Taste zu
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt.
*/
private KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
}
}
};
private Zeichenleinwand canvas;
public Zeichenfenster( int width, int height, String title ) {
this(new Zeichenleinwand(width, height), title, getGraphicsDevice());
}
public Zeichenfenster( int width, int height, String title, GraphicsDevice displayDevice ) {
this(new Zeichenleinwand(width, height), title, displayDevice);
}
public Zeichenfenster( Zeichenleinwand canvas, String title ) {
this(canvas, title, getGraphicsDevice());
}
public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
super(Validator.requireNotNull(displayDevice).getDefaultConfiguration());
this.displayDevice = displayDevice;
Validator.requireNotNull(canvas, "Every Zeichenfenster needs a Zeichenleinwand, but got <null>.");
this.canvasPreferredWidth = canvas.getWidth();
this.canvasPreferredHeight = canvas.getHeight();
//this.add(canvas, BorderLayout.CENTER);
this.add(canvas);
this.canvas = canvas;
// Konfiguration des Frames
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION: title);
// Kann vom Aufrufenden überschrieben werden
this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
if( Zeichenmaschine.MACOS ) {
URL iconUrl = Zeichenmaschine.class.getResource("icon_512.png");
Image icon = ImageIO.read(iconUrl);
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon);
} else {
ArrayList<Image> icons = new ArrayList<>(4);
for( int size = 32; size <= 512; size *= size ) {
icons.add(ImageIO.read(new File("icon_" + size + ".png")));
}
this.setIconImages(icons);
}
} catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage());
} catch( SecurityException | UnsupportedOperationException macex ) {
// Dock Icon in macOS konnte nicht gesetzt werden :(
LOG.warn("Could not set dock icon: %s", macex.getMessage());
}
// Fenster zusammenbauen, auf dem Bildschirm positionieren ...
this.pack();
this.setResizable(false);
this.setLocationByPlatform(true);
// this.centerFrame();
}
public GraphicsDevice getDisplayDevice() {
return displayDevice;
}
public Rectangle getScreenBounds() {
return displayDevice.getDefaultConfiguration().getBounds();
}
public Zeichenleinwand getCanvas() {
return canvas;
}
public Rectangle getCanvasBounds() {
return canvas.getBounds();
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
public final void centerFrame() {
java.awt.Rectangle screenBounds = getScreenBounds();
java.awt.Rectangle frameBounds = getBounds();
this.setLocation(
(int) (screenBounds.x + (screenBounds.width - frameBounds.width) / 2.0),
(int) (screenBounds.y + (screenBounds.height - frameBounds.height) / 2.0)
);
}
public void setCanvasSize( int newWidth, int newHeight ) {
// TODO: (ngb) Put constains on max/min frame/canvas size
if( fullscreen ) {
canvasPreferredWidth = newWidth;
canvasPreferredHeight = newHeight;
setFullscreen(false);
} else {
canvas.setSize(newWidth, newHeight);
canvasPreferredWidth = canvas.getWidth();
canvasPreferredHeight = canvas.getHeight();
this.pack();
}
}
/**
* Aktiviert oder deaktiviert den Vollbildmodus für die Zeichenmaschine.
* <p>
* Der Vollbildmodus wird abhängig von {@code pEnable} entweder aktiviert
* oder deaktiviert. Wird die Zeichenmaschine in den Vollbildmodus versetzt,
* dann wird automatisch ein {@link KeyListener} aktiviert, der bei
* Betätigung der ESCAPE-Taste den Vollbildmodus verlässt. Wird der
* Vollbildmodus verlassen, wird die zuletzt gesetzte Fenstergröße
* wiederhergestellt.
*
* @param pEnable Wenn {@code true}, wird der Vollbildmodus aktiviert,
* ansonsten deaktiviert.
*/
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
this.setResizable(false); // Should be set anyway
displayDevice.setFullScreenWindow(this);
java.awt.Rectangle bounds = getScreenBounds();
// TODO: (ngb) We need to switch layouts to allow the LayoutManger to maximize the canvas
canvas.setSize(bounds.width, bounds.height);
// Register ESC to exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
fullscreen = true;
} else if( !pEnable && fullscreen ) {
fullscreen = false;
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
canvas.setSize(canvasPreferredWidth, canvasPreferredHeight);
this.pack();
// frame.setUndecorated(false);
}
}
}
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win.equals(this);
}
private static final Log LOG = Log.getLogger(Zeichenfenster.class);
}

View File

@@ -1,14 +1,16 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.ShapesLayer;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.Log;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.*;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -35,14 +37,14 @@ public class Zeichenleinwand extends Canvas {
*/
public Zeichenleinwand( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(this.getSize());
this.setMinimumSize(this.getSize());
this.setBackground(Constants.STD_BACKGROUND.getJavaColor());
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
// Liste der Ebenen initialisieren und die Standardebenen einfügen
layers = new LinkedList<>();
synchronized( layers ) {
layers.add(new ColorLayer(width, height, Constants.STD_BACKGROUND));
layers.add(new ColorLayer(width, height, Constants.DEFAULT_BACKGROUND));
}
}
@@ -58,8 +60,8 @@ public class Zeichenleinwand extends Canvas {
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(this.getSize());
this.setMinimumSize(this.getSize());
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
synchronized( layers ) {
for( Layer layer : layers ) {
@@ -90,7 +92,7 @@ public class Zeichenleinwand extends Canvas {
* als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein,
* dass die neue Ebene am Ende wirklich am Index {@code i} steht.
*
* @param i Index der Ebene, beginnend mit <code>0</code>.
* @param i Index der Ebene, beginnend mit 0.
* @param layer Die neue Ebene.
*/
public void addLayer( int i, Layer layer ) {
@@ -125,10 +127,10 @@ public class Zeichenleinwand extends Canvas {
}
/**
* Holt die Ebene am Index <var>i</var> (beginnend bei <code>0</code>).
* Holt die Ebene am Index <var>i</var> (beginnend bei 0).
*
* @param i Index der Ebene (beginnend bei <code>0</code>).
* @return Die Ebene am Index <var>i</var> oder <code>null</code>.
* @param i Index der Ebene (beginnend bei 0).
* @return Die Ebene am Index <var>i</var> oder {@code null}.
* @throws IndexOutOfBoundsException Falls der Index nicht existiert.
*/
public Layer getLayer( int i ) {
@@ -141,7 +143,7 @@ public class Zeichenleinwand extends Canvas {
/**
* Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen.
* Existiert keine solche Ebene, wird <code>null</code> zurückgegeben.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben.
*
* @param clazz Typ der Ebene.
* @param <L>
@@ -176,18 +178,33 @@ public class Zeichenleinwand extends Canvas {
}
public boolean removeLayer( Layer pLayer ) {
synchronized( layers ) {
return layers.remove(pLayer);
}
}
public void removeLayers( Layer... pLayers ) {
synchronized( layers ) {
for( Layer layer : pLayers ) {
layers.remove(layer);
}
}
}
public void clearLayers() {
synchronized( layers ) {
layers.clear();
}
}
public void updateLayers( double delta ) {
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.update(delta);
}
}
}
/**
* Erstellt eine passende {@link BufferStrategy} für diese Ebene.
@@ -220,7 +237,8 @@ public class Zeichenleinwand extends Canvas {
public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
for( Layer layer : layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
}
}
@@ -244,7 +262,8 @@ public class Zeichenleinwand extends Canvas {
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) {
for( Layer layer : layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
}
}
@@ -265,4 +284,6 @@ public class Zeichenleinwand extends Canvas {
}
}
private static final Log LOG = Log.getLogger(Zeichenleinwand.class);
}

View File

@@ -1,11 +1,14 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.ShapesLayer;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.DrawingLayer;
import schule.ngb.zm.layers.ImageLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.tasks.TaskRunner;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;
@@ -14,7 +17,6 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.logging.Level;
/**
* Hauptklasse der Zeichenmaschine.
@@ -23,6 +25,7 @@ import java.util.logging.Level;
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der
* nötigen Komponenten.
*/
@SuppressWarnings( "unused" )
public class Zeichenmaschine extends Constants {
/**
@@ -34,10 +37,19 @@ public class Zeichenmaschine extends Constants {
IN_BLUEJ = System.getProperty("java.class.path").contains("bluej");
}
/**
* Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde.
*/
public static final boolean MACOS;
/**
* Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde.
*/
public static final boolean WINDOWS;
/**
* Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde.
*/
public static final boolean LINUX;
static {
@@ -95,66 +107,78 @@ public class Zeichenmaschine extends Constants {
* Interne Attribute zur Steuerung der Zeichenmaschine.
*/
//@formatter:off
// Das Zeichenfenster der Zeichenmaschine
private JFrame frame;
// Die Graphics-Objekte für das aktuelle Fenster.
private GraphicsEnvironment environment;
private GraphicsDevice displayDevice;
/**
* Speichert, ob die Zeichenmaschine mit {@link #setFullscreen(boolean)}
* in den Vollbildmodus versetzt wurde.
* Das Zeichenfenster der Zeichenmaschine
*/
private boolean fullscreen = false;
/**
* Höhe und Breite der Zeichenmaschine, bevor sie mit
* {@link #setFullscreen(boolean)} in den Vollbild-Modus versetzt wurde.
* Wird verwendet, um die Fenstergröße wiederherzustellen, sobald der
* Vollbild-Modus verlassen wird.
*/
private int initialWidth, initialHeight;
/**
* KeyListener, um den Vollbild-Modus mit der Escape-Taste zu verlassen.
* Wird von {@link #setFullscreen(boolean)} automatisch einzugefügt und
* entfernt.
*/
KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
}
}
};
private Zeichenfenster frame;
// Aktueller Zustand der Zeichenmaschine.
/**
* Zustand der Zeichenmaschine insgesamt
*/
private Options.AppState state = Options.AppState.INITIALIZING;
/**
* Zustand des update/draw Threads
*/
private Options.AppState updateState = Options.AppState.STOPPED;
/**
* Ob der Zeichenthread noch laufen soll, oder beendet.
*/
private boolean running = false;
private boolean terminateImediately = false;
/**
* Ob die ZM nach dem nächsten Frame pausiert werden soll.
*/
private boolean pause_pending = false;
private boolean stop_after_update = false, run_once = true;
/**
* Ob die ZM bei nicht überschriebener update() Methode stoppen soll,
* oder trotzdem weiterläuft.
*/
private boolean run_once = true;
// Aktuelle Frames pro Sekunde der Zeichenmaschine.
/**
* Aktuelle Frames pro Sekunde der Zeichenmaschine.
*/
private int framesPerSecondInternal;
// Hauptthread der Zeichenmaschine.
/**
* Hauptthread der Zeichenmaschine.
*/
private Thread mainThread;
// Queue für geplante Aufgaben
/**
* Queue für geplante Aufgaben
*/
private DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
// Queue für abgefangene InputEvents
/**
* Queue für abgefangene InputEvents
*/
private BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
// Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
// oder das Zeichenfenster weiter geöffnet bleibt.
private boolean quitAfterTeardown = false;
/**
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
* oder das Zeichenfenster weiter geöffnet bleibt.
*/
private boolean quitAfterShutdown = false;
// Mauscursor
// Mauszeiger
/**
* Cache für den unsichtbaren Mauszeiger, wenn {@link #hideCursor()}
* aufgerufen wurde.
*/
private Cursor invisibleCursor = null;
/**
* Ob der Mauszeiger derzeit sichtbar ist (bzw. sein sollte).
*/
protected boolean cursorVisible = true;
//@formatter:on
@@ -194,7 +218,7 @@ public class Zeichenmaschine extends Constants {
* @param title Der Titel, der oben im Fenster steht.
*/
public Zeichenmaschine( String title ) {
this(STD_WIDTH, STD_HEIGHT, title, true);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, true);
}
/**
@@ -209,7 +233,7 @@ public class Zeichenmaschine extends Constants {
* Aufruf von {@code draw()}.
*/
public Zeichenmaschine( String title, boolean run_once ) {
this(STD_WIDTH, STD_HEIGHT, title, run_once);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, run_once);
}
/**
@@ -253,74 +277,32 @@ public class Zeichenmaschine extends Constants {
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
// Setzen des "Look&Feel"
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch( Exception ex ) {
LOG.log(Level.SEVERE, "Error setting the look and feel: " + ex.getMessage(), ex);
}
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = environment.getScreenDevices();
for( GraphicsDevice gd : devices ) {
if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) {
displayDevice = gd;
break;
}
}
// Keinen passenden Bildschirm gefunden. Wir nutzen den Standard.
if( displayDevice == null ) {
displayDevice = environment.getDefaultScreenDevice();
}
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
this.width = width;
this.height = height;
java.awt.Rectangle displayBounds = displayDevice.getDefaultConfiguration().getBounds();
this.screenWidth = (int) displayBounds.getWidth();
this.screenHeight = (int) displayBounds.getHeight();
// Erstellen des Zeichenfensters
frame = new JFrame(displayDevice.getDefaultConfiguration());
frame.setTitle(title);
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
ImageIcon icon = new ImageIcon(ImageIO.read(new File("res/icon_64.png")));
if( MACOS ) {
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon.getImage());
} else {
// Kleines Icon des Frames setzen
frame.setIconImage(icon.getImage());
}
} catch( IOException e ) {
}
// Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height);
frame.add(canvas);
// Die drei Standardebenen merken, für den einfachen Zugriff aus unterklassen.
// Erstellen des Zeichenfensters
frame = createFrame(canvas, title);
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
this.canvasWidth = canvasBounds.width;
this.canvasHeight = canvasBounds.height;
java.awt.Rectangle displayBounds = frame.getScreenBounds();
this.screenWidth = displayBounds.width;
this.screenHeight = displayBounds.height;
// Die drei Standardebenen merken, für den einfachen Zugriff aus Unterklassen.
background = getBackgroundLayer();
drawing = getDrawingLayer();
shapes = getShapesLayer();
// FPS setzen
framesPerSecondInternal = STD_FPS;
framesPerSecondInternal = DEFAULT_FPS;
this.run_once = run_once;
// Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen
// verändert werden soll.
// TODO: When to call settings?
// TODO: (ngb) Wann sollte settings() aufgerufen werden?
settings();
// Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören.
@@ -331,18 +313,20 @@ public class Zeichenmaschine extends Constants {
canvas.addKeyListener(inputListener);
// Programm beenden, wenn Fenster geschlossen wird
// TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
exit();
if( isTerminated() ) {
quit(true);
} else {
exitNow();
}
}
});
// Fenster zusammenbauen, auf dem Bildschirm zentrieren ...
frame.pack();
frame.setResizable(false);
centerFrame();
// ... und anzeigen!
// Fenster anzeigen
frame.centerFrame();
frame.setVisible(true);
// Nach dem Anzeigen kann die Pufferstrategie erstellt werden.
@@ -366,25 +350,30 @@ public class Zeichenmaschine extends Constants {
*
* @param title
*/
// TODO: Implement in conjunction with Zeichenfenster
private final Zeichenfenster createFrame( String title ) {
return null;
private final Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
while( frame == null ) {
try {
TaskRunner.invokeLater(new Runnable() {
@Override
public void run() {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}
}).get();
} catch( InterruptedException e ) {
} catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e);
}
}
return frame;
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
public final void centerFrame() {
// TODO: Center on current display (not main display by default)
// TODO: Position at current BlueJ windows if IN_BLUEJ
//frame.setLocationRelativeTo(null);
//frame.setLocationRelativeTo(displayDevice.getFullScreenWindow());
java.awt.Rectangle bounds = displayDevice.getDefaultConfiguration().getBounds();
frame.setLocation(
(int) (bounds.x + (screenWidth - frame.getWidth()) / 2.0),
(int) (bounds.y + (screenHeight - frame.getHeight()) / 2.0)
);
frame.centerFrame();
}
/**
@@ -401,37 +390,25 @@ public class Zeichenmaschine extends Constants {
* ansonsten deaktiviert.
*/
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
frame.setResizable(false); // Should be set anyway
displayDevice.setFullScreenWindow(frame);
// Update width / height
initialWidth = width;
initialHeight = height;
changeSize(screenWidth, screenHeight);
// Register ESC as exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
fullscreen = true;
if( pEnable && !frame.isFullscreen() ) {
frame.setFullscreen(true);
if( frame.isFullscreen() )
fullscreenChanged();
} else if( !pEnable && fullscreen ) {
fullscreen = false;
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
changeSize(initialWidth, initialHeight);
frame.pack();
// frame.setUndecorated(false);
} else if( !pEnable && frame.isFullscreen() ) {
frame.setFullscreen(false);
if( !frame.isFullscreen() )
fullscreenChanged();
}
}
}
/**
* Prüft, ob das Zeichenfenster im Vollbild läuft.
*
* @return {@code true}, wenn sich das Fesnter im Vollbildmodus befindet,
* {@code false} sonst.
*/
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win != null;
return frame.isFullscreen();
}
/**
@@ -509,14 +486,26 @@ public class Zeichenmaschine extends Constants {
return state == Options.AppState.PAUSED;
}
public final boolean isTerminated() {
return state == Options.AppState.TERMINATED;
}
public final boolean isTerminating() {
return state == Options.AppState.STOPPED || state == Options.AppState.TERMINATED;
}
/**
* Stoppt die Zeichenmaschine.
* <p>
* Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine
* in den Zustand {@link Options.AppState#STOPPED} und ruft
* {@link #teardown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* {@link #shutdown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das
* Zeichenfenster bleibt weiter geöffnet.
* <p>
* Die Zeichenmaschine reagiert in diesem Zustand weiter auf Eingaben,
* allerdings muss die Zeichnung nun manuell mit {@link #redraw()}
* aktualisiert werden.
*/
public final void stop() {
running = false;
@@ -527,10 +516,11 @@ public class Zeichenmaschine extends Constants {
* <p>
* Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und
* verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die
* Äquivalente Methode für Unterklassen ist {@link #teardown()}, die direkt
* Äquivalente Methode für Unterklassen ist {@link #shutdown()}, die direkt
* vor {@code cleanup()} aufgerufen wird.
*/
private void cleanup() {
LOG.debug("%s shutting down.", APP_NAME);
// Alle noch nicht ausgelösten Events werden entfernt
eventQueue.clear();
// Alle noch nicht ausgeführten Tasks werden entfernt
@@ -549,7 +539,18 @@ public class Zeichenmaschine extends Constants {
public final void exit() {
if( running ) {
running = false;
this.quitAfterTeardown = true;
quitAfterShutdown = true;
} else {
quit(true);
}
}
public final void exitNow() {
if( running ) {
running = false;
terminateImediately = true;
quitAfterShutdown = true;
mainThread.interrupt();
} else {
quit(true);
}
@@ -557,6 +558,10 @@ public class Zeichenmaschine extends Constants {
/**
* Beendet das Programm vollständig.
* <p>
* Enspricht dem Aufruf {@code quit(true)}.
*
* @see #quit(boolean)
*/
public final void quit() {
//quit(!IN_BLUEJ);
@@ -566,6 +571,14 @@ public class Zeichenmaschine extends Constants {
/**
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird
* die komplette VM beendet.
* <p>
* Die Methode sorgt nicht für ein ordnungsgemäßes herunterfahren und
* freigeben aller Ressourcen, da die Zeichenmaschine gegebenenfalls
* geöffnet bleiben und weitere Aufgaben erfüllen soll. Aufrufende Methoden
* sollten dies berücksichtigen.
* <p>
* Soll das Programm vollständig beendet werden, ist es ratsamer
* {@link #exit()} zu verwenden.
*
* @param exit Ob die VM beendet werden soll.
* @see System#exit(int)
@@ -580,28 +593,6 @@ public class Zeichenmaschine extends Constants {
}
}
/**
* Interne Methode um die Größe der Zeichenfläche zu ändern.
* <p>
* Die Methode berücksichtigt nicht den Zustand des Fensters (z.B.
* Vollbildmodus) und geht davon aus, dass die aufrufende Methode
* sichergestellt hat, dass eine Änderung der Größe der Zeichenfläche
* zulässig und sinnvoll ist.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
* @see #setSize(int, int)
* @see #setFullscreen(boolean)
*/
private void changeSize( int newWidth, int newHeight ) {
width = Math.min(Math.max(newWidth, 100), screenWidth);
height = Math.min(Math.max(newHeight, 100), screenHeight);
if( canvas != null ) {
canvas.setSize(width, height);
}
}
/**
* Ändert die Größe der {@link Zeichenleinwand}.
*
@@ -610,16 +601,11 @@ public class Zeichenmaschine extends Constants {
* @see Zeichenleinwand#setSize(int, int)
*/
public final void setSize( int width, int height ) {
if( fullscreen ) {
initialWidth = Math.min(Math.max(width, 100), screenWidth);
initialHeight = Math.min(Math.max(height, 100), screenHeight);
setFullscreen(false);
} else {
changeSize(width, height);
frame.setSize(width, height);
//frame.setSize(width, height);
frame.pack();
}
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
canvasWidth = (int) canvasBounds.getWidth();
canvasHeight = (int) canvasBounds.getHeight();
}
/**
@@ -628,7 +614,7 @@ public class Zeichenmaschine extends Constants {
* @return Die Breite der {@link Zeichenleinwand}.
*/
public final int getWidth() {
return width;
return canvasWidth;
}
/**
@@ -637,7 +623,7 @@ public class Zeichenmaschine extends Constants {
* @return Die Höhe der {@link Zeichenleinwand}.
*/
public final int getHeight() {
return height;
return canvasHeight;
}
/**
@@ -717,7 +703,7 @@ public class Zeichenmaschine extends Constants {
public final ColorLayer getBackgroundLayer() {
ColorLayer layer = canvas.getLayer(ColorLayer.class);
if( layer == null ) {
layer = new ColorLayer(STD_BACKGROUND);
layer = new ColorLayer(DEFAULT_BACKGROUND);
canvas.addLayer(0, layer);
}
return layer;
@@ -811,7 +797,7 @@ public class Zeichenmaschine extends Constants {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics();
g.setColor(STD_BACKGROUND.getJavaColor());
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g);
g.dispose();
@@ -835,7 +821,7 @@ public class Zeichenmaschine extends Constants {
BufferedImage img = ImageLoader.createImage(canvas.getWidth(), canvas.getHeight());
Graphics2D g = img.createGraphics();
g.setColor(STD_BACKGROUND.getJavaColor());
g.setColor(DEFAULT_BACKGROUND.getJavaColor());
g.fillRect(0, 0, img.getWidth(), img.getHeight());
canvas.draw(g);
g.dispose();
@@ -877,8 +863,15 @@ public class Zeichenmaschine extends Constants {
return;
}
if( state == Options.AppState.INITIALIZING ||
state == Options.AppState.INITIALIZED ) {
LOG.warn("Don't use delay(int) from within settings() or setup().");
return;
}
long timer = 0L;
if( state == Options.AppState.DRAWING ) {
if( /*updateState == Options.AppState.DRAWING*/
isTerminating() ) {
// Falls gerade draw() ausgeführt wird, zeigen wir den aktuellen
// Stand der Zeichnung auf der Leinwand an. Die Zeit für das
// Rendern wird gemessen und von der Wartezeit abgezogen.
@@ -887,6 +880,7 @@ public class Zeichenmaschine extends Constants {
timer = System.nanoTime() - timer;
}
Options.AppState oldState = updateState;
try {
int sub = (int) Math.ceil(timer / 1000000.0);
@@ -894,9 +888,12 @@ public class Zeichenmaschine extends Constants {
return;
}
updateState = Options.AppState.DELAYED;
Thread.sleep(ms - sub, (int) (timer % 1000000L));
} catch( InterruptedException ex ) {
} catch( InterruptedException ignored ) {
// Nothing
} finally {
updateState = oldState;
}
}
@@ -1032,8 +1029,7 @@ public class Zeichenmaschine extends Constants {
* @param delta
*/
public void update( double delta ) {
//running = !run_once;
stop_after_update = run_once;
running = !run_once;
}
/**
@@ -1047,7 +1043,7 @@ public class Zeichenmaschine extends Constants {
* dar, da hier die Zeichnung des Programms erstellt wird.
*/
public void draw() {
running = !stop_after_update;
// Intentionally left blank
}
/**
@@ -1061,7 +1057,7 @@ public class Zeichenmaschine extends Constants {
* Spiels oder der Abspann einer Animation angezeigt werden, oder mit
* {@link #saveImage()} die erstellte Zeichnung abgespeichert werden.
*/
public void teardown() {
public void shutdown() {
// Intentionally left blank
}
@@ -1096,18 +1092,20 @@ public class Zeichenmaschine extends Constants {
}
/*
* Mouse handling
* Input handling
*/
private void enqueueEvent( InputEvent evt ) {
if( updateState != Options.AppState.DELAYED ) {
eventQueue.add(evt);
}
if( isPaused() ) {
if( isPaused() || isTerminated() ) {
dispatchEvents();
}
}
private void dispatchEvents() {
synchronized( eventQueue ) {
//synchronized( eventQueue ) {
while( !eventQueue.isEmpty() ) {
InputEvent evt = eventQueue.poll();
@@ -1128,7 +1126,7 @@ public class Zeichenmaschine extends Constants {
break;
}
}
}
//}
}
private void handleKeyEvent( KeyEvent evt ) {
@@ -1165,8 +1163,8 @@ public class Zeichenmaschine extends Constants {
break;
case MouseEvent.MOUSE_RELEASED:
mousePressed = false;
mouseButton = NOBUTTON;
mousePressed(evt);
mouseButton = NOMOUSE;
mouseReleased(evt);
break;
case MouseEvent.MOUSE_DRAGGED:
//saveMousePosition(evt);
@@ -1276,60 +1274,145 @@ public class Zeichenmaschine extends Constants {
// Zeichenthread
////
/**
* Globaler Monitor, der einmal pro Frame vom Zeichenthread freigegeben
* wird. Andere Threads können {@link Object#wait()} auf dem Monitor
* aufrufen, um sich mit dem Zeichenthread zu synchronisieren. Der
* {@code wait()} Aufruf sollte sich zur Sicherheit in einer Schleife
* befinden, die prüft, ob sich der Aktuelle {@link #tick} erhöht hat.
* <pre><code>
* int lastTick = Constants.tick;
*
* // Do some work
*
* while( lastTick >= Constants.tick ) {
* synchronized( Zeichenmaschine.globalSyncLock ) {
* try {
* Zeichenmaschine.globalSyncLock.wait();
* } catch( InterruptedException ex ) {}
* }
* }
* // Next frame has started
* </code></pre>
* <p>
* Die {@link schule.ngb.zm.util.tasks.FrameSynchronizedTask} implementiert
* eine {@link schule.ngb.zm.util.tasks.Task}, die sich automatisch auf
* diese Wiese mit dem Zeichenthread synchronisiert.
*/
public static final Object globalSyncLock = new Object[0];
class Zeichenthread extends Thread {
public Zeichenthread() {
super(APP_NAME);
//super(APP_NAME + " " + APP_VERSION);
}
@Override
public final void run() {
// Wait for full initialization before start
while( state != Options.AppState.INITIALIZED ) {
delay(1);
Thread.yield();
}
// start of thread in ms
// ThreadExecutor for the update/draw Thread
final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor();
// Start des Thread in ms
final long start = System.currentTimeMillis();
// current time in ns
long beforeTime = System.nanoTime();
// store for deltas
// Aktuelle Zeit in ns
long beforeTime;
long updateBeforeTime = System.nanoTime();
// Speicher für Änderung
long overslept = 0L;
// internal counters for tick and runtime
// Interne Zähler für tick und runtime
int _tick = 0;
long _runtime = 0;
// public counters for access by subclasses
// Öffentliche Zähler für Unterklassen
tick = 0;
runtime = 0;
// call setup of subclass and wait
// setup() der Unterklasse aufrufen
setup();
// Alles startklar ...
state = Options.AppState.RUNNING;
while( running ) {
// delta in seconds
delta = (System.nanoTime() - beforeTime) / 1000000000.0;
// Aktuelle Zeit in ns merken
beforeTime = System.nanoTime();
// Mausposition einmal pro Frame merken
saveMousePosition(mouseEvent);
if( state != Options.AppState.PAUSED ) {
handleUpdate(delta);
handleDraw();
// update() und draw() der Unterklasse werden in einem
// eigenen Thread ausgeführt, aber der Zeichenthread
// wartet, bis der Thread fertig ist. Außer die Unterklasse
// ruft delay() auf und lässt den Thread eine länger Zeit
// schlafen. Dann wird der nächst Frame vorzeitig gerendert,
// bis der update-Thread wieder bereit ist. Dadurch können
// nebenläufige Aufgaben (z.B. Animationen) weiterlaufen.
if( !updateThreadExecutor.isRunning() ) {
delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0;
updateBeforeTime = System.nanoTime();
// uddate()/draw() ausführen
updateThreadExecutor.execute(() -> {
if( state == Options.AppState.RUNNING
&& updateState == Options.AppState.IDLE ) {
// Call to update()
updateState = Options.AppState.UPDATING;
Zeichenmaschine.this.update(delta);
// Update Layers
canvas.updateLayers(delta);
// Call to draw()
updateState = Options.AppState.DRAWING;
Zeichenmaschine.this.draw();
updateState = Options.AppState.DISPATCHING;
// Send latest input events after finishing draw
// since these may also block
dispatchEvents();
updateState = Options.AppState.IDLE;
}
});
}
// Wait for the update/draw Thread to finish
while( updateThreadExecutor.isRunning()
&& !updateThreadExecutor.isWaiting() ) {
Thread.yield();
if( Thread.interrupted() ) {
running = false;
terminateImediately = true;
break;
}
}
// Display the current buffer content
if( canvas != null ) {
canvas.render();
// canvas.invalidate();
// frame.repaint();
}
dispatchEvents();
// dispatchEvents();
}
// Running pending tasks and notify any
// waiting FrameSynchonizedTasks
// TODO: should this this also happen in the updateThread?
runTasks();
synchronized( globalSyncLock ) {
globalSyncLock.notifyAll();
}
// delta time in ns
long afterTime = System.nanoTime();
long dt = afterTime - beforeTime;
long sleep = ((1000000000L / framesPerSecondInternal) - dt) - overslept;
// Sleep before next frame
if( sleep > 0 ) {
try {
Thread.sleep(sleep / 1000000L, (int) (sleep % 1000000L));
@@ -1342,46 +1425,39 @@ public class Zeichenmaschine extends Constants {
overslept = 0L;
}
// Update stats
_tick += 1;
_runtime = System.currentTimeMillis() - start;
tick = _tick;
runtime = _runtime;
framesPerSecond = framesPerSecondInternal;
// If pause requested, we pause now
if( pause_pending ) {
state = Options.AppState.PAUSED;
pause_pending = false;
}
}
state = Options.AppState.STOPPED;
// Shutdown the updateThread
while( !terminateImediately && updateThreadExecutor.isRunning() ) {
Thread.yield();
}
updateThreadExecutor.shutdownNow();
teardown();
// Cleanup
shutdown();
cleanup();
state = Options.AppState.TERMINATED;
if( quitAfterTeardown ) {
if( quitAfterShutdown ) {
quit();
}
}
public void handleUpdate( double delta ) {
if( state == Options.AppState.RUNNING ) {
state = Options.AppState.UPDATING;
update(delta);
state = Options.AppState.RUNNING;
}
}
public void handleDraw() {
if( state == Options.AppState.RUNNING ) {
state = Options.AppState.DRAWING;
draw();
state = Options.AppState.RUNNING;
}
}
}
// TODO: Remove
class DelayedTask implements Delayed {
long startTime; // in ms
@@ -1408,7 +1484,8 @@ public class Zeichenmaschine extends Constants {
}
class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener{
class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener {
@Override
public void mouseClicked( MouseEvent e ) {
enqueueEvent(e);
@@ -1470,6 +1547,67 @@ public class Zeichenmaschine extends Constants {
}
class UpdateThreadExecutor extends ThreadPoolExecutor {
private Thread updateThread;
private boolean running = false;
public UpdateThreadExecutor() {
super(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
updateState = Options.AppState.IDLE;
}
@Override
public void execute( Runnable command ) {
running = true;
super.execute(command);
}
@Override
protected void beforeExecute( Thread t, Runnable r ) {
// We store the one Thread this Executor holds,
// but it might change if a new Thread needed to be spawned
// due to en error.
updateThread = t;
}
@Override
protected void afterExecute( Runnable r, Throwable t ) {
running = false;
updateState = Options.AppState.IDLE;
}
/**
* Ermittelt, ob der interne Thread gerade eine update/draw Task
* ausführt.
*
* @return
*/
public boolean isRunning() {
return running;
}
/**
* Ermittelt, ob der interne Thread gerade eine update/draw Task
* ausführt und dabei in einen Wartezustand versetzt wurde. Das bedeutet
* in der Regel, dass innerhalb von {@link #update(double)} oder
* {@link #draw()} ein {@link #delay(int)} ausgeführt wurde, oder aus
* einem anderen Grund beispielsweise {@link Thread#sleep(long)}
* aufgerufen wurde. (Dies kann zum Beispiel beim
* {@link Animation#await() Warten auf Animationen} der Fall sein.)
*
* @return
*/
public boolean isWaiting() {
//return running && updateThread.getState() == Thread.State.TIMED_WAITING;
//return running && updateThread != null && updateThread.getState() == Thread.State.TIMED_WAITING;
return running && updateThread != null && updateState == Options.AppState.DELAYED;
}
}
private static final Log LOG = Log.getLogger(Zeichenmaschine.class);
}

View File

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

View File

@@ -0,0 +1,36 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.util.Validator;
import java.util.function.DoubleUnaryOperator;
public class AnimationFacade<S> extends Animation<S> {
private Animation<S> anim;
public AnimationFacade( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.anim = Validator.requireNotNull(anim);
}
@Override
public S getAnimationTarget() {
return anim.getAnimationTarget();
}
@Override
public void animate( double e ) {
anim.animate(e);
}
@Override
public void initialize() {
anim.initialize();
}
@Override
public void finish() {
anim.finish();
}
}

View File

@@ -0,0 +1,116 @@
package schule.ngb.zm.anim;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
@SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> {
List<Animation<T>> anims;
private boolean overrideEasing = false;
private int overrideRuntime = -1;
private int lag = 0;
private int active = 0;
public AnimationGroup( Collection<Animation<T>> anims ) {
this(0, -1, null, anims);
}
public AnimationGroup( int lag, Collection<Animation<T>> anims ) {
this(lag, -1, null, anims);
}
public AnimationGroup( DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(0, -1, easing, anims);
}
public AnimationGroup( int lag, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
this(lag, -1, easing, anims);
}
public AnimationGroup( int lag, int runtime, DoubleUnaryOperator easing, Collection<Animation<T>> anims ) {
super();
this.anims = List.copyOf(anims);
this.lag = lag;
if( easing != null ) {
this.easing = easing;
overrideEasing = true;
}
if( runtime > 0 ) {
this.runtime = anims.size() * lag + runtime;
this.overrideRuntime = runtime;
} else {
this.runtime = 0;
for( int i = 0; i < this.anims.size(); i++ ) {
if( i * lag + this.anims.get(i).getRuntime() > this.runtime ) {
this.runtime = i * lag + this.anims.get(i).getRuntime();
}
}
}
}
@Override
public T getAnimationTarget() {
for( Animation<T> anim : anims ) {
if( anim.isActive() ) {
return anim.getAnimationTarget();
}
}
return anims.get(anims.size() - 1).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();
}
}
running = false;
this.stop();
}
while( active < anims.size() && elapsedTime >= active * lag ) {
anims.get(active).start();
active += 1;
}
for( int i = 0; i < active; i++ ) {
double t = 0.0;
if( overrideRuntime > 0 ) {
t = (double) (elapsedTime - i*lag) / (double) overrideRuntime;
} else {
t = (double) (elapsedTime - i*lag) / (double) anims.get(i).getRuntime();
}
if( t >= 1.0 ) {
anims.get(i).elapsedTime = anims.get(i).runtime;
anims.get(i).stop();
} else {
double e = overrideEasing ?
easing.applyAsDouble(t) :
anims.get(i).easing.applyAsDouble(t);
anims.get(i).animate(e);
}
}
}
@Override
public void animate( double e ) {
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
@SuppressWarnings( "unused" )
public class FadeAnimation extends Animation<Shape> {
public static final int FADE_IN = 255;
public static final int FADE_OUT = 0;
private Shape object;
private Color fill, stroke;
private int fillAlpha, strokeAlpha, tAlpha;
public FadeAnimation( Shape object, int alpha, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
fill = object.getFillColor();
fillAlpha = fill.getAlpha();
stroke = object.getStrokeColor();
strokeAlpha = stroke.getAlpha();
tAlpha = alpha;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@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)));
}
}

View File

@@ -0,0 +1,33 @@
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 Color oFill, tFill;
public FillAnimation( Shape object, Color newFill, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oFill = object.getFillColor();
tFill = newFill;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setFillColor(Color.interpolate(oFill, tFill, e));
}
}

View File

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

View File

@@ -0,0 +1,39 @@
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 double oX, oY, tX, tY;
public MoveAnimation( Shape object, double x, double y, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oX = object.getX();
oY = object.getY();
tX = x;
tY = y;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e));
object.setY(Constants.interpolate(oY, tY, e));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,143 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.GradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
/**
* Eine Ebene, die nur aus einer Farbe (oder einem Farbverlauf) besteht.
* <p>
* Die Farbe der Ebene kann beliebig gesetzt werden und kann gut als
* Hintergundfarbe für eine Szene dienen, oder als halbtransparente "Abdeckung",
* wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt wird.
*/
@SuppressWarnings( "unused" )
public class ColorLayer extends Layer {
/**
* Farbe der Ebene.
*/
private Color color;
/**
* Verlauf der Ebene, falls verwendet.
*/
private Paint background;
/**
* Erstellt eine neue Farbebene mit der angegebenen Farbe.
*
* @param color Die Hintergrundfarbe.
*/
public ColorLayer( Color color ) {
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* Erstellt eine neue Farbebene mit der angegebenen Größe und Farbe.
*
* @param width Breite der Ebene.
* @param height Höhe der Ebene.
* @param color Die Hintergrundfarbe.
*/
public ColorLayer( int width, int height, Color color ) {
super(width, height);
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* {@inheritDoc}
*/
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
clear();
}
/**
* Gibt die Hintergrundfarbe der Ebene zurück.
*
* @return Die aktuelle Hintergrundfarbe.
*/
public Color getColor() {
return color;
}
/**
* Setzt die Farbe der Ebene neu.
*
* @param color Die neue Hintergrundfarbe.
*/
public void setColor( Color color ) {
this.color = color;
this.background = color.getJavaColor();
clear();
}
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
public void setGradient( Color from, Color to, Options.Direction dir ) {
double halfW = getWidth() * .5;
double halfH = getHeight() * .5;
Options.Direction inv = dir.inverse();
int fromX = (int) (halfW + inv.x * halfW);
int fromY = (int) (halfH + inv.y * halfH);
int toX = (int) (halfW + dir.x * halfW);
int toY = (int) (halfH + dir.y * halfH);
setGradient(fromX, fromY, from, toX, toY, to);
}
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
this.color = from;
background = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
clear();
}
public void setGradient( Color from, Color to ) {
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, to);
}
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
this.color = from;
background = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
clear();
}
@Override
public void clear() {
drawing.setPaint(background);
drawing.fillRect(0, 0, getWidth(), getHeight());
}
}

View File

@@ -1,11 +1,16 @@
package schule.ngb.zm;
package schule.ngb.zm.layers;
import java.awt.*;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Layer;
import java.awt.Graphics2D;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class DrawableLayer extends Layer {
protected LinkedList<Drawable> drawables = new LinkedList<>();
protected List<Drawable> drawables = new LinkedList<>();
protected boolean clearBeforeDraw = true;
@@ -43,7 +48,8 @@ public class DrawableLayer extends Layer {
}
synchronized( drawables ) {
for( Drawable d : drawables ) {
List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) {
if( d.isVisible() ) {
d.draw(drawing);
}

View File

@@ -1,6 +1,8 @@
package schule.ngb.zm;
package schule.ngb.zm.layers;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.*;
import java.awt.geom.*;
@@ -8,11 +10,11 @@ import java.util.Stack;
public class DrawingLayer extends Layer {
protected Color fillColor = STD_FILLCOLOR;
protected schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected Color strokeColor = STD_STROKECOLOR;
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected double strokeWeight = STD_STROKEWEIGHT;
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
@@ -43,7 +45,7 @@ public class DrawingLayer extends Layer {
fontMetrics = drawing.getFontMetrics();
}
public Color getColor() {
public schule.ngb.zm.Color getColor() {
return fillColor;
}
@@ -51,7 +53,7 @@ public class DrawingLayer extends Layer {
setFillColor(gray, gray, gray, 255);
}
public void setFillColor( Color color ) {
public void setFillColor( schule.ngb.zm.Color color ) {
fillColor = color;
drawing.setColor(color.getJavaColor());
}
@@ -69,10 +71,10 @@ public class DrawingLayer extends Layer {
}
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
setFillColor(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public Color getStrokeColor() {
public schule.ngb.zm.Color getStrokeColor() {
return strokeColor;
}
@@ -80,7 +82,7 @@ public class DrawingLayer extends Layer {
setStrokeColor(gray, gray, gray, 255);
}
public void setStrokeColor( Color color ) {
public void setStrokeColor( schule.ngb.zm.Color color ) {
strokeColor = color;
drawing.setColor(color.getJavaColor());
}
@@ -98,7 +100,7 @@ public class DrawingLayer extends Layer {
}
public void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
setStrokeColor(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public void setStrokeWeight( double pWeight ) {
@@ -147,8 +149,8 @@ public class DrawingLayer extends Layer {
}
public void resetStroke() {
setStrokeColor(STD_STROKECOLOR);
setStrokeWeight(STD_STROKEWEIGHT);
setStrokeColor(DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID);
}
@@ -169,10 +171,10 @@ public class DrawingLayer extends Layer {
}
public void clear( int red, int green, int blue, int alpha ) {
clear(new Color(red, green, blue, alpha));
clear(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public void clear( Color pColor ) {
public void clear( schule.ngb.zm.Color pColor ) {
/*graphics.setBackground(pColor);
graphics.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());*/
java.awt.Color currentColor = drawing.getColor();
@@ -191,7 +193,8 @@ public class DrawingLayer extends Layer {
}
public void pixel( double x, double y ) {
square(x, y, 1);
// square(x, y, 1);
buffer.setRGB((int)x, (int)y, fillColor.getRGBA());
}
public void square( double x, double y, double w ) {

View File

@@ -1,9 +1,10 @@
package schule.ngb.zm;
package schule.ngb.zm.layers;
import java.awt.Graphics2D;
import java.awt.Image;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader;
public class ImageLayer extends Layer {

View File

@@ -1,15 +1,19 @@
package schule.ngb.zm;
package schule.ngb.zm.layers;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.*;
import java.util.LinkedList;
public final class Shape2DLayer extends Layer {
protected Color strokeColor = STD_STROKECOLOR;
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected Color fillColor = STD_FILLCOLOR;
protected schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected double strokeWeight = STD_STROKEWEIGHT;
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
@@ -39,7 +43,7 @@ public final class Shape2DLayer extends Layer {
this.instantDraw = instantDraw;
}
public Color getFillColor() {
public schule.ngb.zm.Color getFillColor() {
return fillColor;
}
@@ -47,7 +51,7 @@ public final class Shape2DLayer extends Layer {
setFillColor(gray, gray, gray, 255);
}
public void setFillColor( Color pColor ) {
public void setFillColor( schule.ngb.zm.Color pColor ) {
fillColor = pColor;
drawing.setColor(pColor.getJavaColor());
}
@@ -65,10 +69,10 @@ public final class Shape2DLayer extends Layer {
}
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
setFillColor(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public Color getStrokeColor() {
public schule.ngb.zm.Color getStrokeColor() {
return strokeColor;
}
@@ -76,7 +80,7 @@ public final class Shape2DLayer extends Layer {
setStrokeColor(gray, gray, gray, 255);
}
public void setStrokeColor( Color pColor ) {
public void setStrokeColor( schule.ngb.zm.Color pColor ) {
strokeColor = pColor;
drawing.setColor(pColor.getJavaColor());
}

View File

@@ -0,0 +1,167 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Layer;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.anim.AnimationFacade;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.shapes.Shape;
import java.awt.Graphics2D;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
public class ShapesLayer extends Layer {
protected boolean clearBeforeDraw = true;
private List<Shape> shapes;
private List<Animation<? extends Shape>> animations;
public ShapesLayer() {
super();
shapes = new LinkedList<>();
animations = new LinkedList<>();
}
public ShapesLayer( int width, int height ) {
super(width, height);
shapes = new LinkedList<>();
animations = new LinkedList<>();
}
public Shape getShape( int index ) {
return shapes.get(index);
}
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
return (ST) s;
}
}
return null;
}
public List<Shape> getShapes() {
return shapes;
}
public <ST extends Shape> List<ST> getShapes( Class<ST> shapeClass ) {
List<ST> result = new LinkedList<>();
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
result.add((ST) s);
}
}
return result;
}
public void add( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
}
}
public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
}
}
public void remove( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
}
public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
}
public void removeAll() {
synchronized( shapes ) {
shapes.clear();
}
}
public void showAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.show();
}
}
}
public void hideAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.hide();
}
}
}
public <S extends Shape> void play( Animation<S> anim ) {
this.animations.add(anim);
anim.start();
}
public void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim: anims ) {
this.animations.add(anim);
anim.start();
}
}
public <S extends Shape> void play( Animation<S> anim, int runtime ) {
play(anim, runtime, Easing.DEFAULT_EASING);
}
public <S extends Shape> void play( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
AnimationFacade<S> facade = new AnimationFacade<>(anim, runtime, easing);
play(facade);
}
@Override
public void update( double delta ) {
Iterator<Animation<? extends Shape>> it = animations.iterator();
while( it.hasNext() ) {
Animation<? extends Shape> anim = it.next();
anim.update(delta);
if( !anim.isActive() ) {
animations.remove(anim);
}
}
}
@Override
public void draw( Graphics2D pGraphics ) {
if( clearBeforeDraw ) {
clear();
}
synchronized( shapes ) {
List<Shape> it = List.copyOf(shapes);
for( Shape s : it ) {
if( s.isVisible() ) {
s.draw(drawing);
}
}
}
super.draw(pGraphics);
}
}

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.turtle;
package schule.ngb.zm.layers;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
@@ -336,7 +336,7 @@ public class TurtleLayer extends Layer {
if( strokeColor != null ) {
graphics.setColor(strokeColor.getJavaColor());
} else {
graphics.setColor(STD_STROKECOLOR.getJavaColor());
graphics.setColor(DEFAULT_STROKECOLOR.getJavaColor());
}
graphics.fill(shape);
graphics.setColor(Color.BLACK.getJavaColor());

View File

@@ -5,6 +5,8 @@ package schule.ngb.zm.media;
*/
public interface Audio {
String getSource();
/**
* Prüft, ob das Medium gerade abgespielt wird.
*
@@ -18,8 +20,8 @@ public interface Audio {
* {@code isLooping() == true}, dann muss auch immer
* {@code isPlaying() == true} gelten.
*
* @return @return {@code true}, wenn das Medium in einer Schleife
* abgespielt wird, {@code false} sonst.
* @return {@code true}, wenn das Medium in einer Schleife abgespielt wird,
* {@code false} sonst.
*/
boolean isLooping();
@@ -36,17 +38,28 @@ public interface Audio {
*/
void setVolume( double volume );
/**
* Gibt die aktuelle Lautstärkeeinstellung dieses Mediums zurück.
* <p>
* Die Lautstärke wird auf einer linearen Skale angegeben, wobei 0 kein Ton
* und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
* Mediums.
*
* @return Die Lautstärke als linear skalierter Wert.
*/
double getVolume();
/**
* Startet die Wiedergabe des Mediums und beendet die Methode. Das
* Audio-Medium wird einmal abgespielt und stoppt dann.
*/
void playOnce();
void play();
/**
* Startet die Wiedergabe des Mediums und blockiert das Programm, bis die
* Wiedergabe beendet ist.
*/
void playOnceAndWait();
void playAndWait();
/**
* Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode

View File

@@ -0,0 +1,11 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.events.Listener;
public interface AudioListener extends Listener<Audio> {
void start( Audio source );
void stop( Audio source );
}

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm.media;
import schule.ngb.zm.Constants;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.tasks.TaskRunner;
import java.util.ArrayList;
import java.util.List;
@@ -41,6 +41,10 @@ public class Mixer implements Audio {
this.audios = new ArrayList<>(4);
}
public String getSource() {
return "";
}
public void add( Audio pAudio ) {
add(pAudio, 1f);
}
@@ -50,30 +54,60 @@ public class Mixer implements Audio {
pAudio.setVolume(pVolumeFactor * volume);
}
public void remove( Audio pAudio ) {
}
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));
}
/**
* {@inheritDoc}
*/
@Override
public void playOnce() {
audios.stream().forEach(aw -> aw.audio.playOnce());
public double getVolume() {
return volume;
}
/**
* {@inheritDoc}
*/
@Override
public void playOnceAndWait() {
audios.stream().forEach(aw -> aw.audio.playOnce());
public void play() {
audios.stream().forEach(aw -> aw.audio.play());
}
/**
* {@inheritDoc}
*/
@Override
public void playAndWait() {
audios.stream().forEach(aw -> aw.audio.play());
while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) {
try {
Thread.sleep(10);
@@ -83,16 +117,25 @@ 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() ) {
@@ -101,6 +144,18 @@ public class Mixer implements Audio {
audios.stream().forEach(aw -> aw.audio.dispose());
}
/**
* Ändert die Lautstärke aller hinzugefügten Audiomedien in der angegebenen
* Zeit schrittweise, bis die angegebene Lautstärke erreicht ist.
* <p>
* Zu beachten ist, dass die Lautstärke des Mixers angepasst wird. Das
* bedeutet, dass die Lautstärke der hinzugefügten Medien mit ihrem
* Lautstärkefaktor multipliziert werden. Die Medien haben am Ende also
* nicht unbedingt die Lautstärke {@code to}.
*
* @param to Der Zielwert für die Lautstärke.
* @param time Die Zeit, nach der die Änderung abgeschlossen sein soll.
*/
public void fade( final double to, final int time ) {
TaskRunner.run(new Runnable() {
@Override
@@ -108,9 +163,6 @@ public class Mixer implements Audio {
final long start = System.currentTimeMillis();
double t = 0.0;
double from = volume;
if( !isPlaying() ) {
playOnce();
}
do {
setVolume(Constants.interpolate(from, to, t));
t = (double) (System.currentTimeMillis() - start) / (double) time;

View File

@@ -0,0 +1,303 @@
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 javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Ein Musikstück, dass im Projekt abgespielt werden soll.
* <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.
*/
public class Music implements Audio {
// size of the byte buffer used to read/write the audio stream
private static final int BUFFER_SIZE = 4096;
/**
* Ob der Sound gerade abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound gerade in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle des Musikstücks.
*/
private String audioSource;
/**
* Der AudioStream, um die AUdiosdaten zulsen, falls dieser schon geöffnet
* wurde. Sonst {@code null}.
*/
private AudioInputStream audioStream;
/**
* Die Line für die Ausgabe, falls diese schon geöffnet wurde. Sonst
* {@code null}.
*/
private SourceDataLine audioLine;
/**
* Die Lautstärke der Musik.
*/
private float volume = 0.8f;
EventDispatcher<Audio, AudioListener> eventDispatcher;
public Music( String source ) {
Validator.requireNotNull(source);
this.audioSource = source;
}
public String getSource() {
return audioSource;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPlaying() {
return playing;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
/**
* {@inheritDoc}
*/
@Override
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioLine != null ) {
applyVolume();
}
}
/**
* {@inheritDoc}
*/
@Override
public double getVolume() {
return volume;
}
/**
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen
* anzuwenden.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
/**
* {@inheritDoc}
*/
@Override
public void play() {
if( openLine() ) {
TaskRunner.run(new Runnable() {
@Override
public void run() {
stream();
}
});
}
}
/**
* {@inheritDoc}
*/
@Override
public void playAndWait() {
if( openLine() ) {
stream();
}
}
/**
* {@inheritDoc}
*/
@Override
public void loop() {
looping = true;
play();
}
/**
* {@inheritDoc}
*/
@Override
public void stop() {
playing = false;
looping = false;
dispose();
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
playing = false;
audioLine.stop();
}
if( audioLine.isOpen() ) {
audioLine.drain();
audioLine.close();
}
}
try {
if( audioStream != null ) {
audioStream.close();
}
} catch( IOException ex ) {
}
audioLine = null;
audioStream = null;
}
private synchronized void stream() {
audioLine.start();
playing = true;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Music.this);
}
byte[] bytesBuffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
try {
while( playing && (bytesRead = audioStream.read(bytesBuffer)) != -1 ) {
audioLine.write(bytesBuffer, 0, bytesRead);
}
audioLine.drain();
audioLine.stop();
} catch( IOException ex ) {
LOG.warn(ex, "Error while playing Music source <%s>", audioSource);
}
// Wait for the remaining audio to play
/*while( audioLine.isRunning() ) {
try {
Thread.sleep(10);
} catch( InterruptedException ex ) {
// Just keep waiting ...
}
}*/
playing = false;
streamingStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Music.this);
}
}
private boolean openLine() {
if( audioLine != null ) {
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream inStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = inStream.getFormat();
final int ch = format.getChannels();
final float rate = format.getSampleRate();
AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, outFormat);
audioLine = (SourceDataLine) AudioSystem.getLine(info);
audioLine.open(outFormat);
applyVolume();
audioStream = AudioSystem.getAudioInputStream(outFormat, inStream);
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
private void streamingStopped() {
dispose();
if( looping ) {
if( openLine() ) {
stream();
} else {
playing = false;
looping = false;
}
} else {
playing = false;
}
}
public void addListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
public void removeListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, soblad sich auch ein Listener registrieren möchte.
* @return
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.start(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.stop(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Music.class);
}

View File

@@ -0,0 +1,313 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Wiedergabe kurzer Soundclips, die mehrmals wiederverwendet werden.
* <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.
* <p>
* Für längre Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher die
* KLasse {@link Music} an.
*/
@SuppressWarnings( "unused" )
public class Sound implements Audio {
/**
* Ob der Sound gerade abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound gerade in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle des Musikstücks.
*/
private String audioSource;
/**
* Der Clip, falls er schon geladen wurde, sonst {@code null}.
*/
private Clip audioClip;
/**
* Ob die Resourcen des Clips im Speicher nach dem nächsten Abspielen
* freigegeben werden sollen.
*/
private boolean disposeAfterPlay = false;
/**
* Die Lautstärke des Clips.
*/
private float volume = 0.8f;
/**
* Erstellt einen Sound aus der angegebene Quelle.
*
* @param source Ein Dateipfad oder eine Webadresse.
* @throws NullPointerException Falls die Quelle {@code null} ist.
*/
public Sound( String source ) {
Validator.requireNotNull(source);
this.audioSource = source;
}
public String getSource() {
return audioSource;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning();
return playing;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
/**
* {@inheritDoc}
*/
@Override
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioClip != null ) {
applyVolume();
}
}
/**
* {@inheritDoc}
*/
@Override
public double getVolume() {
return volume;
}
/**
* Interne Methode, um die gesetzte Lautstärke vor dem Abspielen
* anzuwenden.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioClip.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
/**
* {@inheritDoc}
*/
@Override
public void stop() {
looping = false;
if( audioClip.isRunning() ) {
audioClip.stop();
}
playing = false;
}
/**
* {@inheritDoc}
*/
@Override
public void play() {
if( this.openClip() ) {
audioClip.start();
playing = true;
}
}
/**
* {@inheritDoc}
*/
@Override
public void playAndWait() {
this.play();
long audioLen = audioClip.getMicrosecondLength();
while( playing ) {
try {
long ms = (audioLen - audioClip.getMicrosecondPosition()) / 1000L;
Thread.sleep(ms);
} catch( InterruptedException ex ) {
// Ignore
}
}
audioClip.close();
}
/**
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips
* frei.
* <p>
* Der Aufruf ist effektiv gleich zu
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
* allerdings wird der aufrufende Thread nicht blockiert und
* {@link #dispose()} automatisch am Ende aufgerufen.
*/
public void playOnce() {
disposeAfterPlay = true;
play();
}
/**
* Spielt den Sound genau einmal ab und gibt danach alle Resourcen des Clips
* frei.
* <p>
* Der Aufruf entspricht
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
*/
public void playOnceAndWait() {
disposeAfterPlay = true;
playAndWait();
}
/**
* {@inheritDoc}
*/
@Override
public void loop() {
loop(Clip.LOOP_CONTINUOUSLY);
}
/**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und
* stoppt die Wiedergabe dann.
*
* @param count Anzahl der Wiederholungen.
*/
public void loop( int count ) {
if( count > 0 ) {
int loopCount = count;
if( loopCount != Clip.LOOP_CONTINUOUSLY ) {
if( loopCount <= 0 ) {
return;
}
// Adjust Number of loops
loopCount -= 1;
}
if( openClip() ) {
looping = true;
audioClip.loop(loopCount);
playing = true;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public synchronized void dispose() {
if( audioClip != null ) {
if( audioClip.isRunning() ) {
audioClip.stop();
}
audioClip.close();
audioClip = null;
}
}
private synchronized boolean openClip() {
if( audioClip != null ) {
audioClip.setFramePosition(0);
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream audioStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = audioStream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, format);
audioClip = (Clip) AudioSystem.getLine(info);
audioClip.addLineListener(new LineListener() {
@Override
public void update( LineEvent event ) {
if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped();
}
}
});
audioClip.open(audioStream);
applyVolume();
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
/*@Override
public void update( LineEvent event ) {
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START ) {
playing = true;
} else if( type == LineEvent.Type.STOP ) {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
}*/
/**
* 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.
*/
private void playbackStopped() {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
private static final Log LOG = Log.getLogger(Sound.class);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,202 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
/**
* Basisklasse für Formen, die eine Füllung besitzen können.
* <p>
* Formen mit einer Füllung können auch immer eine Konturlinie besitzen.
*/
public abstract class FilledShape extends StrokedShape {
/**
* Die aktuelle Füllfarbe der Form oder {@code null}, wenn die Form nicht
* gefüllt werden soll.
*/
protected Color fillColor = DEFAULT_FILLCOLOR;
/**
* Der aktuelle Farbverlauf der Form oder {@code null}, wenn die Form keinen
* Farbverlauf besitzt.
*/
protected Paint fill = null;
/**
* Gibt die aktuelle Füllfarbe der Form zurück.
*
* @return Die aktuelle Füllfarbe oder {@code null}.
*/
public Color getFillColor() {
return fillColor;
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @see Color
*/
public void setFillColor( Color color ) {
fillColor = color;
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe und setzt die Transparenz
* auf den angegebenen Wert. 0 is komplett durchsichtig und 255 komplett
* deckend.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
public void setFillColor( Color color, int alpha ) {
setFillColor(new Color(color, alpha));
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität. 0
* entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität und
* dem angegebenen Transparenzwert. Der Grauwert 0 entspricht schwarz, 255
* entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Füllung der Form.
*/
public void noFill() {
setFillColor(null);
noGradient();
}
/**
* Setzt die Füllfarbe auf den Standardwert zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_FILLCOLOR
*/
public void resetFill() {
setFillColor(DEFAULT_FILLCOLOR);
noGradient();
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
setFillColor(from);
fill = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
}
/**
* Setzt die Füllung auf einen kreisförmigen (radialen) Farbverlauf, mit dem
* Zentrum im Punkt ({@code centerX}, {@code centerY}) und dem angegebenen
* Radius. Der Verlauf starte im Zentrum mit der Farbe {@code from} und
* endet am Rand des durch den Radius beschriebenen Kreises mit der Farbe
* {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
setFillColor(from);
fill = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
}
/**
* Entfernt den Farbverlauf von der Form.
*/
public void noGradient() {
fill = null;
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit der aktuellen
* Füllung auf den Grafik-Kontext zu zeichnen. Die Methode verändert
* gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt sie nicht
* auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void fillShape( java.awt.Shape shape, Graphics2D graphics ) {
if( fill != null ) {
graphics.setPaint(fill);
graphics.fill(shape);
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fill(shape);
}
}
}

View File

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

View File

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

View File

@@ -7,41 +7,147 @@ import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
/**
* Basisklasse für alle Formen in der Zeichenmaschine.
* <p>
* Alle Formen sind als Unterklassen von {@code Shape} implementiert.
* <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die Werten einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert:
*
* <pre><code>
* public Circle( double x, double y, double radius ) {
* super(x, y);
* this.radius = radius;
* }
*
* public Circle( Circle circle ) {
* super(circle.x, circle.y);
* copyFrom(circle);
* }
* </code></pre>
* <p>
* Außerdem implementieren Unterklassen eine passende {@link #toString()} und
* eine {@link #equals(Object)} Methode.
*/
@SuppressWarnings( "unused" )
public abstract class Shape extends FilledShape {
/**
* x-Koordinate der Form.
*/
protected double x;
/**
* y-Koordinate der Form.
*/
protected double y;
/**
* Rotation in Grad um den Punkt (x, y).
*/
protected double rotation = 0.0;
/**
* Skalierungsfaktor.
*/
protected double scale = 1.0;
/**
* Ob die Form angezeigt werden soll.
*/
protected boolean visible = true;
/**
* Ankerpunkt der Form.
*/
protected Options.Direction anchor = Options.Direction.CENTER;
/**
* Setzt die x- und y-Koordinate der Form auf 0.
*/
public Shape() {
this(0.0, 0.0);
}
/**
* Setzt die x- und y-Koordinate der Form.
*
* @param x
* @param y
*/
public Shape( double x, double y ) {
this.x = x;
this.y = y;
}
public Shape() {
this(0.0, 0.0);
/**
* Ob die Form angezeigt wird oder nicht.
*
* @return {@code true}, wenn die From angezeigt werden soll, {@code false}
* sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Form.
*/
public void hide() {
visible = false;
}
/**
* Zeigt die Form an.
*/
public void show() {
visible = true;
}
/**
* Versteckt die Form, wenn sie derzeit angezeigt wird und zeigt sie
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
/**
* Gibt die x-Koordinate der Form zurück.
*
* @return Die x-Koordinate.
*/
public double getX() {
return x;
}
/**
* Setzt die x-Koordinate der Form.
*
* @param x Die neue x-Koordinate.
*/
public void setX( double x ) {
this.x = x;
}
/**
* Gibt die y-Koordinate der Form zurück.
*
* @return Die y-Koordinate.
*/
public double getY() {
return y;
}
/**
* Setzt die y-Koordinate der Form.
*
* @param y Die neue y-Koordinate.
*/
public void setY( double y ) {
this.y = y;
}
@@ -49,8 +155,9 @@ public abstract class Shape extends FilledShape {
/**
* Gibt die Breite dieser Form zurück.
* <p>
* Die Breite einer Form ist immer die Breite ihrer Begrenzung, <b>bevor</b>
* Drehungen und andere Transformationen auf sei angewandt wurden.
* 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.
@@ -62,8 +169,9 @@ public abstract class Shape extends FilledShape {
/**
* Gibt die Höhe dieser Form zurück.
* <p>
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung, <b>bevor</b>
* Drehungen und andere Transformationen auf sei angewandt wurden.
* 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.
@@ -72,28 +180,70 @@ public abstract class Shape extends FilledShape {
*/
public abstract double getHeight();
/**
* Gibt die Rotation in Grad zurück.
*
* @return Rotation in Grad.
*/
public double getRotation() {
return rotation;
}
/**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
* @param angle Drehwinkel in Grad.
*/
public void rotate( double angle ) {
this.rotation += angle % 360;
}
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
public void rotate( Point2D center, double angle ) {
rotate(center.getX(), center.getY(), angle);
}
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
this.x = x2 + x;
this.y = y2 + y;
}
/**
* Gibt den aktuellen Skalierungsfaktor zurück.
*
* @return Der Skalierungsfaktor.
*/
public double getScale() {
return scale;
}
public boolean isVisible() {
return visible;
public void scale( double factor ) {
scale = factor;
}
public void hide() {
visible = false;
public void scaleBy( double factor ) {
scale(scale * factor);
}
public void show() {
visible = true;
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
public void toggle() {
visible = !visible;
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
public Options.Direction getAnchor() {
@@ -117,6 +267,19 @@ public abstract class Shape extends FilledShape {
}
}
/**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkt basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p>
* Die Koordinaten des Ankerpunkt werden relativ zur oberen linken Ecke des
* Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt.
*
* @param width Breite des umschließdenden Rechtecks.
* @param height Höhe des umschließdenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten.
*/
protected static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5;
@@ -164,15 +327,22 @@ public abstract class Shape extends FilledShape {
}
/**
* Kopiert die Eigenschaften der übergebenen Form in diese.
* 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). Mit
* dem Aufruf
* <code>super.copyFrom(shape)</code> sollten die Basiseigenschaften
* kopiert werden.
* Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises).
* Unterklassen 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.
*
* @param shape
* @param shape Die Originalform, von der kopiert werden soll.
*/
public void copyFrom( Shape shape ) {
if( shape != null ) {
@@ -188,10 +358,58 @@ public abstract class Shape extends FilledShape {
}
}
/**
* Erzeugt eine Kopie dieser Form mit denselben Eigenschaften.
* <p>
* Unterklassen implementieren diese Methode mit dem genauen Typ der
* Unterklasse. In {@link Rectangle} sieht die Umsetzung beispielsweise so
* aus:
* <pre><code>
* public Rectangle copy() {
* return new Rectangle(this);
* }
* </code></pre>
* <p>
* Die Methode kann beliebig umgesetzt werden, um eine 1-zu-1-Kopie dieser
* Form zu erhalten. In der Regel sollte aber jede Form einen Konstruktor
* besitzen, die alle Werte einer andern Form übernimmt. Die gezeigte
* Implementierung dürfte daher im Regelfall ausreichend sein.
*
* @return Eine genaue Kopie dieser Form.
*/
public abstract Shape copy();
/**
* Gibt eine {@link java.awt.Shape Java-AWT Shape} Version dieser Form
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen.
* <p>
* Da die AWT-Shape bei jedem Zeichnen (also mindestens einmal pro Frame)
* benötigt wird, wird das aktuelle Shape-Objekt in {@link #awtShape}
* zwischengespeichert. Bei Änderungen der Objekteigenschaften muss daher
* intern {@link #invalidate()} aufgerufen werden, damit beim nächsten
* Aufruf von {@link #draw(Graphics2D)} die Shape mit einem Aufurf von
* {@code getShape()} neu erzeugt wird. Unterklassen können aber auch die
* zwischengespeicherte Shape direkt modifizieren, um eine Neugenerierung zu
* vermeiden.
* <p>
* Wenn diese Form nicht durch eine AWT-Shape dargstellt wird, kann die
* Methode {@code null} zurückgeben.
*
* @return Eine Java-AWT {@code Shape} die diess Form repräsentiert oder
* {@code null}.
*/
public abstract java.awt.Shape getShape();
/**
* Gibt die Begrenzungen der Form zurück.
* <p>
* Ein {@code Bounds}-Objekt beschreibt eine "<a
* href="https://gdbooks.gitbooks.io/3dcollisions/content/Chapter1/aabb.html">Axis
* Aligned Bounding Box</a>" (AABB).
*
* @return Die Abgrenzungen der Form nach Anwendung der Transformationen.
*/
public Bounds getBounds() {
return new Bounds(this);
}
@@ -222,7 +440,7 @@ public abstract class Shape extends FilledShape {
* {@code buff} nach oben verschoben.
*
* @param shape
* @param anchor
* @param dir
* @param buff
*/
public void moveTo( Shape shape, Options.Direction dir, double buff ) {
@@ -243,7 +461,7 @@ public abstract class Shape extends FilledShape {
}
public void alignTo( Options.Direction dir, double buff ) {
Point2D anchorShape = Shape.getAnchorPoint(width, height, dir);
Point2D anchorShape = Shape.getAnchorPoint(canvasWidth, canvasHeight, dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
@@ -274,7 +492,7 @@ public abstract class Shape extends FilledShape {
}
public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, STD_BUFFER);
nextTo(shape, dir, DEFAULT_BUFFER);
}
/**
@@ -296,40 +514,6 @@ public abstract class Shape extends FilledShape {
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
public void scale( double factor ) {
scale = factor;
}
public void scaleBy( double factor ) {
scale(scale * factor);
}
public void rotate( double angle ) {
this.rotation += angle % 360;
}
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
public void rotate( Point2D center, double angle ) {
rotate(center.getX(), center.getY(), angle);
}
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad);
this.x = x2 + x;
this.y = y2 + y;
}
/*public void shear( double dx, double dy ) {
verzerrung.shear(dx, dy);
}*/
@@ -377,16 +561,8 @@ public abstract class Shape extends FilledShape {
}
Color currentColor = graphics.getColor();
if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fill(shape);
}
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.draw(shape);
}
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
}
}

View File

@@ -5,6 +5,7 @@ import schule.ngb.zm.Options;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -28,9 +29,20 @@ import java.util.List;
* benötigen, erst nach Hinzufügen der Gruppenelemente ausgeführt werden.
* Nachdem sich die Zusammensetzung der Gruppe geändert hat, muss die Gruppe
* ggf. neu ausgerichtet werden.
* <p>
* Für die Ausrichtung der Elemente in einer Gruppe können
* {@link #arrange(Options.Direction, double)},
* {@link #arrangeInGrid(int, Options.Direction, double, int)} und
* {@link #align(Options.Direction)} verwendet werden, die jeweils die Position
* der Formen in der Gruppe ändern und nicht die Position der Gruppe selbst (so
* wie z.B. {@link #alignTo(Shape, Options.Direction)}.
*/
public class ShapeGroup extends Shape {
public static final int ARRANGE_ROWS = 0;
public static final int ARRANGE_COLS = 1;
private List<Shape> shapes;
private double groupWidth = -1.0;
@@ -88,7 +100,7 @@ public class ShapeGroup extends Shape {
public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) {
LinkedList<ShapeType> list = new LinkedList<>();
for( Shape s : shapes ) {
if( typeClass.getClass().isInstance(s) ) {
if( typeClass.isInstance(s) ) {
list.add((ShapeType) s);
}
}
@@ -132,6 +144,111 @@ public class ShapeGroup extends Shape {
return groupHeight;
}
public void arrange( Options.Direction dir, double buffer ) {
Shape last = null;
for( Shape s : shapes ) {
if( last != null ) {
s.nextTo(last, dir, buffer);
} else {
s.moveTo(0, 0);
}
last = s;
}
invalidateBounds();
}
public void arrangeInRows( int n, Options.Direction dir, double buffer ) {
arrangeInGrid(n, dir, buffer, ARRANGE_ROWS);
}
public void arrangeInColumns( int n, Options.Direction dir, double buffer ) {
arrangeInGrid(n, dir, buffer, ARRANGE_COLS);
}
public void arrangeInGrid( int n, Options.Direction dir, double buffer, int mode ) {
// Calculate grid size
int rows, cols;
if( mode == ARRANGE_ROWS ) {
rows = n;
cols = (int) ceil(shapes.size() / n);
} else {
cols = n;
rows = (int) ceil(shapes.size() / n);
}
// Calculate grid cell size
double maxHeight = shapes.stream().mapToDouble(
( s ) -> s.getHeight()
).reduce(0.0, Double::max);
double maxWidth = shapes.stream().mapToDouble(
( s ) -> s.getWidth()
).reduce(0.0, Double::max);
double halfHeight = maxHeight * .5;
double halfWidth = maxWidth * .5;
// Layout shapes
for( int i = 0; i < shapes.size(); i++ ) {
// Calculate center of grid cell
int row, col;
switch( dir ) {
case UP:
case NORTH:
row = rows - i % rows;
col = cols - (i / rows);
break;
case LEFT:
case WEST:
row = rows - (i / cols);
col = cols - i % cols;
break;
case RIGHT:
case EAST:
row = i / cols;
col = i % cols;
break;
case DOWN:
case SOUTH:
default:
row = i % rows;
col = i / rows;
break;
}
double centerX = halfWidth + col * (maxWidth + buffer);
double centerY = halfHeight + row * (maxHeight + buffer);
// Move shape to proper anchor location in cell
Shape s = shapes.get(i);
Point2D ap = Shape.getAnchorPoint(maxWidth, maxHeight, s.getAnchor());
s.moveTo(centerX + ap.getX(), centerY + ap.getY());
}
invalidateBounds();
}
public void align( Options.Direction dir ) {
Shape target = shapes.stream().reduce(null,
( t, s ) -> {
if( t == null ) return s;
Point2D apt = t.getAbsAnchorPoint(dir);
Point2D aps = s.getAbsAnchorPoint(dir);
if( apt.getX() * dir.x >= aps.getX() * dir.x && apt.getY() * dir.y >= aps.getY() * dir.y ) {
return t;
} else {
return s;
}
}
);
for( Shape s : shapes ) {
if( s != target ) {
s.alignTo(target, dir);
}
}
invalidateBounds();
}
private void invalidateBounds() {
groupWidth = -1.0;
groupHeight = -1.0;
@@ -148,6 +265,25 @@ public class ShapeGroup extends Shape {
maxy = Math.max(maxy, bounds.y + bounds.height);
}
//groupWidth = maxx - minx;
//groupHeight = maxy - miny;
groupWidth = maxx;
groupHeight = maxy;
}
public void pack() {
double minx = Double.MAX_VALUE, miny = Double.MAX_VALUE,
maxx = Double.MIN_VALUE, maxy = Double.MIN_VALUE;
for( Shape pShape : shapes ) {
Bounds bounds = pShape.getBounds();
minx = Math.min(minx, bounds.x);
maxx = Math.max(maxx, bounds.x + bounds.width);
miny = Math.min(miny, bounds.y);
maxy = Math.max(maxy, bounds.y + bounds.height);
}
x = minx;
y = miny;
groupWidth = maxx - minx;
groupHeight = maxy - miny;
}

View File

@@ -0,0 +1,249 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Stroke;
/**
* Basisklasse für Formen, die eine Konturlinie besitzen.
*/
public abstract class StrokedShape extends Constants implements Drawable {
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn die Form ohne
* kontur dargestellt werden soll.
*/
protected Color strokeColor = DEFAULT_STROKECOLOR;
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
/**
* Die Art der Konturlinie.
*/
protected Options.StrokeType strokeType = SOLID;
/**
* Cache für den aktuellen {@code Stroke} der Kontur. Wird nach Änderung
* einer der Kontureigenschaften auf {@code null} gesetzt und beim nächsten
* Zeichnen neu erstellt.
*/
protected Stroke stroke = null;
/**
* Gibt die aktuelle Farbe der Konturlinie zurück.
*
* @return Die Konturfarbe oder {@code null}.
*/
public Color getStrokeColor() {
return strokeColor;
}
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe.
*
* @param color Die neue Farbe der Konturlinie.
* @see Color
*/
public void setStrokeColor( Color color ) {
this.strokeColor = color;
}
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die
* Transparenz auf den angegebenen Wert. 0 is komplett durchsichtig und 255
* komplett deckend.
*
* @param color Die neue Farbe der Konturlinie oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
public void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha));
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Kontur der Form.
*/
public void noStroke() {
setStrokeColor(null);
}
/**
* Setzt die Farbe der Konturlinie auf die Standardwerte zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_STROKECOLOR
* @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT
* @see schule.ngb.zm.Constants#SOLID
*/
public void resetStroke() {
setStrokeColor(DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID);
}
/**
* Gibt die Dicke der Konturlinie zurück.
*
* @return Die aktuelle Dicke der Linie.
*/
public double getStrokeWeight() {
return strokeWeight;
}
/**
* Setzt die Dicke der Konturlinie. Die Dicke muss größer 0 sein. Wird 0
* übergeben, dann wird keine Kontur mehr angezeigt.
*
* @param weight Die Dicke der Konturlinie.
*/
public void setStrokeWeight( double weight ) {
this.strokeWeight = max(0.0, weight);
this.stroke = null;
}
/**
* Gibt die Art der Konturlinie zurück.
*
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
public Options.StrokeType getStrokeType() {
return strokeType;
}
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
*
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
public void setStrokeType( Options.StrokeType type ) {
this.strokeType = type;
this.stroke = null;
}
@Override
public abstract void draw( Graphics2D graphics );
/**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
* zwischengespeichert.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected Stroke createStroke() {
// TODO: Used global cached Stroke Objects?
if( stroke == null ) {
switch( strokeType ) {
case DOTTED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
break;
case DASHED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
break;
case SOLID:
default:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
break;
}
}
return stroke;
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit den aktuellen
* Kontureigenschaften auf den Grafik-Kontext zu zeichnen. Die Methode
* verändert gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt
* sie nicht auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void strokeShape( java.awt.Shape shape, Graphics2D graphics ) {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.draw(shape);
}
}
}

View File

@@ -2,7 +2,7 @@ package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import schule.ngb.zm.Options;
import schule.ngb.zm.util.FontLoader;
import schule.ngb.zm.util.io.FontLoader;
import java.awt.Canvas;
import java.awt.Font;
@@ -22,7 +22,7 @@ public class Text extends Shape {
protected int width = 0, height = 0, ascent = 0;
public Text( double x, double y, String text ) {
this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE));
this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, DEFAULT_FONTSIZE));
}
public Text( double x, double y, String text, String fontname ) {
super(x, y);
@@ -30,7 +30,7 @@ public class Text extends Shape {
if( userfont != null ) {
font = userfont;
} else {
font = new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE);
font = new Font(Font.SANS_SERIF, Font.PLAIN, DEFAULT_FONTSIZE);
}
setText(text);
fillColor = null;

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package schule.ngb.zm.charts;
package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Options;
import schule.ngb.zm.shapes.Rectangle;

View File

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

View File

@@ -1,7 +1,6 @@
package schule.ngb.zm.charts;
package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.shapes.Rectangle;
import schule.ngb.zm.util.Validator;
import java.awt.BasicStroke;
import java.awt.Graphics2D;

View File

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

View File

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

View File

@@ -0,0 +1,145 @@
package schule.ngb.zm.util;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings( "unused" )
public final class Counter {
public static Counter fromArray( double[] values ) {
return new Counter().countAll(values);
}
public static Counter fromArray( Number[] values ) {
return new Counter().countAll(values);
}
public static Counter fromList( List<Number> values ) {
return new Counter().countAll(values);
}
private AtomicInteger count = new AtomicInteger(0);
private double min = Double.NaN, max = Double.NaN, sum = Double.NaN;
public void Counter() {
}
public void Counter( int initial ) {
count.set(initial);
}
public int getCount() {
return count.get();
}
public double getMax() {
synchronized( count ) {
return max;
}
}
public double getMin() {
synchronized( count ) {
return min;
}
}
public double getSum() {
synchronized( count ) {
return sum;
}
}
public double getAvg() {
if( Double.isNaN(sum) ) {
return Double.NaN;
} else {
return sum / count.doubleValue();
}
}
public Counter setCount( int count ) {
this.count.set(count);
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter reset() {
count.set(0);
synchronized( count ) {
min = Double.NaN;
max = Double.NaN;
sum = Double.NaN;
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter inc() {
this.count.incrementAndGet();
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter dec() {
this.count.decrementAndGet();
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( double value ) {
inc();
// Update stats
synchronized( count ) {
sum = Double.isNaN(sum) ? value : sum + value;
if( Double.isNaN(max) || max < value )
max = value;
if( Double.isNaN(min) ||min > value )
min = value;
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Number num ) {
return count(num.doubleValue());
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Object obj ) {
if( obj instanceof Number ) {
return count((Number) obj);
} else {
inc();
return this;
}
}
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) {
for( double value: values ) {
count(value);
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) {
for( Number value: values ) {
count(value);
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) {
for( Number value: values ) {
count(value);
}
return this;
}
}

View File

@@ -1,6 +1,9 @@
package schule.ngb.zm.util;
import java.util.Enumeration;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.function.Supplier;
import java.util.logging.*;
@@ -28,7 +31,11 @@ import static java.util.logging.Level.*;
*/
public final class Log {
private static final String ROOT_LOGGER = "schule.ngb.zm";
private static final String ROOT_LOGGER = "schule";
private static final String DEFAULT_LOG_FORMAT = "[%1$tT] [%4$11s] %5$s %6$s%n";
private static final String DEFAULT_DEBUG_FORMAT = "[%1$tT] [%4$11s] (%2$s) %5$s %6$s%n";
private static boolean LOGGING_INIT = false;
@@ -43,7 +50,7 @@ public final class Log {
*
* @see #enableGlobalLevel(Level)
*/
public static final void enableGlobalDebugging() {
public static void enableGlobalDebugging() {
enableGlobalLevel(ALL);
}
@@ -68,32 +75,18 @@ public final class Log {
* @param level Das Level, auf das alle {@code Logger} und {@code Handler}
* mindestens herabgesenkt werden sollen.
*/
public static final void xenableGlobalLevel( Level level ) {
public static void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
ensureRootLoggerInitialized();
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(level);
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
}
}
}
}
public static final void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
// Decrease level of root level ConsoleHandlers for outpu
// Decrease level of root level ConsoleHandlers for output
Logger rootLogger = Logger.getLogger("");
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
handler.setFormatter(new LogFormatter());
}
}
}
@@ -117,16 +110,51 @@ public final class Log {
}
}
}
Logger LOG = Logger.getLogger(ROOT_LOGGER);
LOG.fine("Debug logging enabled.");
}
public static final Log getLogger( Class<?> clazz ) {
if( !LOGGING_INIT ) {
Logger.getLogger(ROOT_LOGGER);
LOGGING_INIT = true;
}
public static Log getLogger( Class<?> clazz ) {
ensureRootLoggerInitialized();
return new Log(clazz);
}
private static void ensureRootLoggerInitialized() {
if( LOGGING_INIT ) {
return;
}
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null ) {
System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
}
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(Level.INFO);
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null
&& LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) {
// System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) {
@Override
public synchronized void publish(final LogRecord record) {
super.publish(record);
flush();
}
});
// rootLogger.setUseParentHandlers(false);
}
if( rootLogger.getUseParentHandlers() ) {
// This logger was not configured somewhere else
// Add a Handler and Formatter
//StreamHandler rootHandler = new StreamHandler(System.out, new SimpleFormatter());
//rootLogger.addHandler(rootHandler);
//rootLogger.setUseParentHandlers(false);
}
LOGGING_INIT = true;
}
private final Logger LOGGER;
private final Class<?> sourceClass;
@@ -315,4 +343,40 @@ public final class Log {
this.log(FINER, throwable, msgSupplier);
}
static final class LogFormatter extends Formatter {
@Override
public String format( LogRecord record ) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return String.format(DEFAULT_LOG_FORMAT,
zdt,
source,
record.getLoggerName(),
record.getLevel().getLocalizedName(),
message,
throwable);
}
}
}

View File

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

View File

@@ -0,0 +1,81 @@
package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit;
@SuppressWarnings( "unused" )
public final class Timer {
private final TimeUnit baseUnit;
private boolean running = false;
private long starttime = -1;
private long elapsed = 0;
public Timer() {
this(TimeUnit.MILLISECONDS);
}
public Timer( TimeUnit baseUnit ) {
this.baseUnit = baseUnit;
}
public boolean isRunning() {
return running;
}
@SuppressWarnings( "UnusedReturnValue" )
public Timer start() {
starttime = System.nanoTime();
running = true;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Timer stop() {
running = false;
elapsed += System.nanoTime() - starttime;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Timer reset() {
running = false;
starttime = -1;
elapsed = 0;
return this;
}
public long getTime() {
return getTime(baseUnit);
}
public long getTime( TimeUnit unit ) {
if( running ) {
return unit.convert(System.nanoTime() - starttime + elapsed, TimeUnit.NANOSECONDS);
} else {
return unit.convert(elapsed, TimeUnit.NANOSECONDS);
}
}
public int getMillis() {
if( running ) {
return (int) ((System.nanoTime() - starttime + elapsed) / 1000000);
} else {
return (int) (elapsed / 1000000);
}
}
public double getSeconds() {
if( running ) {
return (System.nanoTime() - starttime + elapsed) / 1000000000.0;
} else {
return elapsed / 1000000000.0;
}
}
}

View File

@@ -136,8 +136,27 @@ 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 ) {
return requireSize(arr, 0, ()->"Parameter array may not be empty");
if( arr.length == 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array may not be empty") : msg.get());
return arr;
}
public static final <T> T[] requireSize( T[] arr, int size ) {
return requireSize(arr, size, (Supplier<String>)null);
}
public static final <T> T[] requireSize( T[] arr, int size, CharSequence msg ) {
return requireSize(arr, size, msg::toString);
}
public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) {
@@ -146,6 +165,20 @@ 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 ? String.format("Parameter array may not be null or empty") : msg.get());
return arr;
}
private Validator() {
}

View File

@@ -0,0 +1,58 @@
package schule.ngb.zm.util.events;
import schule.ngb.zm.util.Validator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiConsumer;
public class EventDispatcher<E, L extends Listener<E>> {
private CopyOnWriteArraySet<L> listeners;
private ConcurrentMap<String, BiConsumer<E, L>> eventRegistry;
public EventDispatcher() {
listeners = new CopyOnWriteArraySet<>();
eventRegistry = new ConcurrentHashMap<>();
}
public void registerEventType( String eventKey, BiConsumer<E, L> dispatcher ) {
Validator.requireNotNull(eventKey);
Validator.requireNotNull(dispatcher);
if( !eventRegistered(eventKey) ) {
eventRegistry.put(eventKey, dispatcher);
}
}
public void addListener( L listener ) {
listeners.add(listener);
}
public void removeListener( L listener ) {
listeners.remove(listener);
}
public boolean hasListeners() {
return !listeners.isEmpty();
}
public boolean eventRegistered( String eventKey ) {
return eventRegistry.containsKey(eventKey);
}
public void dispatchEvent( String eventKey, final E event ) {
Validator.requireNotNull(eventKey);
Validator.requireNotNull(event);
if( eventRegistered(eventKey) ) {
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
listeners.forEach(( listener ) -> {
dispatcher.accept(event, listener);
});
}
}
}

View File

@@ -0,0 +1,7 @@
package schule.ngb.zm.util.events;
public interface Listener<E> {
}

View File

@@ -0,0 +1,109 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public final class FileLoader {
public static final Charset ASCII = StandardCharsets.US_ASCII;
public static final Charset UTF8 = StandardCharsets.UTF_8;
public static final Charset UTF16 = StandardCharsets.UTF_16;
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
public static List<String> loadLines( String source ) {
return loadLines(source, UTF8);
}
public static List<String> loadLines( String source, Charset charset ) {
try {
return Files.readAllLines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source);
}
return Collections.EMPTY_LIST;
}
public static String loadText( String source ) {
return loadText(source, UTF8);
}
public static String loadText( String source, Charset charset ) {
try {
return Files.readString(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading text from source <%s>", source);
}
return "";
}
public static String[][] loadCsv( String source, boolean skipFirst ) {
return loadCsv(source, ',', skipFirst, UTF8);
}
public static String[][] loadCsv( String source, char separator, boolean skipFirst, Charset charset ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> line.split(Character.toString(separator))
).toArray(String[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading csv source <%s>", source);
}
return new String[0][0];
}
public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
return loadValues(source, separator, skipFirst, UTF8);
}
public static double[][] loadValues( String source, char separator, boolean skipFirst, Charset charset ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).toArray(double[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading double values from csv source <%s>", source);
}
return new double[0][0];
}
public FileLoader() {
}
private static final Log LOG = Log.getLogger(FileLoader.class);
}

View File

@@ -1,7 +1,10 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
@@ -12,20 +15,50 @@ public class FontLoader {
private static final Map<String, Font> fontCache = new ConcurrentHashMap<>();
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Schrift wird unter ihrem Dateinamen in den Schriftenspeicher geladen
* und kann danach in der Zeichenmaschine benutzt werden.
*
* Ein Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden.
*
* @param source
* @return
*/
public static Font loadFont( String source ) {
String name = source;
// Dateipfad entfernen
int lastIndex = source.lastIndexOf(File.separatorChar);
if( lastIndex > -1 ) {
source.substring(lastIndex + 1);
}
// Dateiendung entfernen
lastIndex = name.lastIndexOf('.');
if( lastIndex > -1 ) {
name = name.substring(0, lastIndex);
}
return loadFont(name, source);
}
public static Font loadFont( String name, String source ) {
Objects.requireNonNull(source, "Font source may not be null");
if( source.length() == 0 ) {
throw new IllegalArgumentException("Font source may not be empty.");
}
if( fontCache.containsKey(source) ) {
LOG.trace("Retrieved font <%s> from font cache.", source);
return fontCache.get(source);
if( fontCache.containsKey(name) ) {
LOG.trace("Retrieved font <%s> from font cache.", name);
return fontCache.get(name);
}
// Look for System fonts
Font font = Font.decode(source);
if( font != null && source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(name, font);
fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source);
return font;
@@ -34,14 +67,15 @@ public class FontLoader {
}
// Load userfonts
try( InputStream in = ResourceStreamProvider.getResourceStream(source) ) {
try( InputStream in = ResourceStreamProvider.getInputStream(source) ) {
font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(Font.PLAIN);
if( font != null ) {
fontCache.put(name, font);
fontCache.put(source, font);
//ge.registerFont(font);
}
LOG.debug("Loaded custom font from <%s>.", source);
LOG.debug("Loaded custom font from source <%s>.", source);
} catch( IOException ioex ) {
LOG.error(ioex, "Error loading custom font file from source <%s>.", source);
} catch( FontFormatException ffex ) {

View File

@@ -1,4 +1,7 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO;
import java.awt.Color;
@@ -13,11 +16,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class ImageLoader {
public final class ImageLoader {
public static boolean caching = true;
@@ -29,7 +30,7 @@ public class ImageLoader {
* Lädt ein Bild von der angegebenen Quelle {@code source}.
* <p>
* Die Bilddatei wird nach den Regeln von
* {@link ResourceStreamProvider#getResourceStream(String)} gesucht und
* {@link ResourceStreamProvider#getInputStream(String)} gesucht und
* geöffnet. Tritt dabei ein Fehler auf oder konnte keine passende Datei
* gefunden werden, wird {@code null} zurückgegeben.
* <p>
@@ -47,7 +48,7 @@ public class ImageLoader {
/**
* Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* Bild zurück oder <code>null</code>, wenn das Bild nicht geladen werden
* Bild zurück oder {@code null}, wenn das Bild nicht geladen werden
* konnte. Ist ein Bild mit der angegebenen Quelle im Cache, wird das
* gespeicherte Bild zurückgegeben. Dies kann mit {@code caching = false}
* verhindert werden.
@@ -70,7 +71,7 @@ public class ImageLoader {
}
BufferedImage img = null;
try( InputStream in = ResourceStreamProvider.getResourceStream(source) ) {
try( InputStream in = ResourceStreamProvider.getInputStream(source) ) {
//URL url = ResourceStreamProvider.getResourceURL(source);
//BufferedImage img = ImageIO.read(url);

View File

@@ -1,12 +1,11 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.io.*;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.
@@ -50,15 +49,11 @@ public class ResourceStreamProvider {
* einer bestehenden Resource oder falls
* keine passende Resource gefunden wurde.
*/
public static InputStream getResourceStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
if( source == null ) {
throw new NullPointerException("Resource source may not be null");
}
if( source.length() == 0 ) {
throw new IllegalArgumentException("Resource source may not be empty.");
}
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
InputStream in;
InputStream in = null;
// See if source is a readable file
File file = new File(source);
@@ -72,7 +67,9 @@ public class ResourceStreamProvider {
}
// File does not exist, try other means
// load ressource relative to .class-file
if( in == null ) {
in = Zeichenmaschine.class.getResourceAsStream(source);
}
// relative to ClassLoader
if( in == null ) {
@@ -89,6 +86,16 @@ public class ResourceStreamProvider {
return in;
}
public static InputStream getInputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileInputStream(file);
}
public static InputStream getInputStream( URL url ) throws IOException {
Validator.requireNotNull(url, "Provided URL can't be null.");
return url.openStream();
}
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt
@@ -119,12 +126,8 @@ public class ResourceStreamProvider {
* einer bestehenden Resource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
if( source == null ) {
throw new NullPointerException("Resource source may not be null");
}
if( source.length() == 0 ) {
throw new IllegalArgumentException("Resource source may not be empty.");
}
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
File file = new File(source);
if( file.isFile() ) {
@@ -146,20 +149,30 @@ public class ResourceStreamProvider {
return new URL(source);
}
public static InputStream getResourceStream( File file ) throws FileNotFoundException, SecurityException {
if( file == null ) {
throw new NullPointerException("Provided file can't be null.");
/**
* Ver
* @param source
* @return
* @throws IOException
*/
public static OutputStream getOutputStream( String source ) throws IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
URL url = getResourceURL(source);
return getOutputStream(new File(url.getPath()));
}
return new FileInputStream(file);
public static OutputStream getOutputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileOutputStream(file);
}
public static InputStream getResourceStream( URL url ) throws IOException {
if( url == null ) {
throw new NullPointerException("Provided URL can't be null.");
public static Reader getReader( String source ) throws IOException {
return new InputStreamReader(getInputStream(source));
}
return url.openStream();
public static Writer getWriter( String source ) throws IOException {
return new OutputStreamWriter(getOutputStream(source));
}
private ResourceStreamProvider() {

View File

@@ -0,0 +1,66 @@
package schule.ngb.zm.util.tasks;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public abstract class DelayedTask extends Task implements Delayed {
protected long startTime = System.currentTimeMillis(); // in ms
/**
* Gibt die absolute Verzögerung der Task zurück. Im Gegensatz zu
* {@link #getDelay(TimeUnit)} sollte das Ergebnis von {@code getDelay()}
* bei mehrmaligem Aufruf konstant bleiben.
*
* @return Die ursprüngliche Verzögerung in Millisekunden
*/
public abstract int getDelay();
public long getStartTime() {
return startTime + getDelay();
}
/**
* Gibt die verbleibende Verzögerung bis zur Ausführung der Task zurück. Im
* Gegensatz zu {@link #getDelay()} sollte für mehrere Aufrufe von
* {@code getDelay(TimeUnit)} gelten, dass der zeitlich spätere Aufruf einen
* kleineren Wert zurückgibt, als der Frühere (abhängig von der gewählten
* {@link TimeUnit}).
*
* @param unit Die Zeiteinheit für die Verzögerung.
* @return Die verbleibende Verzögerung in der angegebenen Zeiteinheit.
*/
@Override
public long getDelay( TimeUnit unit ) {
int diff = (int) (getStartTime() - System.currentTimeMillis());
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo( Delayed o ) {
return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
@Override
public void run() {
while( getDelay(TimeUnit.MILLISECONDS) > 0 ) {
try {
System.out.println("Waiting for " + getDelay(TimeUnit.MILLISECONDS) + " ms");
Thread.sleep(getDelay(TimeUnit.MILLISECONDS));
//wait(getDelay(TimeUnit.MILLISECONDS));
} catch( InterruptedException e ) {
// Keep waiting
}
}
initialize();
running = true;
this.update(0.0);
running = false;
done = true;
finish();
}
}

View File

@@ -0,0 +1,64 @@
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;
public abstract class FrameSynchronizedTask extends Task {
private static Thread mainThread;
private static final Thread getMainThread() {
if( mainThread == null ) {
mainThread = Thread.currentThread();
if( !mainThread.getName().equals(Constants.APP_NAME) ) {
// Need to search for main Zeichenthread ...
}
}
return mainThread;
}
@Override
public void run() {
initialize();
running = true;
Object lock = Zeichenmaschine.globalSyncLock;
// start of thread in ms
final long start = System.currentTimeMillis();
// current time in ns
long beforeTime = System.nanoTime();
// store for deltas
long overslept = 0L;
// internal counters for tick and runtime
int _tick = 0;
double delta = 0.0;
while( running ) {
// delta in seconds
delta = (System.nanoTime() - beforeTime) / 1000000000.0;
beforeTime = System.nanoTime();
_tick = Constants.tick;
this.update(delta);
synchronized( lock ) {
while( _tick >= Constants.tick ) {
try {
lock.wait();
} catch( InterruptedException e ) {
// We got interrupted ...
}
}
}
}
running = false;
done = true;
finish();
}
}

View File

@@ -0,0 +1,11 @@
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Constants;
public abstract class FramerateLimitedTask extends RateLimitedTask {
public int getRate() {
return Constants.framesPerSecond;
}
}

View File

@@ -0,0 +1,57 @@
package schule.ngb.zm.util.tasks;
public abstract class RateLimitedTask extends Task {
public abstract int getRate();
@Override
public final void run() {
if( running || done ) {
return;
}
initialize();
// current time in ns
long beforeTime = System.nanoTime();
// store for deltas
long overslept = 0L;
// delta in ms
double delta = 0;
running = true;
while( running ) {
// delta in seconds
delta = (System.nanoTime() - beforeTime) / 1000000000.0;
beforeTime = System.nanoTime();
this.update(delta);
// delta time in ns
long afterTime = System.nanoTime();
long dt = afterTime - beforeTime;
long sleep = 0;
if( getRate() > 0 ) {
sleep = ((1000000000L / getRate()) - dt) - overslept;
}
if( sleep > 0 ) {
try {
Thread.sleep(sleep / 1000000L, (int) (sleep % 1000000L));
} catch( InterruptedException e ) {
// Interrupt not relevant
}
} else {
Thread.yield();
}
// Did we sleep to long?
overslept = (System.nanoTime() - afterTime) - sleep;
}
running = false;
done = true;
finish();
}
}

View File

@@ -0,0 +1,30 @@
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Updatable;
public abstract class Task implements Runnable, Updatable {
protected boolean running = false;
protected boolean done = false;
@Override
public boolean isActive() {
return running;
}
public boolean isDone() {
return !running & done;
}
public void stop() {
running = false;
}
protected void initialize() {
}
protected void finish() {
}
}

View File

@@ -0,0 +1,100 @@
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.util.Log;
import javax.swing.*;
import java.util.concurrent.*;
/**
* Führt Aufgaben (Tasks) parallel zum Hauptprogramm aus.
*/
public class TaskRunner {
private static final int POOL_SIZE = 4;
private static final int SHUTDOWN_TIME = 100;
private static TaskRunner runner;
public static TaskRunner getTaskRunner() {
if( runner == null ) {
runner = new TaskRunner();
}
return runner;
}
public static Future<?> run( Task task ) {
TaskRunner r = getTaskRunner();
return r.pool.submit(task);
}
public static Future<?> run( Runnable task ) {
TaskRunner r = getTaskRunner();
return r.pool.submit(task);
}
public static <T> Future<T> run( Runnable task, T result ) {
TaskRunner r = getTaskRunner();
return r.pool.submit(task, result);
}
public static Future<?> invokeLater( Runnable task ) {
FutureTask<Object> future = new FutureTask<>(task, null);
SwingUtilities.invokeLater(future);
return future;
}
public static <T> Future<T> invokeLater( Runnable task, T result ) {
FutureTask<T> future = new FutureTask<>(task, result);
SwingUtilities.invokeLater(future);
return future;
}
public static void shutdown() {
if( runner != null ) {
/*runner.pool.shutdown();
try {
runner.pool.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
} finally {
if( !runner.pool.isTerminated() ) {
runner.pool.shutdownNow();
}
}*/
runner.pool.shutdownNow();
}
}
ExecutorService pool;
private TaskRunner() {
//pool = new ScheduledThreadPoolExecutor(4);
/*pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() {
private final ThreadFactory threadFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread( Runnable r ) {
Thread t = threadFactory.newThread(r);
t.setName("TaskRunner-" + t.getName());
t.setDaemon(true);
return t;
}
});*/
pool = Executors.newCachedThreadPool(new ThreadFactory() {
private final ThreadFactory threadFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread( Runnable r ) {
Thread t = threadFactory.newThread(r);
t.setName("TaskRunner-" + t.getName());
t.setDaemon(true);
return t;
}
});
}
private static final Log LOG = Log.getLogger(TaskRunner.class);
}

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

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