1 Commits

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

16
.gitignore vendored
View File

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

View File

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

View File

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

Binary file not shown.

View File

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

234
gradlew vendored
View File

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

89
gradlew.bat vendored
View File

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

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.shapes.Circle;
import schule.ngb.zm.shapes.Ellipse;
import schule.ngb.zm.shapes.Rectangle;
import schule.ngb.zm.shapes.Shape;
import java.util.function.DoubleUnaryOperator;
public class MoveAnimation extends Animation<Shape> {
private Shape object;
private double oX, oY, tX, tY;
public MoveAnimation( Shape object, double x, double y, int runtime, DoubleUnaryOperator easing ) {
super(runtime, easing);
this.object = object;
oX = object.getX();
oY = object.getY();
tX = x;
tY = y;
}
@Override
public Shape getAnimationTarget() {
return object;
}
@Override
public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e));
object.setY(Constants.interpolate(oY, tY, e));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
package schule.ngb.zm;
public class ColorLayer extends Layer {
private Color background;
public ColorLayer( Color color ) {
this.background = color;
clear();
}
public ColorLayer( int width, int height, Color color ) {
super(width, height);
this.background = color;
clear();
}
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
clear();
}
public Color getColor() {
return background;
}
public void setColor( Color color ) {
background = color;
clear();
}
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
@Override
public void clear() {
drawing.setColor(background.getJavaColor());
drawing.fillRect(0, 0, getWidth(), getHeight());
}
}

View File

@@ -0,0 +1,31 @@
package schule.ngb.zm;
import java.awt.*;
/**
* Zeichenbare Objekte können auf eine Zeichenfläche gezeichnet werden.
* In der Regel werden sie einmal pro Frame gezeichnet.
*/
public interface Drawable {
/**
* Gibt an, ob das Objekt derzeit sichtbar ist (also gezeichnet werden
* muss).
*
* @return <code>true</code>, wenn das Objekt sichtbar ist.
*/
boolean isVisible();
/**
* Wird aufgerufen, um das Objekt auf die Zeichenfläche <var>graphics</var>
* zu draw.
* <p>
* Das Objekt muss dafür Sorge tragen, dass der Zustand der Zeichenfläche
* (Transformationsmatrix, Farbe, ...) erhalten bleibt. Das Objekt sollte
* also etwaige Änderungen am Ende des Aufrufs wieder rückgängig machen.
*
* @param graphics Die Zeichenfläche.
*/
void draw( Graphics2D graphics );
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
public class GraphicsLayer extends Layer {
public Graphics2D getGraphics() {
return drawing;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
package schule.ngb.zm;
/**
* Aktualisierbare Objekte können in regelmäßigen Intervallen (meist einmal
* pro Frame) ihren Zustand update. Diese Änderung kann abhängig vom
* Zeitintervall (in Sekunden) zum letzten Aufruf passieren.
*/
public interface Updatable {
/**
* Gibt an, ob das Objekt gerade auf Aktualisierungen reagiert.
* @return <code>true</code>, wenn das Objekt aktiv ist.
*/
public boolean isActive();
/**
* Änderung des Zustandes des Objekts abhängig vom Zeitintervall
* <var>delta</var> in Sekunden.
* @param delta Zeitintervall seit dem letzten Aufruf (in Sekunden).
*/
public void update( double delta );
}

View File

@@ -0,0 +1,6 @@
package schule.ngb.zm;
// TODO: Auslagern des Frames in diese Klasse (Trennung Swing-GUI/Canvas, Zeichenmaschine und Drawing-Thread)
public class Zeichenfenster {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package schule.ngb.zm.game;
public class InputContext {
}

View File

@@ -0,0 +1,71 @@
package schule.ngb.zm.game;
import javax.swing.*;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.concurrent.ScheduledThreadPoolExecutor;
public class InputType {
static final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);
public class ActionInput extends InputType {
private int inputCode;
private String action;
private ActionListener al;
private double delay = 0.0;
private long lastTriggered = 0L;
private boolean repeats = false;
private boolean onPress = true;
private boolean pressed = false;
public void press( int inputCode, long time ) {
pressed = true;
if( onPress ) {
trigger(inputCode, time);
}
}
public void release( int inputCode, long time ) {
pressed = false;
if( !onPress ) {
trigger(inputCode, time);
}
}
public void trigger( int inputCode, long time ) {
if( shouldTrigger(inputCode, time) ) {
//al.actionTriggered(action);
lastTriggered = time;
}
}
public boolean shouldTrigger( int inputCode, long time ) {
return this.inputCode == inputCode && time - lastTriggered > delay;
}
public int hashCode() {
return inputCode;
}
}
public class TriggerInput extends InputType {
}
public class RangeInput extends InputType {
}
}

View File

@@ -5,8 +5,6 @@ package schule.ngb.zm.media;
*/ */
public interface Audio { public interface Audio {
String getSource();
/** /**
* Prüft, ob das Medium gerade abgespielt wird. * Prüft, ob das Medium gerade abgespielt wird.
* *
@@ -20,8 +18,8 @@ public interface Audio {
* {@code isLooping() == true}, dann muss auch immer * {@code isLooping() == true}, dann muss auch immer
* {@code isPlaying() == true} gelten. * {@code isPlaying() == true} gelten.
* *
* @return {@code true}, wenn das Medium in einer Schleife abgespielt wird, * @return @return {@code true}, wenn das Medium in einer Schleife
* {@code false} sonst. * abgespielt wird, {@code false} sonst.
*/ */
boolean isLooping(); boolean isLooping();
@@ -38,28 +36,17 @@ public interface Audio {
*/ */
void setVolume( double volume ); 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 * Startet die Wiedergabe des Mediums und beendet die Methode. Das
* Audio-Medium wird einmal abgespielt und stoppt dann. * Audio-Medium wird einmal abgespielt und stoppt dann.
*/ */
void play(); void playOnce();
/** /**
* Startet die Wiedergabe des Mediums und blockiert das Programm, bis die * Startet die Wiedergabe des Mediums und blockiert das Programm, bis die
* Wiedergabe beendet ist. * Wiedergabe beendet ist.
*/ */
void playAndWait(); void playOnceAndWait();
/** /**
* Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode * Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm.media; package schule.ngb.zm.media;
import schule.ngb.zm.Constants; import schule.ngb.zm.Constants;
import schule.ngb.zm.util.tasks.TaskRunner; import schule.ngb.zm.tasks.TaskRunner;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -41,10 +41,6 @@ public class Mixer implements Audio {
this.audios = new ArrayList<>(4); this.audios = new ArrayList<>(4);
} }
public String getSource() {
return "";
}
public void add( Audio pAudio ) { public void add( Audio pAudio ) {
add(pAudio, 1f); add(pAudio, 1f);
} }
@@ -54,60 +50,30 @@ public class Mixer implements Audio {
pAudio.setVolume(pVolumeFactor * volume); pAudio.setVolume(pVolumeFactor * volume);
} }
public void remove( Audio pAudio ) {
}
public void removeAll() {
audios.clear();
}
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isPlaying() { public boolean isPlaying() {
return audios.stream().anyMatch(aw -> aw.audio.isPlaying()); return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public boolean isLooping() { public boolean isLooping() {
return audios.stream().anyMatch(aw -> aw.audio.isLooping()); return audios.stream().anyMatch(aw -> aw.audio.isLooping());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void setVolume( double pVolume ) { public void setVolume( double pVolume ) {
volume = (float) pVolume; volume = (float) pVolume;
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * pVolume)); audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * pVolume));
} }
/**
* {@inheritDoc}
*/
@Override @Override
public double getVolume() { public void playOnce() {
return volume; audios.stream().forEach(aw -> aw.audio.playOnce());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void play() { public void playOnceAndWait() {
audios.stream().forEach(aw -> aw.audio.play()); audios.stream().forEach(aw -> aw.audio.playOnce());
}
/**
* {@inheritDoc}
*/
@Override
public void playAndWait() {
audios.stream().forEach(aw -> aw.audio.play());
while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) { while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) {
try { try {
Thread.sleep(10); Thread.sleep(10);
@@ -117,25 +83,16 @@ public class Mixer implements Audio {
} }
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void loop() { public void loop() {
audios.stream().forEach(aw -> aw.audio.loop()); audios.stream().forEach(aw -> aw.audio.loop());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void stop() { public void stop() {
audios.stream().forEach(aw -> aw.audio.stop()); audios.stream().forEach(aw -> aw.audio.stop());
} }
/**
* {@inheritDoc}
*/
@Override @Override
public void dispose() { public void dispose() {
if( isPlaying() ) { if( isPlaying() ) {
@@ -144,18 +101,6 @@ public class Mixer implements Audio {
audios.stream().forEach(aw -> aw.audio.dispose()); 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 ) { public void fade( final double to, final int time ) {
TaskRunner.run(new Runnable() { TaskRunner.run(new Runnable() {
@Override @Override
@@ -163,6 +108,9 @@ public class Mixer implements Audio {
final long start = System.currentTimeMillis(); final long start = System.currentTimeMillis();
double t = 0.0; double t = 0.0;
double from = volume; double from = volume;
if( !isPlaying() ) {
playOnce();
}
do { do {
setVolume(Constants.interpolate(from, to, t)); setVolume(Constants.interpolate(from, to, t));
t = (double) (System.currentTimeMillis() - start) / (double) time; t = (double) (System.currentTimeMillis() - start) / (double) time;

View File

@@ -0,0 +1,199 @@
package schule.ngb.zm.media;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Music implements Audio {
// size of the byte buffer used to read/write the audio stream
private static final int BUFFER_SIZE = 4096;
private boolean playing = false;
private boolean looping = false;
private String audioSource;
private AudioInputStream audioStream;
private SourceDataLine audioLine;
private float volume = 0.8f;
public Music( String source ) {
this.audioSource = source;
}
@Override
public boolean isPlaying() {
return playing;
}
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
@Override
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioLine != null ) {
applyVolume();
}
}
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
@Override
public void playOnce() {
if( openLine() ) {
TaskRunner.run(new Runnable() {
@Override
public void run() {
stream();
}
});
}
}
@Override
public void playOnceAndWait() {
if( openLine() ) {
stream();
}
}
@Override
public void loop() {
looping = true;
playOnce();
}
@Override
public void stop() {
playing = false;
looping = false;
dispose();
}
@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;
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 ) {
LOGGER.warning("Error while playing Music source <" + audioSource + ">");
LOGGER.throwing("Music", "stream", ex);
}
// Wait for the remaining audio to play
/*while( audioLine.isRunning() ) {
try {
Thread.sleep(10);
} catch( InterruptedException ex ) {
// Just keep waiting ...
}
}*/
playing = false;
streamingStopped();
}
private boolean openLine() {
if( audioLine != null ) {
return true;
}
try {
InputStream in = ResourceStreamProvider.getResourceStream(audioSource);
if( in != null ) {
final AudioInputStream inStream = AudioSystem.getAudioInputStream(in);
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 {
LOGGER.warning("Sound source <" + audioSource + "> could not be played: No audio source found.");
}
} catch( UnsupportedAudioFileException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: The specified audio file is not supported.", ex);
} catch( LineUnavailableException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: Audio line for playing back is unavailable.", ex);
} catch( IOException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: Error playing the audio file.", ex);
}
return false;
}
private void streamingStopped() {
playing = false;
dispose();
if( looping ) {
playOnce();
}
}
//private static final Logger LOGGER = Logger.getLogger("schule.ngb.zm.media.Music");
private static final Logger LOGGER = Logger.getLogger(Music.class.getName());
}

View File

@@ -0,0 +1,204 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
public class Sound implements Audio {
private boolean playing = false;
private boolean looping = false;
private String audioSource;
private Clip audioClip;
private boolean disposeAfterPlay = false;
private float volume = 0.8f;
public Sound( String source ) {
this.audioSource = source;
}
public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning();
return playing;
}
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioClip != null ) {
applyVolume();
}
}
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);
}
public void stop() {
looping = false;
if( audioClip.isRunning() ) {
audioClip.stop();
}
playing = false;
}
public void play() {
if( this.openClip() ) {
audioClip.start();
playing = true;
}
}
public void playOnce() {
disposeAfterPlay = true;
play();
}
public void playOnceAndWait() {
disposeAfterPlay = true;
playAndWait();
}
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();
}
public void loop() {
loop(Clip.LOOP_CONTINUOUSLY);
}
public void loop( int count ) {
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;
}
}
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 {
InputStream in = ResourceStreamProvider.getResourceStream(audioSource);
if( in != null ) {
AudioInputStream audioStream = AudioSystem.getAudioInputStream(in);
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 {
LOGGER.warning("Sound source " + audioSource + " could not be played: No audio source found.");
}
} catch( UnsupportedAudioFileException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: The specified audio file is not supported.");
LOGGER.throwing("Sound", "openClip", ex);
} catch( LineUnavailableException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: Audio line for playing back is unavailable.");
LOGGER.throwing("Sound", "openClip", ex);
} catch( IOException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: Error playing the audio file.");
LOGGER.throwing("Sound", "openClip", ex);
}
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;
}
}
}*/
private void playbackStopped() {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
/*
public void addLineListener( LineListener listener ) {
if( audioClip == null ) {
openClip();
}
if( audioClip != null ) {
audioClip.addLineListener(listener);
}
}
*/
private static final Logger LOGGER = Logger.getLogger(Sound.class.getName());
}

View File

@@ -0,0 +1,45 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
public abstract class FilledShape extends StrokedShape {
protected Color fillColor = STD_FILLCOLOR;
public Color getFillColor() {
return fillColor;
}
public void setFillColor( Color color ) {
fillColor = color;
}
public void setFillColor( Color color, int alpha ) {
setFillColor(new Color(color, alpha));
}
public void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
public void noFill() {
setFillColor(null);
}
public void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
public void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
public void resetFill() {
setFillColor(STD_FILLCOLOR);
}
}

View File

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

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