50 Commits

Author SHA1 Message Date
e2e6f8c291 Bug: Synchronized Methoden verschoben 2022-07-26 18:15:23 +02:00
916a581768 Refactorings 2022-07-26 18:14:59 +02:00
5bb2f75193 Bug: getShapes in ShapeGroup war immer leer 2022-07-26 18:14:50 +02:00
f0e4cd6c80 Refactoring des Beendens der ZM 2022-07-26 18:14:23 +02:00
a228b21c84 Verantwortlichkeiten für Layout und Aufgaben klarer getrennt 2022-07-26 08:59:30 +02:00
68c88ec9ca Merge branch 'main' into zeichenfenster
# Conflicts:
#	src/main/java/schule/ngb/zm/media/Sound.java
2022-07-25 19:07:51 +02:00
0d1dd771dd Logger eingefügt 2022-07-25 19:06:01 +02:00
e995bfc4fe Bug: Spielemaschine blockt nicht mehr nebenläufige Threads 2022-07-25 19:05:54 +02:00
97ff03990a Shape caching entfernt
In Tests konnten keine Geschwindigkeitsvorteile festgestellt werden.
2022-07-25 19:05:28 +02:00
bd2364a8df Laden von Schriftarten mit eigenem Namen möglich 2022-07-25 19:05:04 +02:00
617b915874 Refactorings zur Nebenläufigkeit 2022-07-25 17:45:39 +02:00
7772793e8d Kommentar 2022-07-25 17:44:31 +02:00
bd8c0e37a7 Audio-Methoden synchronisiert 2022-07-25 17:44:22 +02:00
ecbe2b4f6b interpolate zu animate umbenannt
Animation erbt zur Vereinfachung nun auch von Constants und dort gibt es schon eine interpolate Methode.
2022-07-25 17:42:06 +02:00
20fe700756 Rechtschreibung und standard Log-Format 2022-07-25 17:41:18 +02:00
0100a3f574 Methode um mehrere Animationen im ShapesLayer zu starten 2022-07-25 17:41:01 +02:00
aceb79c44f Animate Methode zu play umbenannt 2022-07-25 17:40:42 +02:00
a4e29ccdba Loader KLassen in io Paket verschoben 2022-07-25 17:38:53 +02:00
55014c8eec Klasse Zeichenfenster ausgelagert 2022-07-25 17:35:46 +02:00
4f958cd57c ImageLoaders in io Paket verschoben 2022-07-21 22:01:54 +02:00
04506f6e9c JFrame in eine eigene Klasse ausgelagert 2022-07-21 22:01:38 +02:00
5a27e18634 Javadoc und kleine Refactorings 2022-07-21 21:02:50 +02:00
8b23c658e8 Animator Interface entfernt 2022-07-21 21:02:30 +02:00
1ca13c977a Javadoc 2022-07-21 21:02:10 +02:00
78c93666d0 Javadoc 2022-07-21 21:01:46 +02:00
917eb805c6 Bug: Threadsafety 2022-07-21 21:01:33 +02:00
fddd8d621b flush() nach jeder Log-Nachricht
Der Logger sendet nun nach jedem Log die Nachricht zum OutputStream.
2022-07-21 21:00:55 +02:00
4bf0068051 Icons werden nun in allen Größen geladen
Alle vorhandenen Icons werden geladen und mit Jframe.setIconImages() dem Fenster hinzugefügt. Unter macOS wird nur die Größe 512 geladen und als Dock-Icon gesetzt.
2022-07-21 20:59:28 +02:00
371a962432 Syncronisation des Zeichenthreads mit update/draw über eigenen Zustand
delay() setzt den Zustand auf DELAYED und der Zeichenthread läuft weiter, wenn der update/draw Thread in diesen Zustand wechselt (also delay() aufgerufen wurde). Es wird nicht mehr Thread.getState() geprüft, dies zu unzuverlässi gwar.
2022-07-21 10:54:08 +02:00
99848e47f8 colt abhängigkeit nur für’s kompilieren 2022-07-21 10:52:47 +02:00
f75aaf4b7e Predict-Methode für eine Eingabe 2022-07-21 10:52:19 +02:00
e5c6fa634a Anpassung der Package-Struktur 2022-07-20 17:15:29 +02:00
ccc83414c7 Merge branch 'optional-ml' 2022-07-20 17:09:24 +02:00
16477463d4 java doc und refactorings 2022-07-20 17:09:09 +02:00
d3997561fc Streams durch Schleifen ersetzt
Der Overhead durch die parallelen Streams war zu hoch. Jedenfalls bei den relativ kleinen Matrizen im Test. Bei größeren Matrizen könnte die Parallelität einen Vorteil bringen. Ggf. sollte dies getesett werden und abhängig von der Größe die bestte Methode gewählt werden.
2022-07-19 22:53:46 +02:00
b6b4ffe6a5 Weitere Tests eingefügt und verbessert 2022-07-19 22:52:23 +02:00
bf261b5e9b Colt als optionale Abhängigkeit
DAs Anlernen des NN geht um den Faktor 20 schneller, wenn Colt benutzt wird.
2022-07-19 20:05:37 +02:00
b79f26f51e Matric interface umbenannt 2022-07-19 09:14:00 +02:00
538a8215e6 Userinput wird nach Stopp der ZM weiterverarbeitet 2022-07-19 08:56:00 +02:00
cbda5c3077 Bug: Linearer Farbverlauf wurde nicht korrekt berechnet 2022-07-19 08:55:06 +02:00
2caa528a5e Listeniterationen Threadsafe gemacht 2022-07-18 22:48:28 +02:00
bb50abb7bd Javadoc 2022-07-18 22:48:08 +02:00
38d5f22fb6 Bug: UpdateThreadExecutor blockt nun korrekt den Zeichenthread 2022-07-18 22:47:37 +02:00
d34c60505e Bug: mousePressed wurde nicht ausgelöst 2022-07-18 22:46:48 +02:00
4c8e5c8939 USing Colt library as optional dependency 2022-07-18 11:06:08 +02:00
9a9a714050 Javadoc 2022-07-17 16:38:42 +02:00
f0b064a3d5 Changelog 2022-07-17 15:57:34 +02:00
c922357ab7 Bug behoben: Flackern bei Farbverläufen 2022-07-17 15:57:24 +02:00
17c31a1a03 Bug behoben: delay() funktioniert nun auch nach Stopp der ZM 2022-07-17 15:57:02 +02:00
6551bb75c9 Farbverläufe für Formen und neue Konstantennamen 2022-07-17 15:45:05 +02:00
85 changed files with 3361 additions and 968 deletions

View File

@@ -7,12 +7,15 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Added
- System für EventListener erstellt
- System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.
- Pakete für Animationen und Maschinelles-Lernen hinzugefügt
- 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
@@ -24,7 +27,7 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Changed
- Neue Package-Struktur:
- `schule.ngb.zm.media` für Audio-Klassen (und ggf. zukünftig Video).
- `schule.ngb.zm.tasks` für alles Rund um Parallelität.
- `schule.ngb.zm.util.tasks` für alles Rund um Parallelität.
- `Zeichenthread` und `TaskRunner` setzen die Namen der Threads für besseres Debugging.
### Removed

View File

@@ -28,8 +28,13 @@ dependencies {
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'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {

View File

@@ -1,5 +1,10 @@
package schule.ngb.zm;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
/**
* Repräsentiert eine Farbe in der Zeichenmaschine.
* <p>
@@ -9,7 +14,7 @@ package schule.ngb.zm;
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend).
*/
public class Color {
public class Color implements Paint {
//@formatter:off
@@ -146,7 +151,7 @@ public class Color {
* @param alpha Transparentwert zwischen 0 und 255.
*/
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 & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | ((blue & 0xFF) << 0);
}
/**
@@ -462,6 +467,23 @@ public class Color {
return new java.awt.Color(rgba, true);
}
@Override
public PaintContext createContext( ColorModel cm, Rectangle deviceBounds, Rectangle2D userBounds, AffineTransform xform, RenderingHints hints ) {
return getJavaColor().createContext(cm, deviceBounds, userBounds, xform, hints);
}
@Override
public int getTransparency() {
int alpha = getAlpha();
if( alpha == 0xff ) {
return Transparency.OPAQUE;
} else if( alpha == 0 ) {
return Transparency.BITMASK;
} else {
return Transparency.TRANSLUCENT;
}
}
@Override
/**
* Prüft, ob ein anderes Objekt zu diesem gleich ist.
@@ -473,7 +495,9 @@ public class Color {
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/
public boolean equals( Object obj ) {
if( obj == null ) { return false; }
if( obj == null ) {
return false;
}
if( obj instanceof Color ) {
return ((Color) obj).getRGBA() == this.rgba;
} else if( obj instanceof java.awt.Color ) {

View File

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

@@ -1,7 +1,7 @@
package schule.ngb.zm;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Noise;
import java.awt.Cursor;
@@ -75,42 +75,42 @@ public class Constants {
/**
* Standardbreite eines Zeichenfensters.
*/
public static final int STD_WIDTH = 400;
public static final int DEFAULT_WIDTH = 400;
/**
* Standardhöhe eines Zeichenfensters.
*/
public static final int STD_HEIGHT = 400;
public static final int DEFAULT_HEIGHT = 400;
/**
* Standardwert für die Frames pro Sekunde einer Zeichenmaschine.
*/
public static final int STD_FPS = 60;
public static final int DEFAULT_FPS = 60;
/**
* Standardfarbe der Füllungen.
*/
public static final Color STD_FILLCOLOR = Color.WHITE;
public static Color DEFAULT_FILLCOLOR = Color.WHITE;
/**
* Standardfarbe der Konturen.
*/
public static final Color STD_STROKECOLOR = Color.BLACK;
public static Color DEFAULT_STROKECOLOR = Color.BLACK;
/**
* Standardwert für die Dicke der Konturen.
*/
public static final double STD_STROKEWEIGHT = 1.0;
public static double DEFAULT_STROKEWEIGHT = 1.0;
/**
* Standardwert für die Schriftgröße.
*/
public static final int STD_FONTSIZE = 14;
public static int DEFAULT_FONTSIZE = 14;
/**
* Standardwert für den Abstand von Formen.
*/
public static final int STD_BUFFER = 10;
public static int DEFAULT_BUFFER = 10;
public static int DEFAULT_ANIM_RUNTIME = 1000;
@@ -410,11 +410,12 @@ public class Constants {
* wirken sich auf die aktuelle Zeichenmaschine aus und sollten nur von der
* Zeichenmaschine selbst vorgenommen werden.
*/
// TODO: (ngb) volatile ?
/**
* Aktuell dargestellte Bilder pro Sekunde.
*/
public static int framesPerSecond = STD_FPS;
public static int framesPerSecond = DEFAULT_FPS;
/**
* Anzahl der Ticks (Frames), die das Programm bisher läuft.
@@ -1269,7 +1270,8 @@ public class Constants {
}
/**
* Erzeugt eine Pseudozufallszahl nach einer Gaussverteilung.
* Erzeugt eine Pseudozufallszahl zwischen -1 und 1 nach einer
* Normalverteilung mit Mittelwert 0 und Standardabweichung 1.
*
* @return Eine Zufallszahl.
* @see Random#nextGaussian()

View File

@@ -3,14 +3,29 @@ 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.
* {@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.
*/
@@ -18,7 +33,7 @@ public interface Drawable {
/**
* Wird aufgerufen, um das Objekt auf die Zeichenfläche <var>graphics</var>
* zu draw.
* zu zeichnen.
* <p>
* Das Objekt muss dafür Sorge tragen, dass der Zustand der Zeichenfläche
* (Transformationsmatrix, Farbe, ...) erhalten bleibt. Das Objekt sollte

View File

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

View File

@@ -6,19 +6,45 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
/**
* Basisklasse für Ebenen der {@link Zeichenleinwand}.
* <p>
* Die {@code Zeichenleinwand} besteht aus einer Reihe von Ebenen, die
* übereinandergelegt und von "unten" nach "oben" gezeichnet werden. Die Inhalte
* der oberen Ebenen können also Inhalte der darunterliegenden verdecken.
*
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer {@link Zeichenmaschine}.
* Es werden
* Sie erben von {@code Constants}, damit sie beim
*/
public abstract class Layer extends Constants implements Drawable, Updatable {
/**
* Interner Puffer für die Ebene, der einmal pro Frame auf die
* Zeichenleinwand übertragen wird.
*/
protected BufferedImage buffer;
/**
* Der Grafikkontext der Ebene, der zum Zeichnen der Inhalte verwendet
* wird.
*/
protected Graphics2D drawing;
/**
* Ob die Ebene derzeit sichtbar ist.
*/
protected boolean visible = true;
/**
* Ob die Ebene aktiv ist, also {@link #update(double) Updates} empfangen
* soll.
*/
protected boolean active = true;
public Layer() {
this(STD_WIDTH, STD_HEIGHT);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public Layer( int width, int height ) {
@@ -33,6 +59,12 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
return buffer.getHeight();
}
/**
* Ändert die Größe der Ebene auf die angegebene Größe.
*
* @param width Die neue Breite.
* @param height Die neue Höhe.
*/
public void setSize( int width, int height ) {
if( buffer != null ) {
if( buffer.getWidth() != width || buffer.getHeight() != height ) {
@@ -44,8 +76,13 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Gibt die Resourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
drawing = null;
buffer = null;
}
/**

View File

@@ -41,7 +41,9 @@ public final class Options {
PAUSED,
STOPPED,
TERMINATED,
IDLE
IDLE,
DELAYED,
DISPATCHING
}
/**

View File

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

View File

@@ -1,21 +1,42 @@
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
* {@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.
* @return {@code true}, wenn das Objekt aktiv ist.
* <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
* <var>delta</var> in Sekunden.
* {@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,6 +1,239 @@
package schule.ngb.zm;
// TODO: Auslagern des Frames in diese Klasse (Trennung Swing-GUI/Canvas, Zeichenmaschine und Drawing-Thread)
public class Zeichenfenster {
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,12 +1,16 @@
package schule.ngb.zm;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.Log;
import java.awt.*;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -33,8 +37,8 @@ public class Zeichenleinwand extends Canvas {
*/
public Zeichenleinwand( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(this.getSize());
this.setMinimumSize(this.getSize());
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
// Liste der Ebenen initialisieren und die Standardebenen einfügen
@@ -56,8 +60,8 @@ public class Zeichenleinwand extends Canvas {
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(this.getSize());
this.setMinimumSize(this.getSize());
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
synchronized( layers ) {
for( Layer layer : layers ) {
@@ -195,7 +199,8 @@ public class Zeichenleinwand extends Canvas {
public void updateLayers( double delta ) {
synchronized( layers ) {
for( Layer layer : layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.update(delta);
}
}
@@ -232,7 +237,8 @@ public class Zeichenleinwand extends Canvas {
public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
for( Layer layer : layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
}
}
@@ -256,7 +262,8 @@ public class Zeichenleinwand extends Canvas {
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) {
for( Layer layer : layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
}
}
@@ -277,4 +284,6 @@ public class Zeichenleinwand extends Canvas {
}
}
private static final Log LOG = Log.getLogger(Zeichenleinwand.class);
}

View File

@@ -1,12 +1,14 @@
package schule.ngb.zm;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.shapes.ShapesLayer;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.DrawingLayer;
import schule.ngb.zm.layers.ImageLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.tasks.TaskRunner;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.event.MouseInputListener;
import java.awt.*;
@@ -15,7 +17,6 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.logging.Level;
/**
* Hauptklasse der Zeichenmaschine.
@@ -24,7 +25,7 @@ import java.util.logging.Level;
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der
* nötigen Komponenten.
*/
// TODO: Refactorings (besonders in Bezug auf Nebenläufigkeit)
@SuppressWarnings( "unused" )
public class Zeichenmaschine extends Constants {
/**
@@ -110,47 +111,7 @@ public class Zeichenmaschine extends Constants {
/**
* Das Zeichenfenster der Zeichenmaschine
*/
private JFrame frame;
/**
* Die Graphics-Umgebung für das aktuelle Fenster.
*/
private GraphicsEnvironment environment;
/**
* Das Anzeigefenster, auf dem die ZM gestartet wurde (muss nicht gleich
* dem Aktuellen sein, wenn das Fenster verschoben wurde).
*/
private GraphicsDevice displayDevice;
/**
* Speichert, ob die Zeichenmaschine mit {@link #setFullscreen(boolean)}
* in den Vollbildmodus versetzt wurde.
*/
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;
/**
* {@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 Zeichenfenster frame;
// Aktueller Zustand der Zeichenmaschine.
@@ -169,6 +130,8 @@ public class Zeichenmaschine extends Constants {
*/
private boolean running = false;
private boolean terminateImediately = false;
/**
* Ob die ZM nach dem nächsten Frame pausiert werden soll.
*/
@@ -204,7 +167,7 @@ public class Zeichenmaschine extends Constants {
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
* oder das Zeichenfenster weiter geöffnet bleibt.
*/
private boolean quitAfterTeardown = false;
private boolean quitAfterShutdown = false;
// Mauszeiger
/**
@@ -255,7 +218,7 @@ public class Zeichenmaschine extends Constants {
* @param title Der Titel, der oben im Fenster steht.
*/
public Zeichenmaschine( String title ) {
this(STD_WIDTH, STD_HEIGHT, title, true);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, true);
}
/**
@@ -270,7 +233,7 @@ public class Zeichenmaschine extends Constants {
* Aufruf von {@code draw()}.
*/
public Zeichenmaschine( String title, boolean run_once ) {
this(STD_WIDTH, STD_HEIGHT, title, run_once);
this(DEFAULT_WIDTH, DEFAULT_HEIGHT, title, run_once);
}
/**
@@ -314,75 +277,32 @@ public class Zeichenmaschine extends Constants {
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
// Setzen des "Look&Feel"
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch( Exception ex ) {
LOG.log(Level.SEVERE, "Error setting the look and feel: " + ex.getMessage(), ex);
}
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
// das Zeichenfenster dort zu zentrieren.
java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation();
environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] devices = environment.getScreenDevices();
for( GraphicsDevice gd : devices ) {
if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) {
displayDevice = gd;
break;
}
}
// Keinen passenden Bildschirm gefunden. Wir nutzen den Standard.
if( displayDevice == null ) {
displayDevice = environment.getDefaultScreenDevice();
}
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
this.canvasWidth = width;
this.canvasHeight = height;
java.awt.Rectangle displayBounds = displayDevice.getDefaultConfiguration().getBounds();
this.screenWidth = (int) displayBounds.getWidth();
this.screenHeight = (int) displayBounds.getHeight();
// Erstellen des Zeichenfensters
frame = new JFrame(displayDevice.getDefaultConfiguration());
frame.setTitle(title);
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
// TODO: Add image sizes
ImageIcon icon = new ImageIcon(ImageIO.read(new File("res/icon_64.png")));
if( MACOS ) {
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon.getImage());
} else {
// Kleines Icon des Frames setzen
frame.setIconImage(icon.getImage());
}
} catch( IOException e ) {
}
// Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height);
frame.add(canvas);
// Die drei Standardebenen merken, für den einfachen Zugriff aus unterklassen.
// Erstellen des Zeichenfensters
frame = createFrame(canvas, title);
// Wir kennen nun den Bildschirm und können die Breite / Höhe abrufen.
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
this.canvasWidth = canvasBounds.width;
this.canvasHeight = canvasBounds.height;
java.awt.Rectangle displayBounds = frame.getScreenBounds();
this.screenWidth = displayBounds.width;
this.screenHeight = displayBounds.height;
// Die drei Standardebenen merken, für den einfachen Zugriff aus Unterklassen.
background = getBackgroundLayer();
drawing = getDrawingLayer();
shapes = getShapesLayer();
// FPS setzen
framesPerSecondInternal = STD_FPS;
framesPerSecondInternal = DEFAULT_FPS;
this.run_once = run_once;
// Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen
// verändert werden soll.
// TODO: When to call settings?
// TODO: (ngb) Wann sollte settings() aufgerufen werden?
settings();
// Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören.
@@ -393,30 +313,20 @@ public class Zeichenmaschine extends Constants {
canvas.addKeyListener(inputListener);
// Programm beenden, wenn Fenster geschlossen wird
// TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
if( running ) {
running = false;
teardown();
cleanup();
}
// Give the app a minimum amount of time to shut down
// then kill it.
try {
Thread.sleep(5);
} catch( InterruptedException ex ) {
} finally {
if( isTerminated() ) {
quit(true);
} else {
exitNow();
}
}
});
// Fenster zusammenbauen, auf dem Bildschirm zentrieren ...
frame.pack();
frame.setResizable(false);
centerFrame();
// ... und anzeigen!
// Fenster anzeigen
frame.centerFrame();
frame.setVisible(true);
// Nach dem Anzeigen kann die Pufferstrategie erstellt werden.
@@ -440,25 +350,30 @@ public class Zeichenmaschine extends Constants {
*
* @param title
*/
// TODO: Implement in conjunction with Zeichenfenster
private final Zeichenfenster createFrame( String title ) {
return null;
private final Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
while( frame == null ) {
try {
TaskRunner.invokeLater(new Runnable() {
@Override
public void run() {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}
}).get();
} catch( InterruptedException e ) {
} catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e);
}
}
return frame;
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
public final void centerFrame() {
// TODO: Center on current display (not main display by default)
// TODO: Position at current BlueJ windows if IN_BLUEJ
//frame.setLocationRelativeTo(null);
//frame.setLocationRelativeTo(displayDevice.getFullScreenWindow());
java.awt.Rectangle bounds = displayDevice.getDefaultConfiguration().getBounds();
frame.setLocation(
(int) (bounds.x + (screenWidth - frame.getWidth()) / 2.0),
(int) (bounds.y + (screenHeight - frame.getHeight()) / 2.0)
);
frame.centerFrame();
}
/**
@@ -475,37 +390,25 @@ public class Zeichenmaschine extends Constants {
* ansonsten deaktiviert.
*/
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
frame.setResizable(false); // Should be set anyway
displayDevice.setFullScreenWindow(frame);
// Update width / height
initialWidth = canvasWidth;
initialHeight = canvasHeight;
changeSize(screenWidth, screenHeight);
// Register ESC as exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
fullscreen = true;
if( pEnable && !frame.isFullscreen() ) {
frame.setFullscreen(true);
if( frame.isFullscreen() )
fullscreenChanged();
} else if( !pEnable && fullscreen ) {
fullscreen = false;
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
changeSize(initialWidth, initialHeight);
frame.pack();
// frame.setUndecorated(false);
} else if( !pEnable && frame.isFullscreen() ) {
frame.setFullscreen(false);
if( !frame.isFullscreen() )
fullscreenChanged();
}
}
}
/**
* Prüft, ob das Zeichenfenster im Vollbild läuft.
*
* @return {@code true}, wenn sich das Fesnter im Vollbildmodus befindet,
* {@code false} sonst.
*/
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win != null;
return frame.isFullscreen();
}
/**
@@ -583,14 +486,26 @@ public class Zeichenmaschine extends Constants {
return state == Options.AppState.PAUSED;
}
public final boolean isTerminated() {
return state == Options.AppState.TERMINATED;
}
public final boolean isTerminating() {
return state == Options.AppState.STOPPED || state == Options.AppState.TERMINATED;
}
/**
* Stoppt die Zeichenmaschine.
* <p>
* Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine
* in den Zustand {@link Options.AppState#STOPPED} und ruft
* {@link #teardown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* {@link #shutdown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das
* Zeichenfenster bleibt weiter geöffnet.
* <p>
* Die Zeichenmaschine reagiert in diesem Zustand weiter auf Eingaben,
* allerdings muss die Zeichnung nun manuell mit {@link #redraw()}
* aktualisiert werden.
*/
public final void stop() {
running = false;
@@ -601,7 +516,7 @@ public class Zeichenmaschine extends Constants {
* <p>
* Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und
* verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die
* Äquivalente Methode für Unterklassen ist {@link #teardown()}, die direkt
* Äquivalente Methode für Unterklassen ist {@link #shutdown()}, die direkt
* vor {@code cleanup()} aufgerufen wird.
*/
private void cleanup() {
@@ -624,7 +539,18 @@ public class Zeichenmaschine extends Constants {
public final void exit() {
if( running ) {
running = false;
this.quitAfterTeardown = true;
quitAfterShutdown = true;
} else {
quit(true);
}
}
public final void exitNow() {
if( running ) {
running = false;
terminateImediately = true;
quitAfterShutdown = true;
mainThread.interrupt();
} else {
quit(true);
}
@@ -632,6 +558,10 @@ public class Zeichenmaschine extends Constants {
/**
* Beendet das Programm vollständig.
* <p>
* Enspricht dem Aufruf {@code quit(true)}.
*
* @see #quit(boolean)
*/
public final void quit() {
//quit(!IN_BLUEJ);
@@ -641,6 +571,14 @@ public class Zeichenmaschine extends Constants {
/**
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird
* die komplette VM beendet.
* <p>
* Die Methode sorgt nicht für ein ordnungsgemäßes herunterfahren und
* freigeben aller Ressourcen, da die Zeichenmaschine gegebenenfalls
* geöffnet bleiben und weitere Aufgaben erfüllen soll. Aufrufende Methoden
* sollten dies berücksichtigen.
* <p>
* Soll das Programm vollständig beendet werden, ist es ratsamer
* {@link #exit()} zu verwenden.
*
* @param exit Ob die VM beendet werden soll.
* @see System#exit(int)
@@ -655,28 +593,6 @@ public class Zeichenmaschine extends Constants {
}
}
/**
* Interne Methode um die Größe der Zeichenfläche zu ändern.
* <p>
* Die Methode berücksichtigt nicht den Zustand des Fensters (z.B.
* Vollbildmodus) und geht davon aus, dass die aufrufende Methode
* sichergestellt hat, dass eine Änderung der Größe der Zeichenfläche
* zulässig und sinnvoll ist.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
* @see #setSize(int, int)
* @see #setFullscreen(boolean)
*/
private void changeSize( int newWidth, int newHeight ) {
canvasWidth = Math.min(Math.max(newWidth, 100), screenWidth);
canvasHeight = Math.min(Math.max(newHeight, 100), screenHeight);
if( canvas != null ) {
canvas.setSize(canvasWidth, canvasHeight);
}
}
/**
* Ändert die Größe der {@link Zeichenleinwand}.
*
@@ -685,16 +601,11 @@ public class Zeichenmaschine extends Constants {
* @see Zeichenleinwand#setSize(int, int)
*/
public final void setSize( int width, int height ) {
if( fullscreen ) {
initialWidth = Math.min(Math.max(width, 100), screenWidth);
initialHeight = Math.min(Math.max(height, 100), screenHeight);
setFullscreen(false);
} else {
changeSize(width, height);
frame.setSize(width, height);
//frame.setSize(width, height);
frame.pack();
}
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
canvasWidth = (int) canvasBounds.getWidth();
canvasHeight = (int) canvasBounds.getHeight();
}
/**
@@ -952,13 +863,15 @@ public class Zeichenmaschine extends Constants {
return;
}
if( state != Options.AppState.RUNNING ) {
if( state == Options.AppState.INITIALIZING ||
state == Options.AppState.INITIALIZED ) {
LOG.warn("Don't use delay(int) from within settings() or setup().");
return;
}
long timer = 0L;
if( updateState == Options.AppState.DRAWING ) {
if( /*updateState == Options.AppState.DRAWING*/
isTerminating() ) {
// Falls gerade draw() ausgeführt wird, zeigen wir den aktuellen
// Stand der Zeichnung auf der Leinwand an. Die Zeit für das
// Rendern wird gemessen und von der Wartezeit abgezogen.
@@ -967,6 +880,7 @@ public class Zeichenmaschine extends Constants {
timer = System.nanoTime() - timer;
}
Options.AppState oldState = updateState;
try {
int sub = (int) Math.ceil(timer / 1000000.0);
@@ -974,9 +888,12 @@ public class Zeichenmaschine extends Constants {
return;
}
updateState = Options.AppState.DELAYED;
Thread.sleep(ms - sub, (int) (timer % 1000000L));
} catch( InterruptedException ex ) {
} catch( InterruptedException ignored ) {
// Nothing
} finally {
updateState = oldState;
}
}
@@ -1140,7 +1057,7 @@ public class Zeichenmaschine extends Constants {
* Spiels oder der Abspann einer Animation angezeigt werden, oder mit
* {@link #saveImage()} die erstellte Zeichnung abgespeichert werden.
*/
public void teardown() {
public void shutdown() {
// Intentionally left blank
}
@@ -1175,39 +1092,41 @@ public class Zeichenmaschine extends Constants {
}
/*
* Mouse handling
* Input handling
*/
private void enqueueEvent( InputEvent evt ) {
eventQueue.add(evt);
if( updateState != Options.AppState.DELAYED ) {
eventQueue.add(evt);
}
if( isPaused() ) {
if( isPaused() || isTerminated() ) {
dispatchEvents();
}
}
private void dispatchEvents() {
synchronized( eventQueue ) {
while( !eventQueue.isEmpty() ) {
InputEvent evt = eventQueue.poll();
//synchronized( eventQueue ) {
while( !eventQueue.isEmpty() ) {
InputEvent evt = eventQueue.poll();
switch( evt.getID() ) {
case KeyEvent.KEY_TYPED:
case KeyEvent.KEY_PRESSED:
case KeyEvent.KEY_RELEASED:
handleKeyEvent((KeyEvent) evt);
break;
switch( evt.getID() ) {
case KeyEvent.KEY_TYPED:
case KeyEvent.KEY_PRESSED:
case KeyEvent.KEY_RELEASED:
handleKeyEvent((KeyEvent) evt);
break;
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_WHEEL:
handleMouseEvent((MouseEvent) evt);
break;
}
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_WHEEL:
handleMouseEvent((MouseEvent) evt);
break;
}
}
//}
}
private void handleKeyEvent( KeyEvent evt ) {
@@ -1245,7 +1164,7 @@ public class Zeichenmaschine extends Constants {
case MouseEvent.MOUSE_RELEASED:
mousePressed = false;
mouseButton = NOMOUSE;
mousePressed(evt);
mouseReleased(evt);
break;
case MouseEvent.MOUSE_DRAGGED:
//saveMousePosition(evt);
@@ -1376,9 +1295,9 @@ public class Zeichenmaschine extends Constants {
* // Next frame has started
* </code></pre>
* <p>
* Die {@link schule.ngb.zm.tasks.FrameSynchronizedTask} implementiert eine
* {@link schule.ngb.zm.tasks.Task}, die sich automatisch auf diese Wiese
* mit dem Zeichenthread synchronisiert.
* 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];
@@ -1393,48 +1312,51 @@ public class Zeichenmaschine extends Constants {
public final void run() {
// Wait for full initialization before start
while( state != Options.AppState.INITIALIZED ) {
delay(1);
Thread.yield();
}
// ThreadExecutor for the update/draw Thread
final UpdateThreadExecutor updateThreadExecutor = new UpdateThreadExecutor();
// start of thread in ms
// Start des Thread in ms
final long start = System.currentTimeMillis();
// current time in ns
long beforeTime = System.nanoTime();
// Aktuelle Zeit in ns
long beforeTime;
long updateBeforeTime = System.nanoTime();
// store for deltas
// Speicher für Änderung
long overslept = 0L;
// internal counters for tick and runtime
// Interne Zähler für tick und runtime
int _tick = 0;
long _runtime = 0;
// public counters for access by subclasses
// Öffentliche Zähler für Unterklassen
tick = 0;
runtime = 0;
// call setup of subclass and wait
// setup() der Unterklasse aufrufen
setup();
// Alles startklar ...
state = Options.AppState.RUNNING;
while( running ) {
// delta in seconds
// Aktuelle Zeit in ns merken
beforeTime = System.nanoTime();
// Mausposition einmal pro Frame merken
saveMousePosition(mouseEvent);
if( state != Options.AppState.PAUSED ) {
//handleUpdate(delta);
//handleDraw();
// Update and draw are executed in a new thread,
// but we wait for them to finish unless the user
// did call any blocking method, that would also block
// rendering of new frames.
// update() und draw() der Unterklasse werden in einem
// eigenen Thread ausgeführt, aber der Zeichenthread
// wartet, bis der Thread fertig ist. Außer die Unterklasse
// ruft delay() auf und lässt den Thread eine länger Zeit
// schlafen. Dann wird der nächst Frame vorzeitig gerendert,
// bis der update-Thread wieder bereit ist. Dadurch können
// nebenläufige Aufgaben (z.B. Animationen) weiterlaufen.
if( !updateThreadExecutor.isRunning() ) {
delta = (System.nanoTime() - updateBeforeTime) / 1000000000.0;
updateBeforeTime = System.nanoTime();
// uddate()/draw() ausführen
updateThreadExecutor.execute(() -> {
if( state == Options.AppState.RUNNING
&& updateState == Options.AppState.IDLE ) {
@@ -1446,10 +1368,11 @@ public class Zeichenmaschine extends Constants {
// Call to draw()
updateState = Options.AppState.DRAWING;
Zeichenmaschine.this.draw();
updateState = Options.AppState.IDLE;
updateState = Options.AppState.DISPATCHING;
// Send latest input events after finishing draw
// since these may also block
dispatchEvents();
updateState = Options.AppState.IDLE;
}
});
}
@@ -1458,6 +1381,12 @@ public class Zeichenmaschine extends Constants {
while( updateThreadExecutor.isRunning()
&& !updateThreadExecutor.isWaiting() ) {
Thread.yield();
if( Thread.interrupted() ) {
running = false;
terminateImediately = true;
break;
}
}
// Display the current buffer content
@@ -1467,7 +1396,6 @@ public class Zeichenmaschine extends Constants {
// frame.repaint();
}
// dispatchEvents();
}
@@ -1510,37 +1438,23 @@ public class Zeichenmaschine extends Constants {
pause_pending = false;
}
}
// Shutdown the updateThreads
updateThreadExecutor.shutdownNow();
state = Options.AppState.STOPPED;
// Shutdown the updateThread
while( !terminateImediately && updateThreadExecutor.isRunning() ) {
Thread.yield();
}
updateThreadExecutor.shutdownNow();
// Cleanup
teardown();
shutdown();
cleanup();
state = Options.AppState.TERMINATED;
if( quitAfterTeardown ) {
if( quitAfterShutdown ) {
quit();
}
}
public void handleUpdate( double delta ) {
if( state == Options.AppState.RUNNING ) {
state = Options.AppState.UPDATING;
update(delta);
canvas.updateLayers(delta);
state = Options.AppState.RUNNING;
}
}
public void handleDraw() {
if( state == Options.AppState.RUNNING ) {
state = Options.AppState.DRAWING;
draw();
state = Options.AppState.RUNNING;
}
}
}
// TODO: Remove
@@ -1637,7 +1551,7 @@ public class Zeichenmaschine extends Constants {
private Thread updateThread;
private boolean running = false, waiting = false;
private boolean running = false;
public UpdateThreadExecutor() {
super(1, 1, 0L,
@@ -1645,13 +1559,18 @@ public class Zeichenmaschine extends Constants {
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
// We store the one Thread this Executor holds,
// but it might change if a new Thread needed to be spawned
// due to en error.
updateThread = t;
running = true;
}
@Override
@@ -1682,7 +1601,9 @@ public class Zeichenmaschine extends Constants {
* @return
*/
public boolean isWaiting() {
return running && updateThread.getState() == Thread.State.TIMED_WAITING;
//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;
}
}

View File

@@ -2,17 +2,16 @@ package schule.ngb.zm.anim;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Updatable;
import schule.ngb.zm.events.EventDispatcher;
import schule.ngb.zm.tasks.FrameSynchronizedTask;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import java.util.function.DoubleUnaryOperator;
public abstract class Animation<T> implements Updatable {
public abstract class Animation<T> extends Constants implements Updatable {
protected int runtime;
protected int elapsed_time = 0;
protected int elapsedTime = 0;
protected boolean running = false, finished = false;
@@ -25,7 +24,7 @@ public abstract class Animation<T> implements Updatable {
public Animation( DoubleUnaryOperator easing ) {
this.runtime = Constants.DEFAULT_ANIM_RUNTIME;
this.easing = easing;
this.easing = Validator.requireNotNull(easing);
}
public Animation( int runtime ) {
@@ -35,7 +34,7 @@ public abstract class Animation<T> implements Updatable {
public Animation( int runtime, DoubleUnaryOperator easing ) {
this.runtime = runtime;
this.easing = easing;
this.easing = Validator.requireNotNull(easing);
}
public int getRuntime() {
@@ -58,17 +57,17 @@ public abstract class Animation<T> implements Updatable {
public final void start() {
this.initialize();
elapsed_time = 0;
elapsedTime = 0;
running = true;
finished = false;
interpolate(easing.applyAsDouble(0.0));
animate(easing.applyAsDouble(0.0));
initializeEventDispatcher().dispatchEvent("start", this);
}
public final void stop() {
running = false;
// Make sure the last animation frame was interpolated correctly
interpolate(easing.applyAsDouble((double) elapsed_time / (double) runtime));
animate(easing.applyAsDouble((double) elapsedTime / (double) runtime));
this.finish();
finished = true;
initializeEventDispatcher().dispatchEvent("stop", this);
@@ -84,11 +83,7 @@ public abstract class Animation<T> implements Updatable {
public final void await() {
while( !finished ) {
try {
Thread.sleep(1);
} catch( InterruptedException ex ) {
// Keep waiting
}
Thread.yield();
}
}
@@ -99,16 +94,16 @@ public abstract class Animation<T> implements Updatable {
@Override
public void update( double delta ) {
elapsed_time += (int) (delta * 1000);
if( elapsed_time > runtime )
elapsed_time = runtime;
elapsedTime += (int) (delta * 1000);
if( elapsedTime > runtime )
elapsedTime = runtime;
double t = (double) elapsed_time / (double) runtime;
double t = (double) elapsedTime / (double) runtime;
if( t >= 1.0 ) {
running = false;
stop();
} else {
interpolate(easing.applyAsDouble(t));
animate(easing.applyAsDouble(t));
}
}
@@ -123,9 +118,10 @@ public abstract class Animation<T> implements Updatable {
* e = Constants.limit(e, 0, 1);
* </code></pre>
*
* @param e
* @param e Fortschritt der Animation nachdem die Easingfunktion angewandt
* wurde.
*/
public abstract void interpolate( double e );
public abstract void animate( double e );
EventDispatcher<Animation, AnimationListener> eventDispatcher;

View File

@@ -19,8 +19,8 @@ public class AnimationFacade<S> extends Animation<S> {
}
@Override
public void interpolate( double e ) {
anim.interpolate(e);
public void animate( double e ) {
anim.animate(e);
}
@Override

View File

@@ -1,76 +1,116 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.shapes.Shape;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.DoubleUnaryOperator;
public class AnimationGroup extends Animation<Shape> {
@SuppressWarnings( "unused" )
public class AnimationGroup<T> extends Animation<T> {
Animation<? extends Shape>[] anims;
private boolean overrideRuntime = false;
List<Animation<T>> anims;
public AnimationGroup( DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) {
super(easing);
this.anims = anims;
private boolean overrideEasing = false;
int maxRuntime = Arrays.stream(this.anims).mapToInt((a) -> a.getRuntime()).reduce(0, Integer::max);
setRuntime(maxRuntime);
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 runtime, DoubleUnaryOperator easing, Animation<? extends Shape>... anims ) {
super(runtime, easing);
this.anims = anims;
overrideRuntime = true;
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 Shape getAnimationTarget() {
return null;
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 ) {
if( overrideRuntime ) {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
if( anim.isActive() ) {
anim.update(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();
}
}
} else {
super.update(delta);
running = false;
this.stop();
}
}
@Override
public void interpolate( double e ) {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.interpolate(e);
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 initialize() {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.initialize();
}
}
}
@Override
public void finish() {
synchronized( anims ) {
for( Animation<? extends Shape> anim: anims ) {
anim.finish();
}
}
public void animate( double e ) {
}
}

View File

@@ -1,6 +1,6 @@
package schule.ngb.zm.anim;
import schule.ngb.zm.events.Listener;
import schule.ngb.zm.util.events.Listener;
public interface AnimationListener extends Listener<Animation> {

View File

@@ -3,9 +3,8 @@ package schule.ngb.zm.anim;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Vector;
import schule.ngb.zm.tasks.FrameSynchronizedTask;
import schule.ngb.zm.tasks.FramerateLimitedTask;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.tasks.FramerateLimitedTask;
import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
@@ -15,6 +14,7 @@ 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 ) {
@@ -92,27 +92,28 @@ public class Animations {
});
}
private static final <T, R> R callGetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
@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 != null && getter.getReturnType().equals(propType) ) {
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 final <T, R> Method findSetter( T target, String propName, Class<R> propType ) throws NoSuchMethodException {
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 != null && setter.getReturnType().equals(void.class) && setter.getParameterCount() == 1 ) {
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 final String makeMethodName( String prefix, String propName ) {
private static String makeMethodName( String prefix, String propName ) {
String firstChar = propName.substring(0, 1).toUpperCase();
String tail = "";
if( propName.length() > 1 ) {
@@ -124,31 +125,33 @@ public class Animations {
public static final <T> Future<T> animateProperty( T target, final double from, final double to, int runtime, DoubleUnaryOperator easing, DoubleConsumer propSetter ) {
Validator.requireNotNull(target);
Validator.requireNotNull(propSetter);
return animate(target, runtime, easing, ( e ) -> propSetter.accept(Constants.interpolate(from, to, e)));
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 animate(target, runtime, easing, ( e ) -> propSetter.accept(Color.interpolate(from, to, e)));
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 animate(target, runtime, easing, ( e ) -> propSetter.accept(Vector.interpolate(from, to, e)));
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 animate(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
return play(target, runtime, easing, interpolator, ( t, r ) -> propSetter.accept(r));
}
public static final <T, R> Future<T> animate( T target, int runtime, DoubleUnaryOperator easing, DoubleFunction<R> interpolator, BiConsumer<T, R> applicator ) {
return animate(target, runtime, easing, ( e ) -> applicator.accept(target, interpolator.apply(e)));
public static final <T, 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> animate( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
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]
@@ -164,8 +167,8 @@ public class Animations {
}, target);
}
public static final <T> T animateAndWait( T target, int runtime, DoubleUnaryOperator easing, DoubleConsumer stepper ) {
Future<T> future = animate(target, runtime, easing, stepper);
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();
@@ -179,16 +182,17 @@ public class Animations {
return target;
}
public static final <T, R> Future<T> animate( T target, int runtime, Animator<T, R> animator ) {
/*public static final <T, R> Future<T> animate( T target, int runtime, Animator<T, R> animator ) {
return animate(
target, runtime,
animator::easing,
animator::interpolator,
animator::applicator
);
}
}*/
public static <T> Future<Animation<T>> animate( Animation<T> animation ) {
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() {
@@ -203,13 +207,13 @@ public class Animations {
}, animation);
}
public static <T> Animation<T> animateAndWait( Animation<T> animation ) {
Future<Animation<T>> future = animate(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>> animate( Animation<T> animation, DoubleUnaryOperator easing ) {
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

View File

@@ -1,11 +0,0 @@
package schule.ngb.zm.anim;
public interface Animator<T, R> {
double easing(double t);
R interpolator(double e);
void applicator(T target, R value);
}

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ public class FadeAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
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

@@ -26,7 +26,7 @@ public class FillAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
public void animate( double e ) {
object.setFillColor(Color.interpolate(oFill, tFill, e));
}

View File

@@ -27,7 +27,7 @@ public class MorphAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
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));

View File

@@ -31,7 +31,7 @@ public class MoveAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
public void animate( double e ) {
object.setX(Constants.interpolate(oX, tX, e));
object.setY(Constants.interpolate(oY, tY, e));
}

View File

@@ -25,7 +25,7 @@ public class RotateAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
public void animate( double e ) {
object.rotateTo(Constants.interpolate(oA, tA, e));
}

View File

@@ -25,7 +25,7 @@ public class StrokeAnimation extends Animation<Shape> {
}
@Override
public void interpolate( double e ) {
public void animate( double e ) {
object.setStrokeColor(Color.interpolate(oFill, tFill, e));
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
package schule.ngb.zm.shapes;
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.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
public class ShapesLayer extends Layer {
@@ -45,12 +43,12 @@ public class ShapesLayer extends Layer {
return null;
}
public java.util.List<Shape> getShapes() {
public List<Shape> getShapes() {
return shapes;
}
public <ST extends Shape> java.util.List<ST> getShapes( Class<ST> shapeClass ) {
java.util.List<ST> result = new LinkedList<>();
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);
@@ -60,7 +58,7 @@ public class ShapesLayer extends Layer {
}
public void add( Shape... shapes ) {
synchronized( shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
@@ -68,7 +66,7 @@ public class ShapesLayer extends Layer {
}
public void add( Collection<Shape> shapes ) {
synchronized( shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
@@ -76,16 +74,16 @@ public class ShapesLayer extends Layer {
}
public void remove( Shape... shapes ) {
synchronized( shapes ) {
for( Shape s: shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
}
public void remove( Collection<Shape> shapes ) {
synchronized( shapes ) {
for( Shape s: shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
@@ -99,16 +97,16 @@ public class ShapesLayer extends Layer {
public void showAll() {
synchronized( shapes ) {
for( Shape pShape : shapes ) {
pShape.show();
for( Shape s : shapes ) {
s.show();
}
}
}
public void hideAll() {
synchronized( shapes ) {
for( Shape pShape : shapes ) {
pShape.hide();
for( Shape s : shapes ) {
s.hide();
}
}
}
@@ -118,6 +116,14 @@ public class ShapesLayer extends Layer {
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);
}
@@ -147,9 +153,10 @@ public class ShapesLayer extends Layer {
}
synchronized( shapes ) {
for( Shape pShape : shapes ) {
if( pShape.isVisible() ) {
pShape.draw(drawing);
List<Shape> it = List.copyOf(shapes);
for( Shape s : it ) {
if( s.isVisible() ) {
s.draw(drawing);
}
}
}

View File

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

View File

@@ -1,6 +1,6 @@
package schule.ngb.zm.media;
import schule.ngb.zm.events.Listener;
import schule.ngb.zm.util.events.Listener;
public interface AudioListener extends Listener<Audio> {

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm.media;
import schule.ngb.zm.Constants;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.tasks.TaskRunner;
import java.util.ArrayList;
import java.util.List;

View File

@@ -1,11 +1,9 @@
package schule.ngb.zm.media;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.anim.AnimationListener;
import schule.ngb.zm.events.EventDispatcher;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import schule.ngb.zm.util.Validator;
import javax.sound.sampled.*;
@@ -168,7 +166,7 @@ public class Music implements Audio {
* {@inheritDoc}
*/
@Override
public void dispose() {
public synchronized void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
playing = false;
@@ -177,7 +175,6 @@ public class Music implements Audio {
if( audioLine.isOpen() ) {
audioLine.drain();
audioLine.close();
}
}
try {
@@ -191,7 +188,7 @@ public class Music implements Audio {
audioStream = null;
}
private void stream() {
private synchronized void stream() {
audioLine.start();
playing = true;
if( eventDispatcher != null ) {

View File

@@ -1,12 +1,11 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
@@ -206,8 +205,9 @@ public class Sound implements Audio {
}
/**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und stoppt
* die Wiedergabe dann.
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen ab und
* stoppt die Wiedergabe dann.
*
* @param count Anzahl der Wiederholungen.
*/
public void loop( int count ) {
@@ -232,7 +232,7 @@ public class Sound implements Audio {
* {@inheritDoc}
*/
@Override
public void dispose() {
public synchronized void dispose() {
if( audioClip != null ) {
if( audioClip.isRunning() ) {
audioClip.stop();
@@ -242,7 +242,7 @@ public class Sound implements Audio {
}
}
private boolean openClip() {
private synchronized boolean openClip() {
if( audioClip != null ) {
audioClip.setFramePosition(0);
return true;

View File

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

View File

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

View File

@@ -1,82 +0,0 @@
package schule.ngb.zm.ml;
import schule.ngb.zm.Constants;
import java.util.Arrays;
// TODO: Move Math into Matrix class
// TODO: Implement support for optional sci libs
public class Matrix {
private int columns, rows;
double[][] coefficients;
public Matrix( int rows, int cols ) {
this.rows = rows;
this.columns = cols;
coefficients = new double[rows][cols];
}
public Matrix( double[][] coefficients ) {
this.coefficients = coefficients;
this.rows = coefficients.length;
this.columns = coefficients[0].length;
}
public int getColumns() {
return columns;
}
public int getRows() {
return rows;
}
public double[][] getCoefficients() {
return coefficients;
}
public double get( int row, int col ) {
return coefficients[row][col];
}
public void initializeRandom() {
coefficients = MLMath.matrixApply(coefficients, (d) -> Constants.randomGaussian());
}
public void initializeRandom( double lower, double upper ) {
coefficients = MLMath.matrixApply(coefficients, (d) -> ((upper-lower) * (Constants.randomGaussian()+1) * .5) + lower);
}
public void initializeIdentity() {
initializeZero();
for( int i = 0; i < Math.min(rows, columns); i++ ) {
this.coefficients[i][i] = 1.0;
}
}
public void initializeOne() {
coefficients = MLMath.matrixApply(coefficients, (d) -> 1.0);
}
public void initializeZero() {
coefficients = MLMath.matrixApply(coefficients, (d) -> 0.0);
}
@Override
public String toString() {
//return Arrays.deepToString(coefficients);
StringBuilder sb = new StringBuilder();
sb.append('[');
sb.append('\n');
for( int i = 0; i < coefficients.length; i++ ) {
sb.append('\t');
sb.append(Arrays.toString(coefficients[i]));
sb.append('\n');
}
sb.append(']');
return sb.toString();
}
}

View File

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

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm.ml;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.ResourceStreamProvider;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import java.io.*;
import java.util.LinkedList;
@@ -15,7 +15,7 @@ public class NeuralNetwork {
Writer writer = ResourceStreamProvider.getWriter(source);
PrintWriter out = new PrintWriter(writer)
) {
for( NeuronLayer layer: network.layers ) {
for( NeuronLayer layer : network.layers ) {
out.print(layer.getNeuronCount());
out.print(' ');
out.print(layer.getInputCount());
@@ -23,20 +23,44 @@ public class NeuralNetwork {
for( int i = 0; i < layer.getInputCount(); i++ ) {
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.weights.coefficients[i][j]);
out.print(layer.weights.get(i, j));
out.print(' ');
}
out.println();
}
for( int j = 0; j < layer.getNeuronCount(); j++ ) {
out.print(layer.biases[j]);
out.print(layer.biases.get(0, j));
out.print(' ');
}
out.println();
}
out.flush();
} catch( IOException ex ) {
LOG.warn(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, "");
}
}
@@ -56,13 +80,13 @@ public class NeuralNetwork {
for( int i = 0; i < inputs; i++ ) {
split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) {
layer.weights.coefficients[i][j] = Double.parseDouble(split[j]);
layer.weights.set(i, j, Double.parseDouble(split[j]));
}
}
// Load Biases
split = in.readLine().split(" ");
for( int j = 0; j < neurons; j++ ) {
layer.biases[j] = Double.parseDouble(split[j]);
layer.biases.set(0, j, Double.parseDouble(split[j]));
}
layers.add(layer);
@@ -70,29 +94,30 @@ public class NeuralNetwork {
return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) {
LOG.warn(ex, "Could not load neural network from source <%s>", source);
LOG.error(ex, "Could not load neural network from source <%s>", source);
}
return null;
}
/*public static NeuralNetwork loadFromFile( String source ) {
public static NeuralNetwork loadFromDataFile( String source ) {
try(
InputStream stream = ResourceStreamProvider.getInputStream(source);
Scanner in = new Scanner(stream)
DataInputStream in = new DataInputStream(stream)
) {
List<NeuronLayer> layers = new LinkedList<>();
while( in.hasNext() ) {
int neurons = in.nextInt();
int inputs = in.nextInt();
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.coefficients[i][j] = in.nextDouble();
layer.weights.set(i, j, in.readDouble());
}
}
// Load Biases
for( int j = 0; j < neurons; j++ ) {
layer.biases[j] = in.nextDouble();
layer.biases.set(0, j, in.readDouble());
}
layers.add(layer);
@@ -100,14 +125,14 @@ public class NeuralNetwork {
return new NeuralNetwork(layers);
} catch( IOException | NoSuchElementException ex ) {
LOG.warn(ex, "Could not load neural network from source <%s>", source);
LOG.error(ex, "Could not load neural network from source <%s>", source);
}
return null;
}*/
}
private NeuronLayer[] layers;
private double[][] output;
private MLMatrix output;
private double learningRate = 0.1;
@@ -128,7 +153,7 @@ public class NeuralNetwork {
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]);
this.layers[i - 1].setNextLayer(this.layers[i]);
}
}
}
@@ -138,7 +163,7 @@ public class NeuralNetwork {
for( int i = 0; i < layers.length; i++ ) {
this.layers[i] = layers[i];
if( i > 0 ) {
this.layers[i-1].setNextLayer(this.layers[i]);
this.layers[i - 1].setNextLayer(this.layers[i]);
}
}
}
@@ -146,6 +171,7 @@ public class NeuralNetwork {
public int getLayerCount() {
return layers.length;
}
public NeuronLayer[] getLayers() {
return layers;
}
@@ -162,17 +188,28 @@ public class NeuralNetwork {
this.learningRate = pLearningRate;
}
public double[][] getOutput() {
public MLMatrix getOutput() {
return output;
}
public double[][] predict( double[][] inputs ) {
//this.output = layers[1].apply(layers[0].apply(inputs));
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);
}

View File

@@ -1,50 +1,66 @@
package schule.ngb.zm.ml;
import java.util.Arrays;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Function;
public class NeuronLayer implements Function<double[][], double[][]> {
/**
* 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);
}
public static NeuronLayer fromArray( double[][] weights ) {
NeuronLayer layer = new NeuronLayer(weights[0].length, weights.length);
for( int i = 0; i < weights[0].length; i++ ) {
for( int j = 0; j < weights.length; j++ ) {
layer.weights.coefficients[i][j] = weights[i][j];
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 ) {
NeuronLayer layer = new NeuronLayer(weights[0].length, weights.length);
for( int i = 0; i < weights[0].length; i++ ) {
for( int j = 0; j < weights.length; j++ ) {
layer.weights.coefficients[i][j] = weights[i][j];
}
}
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[j] = biases[j];
layer.biases.set(0, j, biases[j]);
}
return layer;
}
Matrix weights;
double[] biases;
MLMatrix weights;
MLMatrix biases;
NeuronLayer previous, next;
DoubleUnaryOperator activationFunction, activationFunctionDerivative;
double[][] lastOutput, lastInput;
MLMatrix lastOutput, lastInput;
public NeuronLayer( int neurons, int inputs ) {
weights = new Matrix(inputs, neurons);
weights.initializeRandom(-1, 1);
weights = MatrixFactory
.create(inputs, neurons)
.initializeRandom();
biases = new double[neurons];
Arrays.fill(biases, 0.0); // TODO: Random?
biases = MatrixFactory
.create(1, neurons)
.initializeZero();
activationFunction = MLMath::sigmoid;
activationFunctionDerivative = MLMath::sigmoidDerivative;
@@ -85,45 +101,42 @@ public class NeuronLayer implements Function<double[][], double[][]> {
}
}
public Matrix getWeights() {
public MLMatrix getWeights() {
return weights;
}
public MLMatrix getBiases() {
return biases;
}
public int getNeuronCount() {
return weights.coefficients[0].length;
return weights.columns();
}
public int getInputCount() {
return weights.coefficients.length;
return weights.rows();
}
public double[][] getLastOutput() {
public MLMatrix getLastOutput() {
return lastOutput;
}
public void setWeights( double[][] newWeights ) {
weights.coefficients = MLMath.copyMatrix(newWeights);
}
public void adjustWeights( double[][] adjustment ) {
weights.coefficients = MLMath.matrixAdd(weights.coefficients, adjustment);
public void setWeights( MLMatrix newWeights ) {
weights = newWeights.duplicate();
}
@Override
public String toString() {
return weights.toString() + "\n" + Arrays.toString(biases);
return "weights:\n" + weights.toString() + "\nbiases:\n" + biases.toString();
}
@Override
public double[][] apply( double[][] inputs ) {
lastInput = inputs;
lastOutput = MLMath.matrixApply(
MLMath.biasAdd(
MLMath.matrixMultiply(inputs, weights.coefficients),
biases
),
activationFunction
);
public MLMatrix apply( MLMatrix inputs ) {
lastInput = inputs.duplicate();
lastOutput = inputs
.multiplyAddBias(weights, biases)
.applyInPlace(activationFunction);
if( next != null ) {
return next.apply(lastOutput);
} else {
@@ -132,36 +145,41 @@ public class NeuronLayer implements Function<double[][], double[][]> {
}
@Override
public <V> Function<V, double[][]> compose( Function<? super V, ? extends double[][]> before ) {
public <V> Function<V, MLMatrix> compose( Function<? super V, ? extends MLMatrix> before ) {
return ( in ) -> apply(before.apply(in));
}
@Override
public <V> Function<double[][], V> andThen( Function<? super double[][], ? extends V> after ) {
public <V> Function<MLMatrix, V> andThen( Function<? super MLMatrix, ? extends V> after ) {
return ( in ) -> after.apply(apply(in));
}
public void backprop( double[][] expected, double learningRate ) {
double[][] error, delta, adjustment;
public void backprop( MLMatrix expected, double learningRate ) {
MLMatrix error, adjustment;
if( next == null ) {
error = MLMath.matrixSub(expected, this.lastOutput);
error = expected.sub(lastOutput);
} else {
error = MLMath.matrixMultiply(expected, MLMath.matrixTranspose(next.weights.coefficients));
error = expected.multiplyTransposed(next.weights);
}
delta = MLMath.matrixScale(error, MLMath.matrixApply(this.lastOutput, this.activationFunctionDerivative));
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(delta, learningRate);
previous.backprop(error, learningRate);
}
biases = MLMath.biasAdjust(biases, MLMath.matrixApply(delta, ( x ) -> learningRate * x));
biases.addInPlace(
error.colSums().scaleInPlace(
-learningRate / (double) error.rows()
)
);
adjustment = MLMath.matrixMultiply(MLMath.matrixTranspose(lastInput), delta);
adjustment = MLMath.matrixApply(adjustment, ( x ) -> learningRate * x);
this.adjustWeights(adjustment);
adjustment = lastInput.transposedMultiplyAndScale(error, learningRate);
weights.addInPlace(adjustment);
}
}

View File

@@ -2,44 +2,201 @@ 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 {
protected Color fillColor = STD_FILLCOLOR;
/**
* 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);
}
public void noFill() {
setFillColor(null);
}
/**
* 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(STD_FILLCOLOR);
setFillColor(DEFAULT_FILLCOLOR);
noGradient();
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
setFillColor(from);
fill = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
}
/**
* Setzt die Füllung auf einen kreisförmigen (radialen) Farbverlauf, mit dem
* Zentrum im Punkt ({@code centerX}, {@code centerY}) und dem angegebenen
* Radius. Der Verlauf starte im Zentrum mit der Farbe {@code from} und
* endet am Rand des durch den Radius beschriebenen Kreises mit der Farbe
* {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
setFillColor(from);
fill = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
}
/**
* Entfernt den Farbverlauf von der Form.
*/
public void noGradient() {
fill = null;
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit der aktuellen
* Füllung auf den Grafik-Kontext zu zeichnen. Die Methode verändert
* gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt sie nicht
* auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void fillShape( java.awt.Shape shape, Graphics2D graphics ) {
if( fill != null ) {
graphics.setPaint(fill);
graphics.fill(shape);
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fill(shape);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -100,7 +100,7 @@ public class ShapeGroup extends Shape {
public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) {
LinkedList<ShapeType> list = new LinkedList<>();
for( Shape s : shapes ) {
if( typeClass.getClass().isInstance(s) ) {
if( typeClass.isInstance(s) ) {
list.add((ShapeType) s);
}
}

View File

@@ -4,67 +4,173 @@ import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import schule.ngb.zm.util.Noise;
import java.awt.*;
import java.awt.Shape;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.util.Arrays;
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 {
protected Color strokeColor = STD_STROKECOLOR;
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn die Form ohne
* kontur dargestellt werden soll.
*/
protected Color strokeColor = DEFAULT_STROKECOLOR;
protected double strokeWeight = STD_STROKEWEIGHT;
/**
* 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);
}
public void noStroke() {
setStrokeColor(null);
}
/**
* 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 = 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;
}
@@ -73,7 +179,8 @@ public abstract class StrokedShape extends Constants implements Drawable {
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
*
* @param type
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
public void setStrokeType( Options.StrokeType type ) {
this.strokeType = type;
@@ -84,9 +191,11 @@ public abstract class StrokedShape extends Constants implements Drawable {
public abstract void draw( Graphics2D graphics );
/**
* Erstellt ein {@link Stroke} Objekt für den Konturtyp.
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
* zwischengespeichert.
*
* @return
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected Stroke createStroke() {
// TODO: Used global cached Stroke Objects?
@@ -118,10 +227,23 @@ public abstract class StrokedShape extends Constants implements Drawable {
return stroke;
}
public void resetStroke() {
setStrokeColor(STD_STROKECOLOR);
setStrokeWeight(STD_STROKEWEIGHT);
setStrokeType(SOLID);
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit den aktuellen
* Kontureigenschaften auf den Grafik-Kontext zu zeichnen. Die Methode
* verändert gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt
* sie nicht auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void strokeShape( java.awt.Shape shape, Graphics2D graphics ) {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.draw(shape);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
package schule.ngb.zm.util;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.function.Supplier;
import java.util.logging.*;
@@ -34,7 +31,7 @@ import static java.util.logging.Level.*;
*/
public final class Log {
private static final String ROOT_LOGGER = "schule.ngb.zm";
private static final String ROOT_LOGGER = "schule";
private static final String DEFAULT_LOG_FORMAT = "[%1$tT] [%4$11s] %5$s %6$s%n";
@@ -80,7 +77,7 @@ public final class Log {
*/
public static void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
ensureRootLoggerIntialized();
ensureRootLoggerInitialized();
// Decrease level of root level ConsoleHandlers for output
Logger rootLogger = Logger.getLogger("");
@@ -119,23 +116,32 @@ public final class Log {
}
public static Log getLogger( Class<?> clazz ) {
ensureRootLoggerIntialized();
ensureRootLoggerInitialized();
return new Log(clazz);
}
private static void ensureRootLoggerIntialized() {
private static void ensureRootLoggerInitialized() {
if( LOGGING_INIT ) {
return;
}
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null ) {
System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
}
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(Level.INFO);
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null
&& LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) {
System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()));
rootLogger.setUseParentHandlers(false);
// System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) {
@Override
public synchronized void publish(final LogRecord record) {
super.publish(record);
flush();
}
});
// rootLogger.setUseParentHandlers(false);
}
if( rootLogger.getUseParentHandlers() ) {
// This logger was not configured somewhere else

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.events;
package schule.ngb.zm.util.events;
import schule.ngb.zm.util.Validator;

View File

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

View File

@@ -1,4 +1,6 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.io.IOException;
import java.net.URISyntaxException;

View File

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

View File

@@ -1,4 +1,7 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO;
import java.awt.Color;

View File

@@ -1,12 +1,11 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.io.*;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.util.stream.StreamSupport;
/**
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.

View File

@@ -1,9 +1,6 @@
package schule.ngb.zm.tasks;
import schule.ngb.zm.Zeichenmaschine;
package schule.ngb.zm.util.tasks;
import java.util.concurrent.Delayed;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public abstract class DelayedTask extends Task implements Delayed {

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.tasks;
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.tasks;
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Constants;

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.tasks;
package schule.ngb.zm.util.tasks;
public abstract class RateLimitedTask extends Task {

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.tasks;
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Updatable;

View File

@@ -1,10 +1,9 @@
package schule.ngb.zm.tasks;
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.util.Log;
import javax.swing.*;
import java.util.concurrent.*;
import java.util.logging.Logger;
/**
* Führt Aufgaben (Tasks) parallel zum Hauptprogramm aus.

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,5 +1,6 @@
package schule.ngb.zm;
import schule.ngb.zm.layers.Shape2DLayer;
import schule.ngb.zm.shapes.Rectangle;
import java.awt.geom.Rectangle2D;

View File

@@ -1,7 +1,7 @@
package schule.ngb.zm;
import schule.ngb.zm.turtle.TurtleLayer;
import schule.ngb.zm.turtle.TurtleLayer.Turtle;
import schule.ngb.zm.layers.TurtleLayer;
import schule.ngb.zm.layers.TurtleLayer.Turtle;
public class TestTurtle extends Zeichenmaschine {

View File

@@ -9,6 +9,7 @@ import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Options;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.shapes.*;
import java.util.concurrent.ExecutionException;
@@ -60,7 +61,7 @@ class AnimationsTest {
private void _animateMove( Shape s, int runtime, DoubleUnaryOperator easing ) {
s.moveTo(0, 0);
Future<Shape> future = Animations.animate(
Future<Shape> future = Animations.play(
s, runtime,
easing,
( e ) -> Constants.interpolate(0, zm.getWidth(), e),
@@ -89,25 +90,11 @@ class AnimationsTest {
final int midY = (int) (zm.getHeight() * .5);
final int radius = (int) (zm.getWidth() * .25);
Animator<Shape, Double> ani = new Animator<Shape, Double>() {
@Override
public double easing( double t ) {
return easing.applyAsDouble(t);
}
@Override
public Double interpolator( double e ) {
return Constants.interpolate(0, 360, e);
}
@Override
public void applicator( Shape s, Double angle ) {
double rad = Math.toRadians(angle);
Future<Shape> future = Animations.play(
s, runtime, easing, (e) -> {
double rad = Math.toRadians(Constants.interpolate(0, 360, e));
s.moveTo(midX + radius * Math.cos(rad), midY + radius * Math.sin(rad));
}
};
Future<Shape> future = Animations.animate(s, runtime, ani);
});
assertNotNull(future);
try {
assertEquals(s, future.get());
@@ -146,7 +133,7 @@ class AnimationsTest {
private void _animateRotate( Shape s, int runtime, DoubleUnaryOperator easing ) {
s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5);
s.rotateTo(0);
Future<Shape> future = Animations.animate(
Future<Shape> future = Animations.play(
s, runtime,
easing,
( e ) -> s.rotateTo(Constants.interpolate(0, 720, e))
@@ -178,7 +165,7 @@ class AnimationsTest {
private void _animateColor( Shape s, Color to, int runtime, DoubleUnaryOperator easing ) {
s.moveTo(zm.getWidth() * .5, zm.getHeight() * .5);
final Color from = s.getFillColor();
Future<Shape> future = Animations.animate(
Future<Shape> future = Animations.play(
s, runtime,
easing,
( e ) -> Color.interpolate(from, to, e),

View File

@@ -0,0 +1,426 @@
package schule.ngb.zm.ml;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import schule.ngb.zm.util.Timer;
import static org.junit.jupiter.api.Assertions.*;
class MLMatrixTest {
private TestInfo info;
@BeforeEach
void saveTestInfo( TestInfo info ) {
this.info = info;
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void get( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 2, 3},
{4, 5, 6}
});
assertEquals(mType, M.getClass());
assertEquals(1.0, M.get(0,0));
assertEquals(4.0, M.get(1,0));
assertEquals(6.0, M.get(1,2));
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void initializeOne( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix m = MatrixFactory.create(4, 4);
m.initializeOne();
assertEquals(mType, m.getClass());
for( int i = 0; i < m.rows(); i++ ) {
for( int j = 0; j < m.columns(); j++ ) {
assertEquals(1.0, m.get(i, j));
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void initializeZero( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix m = MatrixFactory.create(4, 4);
m.initializeZero();
assertEquals(mType, m.getClass());
for( int i = 0; i < m.rows(); i++ ) {
for( int j = 0; j < m.columns(); j++ ) {
assertEquals(0.0, m.get(i, j));
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void initializeRandom( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix m = MatrixFactory.create(4, 4);
m.initializeRandom();
assertEquals(mType, m.getClass());
for( int i = 0; i < m.rows(); i++ ) {
for( int j = 0; j < m.columns(); j++ ) {
double d = m.get(i, j);
assertTrue(-1.0 <= d && d < 1.0);
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void multiplyTransposed( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix A = MatrixFactory.create(new double[][]{
{1.0, 2.0, 3.0, 4.0},
{1.0, 2.0, 3.0, 4.0},
{1.0, 2.0, 3.0, 4.0}
});
MLMatrix B = MatrixFactory.create(new double[][]{
{1, 3, 5, 7},
{2, 4, 6, 8}
});
MLMatrix C = A.multiplyTransposed(B);
assertEquals(mType, A.getClass());
assertEquals(mType, B.getClass());
assertEquals(mType, C.getClass());
assertEquals(3, C.rows());
assertEquals(2, C.columns());
for( int i = 0; i < C.rows(); i++ ) {
assertEquals(50.0, C.get(i, 0));
assertEquals(60.0, C.get(i, 1));
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void multiplyAddBias( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix A = MatrixFactory.create(new double[][]{
{1.0, 2.0, 3.0, 4.0},
{1.0, 2.0, 3.0, 4.0},
{1.0, 2.0, 3.0, 4.0}
});
MLMatrix B = MatrixFactory.create(new double[][]{
{1.0, 2.0},
{3.0, 4.0},
{5.0, 6.0},
{7.0, 8.0}
});
MLMatrix V = MatrixFactory.create(new double[][]{
{1000.0, 2000.0}
});
MLMatrix C = A.multiplyAddBias(B, V);
assertEquals(mType, A.getClass());
assertEquals(mType, B.getClass());
assertEquals(mType, C.getClass());
assertEquals(mType, V.getClass());
assertEquals(3, C.rows());
assertEquals(2, C.columns());
for( int i = 0; i < C.rows(); i++ ) {
assertEquals(1050.0, C.get(i, 0));
assertEquals(2060.0, C.get(i, 1));
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void transposedMultiplyAndScale( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix A = MatrixFactory.create(new double[][]{
{1, 1, 1},
{2, 2, 2},
{3, 3, 3},
{4, 4, 4}
});
MLMatrix B = MatrixFactory.create(new double[][]{
{1.0, 2.0},
{3.0, 4.0},
{5.0, 6.0},
{7.0, 8.0}
});
MLMatrix C = A.transposedMultiplyAndScale(B, 2.0);
assertEquals(mType, A.getClass());
assertEquals(mType, B.getClass());
assertEquals(mType, C.getClass());
assertEquals(3, C.rows());
assertEquals(2, C.columns());
for( int i = 0; i < C.rows(); i++ ) {
assertEquals(100.0, C.get(i, 0));
assertEquals(120.0, C.get(i, 1));
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void apply( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 1, 1},
{2, 2, 2},
{3, 3, 3},
{4, 4, 4}
});
MLMatrix R = M.apply(( d ) -> d * d);
assertEquals(mType, M.getClass());
assertEquals(mType, R.getClass());
assertNotSame(M, R);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(i + 1) * (i + 1), R.get(i, j),
msg("(%d,%d)", "apply", i, j)
);
}
}
MLMatrix M2 = M.applyInPlace(( d ) -> d * d * d);
assertSame(M, M2);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(i + 1) * (i + 1) * (i + 1), M.get(i, j),
msg("(%d,%d)", "applyInPlace", i, j)
);
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void add( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 1, 1},
{2, 2, 2},
{3, 3, 3},
{4, 4, 4}
});
MLMatrix R = M.add(M);
assertEquals(mType, M.getClass());
assertEquals(mType, R.getClass());
assertNotSame(M, R);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(i + 1) + (i + 1), R.get(i, j),
msg("(%d,%d)", "add", i, j)
);
}
}
MLMatrix M2 = M.addInPlace(R);
assertSame(M, M2);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(i + 1) + (i + 1) + (i + 1), M.get(i, j),
msg("(%d,%d)", "addInPlace", i, j)
);
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void sub( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 1, 1},
{2, 2, 2},
{3, 3, 3},
{4, 4, 4}
});
MLMatrix R = M.sub(M);
assertEquals(mType, M.getClass());
assertEquals(mType, R.getClass());
assertNotSame(M, R);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
0.0, R.get(i, j),
msg("(%d,%d)", "sub", i, j)
);
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void colSums( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 2, 3},
{1, 2, 3},
{1, 2, 3},
{1, 2, 3}
});
MLMatrix R = M.colSums();
assertEquals(mType, M.getClass());
assertEquals(mType, R.getClass());
assertNotSame(M, R);
assertEquals(1, R.rows());
assertEquals(3, R.columns());
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(j+1)*4, R.get(0, j),
msg("(%d,%d)", "colSums", 0, j)
);
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void duplicate( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 2, 3},
{1, 2, 3},
{1, 2, 3},
{1, 2, 3}
});
MLMatrix R = M.duplicate();
assertEquals(mType, M.getClass());
assertEquals(mType, R.getClass());
assertNotSame(M, R);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
M.get(i, j), R.get(i, j),
msg("(%d,%d)", "duplicate", i, j)
);
}
}
}
@ParameterizedTest
@ValueSource( classes = {DoubleMatrix.class, MatrixFactory.ColtMatrix.class} )
void scale( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
MLMatrix M = MatrixFactory.create(new double[][]{
{1, 1, 1},
{2, 2, 2},
{3, 3, 3},
{4, 4, 4}
});
MLMatrix M2 = M.scaleInPlace(2.0);
assertEquals(mType, M.getClass());
assertEquals(mType, M2.getClass());
assertSame(M, M2);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
(i+1)*2.0, M2.get(i, j),
msg("(%d,%d)", "scaleInPlace", i, j)
);
}
}
MLMatrix M3 = M.scaleInPlace(M);
assertSame(M, M3);
for( int i = 0; i < M.rows(); i++ ) {
for( int j = 0; j < M.columns(); j++ ) {
assertEquals(
((i+1)*2.0)*((i+1)*2.0), M.get(i, j),
msg("(%d,%d)", "addInPlace", i, j)
);
}
}
}
private String msg( String msg, String methodName, Object... args ) {
String testName = this.info.getTestMethod().get().getName();
String className = MatrixFactory.matrixType.getSimpleName();
return String.format("[" + testName + "(" + className + ") " + methodName + "()] " + msg, args);
}
//@ParameterizedTest
//@ValueSource( classes = {MatrixFactory.ColtMatrix.class, DoubleMatrix.class} )
void speed( Class<? extends MLMatrix> mType ) {
MatrixFactory.matrixType = mType;
int N = 10;
int rows = 1000;
int cols = 1000;
Timer timer = new Timer();
MLMatrix M = MatrixFactory.create(rows, cols);
timer.start();
for( int i = 0; i < N; i++ ) {
M.initializeRandom();
}
timer.stop();
System.err.println(msg("%d iterations: %d ms", "initializeRandom", N, timer.getMillis()));
timer.reset();
MLMatrix B = MatrixFactory.create(rows*2, M.columns());
B.initializeRandom();
timer.start();
for( int i = 0; i < N; i++ ) {
M.multiplyTransposed(B);
}
timer.stop();
System.err.println(msg("%d iterations: %d ms", "multiplyTransposed", N, timer.getMillis()));
}
}

View File

@@ -1,57 +0,0 @@
package schule.ngb.zm.ml;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
class MatrixTest {
@Test
void initializeIdentity() {
Matrix m = new Matrix(4, 4);
m.initializeIdentity();
assertArrayEquals(new double[]{1.0, 0.0, 0.0, 0.0}, m.coefficients[0]);
assertArrayEquals(new double[]{0.0, 1.0, 0.0, 0.0}, m.coefficients[1]);
assertArrayEquals(new double[]{0.0, 0.0, 1.0, 0.0}, m.coefficients[2]);
assertArrayEquals(new double[]{0.0, 0.0, 0.0, 1.0}, m.coefficients[3]);
}
@Test
void initializeOne() {
Matrix m = new Matrix(4, 4);
m.initializeOne();
double[] ones = new double[]{1.0, 1.0, 1.0, 1.0};
assertArrayEquals(ones, m.coefficients[0]);
assertArrayEquals(ones, m.coefficients[1]);
assertArrayEquals(ones, m.coefficients[2]);
assertArrayEquals(ones, m.coefficients[3]);
}
@Test
void initializeZero() {
Matrix m = new Matrix(4, 4);
m.initializeZero();
double[] zeros = new double[]{0.0, 0.0, 0.0, 0.0};
assertArrayEquals(zeros, m.coefficients[0]);
assertArrayEquals(zeros, m.coefficients[1]);
assertArrayEquals(zeros, m.coefficients[2]);
assertArrayEquals(zeros, m.coefficients[3]);
}
@Test
void initializeRandom() {
Matrix m = new Matrix(4, 4);
m.initializeRandom(-1, 1);
assertTrue(Arrays.stream(m.coefficients[0]).allMatch((d) -> -1.0 <= d && d < 1.0));
assertTrue(Arrays.stream(m.coefficients[1]).allMatch((d) -> -1.0 <= d && d < 1.0));
assertTrue(Arrays.stream(m.coefficients[2]).allMatch((d) -> -1.0 <= d && d < 1.0));
assertTrue(Arrays.stream(m.coefficients[3]).allMatch((d) -> -1.0 <= d && d < 1.0));
}
}

View File

@@ -2,15 +2,14 @@ package schule.ngb.zm.ml;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Timer;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
class NeuralNetworkTest {
@BeforeAll
@@ -18,7 +17,14 @@ class NeuralNetworkTest {
Log.enableGlobalDebugging();
}
@Test
@BeforeAll
static void setupMatrixLibrary() {
Constants.setSeed(1001);
//MatrixFactory.matrixType = MatrixFactory.ColtMatrix.class;
MatrixFactory.matrixType = DoubleMatrix.class;
}
/*@Test
void readWrite() {
// XOR Dataset
NeuralNetwork net = new NeuralNetwork(2, 4, 1);
@@ -53,7 +59,7 @@ class NeuralNetworkTest {
}
assertArrayEquals(net.predict(inputs), net2.predict(inputs));
}
}*/
@Test
void learnXor() {
@@ -78,14 +84,14 @@ class NeuralNetworkTest {
}
// calculate predictions
double[][] predictions = net.predict(inputs);
MLMatrix predictions = net.predict(inputs);
for( int i = 0; i < 4; i++ ) {
int parsed_pred = predictions[i][0] < 0.5 ? 0 : 1;
int parsed_pred = predictions.get(i, 0) < 0.5 ? 0 : 1;
System.out.printf(
"{%.0f, %.0f} = %.4f (%d) -> %s\n",
inputs[i][0], inputs[i][1],
predictions[i][0],
predictions.get(i, 0),
parsed_pred,
parsed_pred == outputs[i][0] ? "correct" : "miss"
);
@@ -109,12 +115,16 @@ class NeuralNetworkTest {
for( int i = 0; i < trainingData.size(); i++ ) {
inputs[i][0] = trainingData.get(i).a;
inputs[i][1] = trainingData.get(i).b;
outputs[i][0] = trainingData.get(i).result;
outputs[i][0] = trainingData.get(i).getResult();
}
Timer timer = new Timer();
System.out.println("Training the neural net to learn "+OPERATION+"...");
timer.start();
net.train(inputs, outputs, TRAINING_CYCLES);
System.out.println(" finished training");
timer.stop();
System.out.println(" finished training (" + timer.getMillis() + "ms)");
for( int i = 1; i <= net.getLayerCount(); i++ ) {
System.out.println("Layer " +i + " weights");
@@ -136,19 +146,18 @@ class NeuralNetworkTest {
System.out.printf(
"Prediction on data (%.2f, %.2f) was %.4f, expected %.2f (of by %.4f)\n",
data.a, data.b,
net.getOutput()[0][0],
data.result,
net.getOutput()[0][0] - data.result
net.getOutput().get(0, 0),
data.getResult(),
net.getOutput().get(0, 0) - data.getResult()
);
}
private List<TestData> createTrainingSet( int trainingSetSize, CalcType operation ) {
Random random = new Random();
List<TestData> tuples = new ArrayList<>();
for( int i = 0; i < trainingSetSize; i++ ) {
double s1 = random.nextDouble() * 0.5;
double s2 = random.nextDouble() * 0.5;
double s1 = Constants.random() * 0.5;
double s2 = Constants.random() * 0.5;
switch( operation ) {
case ADD:
@@ -181,7 +190,6 @@ class NeuralNetworkTest {
double a;
double b;
double result;
CalcType type;
TestData( double a, double b ) {
@@ -189,6 +197,8 @@ class NeuralNetworkTest {
this.b = b;
}
abstract double getResult();
}
private static final class AddData extends TestData {
@@ -197,7 +207,9 @@ class NeuralNetworkTest {
public AddData( double a, double b ) {
super(a, b);
result = a + b;
}
double getResult() {
return a+b;
}
}
@@ -208,7 +220,9 @@ class NeuralNetworkTest {
public SubData( double a, double b ) {
super(a, b);
result = a - b;
}
double getResult() {
return a-b;
}
}
@@ -219,7 +233,9 @@ class NeuralNetworkTest {
public MulData( double a, double b ) {
super(a, b);
result = a * b;
}
double getResult() {
return a*b;
}
}
@@ -233,7 +249,9 @@ class NeuralNetworkTest {
if( b == 0.0 ) {
b = .1;
}
result = a / b;
}
double getResult() {
return a/b;
}
}
@@ -244,7 +262,12 @@ class NeuralNetworkTest {
public ModData( double b, double a ) {
super(b, a);
result = a % b;
if( b == 0.0 ) {
b = .1;
}
}
double getResult() {
return a%b;
}
}

View File

@@ -1,6 +1,7 @@
package schule.ngb.zm.util;
import org.junit.jupiter.api.Test;
import schule.ngb.zm.util.io.FileLoader;
import java.util.List;

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.events;
package schule.ngb.zm.util.events;
import org.junit.jupiter.api.Test;