Compare commits
70 Commits
97f8eaeb49
...
v0.0.22-SN
| Author | SHA1 | Date | |
|---|---|---|---|
| 3931e610c6 | |||
| bf14bf14dd | |||
| 222bc0ff7d | |||
| 33fb503ab8 | |||
| aad53d51d1 | |||
| d87e455e9d | |||
| 4fd4aa9a94 | |||
| 7031aa40cc | |||
| c295821d85 | |||
| 91805f7794 | |||
| 22c0547caa | |||
| d443d4d11d | |||
| 4366726671 | |||
| 6040545274 | |||
| 7b84570d18 | |||
| b24eec5063 | |||
| d5abd4ef68 | |||
| 9cd37fdce0 | |||
| 7e023026ce | |||
| 005f8299ac | |||
| 763d9eff22 | |||
| b244a6e094 | |||
| 4597fb411a | |||
| 9634d11842 | |||
| e6882fcbf7 | |||
| c0831688ba | |||
| 1e4b865492 | |||
| 0098621ebe | |||
| 949f058b5d | |||
| 740bb37279 | |||
| f8550ceae5 | |||
| 99d0e702aa | |||
| 5557030e0d | |||
| 855d67c873 | |||
| bc791a9dc3 | |||
| f79cff18a6 | |||
| 921e2fb3ef | |||
| 944249ce82 | |||
| e1f01fe620 | |||
| 9f56b49fb7 | |||
| ec8e5cea91 | |||
| 6a2adc9d4d | |||
| 9088e6eceb | |||
| 2c322eb678 | |||
| b60ffa9ef3 | |||
| 0a72b97d56 | |||
| 4ea526b239 | |||
| 54762cb3e6 | |||
| 2e5d5d7e83 | |||
| 43c5b9f28e | |||
| 62f221e18e | |||
| b3ff1a507e | |||
| 600a49af64 | |||
| b575c47ab3 | |||
| 447accc567 | |||
| 2de37e5501 | |||
| 98a62f35cd | |||
| a52125aeb2 | |||
| 476545f721 | |||
| a8bbce72a2 | |||
| ced0aa6842 | |||
| d48b167fb3 | |||
| e4818d4f3e | |||
| 9ee7c606fe | |||
| 303b667cbf | |||
| 2f59d29d08 | |||
| fc7ee36367 | |||
| 3030445dcf | |||
| b6b8263e5e | |||
| 49ad98fd82 |
16
.gitignore
vendored
@@ -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
|
||||
|
||||
16
CHANGELOG.md
@@ -6,14 +6,26 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## Added
|
||||
### Added
|
||||
- System für EventListener erstellt
|
||||
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.
|
||||
- Pakete für Animationen und Maschinelles-Lernen hinzugefügt
|
||||
|
||||
### Changed
|
||||
- `update(double)` und `draw()` werden nun in einem eigenen Thread aufgerufen.
|
||||
|
||||
## 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.
|
||||
- `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging.
|
||||
|
||||
### Removed
|
||||
- Beispielprojekte in [eigenes Repository](https://github.com/jneug/zeichenmaschine-examples) verschoben.
|
||||
|
||||
46
build.gradle
Normal file
@@ -0,0 +1,46 @@
|
||||
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'
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api: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
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
rootProject.name = 'zeichenmaschine'
|
||||
|
||||
@@ -3,17 +3,15 @@ package schule.ngb.zm;
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
|
||||
|
||||
//@formatter:off
|
||||
/**
|
||||
* Die Farbe Schwarz (Grauwert 0).
|
||||
@@ -106,7 +104,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 +114,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 +122,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 +135,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 +180,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 +207,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 +232,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 +333,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 +390,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Erzeugt eine Kopie dieser Farbe.
|
||||
*
|
||||
* @return Ein neues Farbobjekt.
|
||||
*/
|
||||
public Color copy() {
|
||||
@@ -402,10 +399,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 +416,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 +425,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 +434,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 +443,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Gibt den Transparenzwert dieser Farbe zurück.
|
||||
*
|
||||
* @return Der Transparenzwert der Farbe zwischen 0 und 255.
|
||||
*/
|
||||
public int getAlpha() {
|
||||
@@ -450,9 +452,10 @@ 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() {
|
||||
@@ -470,11 +473,18 @@ 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 +494,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Berechnet einen Hashcode für dieses Farbobjekt.
|
||||
*
|
||||
* @return Ein Hashcode für diese Rabe.
|
||||
*/
|
||||
@Override
|
||||
@@ -493,6 +504,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Erzeugt eine um 30% hellere Version dieser Farbe.
|
||||
*
|
||||
* @return Ein Farbobjekt mit einer helleren Farbe.
|
||||
*/
|
||||
public Color brighter() {
|
||||
@@ -501,6 +513,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 +525,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Erzeugt eine um 30% dunklere Version dieser Farbe.
|
||||
*
|
||||
* @return Ein Farbobjekt mit einer dunkleren Farbe.
|
||||
*/
|
||||
public Color darker() {
|
||||
@@ -520,6 +534,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 +545,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 +560,7 @@ public class Color {
|
||||
|
||||
/**
|
||||
* Erzeugt die Komplementärfarbe zu dieser.
|
||||
*
|
||||
* @return Ein Farbobjekt mit der Komplementärfarbe.
|
||||
*/
|
||||
public Color complement() {
|
||||
@@ -556,11 +573,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;
|
||||
@@ -12,7 +12,7 @@ public interface Drawable {
|
||||
* Gibt an, ob das Objekt derzeit sichtbar ist (also gezeichnet werden
|
||||
* muss).
|
||||
*
|
||||
* @return <code>true</code>, wenn das Objekt sichtbar ist.
|
||||
* @return {@code true}, wenn das Objekt sichtbar ist.
|
||||
*/
|
||||
boolean isVisible();
|
||||
|
||||
@@ -191,7 +191,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 ) {
|
||||
@@ -1,7 +1,9 @@
|
||||
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;
|
||||
|
||||
public abstract class Layer extends Constants implements Drawable, Updatable {
|
||||
@@ -73,8 +75,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 +88,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
|
||||
@@ -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,19 @@ public final class Options {
|
||||
DRAWING,
|
||||
PAUSED,
|
||||
STOPPED,
|
||||
TERMINATED
|
||||
TERMINATED,
|
||||
IDLE
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +60,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 +100,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;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public interface Updatable {
|
||||
|
||||
/**
|
||||
* Gibt an, ob das Objekt gerade auf Aktualisierungen reagiert.
|
||||
* @return <code>true</code>, wenn das Objekt aktiv ist.
|
||||
* @return {@code true}, wenn das Objekt aktiv ist.
|
||||
*/
|
||||
public boolean isActive();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import schule.ngb.zm.shapes.ShapesLayer;
|
||||
|
||||
import java.awt.Canvas;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
@@ -37,12 +35,12 @@ public class Zeichenleinwand extends Canvas {
|
||||
super.setSize(width, height);
|
||||
this.setPreferredSize(this.getSize());
|
||||
this.setMinimumSize(this.getSize());
|
||||
this.setBackground(Constants.STD_BACKGROUND.getJavaColor());
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +88,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 +123,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 +139,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 +174,32 @@ 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 ) {
|
||||
for( Layer layer : layers ) {
|
||||
layer.update(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine passende {@link BufferStrategy} für diese Ebene.
|
||||
@@ -1,5 +1,6 @@
|
||||
package schule.ngb.zm;
|
||||
|
||||
import schule.ngb.zm.anim.Animation;
|
||||
import schule.ngb.zm.shapes.ShapesLayer;
|
||||
import schule.ngb.zm.tasks.TaskRunner;
|
||||
import schule.ngb.zm.util.ImageLoader;
|
||||
@@ -23,6 +24,7 @@ import java.util.logging.Level;
|
||||
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der
|
||||
* nötigen Komponenten.
|
||||
*/
|
||||
// TODO: Refactorings (besonders in Bezug auf Nebenläufigkeit)
|
||||
public class Zeichenmaschine extends Constants {
|
||||
|
||||
/**
|
||||
@@ -34,10 +36,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,10 +106,21 @@ public class Zeichenmaschine extends Constants {
|
||||
* Interne Attribute zur Steuerung der Zeichenmaschine.
|
||||
*/
|
||||
//@formatter:off
|
||||
// Das Zeichenfenster der Zeichenmaschine
|
||||
|
||||
/**
|
||||
* Das Zeichenfenster der Zeichenmaschine
|
||||
*/
|
||||
private JFrame frame;
|
||||
// Die Graphics-Objekte für das aktuelle Fenster.
|
||||
|
||||
/**
|
||||
* Die Graphics-Umgebung für das aktuelle Fenster.
|
||||
*/
|
||||
private GraphicsEnvironment environment;
|
||||
|
||||
/**
|
||||
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich
|
||||
* dem Aktuellen sein, wenn das Fenster verschoben wurde).
|
||||
*/
|
||||
private GraphicsDevice displayDevice;
|
||||
|
||||
/**
|
||||
@@ -116,11 +138,11 @@ public class Zeichenmaschine extends Constants {
|
||||
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.
|
||||
* {@code KeyListener}, um den Vollbild-Modus mit der Escape-Taste zu
|
||||
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
|
||||
* hinzugefügt und entfernt.
|
||||
*/
|
||||
KeyListener fullscreenExitListener = new KeyAdapter() {
|
||||
private KeyListener fullscreenExitListener = new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed( KeyEvent e ) {
|
||||
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
|
||||
@@ -131,30 +153,69 @@ public class Zeichenmaschine extends Constants {
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
/**
|
||||
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
|
||||
* oder das Zeichenfenster weiter geöffnet bleibt.
|
||||
*/
|
||||
private boolean quitAfterTeardown = 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
|
||||
|
||||
@@ -277,8 +338,8 @@ public class Zeichenmaschine extends Constants {
|
||||
}
|
||||
|
||||
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.canvasWidth = width;
|
||||
this.canvasHeight = height;
|
||||
java.awt.Rectangle displayBounds = displayDevice.getDefaultConfiguration().getBounds();
|
||||
this.screenWidth = (int) displayBounds.getWidth();
|
||||
this.screenHeight = (int) displayBounds.getHeight();
|
||||
@@ -291,6 +352,7 @@ public class Zeichenmaschine extends Constants {
|
||||
// Das Icon des Fensters ändern
|
||||
|
||||
try {
|
||||
// TODO: Add image sizes
|
||||
ImageIcon icon = new ImageIcon(ImageIO.read(new File("res/icon_64.png")));
|
||||
|
||||
if( MACOS ) {
|
||||
@@ -334,7 +396,19 @@ public class Zeichenmaschine extends Constants {
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing( WindowEvent e ) {
|
||||
exit();
|
||||
if( running ) {
|
||||
running = false;
|
||||
teardown();
|
||||
cleanup();
|
||||
}
|
||||
// Give the app a minimum amount of time to shut down
|
||||
// then kill it.
|
||||
try {
|
||||
Thread.sleep(5);
|
||||
} catch( InterruptedException ex ) {
|
||||
} finally {
|
||||
quit(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -408,8 +482,8 @@ public class Zeichenmaschine extends Constants {
|
||||
frame.setResizable(false); // Should be set anyway
|
||||
displayDevice.setFullScreenWindow(frame);
|
||||
// Update width / height
|
||||
initialWidth = width;
|
||||
initialHeight = height;
|
||||
initialWidth = canvasWidth;
|
||||
initialHeight = canvasHeight;
|
||||
changeSize(screenWidth, screenHeight);
|
||||
// Register ESC as exit fullscreen
|
||||
canvas.addKeyListener(fullscreenExitListener);
|
||||
@@ -531,6 +605,7 @@ public class Zeichenmaschine extends Constants {
|
||||
* 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
|
||||
@@ -594,11 +669,11 @@ public class Zeichenmaschine extends Constants {
|
||||
* @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);
|
||||
canvasWidth = Math.min(Math.max(newWidth, 100), screenWidth);
|
||||
canvasHeight = Math.min(Math.max(newHeight, 100), screenHeight);
|
||||
|
||||
if( canvas != null ) {
|
||||
canvas.setSize(width, height);
|
||||
canvas.setSize(canvasWidth, canvasHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,7 +703,7 @@ public class Zeichenmaschine extends Constants {
|
||||
* @return Die Breite der {@link Zeichenleinwand}.
|
||||
*/
|
||||
public final int getWidth() {
|
||||
return width;
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -637,7 +712,7 @@ public class Zeichenmaschine extends Constants {
|
||||
* @return Die Höhe der {@link Zeichenleinwand}.
|
||||
*/
|
||||
public final int getHeight() {
|
||||
return height;
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,7 +792,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 +886,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 +910,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 +952,13 @@ public class Zeichenmaschine extends Constants {
|
||||
return;
|
||||
}
|
||||
|
||||
if( state != Options.AppState.RUNNING ) {
|
||||
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 ) {
|
||||
// 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.
|
||||
@@ -1032,8 +1112,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 +1126,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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1165,7 +1244,7 @@ public class Zeichenmaschine extends Constants {
|
||||
break;
|
||||
case MouseEvent.MOUSE_RELEASED:
|
||||
mousePressed = false;
|
||||
mouseButton = NOBUTTON;
|
||||
mouseButton = NOMOUSE;
|
||||
mousePressed(evt);
|
||||
break;
|
||||
case MouseEvent.MOUSE_DRAGGED:
|
||||
@@ -1276,8 +1355,40 @@ 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.tasks.FrameSynchronizedTask} implementiert eine
|
||||
* {@link schule.ngb.zm.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
|
||||
@@ -1285,10 +1396,14 @@ public class Zeichenmaschine extends Constants {
|
||||
delay(1);
|
||||
}
|
||||
|
||||
// ThreadExecutor for the update/draw Thread
|
||||
final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor();
|
||||
|
||||
// start of thread in ms
|
||||
final long start = System.currentTimeMillis();
|
||||
// current time in ns
|
||||
long beforeTime = System.nanoTime();
|
||||
long updateBeforeTime = System.nanoTime();
|
||||
// store for deltas
|
||||
long overslept = 0L;
|
||||
// internal counters for tick and runtime
|
||||
@@ -1304,32 +1419,72 @@ public class Zeichenmaschine extends Constants {
|
||||
state = Options.AppState.RUNNING;
|
||||
while( running ) {
|
||||
// delta in seconds
|
||||
delta = (System.nanoTime() - beforeTime) / 1000000000.0;
|
||||
beforeTime = System.nanoTime();
|
||||
|
||||
saveMousePosition(mouseEvent);
|
||||
|
||||
if( state != Options.AppState.PAUSED ) {
|
||||
handleUpdate(delta);
|
||||
handleDraw();
|
||||
//handleUpdate(delta);
|
||||
//handleDraw();
|
||||
|
||||
// Update and draw are executed in a new thread,
|
||||
// but we wait for them to finish unless the user
|
||||
// did call any blocking method, that would also block
|
||||
// rendering of new frames.
|
||||
if( !updateThreadExecutor.isRunning() ) {
|
||||
delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0;
|
||||
updateBeforeTime = System.nanoTime();
|
||||
|
||||
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.IDLE;
|
||||
// Send latest input events after finishing draw
|
||||
// since these may also block
|
||||
dispatchEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for the update/draw Thread to finish
|
||||
while( updateThreadExecutor.isRunning()
|
||||
&& !updateThreadExecutor.isWaiting() ) {
|
||||
Thread.yield();
|
||||
}
|
||||
|
||||
// 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,19 +1497,24 @@ 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;
|
||||
}
|
||||
}
|
||||
// Shutdown the updateThreads
|
||||
updateThreadExecutor.shutdownNow();
|
||||
state = Options.AppState.STOPPED;
|
||||
|
||||
// Cleanup
|
||||
teardown();
|
||||
cleanup();
|
||||
state = Options.AppState.TERMINATED;
|
||||
@@ -1368,6 +1528,7 @@ public class Zeichenmaschine extends Constants {
|
||||
if( state == Options.AppState.RUNNING ) {
|
||||
state = Options.AppState.UPDATING;
|
||||
update(delta);
|
||||
canvas.updateLayers(delta);
|
||||
state = Options.AppState.RUNNING;
|
||||
}
|
||||
}
|
||||
@@ -1382,6 +1543,7 @@ public class Zeichenmaschine extends Constants {
|
||||
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
class DelayedTask implements Delayed {
|
||||
|
||||
long startTime; // in ms
|
||||
@@ -1408,7 +1570,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 +1633,60 @@ public class Zeichenmaschine extends Constants {
|
||||
|
||||
}
|
||||
|
||||
class UpdateThreadExecutor extends ThreadPoolExecutor {
|
||||
|
||||
private Thread updateThread;
|
||||
|
||||
private boolean running = false, waiting = false;
|
||||
|
||||
public UpdateThreadExecutor() {
|
||||
super(1, 1, 0L,
|
||||
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
|
||||
updateState = Options.AppState.IDLE;
|
||||
}
|
||||
|
||||
@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;
|
||||
running = true;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final Log LOG = Log.getLogger(Zeichenmaschine.class);
|
||||
|
||||
}
|
||||
149
src/main/java/schule/ngb/zm/anim/Animation.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import schule.ngb.zm.Constants;
|
||||
import schule.ngb.zm.Updatable;
|
||||
import schule.ngb.zm.events.EventDispatcher;
|
||||
import schule.ngb.zm.tasks.FrameSynchronizedTask;
|
||||
import schule.ngb.zm.tasks.TaskRunner;
|
||||
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
public abstract class Animation<T> implements Updatable {
|
||||
|
||||
protected int runtime;
|
||||
|
||||
protected int elapsed_time = 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 = easing;
|
||||
}
|
||||
|
||||
public Animation( int runtime ) {
|
||||
this.runtime = runtime;
|
||||
this.easing = Constants.DEFAULT_EASING;
|
||||
}
|
||||
|
||||
public Animation( int runtime, DoubleUnaryOperator easing ) {
|
||||
this.runtime = runtime;
|
||||
this.easing = easing;
|
||||
}
|
||||
|
||||
public int getRuntime() {
|
||||
return runtime;
|
||||
}
|
||||
|
||||
public void setRuntime( int pRuntime ) {
|
||||
this.runtime = pRuntime;
|
||||
}
|
||||
|
||||
public DoubleUnaryOperator getEasing() {
|
||||
return easing;
|
||||
}
|
||||
|
||||
public void setEasing( DoubleUnaryOperator pEasing ) {
|
||||
this.easing = pEasing;
|
||||
}
|
||||
|
||||
public abstract T getAnimationTarget();
|
||||
|
||||
public final void start() {
|
||||
this.initialize();
|
||||
elapsed_time = 0;
|
||||
running = true;
|
||||
finished = false;
|
||||
interpolate(easing.applyAsDouble(0.0));
|
||||
initializeEventDispatcher().dispatchEvent("start", this);
|
||||
}
|
||||
|
||||
public final void stop() {
|
||||
running = false;
|
||||
// Make sure the last animation frame was interpolated correctly
|
||||
interpolate(easing.applyAsDouble((double) elapsed_time / (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 ) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch( InterruptedException ex ) {
|
||||
// Keep waiting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
elapsed_time += (int) (delta * 1000);
|
||||
if( elapsed_time > runtime )
|
||||
elapsed_time = runtime;
|
||||
|
||||
double t = (double) elapsed_time / (double) runtime;
|
||||
if( t >= 1.0 ) {
|
||||
running = false;
|
||||
stop();
|
||||
} else {
|
||||
interpolate(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
|
||||
*/
|
||||
public abstract void interpolate( 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);
|
||||
}
|
||||
|
||||
}
|
||||
36
src/main/java/schule/ngb/zm/anim/AnimationFacade.java
Normal 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 interpolate( double e ) {
|
||||
anim.interpolate(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
anim.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
anim.finish();
|
||||
}
|
||||
|
||||
}
|
||||
76
src/main/java/schule/ngb/zm/anim/AnimationGroup.java
Normal file
@@ -0,0 +1,76 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import schule.ngb.zm.shapes.Shape;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
public class AnimationGroup extends Animation<Shape> {
|
||||
|
||||
Animation<? extends Shape>[] anims;
|
||||
|
||||
private boolean overrideRuntime = false;
|
||||
|
||||
|
||||
public AnimationGroup( DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) {
|
||||
super(easing);
|
||||
this.anims = anims;
|
||||
|
||||
int maxRuntime = Arrays.stream(this.anims).mapToInt((a) -> a.getRuntime()).reduce(0, Integer::max);
|
||||
setRuntime(maxRuntime);
|
||||
}
|
||||
|
||||
public AnimationGroup( int runtime, DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) {
|
||||
super(runtime, easing);
|
||||
this.anims = anims;
|
||||
overrideRuntime = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Shape getAnimationTarget() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update( double delta ) {
|
||||
if( overrideRuntime ) {
|
||||
synchronized( anims ) {
|
||||
for( Animation<? extends Shape> anim: anims ) {
|
||||
if( anim.isActive() ) {
|
||||
anim.update(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.update(delta);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interpolate( double e ) {
|
||||
synchronized( anims ) {
|
||||
for( Animation<? extends Shape> anim: anims ) {
|
||||
anim.interpolate(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
synchronized( anims ) {
|
||||
for( Animation<? extends Shape> anim: anims ) {
|
||||
anim.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
synchronized( anims ) {
|
||||
for( Animation<? extends Shape> anim: anims ) {
|
||||
anim.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/main/java/schule/ngb/zm/anim/AnimationListener.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import schule.ngb.zm.events.Listener;
|
||||
|
||||
public interface AnimationListener extends Listener<Animation> {
|
||||
|
||||
void animationStarted( Animation anim );
|
||||
|
||||
void animationStopped( Animation anim );
|
||||
|
||||
}
|
||||
230
src/main/java/schule/ngb/zm/anim/Animations.java
Normal file
@@ -0,0 +1,230 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
import schule.ngb.zm.Color;
|
||||
import schule.ngb.zm.Constants;
|
||||
import schule.ngb.zm.Vector;
|
||||
import schule.ngb.zm.tasks.FrameSynchronizedTask;
|
||||
import schule.ngb.zm.tasks.FramerateLimitedTask;
|
||||
import schule.ngb.zm.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.*;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final <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 != null && getter.getReturnType().equals(propType) ) {
|
||||
return (R) getter.invoke(target);
|
||||
} else {
|
||||
throw new NoSuchMethodException(String.format("No getter for property <%s> found.", propName));
|
||||
}
|
||||
}
|
||||
|
||||
private static final <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 != null && 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 final 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 animate(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 animate(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 animate(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 animate(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
|
||||
}
|
||||
|
||||
|
||||
public static final <T, R> Future<T> animate( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, BiConsumer<T, R> applicator ) {
|
||||
return animate(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e)));
|
||||
}
|
||||
|
||||
public static final <T> Future<T> animate( 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 animateAndWait( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
|
||||
Future<T> future = animate(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>> animate( Animation<T> animation ) {
|
||||
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> animateAndWait( Animation<T> animation ) {
|
||||
Future<Animation<T>> future = animate(animation);
|
||||
animation.await();
|
||||
return animation;
|
||||
}
|
||||
|
||||
public static <T> Future<Animation<T>> animate( 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);
|
||||
|
||||
}
|
||||
11
src/main/java/schule/ngb/zm/anim/Animator.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package schule.ngb.zm.anim;
|
||||
|
||||
public interface Animator<T, R> {
|
||||
|
||||
double easing(double t);
|
||||
|
||||
R interpolator(double e);
|
||||
|
||||
void applicator(T target, R value);
|
||||
|
||||
}
|
||||
320
src/main/java/schule/ngb/zm/anim/Easing.java
Normal 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() {
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/java/schule/ngb/zm/anim/FadeAnimation.java
Normal 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 interpolate( double e ) {
|
||||
object.setFillColor(new Color(fill, (int) Constants.interpolate(fillAlpha, tAlpha, e)));
|
||||
object.setStrokeColor(new Color(stroke, (int) Constants.interpolate(strokeAlpha, tAlpha, e)));
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/schule/ngb/zm/anim/FillAnimation.java
Normal 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 interpolate( double e ) {
|
||||
object.setFillColor(Color.interpolate(oFill, tFill, e));
|
||||
}
|
||||
|
||||
}
|
||||
53
src/main/java/schule/ngb/zm/anim/MorphAnimation.java
Normal 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 interpolate( 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/main/java/schule/ngb/zm/anim/MoveAnimation.java
Normal 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 interpolate( double e ) {
|
||||
object.setX(Constants.interpolate(oX, tX, e));
|
||||
object.setY(Constants.interpolate(oY, tY, e));
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/schule/ngb/zm/anim/RotateAnimation.java
Normal 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 interpolate( double e ) {
|
||||
object.rotateTo(Constants.interpolate(oA, tA, e));
|
||||
}
|
||||
|
||||
}
|
||||
32
src/main/java/schule/ngb/zm/anim/StrokeAnimation.java
Normal 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 interpolate( double e ) {
|
||||
object.setStrokeColor(Color.interpolate(oFill, tFill, e));
|
||||
}
|
||||
|
||||
}
|
||||
58
src/main/java/schule/ngb/zm/events/EventDispatcher.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package schule.ngb.zm.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
7
src/main/java/schule/ngb/zm/events/Listener.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package schule.ngb.zm.events;
|
||||
|
||||
public interface Listener<E> {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
11
src/main/java/schule/ngb/zm/media/AudioListener.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package schule.ngb.zm.media;
|
||||
|
||||
import schule.ngb.zm.events.Listener;
|
||||
|
||||
public interface AudioListener extends Listener<Audio> {
|
||||
|
||||
void start( Audio source );
|
||||
|
||||
void stop( Audio source );
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
306
src/main/java/schule/ngb/zm/media/Music.java
Normal file
@@ -0,0 +1,306 @@
|
||||
package schule.ngb.zm.media;
|
||||
|
||||
import schule.ngb.zm.anim.Animation;
|
||||
import schule.ngb.zm.anim.AnimationListener;
|
||||
import schule.ngb.zm.events.EventDispatcher;
|
||||
import schule.ngb.zm.tasks.TaskRunner;
|
||||
import schule.ngb.zm.util.Log;
|
||||
import schule.ngb.zm.util.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 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 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);
|
||||
|
||||
}
|
||||
313
src/main/java/schule/ngb/zm/media/Sound.java
Normal file
@@ -0,0 +1,313 @@
|
||||
package schule.ngb.zm.media;
|
||||
|
||||
import schule.ngb.zm.util.Log;
|
||||
import schule.ngb.zm.util.ResourceStreamProvider;
|
||||
import schule.ngb.zm.util.Validator;
|
||||
|
||||
import javax.sound.sampled.*;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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 void dispose() {
|
||||
if( audioClip != null ) {
|
||||
if( audioClip.isRunning() ) {
|
||||
audioClip.stop();
|
||||
}
|
||||
audioClip.close();
|
||||
audioClip = null;
|
||||
}
|
||||
}
|
||||
|
||||
private 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);
|
||||
|
||||
}
|
||||
183
src/main/java/schule/ngb/zm/ml/MLMath.java
Normal 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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
82
src/main/java/schule/ngb/zm/ml/Matrix.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package schule.ngb.zm.ml;
|
||||
|
||||
import schule.ngb.zm.Constants;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
// TODO: Move Math into Matrix class
|
||||
// TODO: Implement support for optional sci libs
|
||||
public class Matrix {
|
||||
|
||||
private int columns, rows;
|
||||
|
||||
double[][] coefficients;
|
||||
|
||||
public Matrix( int rows, int cols ) {
|
||||
this.rows = rows;
|
||||
this.columns = cols;
|
||||
coefficients = new double[rows][cols];
|
||||
}
|
||||
|
||||
public Matrix( double[][] coefficients ) {
|
||||
this.coefficients = coefficients;
|
||||
this.rows = coefficients.length;
|
||||
this.columns = coefficients[0].length;
|
||||
}
|
||||
|
||||
public int getColumns() {
|
||||
return columns;
|
||||
}
|
||||
|
||||
public int getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public double[][] getCoefficients() {
|
||||
return coefficients;
|
||||
}
|
||||
|
||||
public double get( int row, int col ) {
|
||||
return coefficients[row][col];
|
||||
}
|
||||
|
||||
public void initializeRandom() {
|
||||
coefficients = MLMath.matrixApply(coefficients, (d) -> Constants.randomGaussian());
|
||||
}
|
||||
|
||||
public void initializeRandom( double lower, double upper ) {
|
||||
coefficients = MLMath.matrixApply(coefficients, (d) -> ((upper-lower) * (Constants.randomGaussian()+1) * .5) + lower);
|
||||
}
|
||||
|
||||
public void initializeIdentity() {
|
||||
initializeZero();
|
||||
for( int i = 0; i < Math.min(rows, columns); i++ ) {
|
||||
this.coefficients[i][i] = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeOne() {
|
||||
coefficients = MLMath.matrixApply(coefficients, (d) -> 1.0);
|
||||
}
|
||||
|
||||
public void initializeZero() {
|
||||
coefficients = MLMath.matrixApply(coefficients, (d) -> 0.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
//return Arrays.deepToString(coefficients);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('[');
|
||||
sb.append('\n');
|
||||
for( int i = 0; i < coefficients.length; i++ ) {
|
||||
sb.append('\t');
|
||||
sb.append(Arrays.toString(coefficients[i]));
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(']');
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
}
|
||||
194
src/main/java/schule/ngb/zm/ml/NeuralNetwork.java
Normal file
@@ -0,0 +1,194 @@
|
||||
package schule.ngb.zm.ml;
|
||||
|
||||
import schule.ngb.zm.util.Log;
|
||||
import schule.ngb.zm.util.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.coefficients[i][j]);
|
||||
out.print(' ');
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
|
||||
out.print(layer.biases[j]);
|
||||
out.print(' ');
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
out.flush();
|
||||
} catch( IOException ex ) {
|
||||
LOG.warn(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.coefficients[i][j] = Double.parseDouble(split[j]);
|
||||
}
|
||||
}
|
||||
// Load Biases
|
||||
split = in.readLine().split(" ");
|
||||
for( int j = 0; j < neurons; j++ ) {
|
||||
layer.biases[j] = Double.parseDouble(split[j]);
|
||||
}
|
||||
|
||||
layers.add(layer);
|
||||
}
|
||||
|
||||
return new NeuralNetwork(layers);
|
||||
} catch( IOException | NoSuchElementException ex ) {
|
||||
LOG.warn(ex, "Could not load neural network from source <%s>", source);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*public static NeuralNetwork loadFromFile( String source ) {
|
||||
try(
|
||||
InputStream stream = ResourceStreamProvider.getInputStream(source);
|
||||
Scanner in = new Scanner(stream)
|
||||
) {
|
||||
List<NeuronLayer> layers = new LinkedList<>();
|
||||
while( in.hasNext() ) {
|
||||
int neurons = in.nextInt();
|
||||
int inputs = in.nextInt();
|
||||
|
||||
NeuronLayer layer = new NeuronLayer(neurons, inputs);
|
||||
for( int i = 0; i < inputs; i++ ) {
|
||||
for( int j = 0; j < neurons; j++ ) {
|
||||
layer.weights.coefficients[i][j] = in.nextDouble();
|
||||
}
|
||||
}
|
||||
for( int j = 0; j < neurons; j++ ) {
|
||||
layer.biases[j] = in.nextDouble();
|
||||
}
|
||||
|
||||
layers.add(layer);
|
||||
}
|
||||
|
||||
return new NeuralNetwork(layers);
|
||||
} catch( IOException | NoSuchElementException ex ) {
|
||||
LOG.warn(ex, "Could not load neural network from source <%s>", source);
|
||||
}
|
||||
return null;
|
||||
}*/
|
||||
|
||||
private NeuronLayer[] layers;
|
||||
|
||||
private double[][] 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 double[][] getOutput() {
|
||||
return output;
|
||||
}
|
||||
|
||||
public double[][] predict( double[][] inputs ) {
|
||||
//this.output = layers[1].apply(layers[0].apply(inputs));
|
||||
this.output = layers[0].apply(inputs);
|
||||
return this.output;
|
||||
}
|
||||
|
||||
public void learn( double[][] 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);
|
||||
|
||||
}
|
||||
167
src/main/java/schule/ngb/zm/ml/NeuronLayer.java
Normal file
@@ -0,0 +1,167 @@
|
||||
package schule.ngb.zm.ml;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class NeuronLayer implements Function<double[][], double[][]> {
|
||||
|
||||
public static NeuronLayer fromArray( double[][] weights ) {
|
||||
NeuronLayer 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++ ) {
|
||||
layer.weights.coefficients[i][j] = weights[i][j];
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
|
||||
public static NeuronLayer fromArray( double[][] weights, double[] biases ) {
|
||||
NeuronLayer 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++ ) {
|
||||
layer.weights.coefficients[i][j] = weights[i][j];
|
||||
}
|
||||
}
|
||||
for( int j = 0; j < biases.length; j++ ) {
|
||||
layer.biases[j] = biases[j];
|
||||
}
|
||||
return layer;
|
||||
}
|
||||
|
||||
Matrix weights;
|
||||
|
||||
double[] biases;
|
||||
|
||||
NeuronLayer previous, next;
|
||||
|
||||
DoubleUnaryOperator activationFunction, activationFunctionDerivative;
|
||||
|
||||
double[][] lastOutput, lastInput;
|
||||
|
||||
public NeuronLayer( int neurons, int inputs ) {
|
||||
weights = new Matrix(inputs, neurons);
|
||||
weights.initializeRandom(-1, 1);
|
||||
|
||||
biases = new double[neurons];
|
||||
Arrays.fill(biases, 0.0); // TODO: Random?
|
||||
|
||||
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 Matrix getWeights() {
|
||||
return weights;
|
||||
}
|
||||
|
||||
public int getNeuronCount() {
|
||||
return weights.coefficients[0].length;
|
||||
}
|
||||
|
||||
public int getInputCount() {
|
||||
return weights.coefficients.length;
|
||||
}
|
||||
|
||||
public double[][] getLastOutput() {
|
||||
return lastOutput;
|
||||
}
|
||||
|
||||
public void setWeights( double[][] newWeights ) {
|
||||
weights.coefficients = MLMath.copyMatrix(newWeights);
|
||||
}
|
||||
|
||||
public void adjustWeights( double[][] adjustment ) {
|
||||
weights.coefficients = MLMath.matrixAdd(weights.coefficients, adjustment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return weights.toString() + "\n" + Arrays.toString(biases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double[][] apply( double[][] inputs ) {
|
||||
lastInput = inputs;
|
||||
lastOutput = MLMath.matrixApply(
|
||||
MLMath.biasAdd(
|
||||
MLMath.matrixMultiply(inputs, weights.coefficients),
|
||||
biases
|
||||
),
|
||||
activationFunction
|
||||
);
|
||||
if( next != null ) {
|
||||
return next.apply(lastOutput);
|
||||
} else {
|
||||
return lastOutput;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> Function<V, double[][]> compose( Function<? super V, ? extends double[][]> before ) {
|
||||
return ( in ) -> apply(before.apply(in));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> Function<double[][], V> andThen( Function<? super double[][], ? extends V> after ) {
|
||||
return ( in ) -> after.apply(apply(in));
|
||||
}
|
||||
|
||||
public void backprop( double[][] expected, double learningRate ) {
|
||||
double[][] error, delta, adjustment;
|
||||
if( next == null ) {
|
||||
error = MLMath.matrixSub(expected, this.lastOutput);
|
||||
} else {
|
||||
error = MLMath.matrixMultiply(expected, MLMath.matrixTranspose(next.weights.coefficients));
|
||||
}
|
||||
|
||||
delta = MLMath.matrixScale(error, MLMath.matrixApply(this.lastOutput, 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(delta, learningRate);
|
||||
}
|
||||
|
||||
biases = MLMath.biasAdjust(biases, MLMath.matrixApply(delta, ( x ) -> learningRate * x));
|
||||
|
||||
adjustment = MLMath.matrixMultiply(MLMath.matrixTranspose(lastInput), delta);
|
||||
adjustment = MLMath.matrixApply(adjustment, ( x ) -> learningRate * x);
|
||||
this.adjustWeights(adjustment);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -168,8 +168,7 @@ public abstract class Shape extends FilledShape {
|
||||
* <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
|
||||
* dem Aufruf {@code super.copyFrom(shape)} sollten die Basiseigenschaften
|
||||
* kopiert werden.
|
||||
*
|
||||
* @param shape
|
||||
@@ -222,7 +221,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 +242,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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,25 +1,35 @@
|
||||
package schule.ngb.zm.shapes;
|
||||
|
||||
import schule.ngb.zm.Layer;
|
||||
import schule.ngb.zm.anim.Animation;
|
||||
import schule.ngb.zm.anim.AnimationFacade;
|
||||
import schule.ngb.zm.anim.Easing;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.DoubleUnaryOperator;
|
||||
|
||||
public class ShapesLayer extends Layer {
|
||||
|
||||
protected boolean clearBeforeDraw = true;
|
||||
|
||||
private LinkedList<Shape> shapes;
|
||||
private List<Shape> shapes;
|
||||
|
||||
private List<Animation<? extends Shape>> animations;
|
||||
|
||||
public ShapesLayer() {
|
||||
super();
|
||||
shapes = new LinkedList<Shape>();
|
||||
shapes = new LinkedList<>();
|
||||
animations = new LinkedList<>();
|
||||
}
|
||||
|
||||
public ShapesLayer( int width, int height ) {
|
||||
super(width, height);
|
||||
shapes = new LinkedList<Shape>();
|
||||
shapes = new LinkedList<>();
|
||||
animations = new LinkedList<>();
|
||||
}
|
||||
|
||||
public Shape getShape( int index ) {
|
||||
@@ -103,6 +113,33 @@ public class ShapesLayer extends Layer {
|
||||
}
|
||||
}
|
||||
|
||||
public <S extends Shape> void play( Animation<S> anim ) {
|
||||
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 ) {
|
||||
@@ -4,8 +4,14 @@ import schule.ngb.zm.Color;
|
||||
import schule.ngb.zm.Constants;
|
||||
import schule.ngb.zm.Drawable;
|
||||
import schule.ngb.zm.Options;
|
||||
import schule.ngb.zm.util.Noise;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.FlatteningPathIterator;
|
||||
import java.awt.geom.GeneralPath;
|
||||
import java.awt.geom.PathIterator;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public abstract class StrokedShape extends Constants implements Drawable {
|
||||
@@ -26,7 +32,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
|
||||
this.strokeColor = color;
|
||||
}
|
||||
|
||||
public void setStrokeColor( Color color , int alpha ) {
|
||||
public void setStrokeColor( Color color, int alpha ) {
|
||||
setStrokeColor(new Color(color, alpha));
|
||||
}
|
||||
|
||||
@@ -66,10 +72,11 @@ public abstract class StrokedShape extends Constants implements Drawable {
|
||||
/**
|
||||
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
|
||||
* {@link #DOTTED} und {@link #SOLID}.
|
||||
*
|
||||
* @param type
|
||||
*/
|
||||
public void setStrokeType( Options.StrokeType type ) {
|
||||
this.strokeType = DASHED;
|
||||
this.strokeType = type;
|
||||
this.stroke = null;
|
||||
}
|
||||
|
||||
@@ -78,9 +85,11 @@ public abstract class StrokedShape extends Constants implements Drawable {
|
||||
|
||||
/**
|
||||
* Erstellt ein {@link Stroke} Objekt für den Konturtyp.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
protected Stroke createStroke() {
|
||||
// TODO: Used global cached Stroke Objects?
|
||||
if( stroke == null ) {
|
||||
switch( strokeType ) {
|
||||
case DOTTED:
|
||||
69
src/main/java/schule/ngb/zm/tasks/DelayedTask.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package schule.ngb.zm.tasks;
|
||||
|
||||
import schule.ngb.zm.Zeichenmaschine;
|
||||
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.FutureTask;
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
64
src/main/java/schule/ngb/zm/tasks/FrameSynchronizedTask.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package schule.ngb.zm.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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
11
src/main/java/schule/ngb/zm/tasks/FramerateLimitedTask.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package schule.ngb.zm.tasks;
|
||||
|
||||
import schule.ngb.zm.Constants;
|
||||
|
||||
public abstract class FramerateLimitedTask extends RateLimitedTask {
|
||||
|
||||
public int getRate() {
|
||||
return Constants.framesPerSecond;
|
||||
}
|
||||
|
||||
}
|
||||
57
src/main/java/schule/ngb/zm/tasks/RateLimitedTask.java
Normal file
@@ -0,0 +1,57 @@
|
||||
package schule.ngb.zm.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();
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/schule/ngb/zm/tasks/Task.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package schule.ngb.zm.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() {
|
||||
}
|
||||
|
||||
}
|
||||
101
src/main/java/schule/ngb/zm/tasks/TaskRunner.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package schule.ngb.zm.tasks;
|
||||
|
||||
import schule.ngb.zm.util.Log;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
145
src/main/java/schule/ngb/zm/util/Counter.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
107
src/main/java/schule/ngb/zm/util/FileLoader.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package schule.ngb.zm.util;
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ 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 ) {
|
||||
@@ -13,11 +13,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 +27,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 +45,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 +68,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);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package schule.ngb.zm.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Supplier;
|
||||
@@ -30,6 +36,10 @@ public final class Log {
|
||||
|
||||
private static final String ROOT_LOGGER = "schule.ngb.zm";
|
||||
|
||||
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 +53,7 @@ public final class Log {
|
||||
*
|
||||
* @see #enableGlobalLevel(Level)
|
||||
*/
|
||||
public static final void enableGlobalDebugging() {
|
||||
public static void enableGlobalDebugging() {
|
||||
enableGlobalLevel(ALL);
|
||||
}
|
||||
|
||||
@@ -68,32 +78,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();
|
||||
ensureRootLoggerIntialized();
|
||||
|
||||
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 +113,42 @@ 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 ) {
|
||||
ensureRootLoggerIntialized();
|
||||
return new Log(clazz);
|
||||
}
|
||||
|
||||
private static void ensureRootLoggerIntialized() {
|
||||
if( LOGGING_INIT ) {
|
||||
return;
|
||||
}
|
||||
|
||||
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()));
|
||||
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 +337,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
366
src/main/java/schule/ngb/zm/util/Noise.java
Normal 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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package schule.ngb.zm.util;
|
||||
import schule.ngb.zm.Zeichenmaschine;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogManager;
|
||||
import java.util.logging.Logger;
|
||||
import java.nio.file.Files;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.
|
||||
@@ -50,15 +50,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 +68,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 +87,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 +127,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 +150,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() {
|
||||
81
src/main/java/schule/ngb/zm/util/Timer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |