Merge branch 'main' into games

This commit is contained in:
ngb
2022-12-19 15:54:27 +01:00
222 changed files with 27793 additions and 5936 deletions

View File

@@ -0,0 +1,194 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.Stroke;
/**
* Basisimplementierung der {@link Strokeable} und {@link Fillable} APIs.
*
* Die Klasse bietet eine Grundlage zur Implementierung eigener Zeichenobjekte,
* die eine Füllung und eine Konturlinie haben können.
*/
public abstract class BasicDrawable extends Constants implements Strokeable, Fillable {
/**
* Ob das Objekt gezeichnet werden soll.
*/
protected boolean visible = true;
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn das Objekt ohne
* Kontur dargestellt werden soll.
*/
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
/**
* Die Art der Konturlinie.
*/
protected Options.StrokeType strokeType = SOLID;
/**
* Cache für den aktuellen {@code Stroke} der Kontur. Wird nach Änderung
* einer der Kontureigenschaften auf {@code null} gesetzt und beim nächsten
* Zeichnen neu erstellt.
*/
protected Stroke stroke = null;
/**
* Die aktuelle Füllfarbe der Form oder {@code null}, wenn das Objekt nicht
* gefüllt werden soll.
*/
protected Color fillColor = DEFAULT_FILLCOLOR;
/**
* Der aktuelle Farbverlauf des Objektes oder {@code null}, wenn es keinen
* Farbverlauf besitzt.
*/
protected MultipleGradientPaint fill = null;
// Implementierung Drawable Interface
/**
* Ob das Objekt angezeigt bzw. gezeichnet werden soll.
*
* @return {@code true}, wenn das Objekt angezeigt werden soll,
* {@code false} sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt das Objekt.
*/
public void hide() {
visible = false;
}
/**
* Zeigt das Objekt an.
*/
public void show() {
visible = true;
}
/**
* Versteckt da Objekt, wenn es derzeit angezeigt wird und zeigt es
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
public abstract void draw( Graphics2D graphics );
// Implementierung Fillable Interface
@Override
public void setFill( Paint fill ) {
if( fill == null ) {
this.fill = null;
} else if( fill instanceof Color ) {
this.fillColor = ((Color) fill);
this.fill = null;
} else if( fill instanceof MultipleGradientPaint ) {
this.fillColor = null;
this.fill = (MultipleGradientPaint) fill;
}
}
@Override
public Paint getFill() {
if( fill != null ) {
return fill;
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
return fillColor;
} else {
return null;
}
}
@Override
public boolean hasFillColor() {
return fillColor != null;
}
@Override
public boolean hasGradient() {
return fill != null;
}
@Override
public Color getFillColor() {
return fillColor;
}
@Override
public void setFillColor( Color color ) {
fillColor = color;
fill = null;
}
@Override
public MultipleGradientPaint getGradient() {
return fill;
}
// Implementierung Strokeable Interface
@Override
public void setStroke( Stroke stroke ) {
this.stroke = stroke;
}
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight);
}
return stroke;
}
@Override
public Color getStrokeColor() {
return strokeColor;
}
@Override
public void setStrokeColor( Color color ) {
strokeColor = color;
}
@Override
public double getStrokeWeight() {
return strokeWeight;
}
@Override
public void setStrokeWeight( double weight ) {
strokeWeight = weight;
this.stroke = null;
}
@Override
public Options.StrokeType getStrokeType() {
return strokeType;
}
@Override
public void setStrokeType( Options.StrokeType type ) {
strokeType = type;
this.stroke = null;
}
}

View File

@@ -1,17 +1,25 @@
package schule.ngb.zm;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
/**
* Repräsentiert eine Farbe in der Zeichenmaschine.
* <p>
* Farben bestehen entweder aus einem Grauwert (zwischen <code>0</code> und
* <code>255</code>) oder einem Rot-, Grün- und Blauanteil (jeweils zwischen
* <code>0</code> und <code>255</code>).
* Farben bestehen entweder aus einem Grauwert (zwischen 0 und 255) oder einem
* Rot-, Grün- und Blauanteil (jeweils zwischen 0 und 255).
* <p>
* Eine Farbe hat außerdem einen Transparenzwert zwischen <code>0</code>
* (unsichtbar) und <code>255</code> (deckend).
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend).
*/
public class Color {
@SuppressWarnings( "unused" )
public class Color implements Paint {
//@formatter:off
@@ -44,38 +52,67 @@ public class Color {
* Die Farbe Zeichenmaschinen-Rot.
*/
public static final Color RED = new Color(240, 80, 37);
/**
* Die Farbe Rot.
*/
public static final Color PURE_RED = new Color(255, 0, 0);
/**
* Die Farbe Zeichenmaschinen-Grün.
*/
public static final Color GREEN = new Color(98, 199, 119);
/**
* Die Farbe Grün.
*/
public static final Color PURE_GREEN = new Color(0, 255, 0);
/**
* Die Farbe Zeichenmaschinen-Blau.
*/
public static final Color BLUE = new Color(49, 197, 244);
public static final Color BLUE = new Color(43, 128, 243); // 49, 197, 244
/**
* Die Farbe Blau.
*/
public static final Color PURE_BLUE = new Color(0, 0, 255);
/**
* Die Farbe Zeichenmaschinen-Gelb.
*/
public static final Color YELLOW = new Color(248, 239, 34);
/**
* Die Farbe Gelb.
*/
public static final Color PURE_YELLOW = new Color(255, 255, 0);
/**
* Die Farbe Zeichenmaschinen-Orange.
*/
public static final Color ORANGE = new Color(248, 158, 80);
/**
* Die Farbe Zeichenmaschinen-Türkis.
*/
public static final Color CYAN = new Color(java.awt.Color.CYAN);
/**
* Die Farbe Zeichenmaschinen-Magenta.
*/
public static final Color MAGENTA = new Color(java.awt.Color.MAGENTA);
/**
* Die Farbe Zeichenmaschinen-Rosa.
*/
public static final Color PINK = new Color(240, 99, 164);
/**
* Die Farbe Zeichenmaschinen-Lila.
*/
public static final Color PURPLE = new Color(101, 0, 191);
/**
* Die Farbe Zeichenmaschinen-Braun.
*/
@@ -85,6 +122,7 @@ public class Color {
* Die Farbe Helmholtz-Grün.
*/
public static final Color HGGREEN = new Color(0, 165, 81);
/**
* Die Farbe Helmholtz-Rot.
*/
@@ -104,72 +142,68 @@ public class Color {
}
/**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var>.
* Erstellt eine graue Farbe entsprechend dem Grauwert {@code gray}.
*
* @param gray Ein Grauwert zwischen <code>0</code> und <code>255</code>.
* @param gray Ein Grauwert zwischen 0 und 255.
*/
public Color( int gray ) {
this(gray, gray, gray, 255);
}
/**
* Erstellt eine graue Farbe entsprechend des Grauwertes <var>gray</var> und
* des Transparentwertes <var>alpha</var>.
* Erstellt eine graue Farbe entsprechend dem Grauwert {@code gray} und dem
* Transparenzwert {@code alpha}.
*
* @param gray Ein Grauwert zwischen <code>0</code> und <code>255</code>.
* @param gray Ein Grauwert zwischen 0 und 255.
*/
public Color( int gray, int alpha ) {
this(gray, gray, gray, alpha);
}
/**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>.
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen 0 und 255.
*
* @param red Rotwert zwischen <code>0</code> und <code>255</code>.
* @param green Grünwert zwischen <code>0</code> und <code>255</code>.
* @param blue Blauwert zwischen <code>0</code> und <code>255</code>.
* @return Ein passendes Farbobjekt.
* @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255.
* @param blue Blauwert zwischen 0 und 255.
*/
public Color( int red, int green, int blue ) {
this(red, green, blue, 255);
}
/**
* Erstellt eine Farbe. Die Parameter <var>red</var>, <var>green</var> und
* <var>blue</var> geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>.
* <var>alpha</var> gibt den den Transparentwert an (auch zwischen
* code>0</code> und <code>255</code>), wobei
* <code>0</code> komplett durchsichtig ist und <code>255</code> komplett
* deckend.
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen 0 und 255. {@code alpha} gibt den den Transparentwert an
* (auch zwischen 0 und 255), wobei 0 komplett durchsichtig ist und 255
* komplett deckend.
*
* @param red Rotwert zwischen <code>0</code> und <code>255</code>.
* @param green Grünwert zwischen <code>0</code> und <code>255</code>.
* @param blue Blauwert zwischen <code>0</code> und <code>255</code>.
* @param alpha Transparentwert zwischen <code>0</code> und <code>255</code>.
* @return Ein passendes Farbobjekt.
* @param red Rotwert zwischen 0 und 255.
* @param green Grünwert zwischen 0 und 255.
* @param blue Blauwert zwischen 0 und 255.
* @param alpha Transparentwert zwischen 0 und 255.
*/
public Color( int red, int green, int blue, int alpha ) {
rgba = (alpha << 24) | (red << 16) | (green << 8) | blue;
rgba = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF);
}
/**
* Erstellt eine Farbe als Kopie von <var>color</var>.
* Erstellt eine Farbe als Kopie von {@code color}.
*
* @param color
* @param color Eine Farbe.
*/
public Color( Color color ) {
this(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
this(color.getRGBA(), true);
}
/**
* Erstellt eine Farbe als Kopie von <var>color</var> und ersetzt den
* Transparentwert durch <var>alpha</var>.
* Erstellt eine Farbe als Kopie von {@code color} und ersetzt den
* Transparentwert durch {@code alpha}.
*
* @param color
* @param alpha
* @param color Eine Farbe.
* @param alpha Der neue Transparenzwert.
*/
public Color( Color color, int alpha ) {
this(color.getRed(), color.getGreen(), color.getBlue(), alpha);
@@ -186,10 +220,11 @@ public class Color {
/**
* Interner Konstruktor für die Initialisierung einer Farbe mit einem
* RGBA-Wert.
*
* <p>
* Da der Konstruktor {@link #Color(int)} schon besetzt ist, muss hier der
* Parameter {@code isRGBA} auf {@code true} gesetzt werden, damit {@code rgba}
* korrekt interpretiert wird.
* Parameter {@code isRGBA} auf {@code true} gesetzt werden, damit
* {@code rgba} korrekt interpretiert wird.
*
* @param rgba RGBA-wert der Farbe.
* @param isRGBA Sollte immer {@code true} sein.
*/
@@ -203,46 +238,97 @@ public class Color {
/**
* Erzeugt eine Farbe aus einem kodierten RGBA Integer-Wert.
* <p>
* Der 32-bit Integer enthält (von rechts) in Bit 1 bis 8 den Rotwert, in
* Bit 9 bis 16 Grünwert, in Bit 17 bis 24 den Blauwert und in Bit 25 bis 32
* den Transparenzwert der Farbe.
*
* @param rgba
* @return
* @param rgba Eine RGBA-Farbe.
* @return Ein Farbobjekt.
*/
public static Color getRGBColor( int rgba ) {
Color c = new Color(rgba, true);
return c;
}
public static Color getHSBColor(double h, double s, double b) {
return new Color(java.awt.Color.getHSBColor((float)h, (float)s, (float)b));
}
public static Color getHSLColor(double h, double s, double l) {
int rgb = Color.HSLtoRGB(new float[]{(float)h, (float)s, (float)l});
return Color.getRGBColor(rgb);
}
public static Color parseString( String pColor ) {
pColor = pColor.toLowerCase().strip();
if( pColor.contains("red") || pColor.contains("rot") ) {
return Color.RED.copy();
} else if( pColor.contains("blue") || pColor.contains("blau") ) {
return Color.BLUE.copy();
} else if( pColor.contains("green") || pColor.contains("grün") || pColor.contains("gruen") ) {
return Color.GREEN.copy();
} else if( pColor.contains("yellow") || pColor.contains("gelb") ) {
return Color.YELLOW.copy();
} else {
return new Color();
}
return new Color(rgba, true);
}
/**
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann
* sechs- oder achtstellig sein (wenn ein Transparentwert vorhanden ist).
* Dem Code kann ein <code>#</code> Zeichen vorangestellt sein.
* Erzeugt eine Farbe aus Werten im
* <a href="https://de.wikipedia.org/wiki/HSV-Farbraum">HSB-Farbraum</a>.
* <p>
* {code h} beschreibt den Farbwert (engl. <em>hue</em>), {@code s} die
* Sättigung (engl. <em>saturation</em>) und {@code b} die absolute
* Helligkeit (engl. <em>brightness</em>) der Farbe. Alle Werte werden
* zwischen 0.0 und 1.0 angegeben.
*
* @param hexcode
* @return
* @param h Der Farbwert.
* @param s Die Sättigung.
* @param b Die absolute Helligkeit.
* @return Ein Farbobjekt.
* @see java.awt.Color#getHSBColor(float, float, float)
*/
public static Color getHSBColor( double h, double s, double b ) {
return new Color(java.awt.Color.getHSBColor((float) h, (float) s, (float) b));
}
/**
* Erzeugt eine Farbe aus Werten im
* <a href="https://de.wikipedia.org/wiki/HSV-Farbraum">HSL-Farbraum</a>.
* <p>
* {code h} beschreibt den Farbwert (engl. <em>hue</em>), {@code s} die
* Sättigung (engl. <em>saturation</em>) und {@code l} die relative
* Helligkeit (engl. <em>lightness</em>) der Farbe. Alle Werte werden
* zwischen 0.0 und 1.0 angegeben.
*
* @param h Der Farbwert.
* @param s Die Sättigung.
* @param l Die relative Helligkeit.
* @return Ein Farbobjekt.
*/
public static Color getHSLColor( double h, double s, double l ) {
int rgb = Color.HSLtoRGB(new float[]{(float) h, (float) s, (float) l});
return Color.getRGBColor(rgb);
}
/**
* Erstellt aus einem Farbnamen ein Farbobjekt.
* <p>
* Die gültigen Farbnamen können unter <a
* href="https://htmlcolors.com/color-names">https://htmlcolors.com/color-names</a>
* nachgeschlagen werden.
*
* @param color Der Name einer Farbe.
* @return Ein Farbobjekt.
*/
public static Color parseString( String color ) {
color = color.toLowerCase().strip();
// Parse colornames file and return first match
try( InputStream in = Color.class.getResourceAsStream("colornames.csv"); BufferedReader reader = new BufferedReader(new InputStreamReader(in)) ) {
String line;
while( (line = reader.readLine()) != null ) {
String[] parts = line.split(",");
if( parts.length == 2 ) {
if( parts[0].equals(color) ) {
return Color.parseHexcode(parts[1]);
}
}
}
} catch( IOException ex ) {
// LOG?
}
return new Color();
}
/**
* Erzeugt eine Farbe aus einem hexadezimalen Code. Der Hexcode kann drei-,
* sechs- oder achtstellig sein (wenn ein Transparentwert vorhanden ist).
* Dem Code kann ein {@code #} Zeichen vorangestellt sein, muss es aber
* nicht.
* <p>
* Bei einem dreistelligen Code wird jedes zeichen doppelt interpretiert.
* Das beduetet {@code #ABC} ist gleichbedeutend mit {@code #AABBCC}.
*
* @param hexcode Eine Farbe als Hexcode.
* @return Ein Farbobjekt.
*/
public static Color parseHexcode( String hexcode ) {
if( hexcode.startsWith("#") ) {
@@ -259,8 +345,6 @@ public class Color {
} else if( hexcode.length() == 8 ) {
alpha = Integer.valueOf(hexcode.substring(6, 8), 16);
hexcode = hexcode.substring(0, 6);
} else {
hexcode = hexcode;
}
return Color.getRGBColor((alpha << 24) | Integer.valueOf(hexcode, 16));
@@ -281,7 +365,7 @@ public class Color {
if( color1 == null && color2 == null ) {
throw new IllegalArgumentException("Color.interpolate() needs at least one color to be not null.");
}
if( t < 0.0 || color2 == null ) {
if( (color1 != null && t < 0.0) || color2 == null ) {
return color1.copy();
}
if( t > 1.0 || color1 == null ) {
@@ -307,12 +391,12 @@ public class Color {
if( c == 0 ) {
h_ = 0;
} else if( max == r ) {
h_ = (float) (g - b) / c;
h_ = (g - b) / c;
if( h_ < 0 ) h_ += 6.f;
} else if( max == g ) {
h_ = (float) (b - r) / c + 2.f;
h_ = (b - r) / c + 2.f;
} else if( max == b ) {
h_ = (float) (r - g) / c + 4.f;
h_ = (r - g) / c + 4.f;
}
float h = 60.f * h_;
@@ -331,6 +415,14 @@ public class Color {
return hsl;
}
/**
* Konvertiert die Komponenten einer Farbe aus dem HSL-Farbraum in den
* RGB-Farbraum.
*
* @param hsl Die HSL-Komponenten als float-Array.
* @return Der RGBA-Farbwert.
* @see #HSLtoRGB(float[], int)
*/
public static int HSLtoRGB( float[] hsl ) {
return HSLtoRGB(hsl, 255);
}
@@ -338,10 +430,11 @@ public class Color {
/**
* Konvertiert eine Farbe mit Komponenten im HSL-Farbraum in den
* RGB-Farbraum.
* <p>
* Die Farbkomponenten werden als float-Array übergeben. Im Index 0 steht
* der H-Wert im Bereich 0 bis 360, Index 1 und 2 enthalten den S- und
* L-Wert im Bereich von 0 bis 1.
*
* Die Farbkomponenten werden als Float-Array übergeben. Im Index 0 steht
* der h-Wert im Bereich 0 bis 360, Index 1 und 2 enthalten den s- und
* l-Wert im Bereich von 0 bis 1.
* @param hsl Die Farbkomponenten im HSL-Farbraum.
* @param alpha Ein Transparenzwert im Bereich 0 bis 255.
* @return Der RGBA-Wert der Farbe.
@@ -394,6 +487,7 @@ public class Color {
/**
* Erzeugt eine Kopie dieser Farbe.
*
* @return Ein neues Farbobjekt.
*/
public Color copy() {
@@ -402,10 +496,11 @@ public class Color {
/**
* Gibt den RGBA-Wert dieser Farbe zurück.
*
* Eine Farbe wird als 32-Bit Integer gespeichert. Bits 24-31 enthalten
* den Transparenzwert, 16-23 den Rotwert, 8-15 den Grünwert und 0-7 den
* <p>
* Eine Farbe wird als 32-Bit Integer gespeichert. Bits 24-31 enthalten den
* Transparenzwert, 16-23 den Rotwert, 8-15 den Grünwert und 0-7 den
* Blauwert der Farbe.
*
* @return Der RGBA-Wert der Farbe.
* @see #getRed()
* @see #getGreen()
@@ -418,6 +513,7 @@ public class Color {
/**
* Gibt den Rotwert dieser Farbe zurück.
*
* @return Der Rotwert der Farbe zwischen 0 und 255.
*/
public int getRed() {
@@ -426,6 +522,7 @@ public class Color {
/**
* Gibt den Grünwert dieser Farbe zurück.
*
* @return Der Grünwert der Farbe zwischen 0 und 255.
*/
public int getGreen() {
@@ -434,6 +531,7 @@ public class Color {
/**
* Gibt den Blauwert dieser Farbe zurück.
*
* @return Der Blauwert der Farbe zwischen 0 und 255.
*/
public int getBlue() {
@@ -442,6 +540,7 @@ public class Color {
/**
* Gibt den Transparenzwert dieser Farbe zurück.
*
* @return Der Transparenzwert der Farbe zwischen 0 und 255.
*/
public int getAlpha() {
@@ -450,9 +549,10 @@ public class Color {
/**
* Erzeugt ein {@link java.awt.Color}-Objekt aus dieser Farbe.
* <p>
* Das erzeugte Farbobjekt hat dieselben Rot-, Grün-, Blau- und
* Transparenzwerte wie diese Farbe.
*
* Das erzeugte Farbobjekt hat dieselben Rot-, Grün-, Blau-
* und Transparenzwerte wie diese Farbe.
* @return Ein Java-Farbobjekt.
*/
public java.awt.Color getJavaColor() {
@@ -460,21 +560,48 @@ public class Color {
}
@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;
}
}
/**
* Prüft, ob ein anderes Objekt zu diesem gleich ist.
* <p>
* Die Methode gibt genau dann {@code true} zurück, wenn das andere Objekt
* nicht {@code null} ist, vom Typ {@code Color} ist und es dieselben Rot-,
* Grün-, Blau- und Transparenzwerte hat.
*
* Die Methode gibt genau dann {@code true} zurück, wenn das andere
* Objekt nicht {@code null} ist, vom Typ {@code Color} ist und es
* dieselben Rot-, Grün-, Blau- und Transparenzwerte hat.
* @param obj Das zu vergleichende Objekt.
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/
@Override
public boolean equals( Object obj ) {
return obj instanceof Color && ((Color)obj).getRGBA() == this.rgba;
if( obj == null ) {
return false;
}
if( obj instanceof Color ) {
return ((Color) obj).getRGBA() == this.rgba;
} else if( obj instanceof java.awt.Color ) {
return ((java.awt.Color) obj).getRGB() == this.rgba;
}
return false;
}
/**
* Erzeugt einen Text-String, der diese Farbe beschreibt.
*
* @return Eine Textversion der Farbe.
*/
@Override
@@ -484,6 +611,7 @@ public class Color {
/**
* Berechnet einen Hashcode für dieses Farbobjekt.
*
* @return Ein Hashcode für diese Rabe.
*/
@Override
@@ -492,7 +620,8 @@ public class Color {
}
/**
* Erzeugt eine um 30% hellere Version dieser Farbe.
* Erzeugt eine um 30% hellere Version dieser Farbe.
*
* @return Ein Farbobjekt mit einer helleren Farbe.
*/
public Color brighter() {
@@ -501,6 +630,7 @@ public class Color {
/**
* Erzeugt eine um {@code percent} hellere Version dieser Farbe.
*
* @param percent Eine Prozentzahl zwischen 0 und 100.
* @return Ein Farbobjekt mit einer helleren Farbe.
*/
@@ -512,6 +642,7 @@ public class Color {
/**
* Erzeugt eine um 30% dunklere Version dieser Farbe.
*
* @return Ein Farbobjekt mit einer dunkleren Farbe.
*/
public Color darker() {
@@ -520,6 +651,7 @@ public class Color {
/**
* Erzeugt eine um {@code percent} dunklere Version dieser Farbe.
*
* @param percent Eine Prozentzahl zwischen 0 und 100.
* @return Ein Farbobjekt mit einer dunkleren Farbe.
*/
@@ -530,11 +662,12 @@ public class Color {
}
public Color greyscale() {
return new Color((int)(getRed()*.299 + getGreen()*.587 + getBlue()*0.114));
return new Color((int) (getRed() * .299 + getGreen() * .587 + getBlue() * 0.114));
}
/**
* Erzeugt eine zu dieser invertierte Farbe.
*
* @return Ein Farbobjekt mit der invertierten Farbe.
*/
public Color inverted() {
@@ -544,6 +677,7 @@ public class Color {
/**
* Erzeugt die Komplementärfarbe zu dieser.
*
* @return Ein Farbobjekt mit der Komplementärfarbe.
*/
public Color complement() {
@@ -556,11 +690,12 @@ public class Color {
* Wählt entweder {@link #WHITE weiß} oder {@link #BLACK schwarz} aus, je
* nachdem, welche der Farben besser als Textfarbe mit dieser Farbe als
* Hintergrund funktioniert (besser lesbar ist).
*
* @return Schwarz oder weiß.
*/
public Color textcolor() {
// Basiert auf https://stackoverflow.com/questions/946544/good-text-foreground-color-for-a-given-background-color
if( (getRed()*.299 + getGreen()*.587 + getBlue()*0.114) < 186 ) {
if( (getRed() * .299 + getGreen() * .587 + getBlue() * 0.114) < 186 ) {
return WHITE;
} else {
return BLACK;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,279 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.Shape;
import java.awt.*;
/**
* {@link Drawable} Klassen, die mit einer Füllung versehen werden können.
* <p>
* Das {@code Fillable} Interface dient hauptsächlich zur Vereinheitlichung der
* API für Füllungen. Durch Implementation wird sichergestellt, dass alle
* Objekte, die eine Füllung haben können, dieselben Methoden zur Verfügung
* stellen. Wenn eine {@link Shape} eine
* {@link Fillable#setFillColor(Color, int)} Methode hat, dann sollte auch eine
* {@link schule.ngb.zm.layers.TurtleLayer.Turtle} dieselbe Methode anbieten. Im
* Einzelfall kann es sinnvoll sein, weitere Methoden für Füllungen zur
* Verfügung zu stellen. Allerdings sollte davon nach Möglichkeit zugunsten
* einer einheitlichen API abgesehen werden.
* <p>
* Das Äquivalent für Konturlinien stellt {@link Strokeable} dar.
* <p>
* Im einfachsten Fall reicht es {@link #setFill(Paint)} und {@link #getFill()}
* zu implementieren. Die anderen Methoden besitzen Standardimplementierungen,
* die sich auf die beiden Methoden beziehen. Allerdings ist es in vielen Fällen
* sinnvoll, einige der Methoden gezielt zu überschreiben, um sie an spezifische
* Situationen anzupassen.
*/
public interface Fillable extends Drawable {
/**
* Setzt die Füllung direkt auf das angegebene {@code Paint}-Objekt.
*
* @param fill Die neue Füllung.
*/
void setFill( Paint fill );
/**
* Gibt die aktuell gesetzte Füllung zurück.
* <p>
* Die Art der Füllung kann anhand der Abfragen {@link #hasFillColor()} und
* {@link #hasGradient()} ermittelt werden.
*
* @return Die aktuelle Füllung.
*/
Paint getFill();
/**
* Gibt an, ob aktuell eine sichtbare Füllung konfiguriert ist.
* <p>
* Eine Füllung gilt als sichtbar, wenn eine nciht transparente Füllfarbe
* oder ein Farbverlauf eingestellt ist.
*
* @return {@code true}, wenn die Füllung sichtbar ist, {@code false} sonst.
*/
default boolean hasFill() {
return (hasFillColor() && getFillColor().getAlpha() > 0) || hasGradient();
}
/**
* Gibt an, ob eine Füllfarbe konfiguriert ist.
* <p>
* Im Gegensatz zu {@link #hasFill()} prüft die Methode <em>nicht</em>, ob
* die Füllfarbe transparent ist.
*
* @return {@code true}, wenn eine Füllfarbe gesetzt ist.
*/
default boolean hasFillColor() {
Paint fill = getFill();
return fill instanceof Color;
}
/**
* Gibt an, ob ein Farbverlauf konfiguriert ist.
*
* @return {@code true}, wenn ein Farbverlauf gesetzt ist.
*/
default boolean hasGradient() {
Paint fill = getFill();
return fill instanceof MultipleGradientPaint;
}
/**
* Gibt die aktuelle Füllfarbe der Form zurück.
*
* @return Die aktuelle Füllfarbe oder {@code null}.
*/
default Color getFillColor() {
if( hasFillColor() ) {
return (Color) getFill();
} else {
return null;
}
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @see Color
*/
default void setFillColor( Color color ) {
setFill(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)
*/
default 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)
*/
default void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität und
* dem angegebenen Transparenzwert. Der Grauwert 0 entspricht schwarz, 255
* entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
default 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>
*/
default 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>
*/
default void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Füllung der Form.
*/
default void noFill() {
setFillColor(null);
noGradient();
}
/**
* Setzt die Füllfarbe auf den Standardwert zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_FILLCOLOR
*/
default void resetFill() {
setFillColor(Constants.DEFAULT_FILLCOLOR);
noGradient();
}
/**
* Gibt den aktuellen Farbverlauf der Form zurück.
*
* @return Der aktuelle Farbverlauf oder {@code null}.
*/
default MultipleGradientPaint getGradient() {
if( hasGradient() ) {
return (MultipleGradientPaint) getFill();
} else {
return null;
}
}
/**
* 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.
*/
default void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
setFill(new LinearGradientPaint(
(float) fromX, (float) fromY,
(float) toX, (float) toY,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()}
));
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der in die angegebene
* Richtung verläuft.
*
* @param from Farbe am Startpunkt.
* @param to Farbe am Endpunkt.
* @param dir Richtung des Farbverlaufs.
*/
default void setGradient( Color from, Color to, Options.Direction dir ) {
int whalf = (Constants.canvasWidth / 2);
int hhalf = (Constants.canvasHeight / 2);
setGradient(whalf - dir.x * whalf, hhalf - dir.y * hhalf, from, whalf + dir.x * whalf, hhalf + dir.y * hhalf, to);
}
/**
* 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.
*/
default void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
setFill(new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()}));
}
/**
* Setzt die Füllung auf einen kreisförmigen (radialen) Farbverlauf, der im
* Zentrum beginnt.
*
* @param from Farbe im Zentrum.
* @param to Farbe am Rand.
*/
default void setGradient( Color from, Color to ) {
int whalf = (Constants.canvasWidth / 2);
int hhalf = (Constants.canvasHeight / 2);
setGradient(whalf, hhalf, Math.min(whalf, hhalf), from, to);
}
/**
* Entfernt den Farbverlauf von der Form.
*/
default void noGradient() {
setFill(null);
}
}

View File

@@ -0,0 +1,228 @@
package schule.ngb.zm;
import java.awt.AlphaComposite;
import java.awt.Color;
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.
* <p>
* Ebenen sind ein zentraler Bestandteil bei der Implementierung einer
* {@link Zeichenmaschine}. Sie erben von {@code Constants}, da neue Ebenentypen
* von Nutzern implementiert werden können.
*/
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;
/**
* Erstellt eine neue Ebene mit den Standardmaßen.
*/
public Layer() {
this(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
/**
* Erstellt eine neue Ebene mit den angegebenen Maßen.
*
* @param width Die Breite der Ebene.
* @param height Die Höhe der Ebene.
*/
public Layer( int width, int height ) {
createCanvas(width, height);
}
/**
* @return Die Breite der Ebene.
*/
public int getWidth() {
return buffer.getWidth();
}
/**
* @return Die Höhe der Ebene.
*/
public int getHeight() {
return buffer.getHeight();
}
/**
* Ändert die Größe der Ebene auf die angegebenen Maß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 ) {
recreateCanvas(width, height);
}
} else {
createCanvas(width, height);
}
}
// TODO: prevent access to graphics context?
public Graphics2D getGraphics() {
return this.drawing;
}
/**
* Gibt die Ressourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
drawing = null;
buffer = null;
}
/**
* Erstellt einen neuen Puffer für die Ebene und konfiguriert diesen.
*
* @param width Breite des neuen Puffers.
* @param height Höhe des neuen Puffers.
*/
private void createCanvas( int width, int height ) {
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
drawing = buffer.createGraphics();
// add antialiasing
RenderingHints hints = new RenderingHints(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
hints.put(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON
);
hints.put(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY
);
drawing.addRenderingHints(hints);
}
/**
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe und
* kopiert den Inhalt des alten Puffers in den Neuen.
*
* @param width Breite des neuen Puffers.
* @param height Höhe des neuen Puffers.
*/
private void recreateCanvas( int width, int height ) {
BufferedImage oldCanvas = buffer;
createCanvas(width, height);
drawing.drawImage(oldCanvas, 0, 0, null);
}
/**
* Leert die Ebene und löscht alles bisher gezeichnete. Alle Pixel der Ebene
* werden transparent, damit unterliegende Ebenen durchscheinen können.
*/
public void clear() {
// https://stackoverflow.com/questions/31149206/set-pixels-of-bufferedimage-as-transparent
Color current = drawing.getColor();
drawing.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR));
drawing.setColor(new Color(255, 255, 255, 255));
drawing.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
drawing.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
drawing.setColor(current);
}
/**
* Zeichnet den Puffer auf den Grafikkontext.
*
* @param graphics Der Grafikkontext, auf den gezeichnet wird.
*/
@Override
public void draw( Graphics2D graphics ) {
if( visible ) {
graphics.drawImage(buffer, 0, 0, null);
}
}
@Override
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Ebene.
*/
public void hide() {
visible = false;
}
/**
* Zeigt die Ebene an, falls sie versteckt war.
*/
@SuppressWarnings( "unused" )
public void show() {
visible = true;
}
/**
* Versteckt oder zeigt die Ebene, je nachdem, welchen Zustand sie derzeit
* hat.
*/
@SuppressWarnings( "unused" )
public void toggle() {
visible = !visible;
}
@Override
public void update( double delta ) {
}
@Override
public boolean isActive() {
return active;
}
/**
* Prüft, ob die angegebenen Koordinaten innerhalb der Ebene liegen, oder
* nicht.
* <p>
* Eine Koordinate liegt in der Ebene, wenn die {@code x}- und
* {@code y}-Koordinaten größer oder gleich Null und kleiner als die Breite
* bzw. Höhe der Ebene sind.
*
* @param x Die x-Koordinate.
* @param y Die y-Koordinate.
* @return {@code true}, wenn die Koordinaten innerhalb der Ebene liegen,
* {@code false}, wenn sie außerhalb liegen.
*/
@SuppressWarnings( "unused" )
public boolean isInBounds( int x, int y ) {
return (x >= 0 && y >= 0 && x < getWidth() && y < getHeight());
}
}

View File

@@ -0,0 +1,334 @@
package schule.ngb.zm;
import java.awt.geom.Arc2D;
/**
* Diese Klasse sammelt Enumerationen, die verschiedene Eigenschaften der zu
* zeichnenden Formen darstellen.
*/
public final class Options {
private Options() {
}
/**
* Linienstile für Konturlinien.
*/
public enum StrokeType {
/**
* Durchgezogene Linien.
*/
SOLID,
/**
* Getrichelte Linien.
*/
DASHED,
/**
* Gepunktete Linien.
*/
DOTTED
}
/**
* Stile für Pfeilspitzen.
*/
public enum ArrowHead {
/**
* Einfache Pfeilspitze aus zwei Linien.
*/
LINES,
/**
* Gefülltes Dreieck.
*/
FILLED
}
/**
* Arten von Bögen.
* <p>
* Die Werte legen fest, wie Bögen geschlossen werden sollen, wenn sie
* beispielsweise gefüllt werden.
* <p>
* Wrapper für die AWT-Konstanten in {@link Arc2D}.
*/
public enum PathType {
/**
* Offener Pfad, bei dem die Pfadenden direkt miteinander verbunden
* werden ohne eine Linie zu ziehen.
*/
OPEN(Arc2D.OPEN),
/**
* Geschlossener Pfad, bei dem die Pfadenden direkt miteinander
* verbunden werden, indem eine Linie gezogen wird.
*/
CLOSED(Arc2D.CHORD),
/**
* Geschlossener Pfad, bei dem Linien von den Pfadenden zum Mittelpunkt
* des Kreises, der den Kreisbogen festlegt, gezogen werden.
*/
PIE(Arc2D.PIE);
/**
* Der entsprechende Wert der Konstanten in {@link Arc2D}
*/
public final int awt_type;
PathType( int type ) {
awt_type = type;
}
}
/**
* Zustände in denen sich die Zeichenmaschine befinden kann.
*/
public enum AppState {
/**
* Die Zeichenmaschine befindet sich in der Initialisierung. Die
* Laufzeitumgebung wird konfiguriert und alle nötigen Komonenten
* ({@link Zeichenfenster}, {@link Zeichenleinwand}, ...) werden
* erstellt.
*/
INITIALIZING,
/**
* Die Initialisierung der Zeichenmaschine ist beendet, aber der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde noch nicht gestartet.
*/
INITIALIZED,
/**
* Die Zeichenmaschine wurde gestartet und der
* {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* arbeitet.
*/
RUNNING,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde pausiert.
*/
PAUSED,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wurde gestoppt, die Zeichenmaschine ist aber noch nicht vollständig
* heruntergefahren und hat noch nicht alle Ressourcen freigegeben.
*/
STOPPED,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* ist beendet.
*/
TERMINATED,
/**
* Die Zeichenmaschine ist dabei, vollständig herunterzufahren und alle
* Ressourcen freizugeben.
*/
QUITING,
/**
* Der {@link schule.ngb.zm.Zeichenmaschine.Zeichenthread Zeichenthread}
* wartet gerade auf den nächsten Frame.
*/
IDLE,
/**
* Die Zeichenmaschine führt gerade
* {@link Zeichenmaschine#update(double)} aus.
*/
UPDATING,
/**
* Die Zeichenmaschine führt gerade {@link Zeichenmaschine#draw()} aus.
*/
DRAWING,
/**
* Die Ausführung der Zeichenmaschine wurde mit
* {@link Zeichenmaschine#delay(int)} verzögert und wartet auf
* Fortsetzung.
*/
DELAYED,
/**
* Die Zeichenmaschine sendet gereade gesammelte Events und führt Tasks
* aus.
*/
DISPATCHING
}
/**
* Richtungen für die Ausrichtung von Formen. Richtungen sind durch
* Einheitsvektoren bzw. deren Kombination im Koordinatensystem der
* Zeichenfläche repräsentiert, wodurch mit ihnen gerechnet werden kann. Die
* Richtung {@link #DOWN} ist beispielsweise gleich {@code (1, 0)}.
* <p>
* Jede Richtung ist zusätzlich als Himmelsrichtung definiert. {@link #EAST}
* ist äquivalent zu {@link #RIGHT} als {@code (0, 1)} definiert. Auch wenn
* beide Werte dieselbe Richtung beschreiben sind sie nicht "gleich"
* ({@code EAST != RIGHT}). Um verschiedene Richtungen zuverlässig zu
* vergleichen, sollte daher {@link #equals(Direction)} verwendet werden.
* <p>
* Für zusammengesetzten Richtungen wie {@link #NORTHEAST} bzw
* {@link #UPRIGHT} lassen sich mit {@link #in(Direction)} und
* {@link #contains(Direction)} Beziehungen zu den anderen Richtungen
* prüfen. Beispielsweise ist {@code NORTHEAST.contains(NORTH)} wahr.
*/
public enum Direction {
CENTER(0, 0),
NORTH(0, -1),
EAST(1, 0),
SOUTH(0, 1),
WEST(-1, 0),
NORTHEAST(1, -1),
SOUTHEAST(1, 1),
SOUTHWEST(-1, 1),
NORTHWEST(-1, -1),
MIDDLE(CENTER),
UP(NORTH),
RIGHT(EAST),
DOWN(SOUTH),
LEFT(WEST),
UPLEFT(NORTHWEST),
DOWNLEFT(SOUTHWEST),
DOWNRIGHT(SOUTHEAST),
UPRIGHT(NORTHEAST);
public final byte x, y;
Direction( int x, int y ) {
this.x = (byte) x;
this.y = (byte) y;
}
Direction( Direction original ) {
this.x = original.x;
this.y = original.y;
}
/**
* Prüft, ob die angegebene Richtung gleich dieser ist. Dabei werden die
* Komponenten des Richtungsvektors geprüft. Daher sind für die Methode
* beispielsweise {@link #NORTH} und {@link #UP} gleich.
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn die Richtungen dieselben Komponenten
* haben, {@code false} sonst.
*/
public boolean equals( Direction dir ) {
return this.x == dir.x && this.y == dir.y;
}
/**
* Prüft, ob diese Richtung Tile der angegebenen Richtung ist.
* <p>
* Beispielsweise ist {@link #NORTH} Teil von {@link #NORTHWEST}, aber
* nicht von {@link #SOUTHWEST}. Dabei wird doe Art der Richtung nicht
* beachtet. {@link #UP} ist daher auch Teil von {@link #NORTHWEST}.
*
* <pre>
* NORTH.in(NORTHWEST) // true
* NORTH.in(SOUTHWEST) // false
* UP.in(NORTHWEST) // true
* </pre>
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn diese Richtungen Teil der anderen ist,
* {@code false} sonst.
*/
public boolean in( Direction dir ) {
return (this.x == dir.x && this.y == 0) || (this.y == dir.y && this.x == 0) || (this.x == dir.x && this.y == dir.y);
}
/**
* Prüft, ob die angegebene Richtung Teil dieser Richtung ist.
* <p>
* Beispielsweise ist {@link #NORTH} Teil von {@link #NORTHWEST}, aber
* nicht von {@link #SOUTHWEST}. Dabei wird die Art der Richtung nicht
* beachtet. {@link #UP} ist daher auch Teil von {@link #NORTHWEST}.
*
* <pre>
* NORTHWEST.contains(NORTH) // true
* SOUTHWEST.in(NORTH) // false
* NORTHWEST.in(UP) // true
* </pre>
*
* @param dir Eine andere Richtung.
* @return {@code true}, wenn diese Richtungen Teil der anderen ist,
* {@code false} sonst.
*/
public boolean contains( Direction dir ) {
return dir.in(this);
}
/**
* @return Diese Richtung als Vektor-Objekt.
*/
public Vector asVector() {
return new Vector(x, y);
}
/**
* Liefert die entgegengesetzte Richtung zu dieser.
* <p>
* Es wird die Art der Richtung berücksichtigt. Das bedeutet, das
* Inverse von {@link #UP} ist {@link #DOWN}, während das Inverse von
* {@link #NORTH} zu {@link #SOUTH} wird.
*
* @return Die entgegengesetzte Richtung zu dieser.
*/
public Direction inverse() {
switch( this ) {
case UP:
return DOWN;
case DOWN:
return UP;
case LEFT:
return RIGHT;
case RIGHT:
return LEFT;
case UPLEFT:
return DOWNRIGHT;
case UPRIGHT:
return DOWNLEFT;
case DOWNLEFT:
return UPRIGHT;
case DOWNRIGHT:
return UPLEFT;
case MIDDLE:
return MIDDLE;
case NORTH:
return SOUTH;
case SOUTH:
return NORTH;
case EAST:
return WEST;
case WEST:
return EAST;
case SOUTHWEST:
return NORTHEAST;
case SOUTHEAST:
return NORTHWEST;
case NORTHEAST:
return SOUTHWEST;
case NORTHWEST:
return SOUTHEAST;
default:
return CENTER;
}
}
}
}

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 graphics ) {
clear();
List<Drawable> it = List.copyOf(drawables);
for( Drawable d: it ) {
if( d.isVisible() ) {
d.draw(drawing);
}
}
super.draw(graphics);
}
}
}

View File

@@ -0,0 +1,231 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.Shape;
import java.awt.BasicStroke;
import java.awt.Stroke;
/**
* {@link Drawable} Klassen, die mit einer Konturlinie versehen werden können.
* <p>
* Das {@code Strokeable} Interface dient hauptsächlich zur Vereinheitlichung
* der API für Konturlinien. Durch Implementation wird sichergestellt, dass alle
* Objekte, die eine Konturlinie haben können, dieselben Methoden zur Verfügung
* stellen. Wenn eine {@link Shape} eine
* {@link Strokeable#setStrokeColor(Color, int)} Methode hat, dann sollte auch
* eine {@link schule.ngb.zm.layers.TurtleLayer.Turtle} dieselbe Methode
* anbieten. Im Einzelfall kann es sinnvoll sein, weitere Methoden für
* Konturlinien zur verfügung zu stellen. Allerdings sollte davon nach
* Möglichkeit zugunsten einer einheitlichen API abgesehen werden.
* <p>
* Das Äquivalent für Füllungen stellt {@link Fillable} dar.
*/
public interface Strokeable extends Drawable {
/**
* Setzt den {@code Stroke} für die Konturlinie direkt.
*
* @param stroke Ein {@code Stroke}-Objekt.
*/
void setStroke( Stroke stroke );
/**
* Gibt ein {@code Stroke}-Objekt mit den aktuell gesetzten Eigenschaften
* zurück.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
Stroke getStroke();
/**
* Gibt an, ob die aktuell gesetzten Eigenschaften eine sichtbare
* Konturlinie erzeugen.
* <p>
* Die Konturlinie gilt als sichtbar, wenn sie eine nicht transparente Farbe
* und eine Dicke größer 0 besitzt.
* <p>
* Das bedeutet, falls die Methode {@code false} zurückgibt, dann kann
* {@link #getStroke()} trotzdem ein gültiges {@link Stroke}-Objekt
* zurückgeben, beispielsweise wenn keine Farbe gesetzt wurde.
*
* @return {@code true}, wenn die Konturlinie sichtbar ist, {@code false}
* sonst.
*/
default boolean hasStroke() {
Color strokeColor = getStrokeColor();
double strokeWeight = getStrokeWeight();
return strokeColor != null && strokeColor.getAlpha() > 0 && strokeWeight > 0;
}
/**
* Gibt die aktuelle Farbe der Konturlinie zurück.
*
* @return Die Konturfarbe oder {@code null}.
*/
Color getStrokeColor();
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe.
*
* @param color Die neue Farbe der Konturlinie.
* @see Color
*/
void setStrokeColor( Color 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)
*/
default 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)
*/
default void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
default 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>
*/
default 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>
*/
default void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Kontur der Form.
*/
default 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
*/
default void resetStroke() {
setStrokeColor(Constants.DEFAULT_STROKECOLOR);
setStrokeWeight(Constants.DEFAULT_STROKEWEIGHT);
setStrokeType(Constants.SOLID);
}
/**
* Gibt die Dicke der Konturlinie zurück.
*
* @return Die aktuelle Dicke der Linie.
*/
double getStrokeWeight();
/**
* 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.
*/
default void setStrokeWeight( double weight ) {
setStroke(createStroke(getStrokeType(), weight));
}
/**
* Gibt die Art der Konturlinie zurück.
*
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
Options.StrokeType getStrokeType();
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link Constants#DASHED},
* {@link Constants#DOTTED} und {@link Constants#SOLID}.
*
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
default void setStrokeType( Options.StrokeType type ) {
setStroke(createStroke(type, getStrokeWeight()));
}
/**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
* zwischengespeichert.
*
* @param strokeType
* @param strokeWeight
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
static Stroke createStroke( Options.StrokeType strokeType, double strokeWeight ) {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
case DASHED:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
case SOLID:
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
}
}
}

View File

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

View File

@@ -23,6 +23,7 @@ import java.awt.geom.Point2D;
* Der Vektor der Zeichenmaschine erweitert die Klasse {@link Point2D} und lässt
* sich dadurch einfach mit den Klassen des {@link java.awt} Pakets benutzen.
*/
@SuppressWarnings( "unused" )
public class Vector extends Point2D.Double {
/**
@@ -127,7 +128,7 @@ public class Vector extends Point2D.Double {
}
/**
* Erzeugt einen neuen Vektor mit derselben Richtun wie der angegebene
* Erzeugt einen neuen Vektor mit derselben Richtung wie der angegebene
* Vektor und der Länge 1.
*
* @param vector Der original Vektor.
@@ -197,6 +198,7 @@ public class Vector extends Point2D.Double {
* @param y Der neue y-Wert.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( double x, double y ) {
this.x = x;
this.y = y;
@@ -210,6 +212,7 @@ public class Vector extends Point2D.Double {
* @param vector Ein Vektor.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Vector vector ) {
x = vector.x;
y = vector.y;
@@ -223,6 +226,7 @@ public class Vector extends Point2D.Double {
* @param pPunkt Ein Punkt.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector set( Point2D pPunkt ) {
x = pPunkt.getX();
x = pPunkt.getY();
@@ -271,6 +275,7 @@ public class Vector extends Point2D.Double {
/**
* Legt die Länge des Vektors fest.
*
* @param length Die neue Länge des Vektors.
* @return Dieser Vektor selbst (method chaining)
*/
@@ -294,9 +299,9 @@ public class Vector extends Point2D.Double {
}
/**
*
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector normalize() {
double len = length();
if( len != 0 && len != 1 ) {
@@ -308,9 +313,11 @@ public class Vector extends Point2D.Double {
/**
* Addiert den Vektor {@code vector} zu diesem.
*
* @param vector Ein anderer Vektor.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( Vector vector ) {
x += vector.x;
y += vector.y;
@@ -319,10 +326,12 @@ public class Vector extends Point2D.Double {
/**
* Addiert die angegebenen Werte zur x- und y-Komponente des Vektors.
*
* @param x Summand x-Komponente.
* @param y Summand y-Komponente.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector add( double x, double y ) {
this.x += x;
this.y += y;
@@ -342,24 +351,28 @@ public class Vector extends Point2D.Double {
return vec;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( Vector vector ) {
x -= vector.x;
y -= vector.y;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector sub( double x, double y ) {
this.x -= x;
this.y -= y;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector scale( double scalar ) {
x *= scalar;
y *= scalar;
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public Vector div( double scalar ) {
if( scalar == 0.0 ) {
throw new IllegalArgumentException("Can't divide by zero.");
@@ -409,8 +422,8 @@ public class Vector extends Point2D.Double {
* dem quadrierten Abstand durchführen, wenn auch die gewünschte Entfernung
* quadriert wird.
*
* @param vector
* @return
* @param vector Ein anderer Vektor.
* @return Das Quadrat der Entfernung zum anderen Vektor.
*/
public double distanceSq( Vector vector ) {
return super.distanceSq(vector);
@@ -487,6 +500,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #setLength(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double max ) {
if( lengthSq() > max * max ) {
setLength(max);
@@ -503,6 +517,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #setLength(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector limit( double min, double max ) {
if( min > max ) {
double t = min;
@@ -556,6 +571,7 @@ public class Vector extends Point2D.Double {
* @return Dieser Vektor selbst (method chaining)
* @see #rotate(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector rotateRad( double rad ) {
double temp = x;
x = x * Math.cos(rad) - y * Math.sin(rad);
@@ -587,6 +603,7 @@ public class Vector extends Point2D.Double {
* @param t Ein Wert zwischen 0 und 1.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector morph( Vector vector, double t ) {
double tt = Math.min(Math.max(t, 0.0), 1.0);
x = x + (vector.x - x) * tt;
@@ -631,6 +648,7 @@ public class Vector extends Point2D.Double {
* @param t Ein Wert zwischen 0 und 1.
* @return Dieser Vektor selbst (method chaining)
*/
@SuppressWarnings( "UnusedReturnValue" )
public Vector interpolate( Vector vector, double t ) {
x = x + (vector.x - x) * t;
y = y + (vector.y - y) * t;

View File

@@ -0,0 +1,371 @@
package schule.ngb.zm;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.io.ImageLoader;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
/**
* 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 {
/**
* Setzt das Look and Feel auf den Standard des Systems.
* <p>
* Sollte einmalig vor erstellen des erstyen Programmfensters aufgerufen
* werden.
*/
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());
}
}
/**
* Ermittelt ein {@link GraphicsDevice Anzeigegerät}, auf dem ein neues
* Zeichenfenster angezeigt werden soll. In der Regel ist dies der
* Bildschirm, auf dem sich derzeit der Mauszeiger befindet. Kann kein
* solcher Bildschirm ermittelt werden, wird das
* {@link GraphicsEnvironment#getDefaultScreenDevice() Standardgerät}
* zurückgegeben.
*
* @return Das Anzeigegerät, auf dem ein neues Fenster angezeigt werden
* sollte.
*/
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 Anzeigegerät, auf dem die Zeichenmaschine gestartet wurde (muss nicht
* gleich dem Aktuellen sein, wenn das Fenster verschoben wurde).
*/
private final 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 final KeyListener fullscreenExitListener = new KeyAdapter() {
@Override
public void keyPressed( KeyEvent e ) {
if( e.getKeyCode() == KeyEvent.VK_ESCAPE ) {
// canvas.removeKeyListener(this);
setFullscreen(false);
e.consume();
}
}
};
// Die Zeichenleinwand dieses Fensters.
private final Zeichenleinwand canvas;
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und einer
* {@link Zeichenleinwand} in der angegebenen Größe.
*
* @param width Die Breite der Zeichenleinwand.
* @param height Die Höhe der Zeichenleinwand.
* @param title Der Titel des Fensters.
*/
@SuppressWarnings( "unused" )
public Zeichenfenster( int width, int height, String title ) {
this(new Zeichenleinwand(width, height), title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und einer
* {@link Zeichenleinwand} in der angegebenen Größe auf dem angegebenen
* Anzeigegerät.
*
* @param width Die Breite der Zeichenleinwand.
* @param height Die Höhe der Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
@SuppressWarnings( "unused" )
public Zeichenfenster( int width, int height, String title, GraphicsDevice displayDevice ) {
this(new Zeichenleinwand(width, height), title, displayDevice);
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand}.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title ) {
this(canvas, title, getGraphicsDevice());
}
/**
* Erstellt ein neues Zeichenfenster mit dem angegebenen Titel und der
* angegebene {@link Zeichenleinwand} auf dem angegebenen Anzeigegerät.
*
* @param canvas Die Zeichenleinwand.
* @param title Der Titel des Fensters.
* @param displayDevice Das Anzeigegerät für das Fenster.
*/
public Zeichenfenster( Zeichenleinwand canvas, String title, GraphicsDevice displayDevice ) {
super(Validator.requireNotNull(displayDevice, "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.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 ) {
InputStream iconStream = this.getClass().getResourceAsStream("icon_512.png");
if( iconStream != null ) {
Image icon = ImageIO.read(iconStream);
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon);
} else {
LOG.warn("Could not load dock-icon");
}
} else {
ArrayList<Image> icons = new ArrayList<>(4);
for( int size : new int[]{32, 64, 128, 512} ) {
URL icnUrl = Zeichenmaschine.class.getResource("icon_" + size + ".png");
if( icnUrl != null ) {
icons.add(ImageIO.read(icnUrl));
}
}
if( icons.isEmpty() ) {
LOG.warn("Could not load dock-icon");
} else {
this.setIconImages(icons);
}
}
} catch( IllegalArgumentException | IOException e ) {
LOG.warn("Could not load image icons: %s", e.getMessage());
} 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.setFocusable(true);
this.setLocationByPlatform(true);
// this.centerFrame();
}
/**
* Liefert das Anzeigegerät, auf dem dieses Fenster erstellt wurde.
* <p>
* Das Anzeigegerät muss nicht unbedingt gleich dem sein, auf dem sich das
* Fenster derzeit befindet, wenn das Fenster verschoben wurde.
*
* @return Das Anzeigegerät.
*/
@SuppressWarnings( "unused" )
public GraphicsDevice getDisplayDevice() {
return displayDevice;
}
/**
* Liefert die Abmessungen des Anzeigegeräts, auf dem das Fenster gestartet
* wurde.
*
* @return Die Abmessungen des Anzeigegeräts.
*/
public Rectangle getScreenBounds() {
// return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return displayDevice.getDefaultConfiguration().getBounds();
}
/**
* Liefert die Zeichenleinwand dieses Fensters.
*
* @return Die Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public Zeichenleinwand getCanvas() {
return canvas;
}
/**
* Liefert die Abmessungen der Zeichenleinwand zurück.
*
* @return Die Abmessungen der Zeichenleinwand.
*/
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)
);
}
/**
* Setzt die Größe der Zeichenleinwand auf die angegebenen Werte.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
*/
public void setCanvasSize( int newWidth, int newHeight ) {
// TODO: (ngb) Put constrains 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() ) {
// Temporarily stop rendering
while( canvas.isRendering() ) {
try {
canvas.suspendRendering();
} catch( InterruptedException ex ) {
LOG.info(ex, "setFullsceen(true) was interrupted and canceled.");
return;
}
}
if( pEnable && !fullscreen ) {
// Activate fullscreen
dispose();
setUndecorated(true);
setResizable(false);
displayDevice.setFullScreenWindow(this);
// Register ESC to exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
// Reset canvas size to its new bounds to recreate buffer and drawing surface
java.awt.Rectangle canvasBounds = getCanvasBounds();
canvas.setSize(canvasBounds.width, canvasBounds.height);
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = true;
} else if( !pEnable && fullscreen ) {
displayDevice.setFullScreenWindow(null);
dispose();
setUndecorated(false);
setResizable(false);
canvas.removeKeyListener(fullscreenExitListener);
canvas.setSize(canvasPreferredWidth, canvasPreferredHeight);
setVisible(true);
pack();
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = false;
}
// Resume rendering
canvas.resumeRendering();
}
}
/**
* Prüft, ob sich dieses Zeichenfenster im Vollbild befindet.
*
* @return {@code true}, wenn das Fenster im Vollbild ist, {@code false}
* sonst.
*/
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win.equals(this);
}
private static final Log LOG = Log.getLogger(Zeichenfenster.class);
}

View File

@@ -0,0 +1,355 @@
package schule.ngb.zm;
import schule.ngb.zm.util.Log;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
* mehreren Ebenen, auf denen auf verschiedene Arten gezeichnet werden kann. Die
* Ebenen lassen sich beliebig übereinander legen, ausblenden oder wieder
* löschen.
* <p>
* Jede Ebene besitzt eine Zeichenfläche, auf der ihre Zeichnung liegt. Diese
* Zeichenflächen werden pro Frame einmal von "unten" nach "oben" auf diese
* Leinwand gezeichnet.
*/
public class Zeichenleinwand extends Canvas {
// Lokales Lock für Rendervorgänge.
private final Object[] renderLock = new Object[0];
// Liste der hinzugefügten Ebenen.
private final List<Layer> layers;
// Status der Zeichenleinwand.
private boolean rendering = false, suspended = false;
/**
* Erstellt eine neue Zeichenleinwand mit einer festen Größe.
*
* @param width Breite der Zeichenleinwand.
* @param height Höhe der Zeichenleinwand.
*/
public Zeichenleinwand( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
layers = Collections.synchronizedList(new LinkedList<>());
}
/**
* Ob die Leinwand ihren Inhalt gerade zeichnet.
*
* @return {@code true}, wenn die Inhalte gerade gezeichnet werden.
*/
public boolean isRendering() {
return rendering;
}
/**
* Pausiert das Zeichnen der Leinwand kurzzeitig.
* <p>
* Falls die Leinwand gerade beim Zeichnen ist
* ({@code isRendering() == true}, blockt die Methode den aufrufenden Thread
* so lange, bis das Rendern beendet ist. Danach wird die Ebene nicht mehr
* neu gezeichnet, bis {@link #resumeRendering()} aufgerufen wird.
* <p>
* Das Zeichnen sollte nur dann unterbrochen werden, wenn sich der Kontext
* der Canvas-Komponente in seinem Elterncontainer ändert, um Fehler bei
* einer fehlenden Container-Hierarchie zu vermeiden.
*
* @throws InterruptedException Falls der Thread beim Warten unterbrochen
* wird.
*/
public void suspendRendering() throws InterruptedException {
synchronized( renderLock ) {
if( isRendering() ) {
renderLock.wait();
}
suspended = true;
}
}
/**
* Setzt das Zeichnen der Leinwand fort, falls es zuvor mit
* {@link #suspendRendering()} ausgesetzt wurde.
*/
public void resumeRendering() {
suspended = false;
}
/**
* Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln.
* <p>
* Bei einer Größenänderung wird auch die Größe aller bisher hinzugefügter
* {@link Layer Ebenen} angepasst, sodass sie die gesamte Leinwand füllen.
*
* @param width Neue Width der Leinwand in Pixeln.
* @param height Neue Höhe der Leinwand in Pixeln.
*/
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
this.setPreferredSize(getSize());
this.setMinimumSize(getSize());
synchronized( layers ) {
for( Layer layer : layers ) {
layer.setSize(width, height);
}
}
}
/**
* Fügt der Zeichenleinwand eine Ebene hinzu, die oberhalb aller bisherigen
* Ebenen eingefügt wird.
*
* @param layer Die neue Ebene.
*/
public void addLayer( Layer layer ) {
if( layer != null ) {
layer.setSize(getWidth(), getHeight());
layers.add(layer);
}
}
/**
* Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn
* der Index noch nicht existiert (also größer als die
* {@link #getLayerCount() Anzahl der Ebenen} ist), dann wird die neue Ebene
* als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein,
* dass die neue Ebene am Ende wirklich am Index {@code i} steht.
*
* @param i Index der Ebene beginnend mit 0.
* @param layer Die neue Ebene.
*/
public void addLayer( int i, Layer layer ) {
if( layer != null ) {
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
}
}
/**
* Gibt die Anzahl der {@link Layer Ebenen} in dieser Leinwand zurück.
*
* @return Die Anzahl der Ebenen.
*/
public int getLayerCount() {
return layers.size();
}
/**
* Gibt eine Kopie der Liste der bisher hinzugefügten Ebenen zurück.
*
* @return Liste der Ebenen.
*/
public List<Layer> getLayers() {
return List.copyOf(layers);
}
/**
* Holt die Ebene am Index {@code i} (beginnend bei 0).
*
* @param i Index der Ebene (beginnend bei 0).
* @return Die Ebene am Index {@code i} oder {@code null}.
* @throws IndexOutOfBoundsException Falls der Index nicht existiert.
*/
public Layer getLayer( int i ) {
if( layers.size() > i ) {
return layers.get(i);
} else {
throw new IndexOutOfBoundsException("No layer at index " + i + " (max: " + (layers.size() - 1) + ").");
}
}
/**
* Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben.
*
* @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> Typ der Ebenen, die abgefragt werden.
* @return Erste Ebene vom angegeben Typ.
*/
public <L extends Layer> L getLayer( Class<L> type ) {
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
return type.cast(layer);
}
}
}
return null;
}
/**
* Sucht alle Ebenen von einem bestimmten Typ aus der Liste der Ebenen und
* gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der
* Reihenfolge der Ebenen in der Leinwand (von unten nach oben).
*
* @param type Klasse der Ebenen, die abgefragt werden.
* @param <L> Typ der Ebenen, die abgefragt werden.
* @return Eine Liste mit den vorhandenen Ebenen des abgefragten Typs.
*/
@SuppressWarnings( "unused" )
public <L extends Layer> List<L> getLayers( Class<L> type ) {
ArrayList<L> result = new ArrayList<>(layers.size());
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
result.add(type.cast(layer));
}
}
}
return result;
}
/**
* Entfernt die angegebene Ebene von dieser Zeichenleinwand.
*
* @param pLayer Die Ebene, die entfernt werden soll.
* @return {@code true}, wenn die Liste vorhanden war und entfernt wurde,
* {@code false} sonst.
*/
@SuppressWarnings( "unused" )
public boolean removeLayer( Layer pLayer ) {
return layers.remove(pLayer);
}
/**
* Entfernt alle angegebenen Ebenen von dieser Zeichenleinwand.
*
* @param removeLayers Die Ebenen, die entfernt werden sollen.
*/
@SuppressWarnings( "unused" )
public void removeLayers( Layer... removeLayers ) {
synchronized( layers ) {
for( Layer layer : removeLayers ) {
layers.remove(layer);
}
}
}
/**
* Entfernt alle vorhandenen Ebenen von dieser Zeichenleinwand.
*/
@SuppressWarnings( "unused" )
public void clearLayers() {
layers.clear();
}
/**
* Aktualisiert alle {@link Layer Ebenen}, die dieser Zeichenleinwand
* hinzugefügt wurden.
*
* @param delta Die Zeit seit dem letzten Aufruf in Sekunden.
* @see Layer#update(double)
*/
public void updateLayers( double delta ) {
synchronized( layers ) {
for( Layer layer : List.copyOf(layers) ) {
layer.update(delta);
}
}
}
/**
* Erstellt eine passende {@link BufferStrategy} für diese Ebene.
*/
public void allocateBuffer() {
this.createBufferStrategy(2);
}
/**
* Löscht alle Ebenen der Zeichenebene und gibt deren Ressourcen frei.
*/
public void dispose() {
synchronized( layers ) {
for( Layer layer : layers ) {
layer.dispose();
}
}
}
@Override
public void paint( Graphics g ) {
render();
}
/**
* Zeichnet den Inhalt aller {@link Layer Ebenen} in den Grafikkontext.
*
* @param graphics Der Grafikkontext.
*/
public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
for( Layer layer : layers ) {
layer.draw(g2d);
}
}
g2d.dispose();
}
/**
* Zeigt den aktuellen Inhalt der Zeichenleinwand an.
*/
public void render() {
if( !suspended && isDisplayable() ) {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
synchronized( renderLock ) {
rendering = true;
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) {
for( Layer layer : List.copyOf(layers) ) {
layer.draw(g2d);
}
}
g2d.dispose();
} while( strategy.contentsRestored() );
// Display the buffer
if( !strategy.contentsLost() ) {
strategy.show();
Toolkit.getDefaultToolkit().sync();
}
// Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() );
}
rendering = false;
renderLock.notifyAll();
}
}
}
private static final Log LOG = Log.getLogger(Zeichenleinwand.class);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
/**
* Dieses Paket enthält Klassen zur Animation von
* {@link schule.ngb.zm.shapes.Shape} Objekten auf einem
* {@link schule.ngb.zm.layers.ShapesLayer}.
* <p>
* Mit den Animationsklassen lassen sich neben {@code Shape} Objekten aber auch
* andere Objekte animieren.
* <p>
* Das Paket setzt auf den funktionalen Programmierschnittstellen von Java auf
* und kann als Einführung in das Paradigma dienen.
*/
package schule.ngb.zm.anim;

View File

@@ -0,0 +1,224 @@
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>
* Ein {@code ColorLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
* <p>
* Die Farbe der Ebene kann beliebig gesetzt werden und kann gut als
* Hintergrundfarbe 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();
}
@Override
public void setSize( int width, int height ) {
super.setSize(width, height);
clear();
}
/**
* @return Die aktuelle Hintergrundfarbe der Ebene.
*/
public Color getColor() {
return color;
}
/**
* Setzt die Farbe der Ebene auf die angegebene Farbe.
*
* @param color Die neue Hintergrundfarbe.
*/
public void setColor( Color color ) {
this.color = color;
this.background = color.getJavaColor();
clear();
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setColor( int gray ) {
setColor(gray, gray, gray, 255);
}
/**
* Setzt die Farbe der Ebene auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setColor( int gray, int alpha ) {
setColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue ) {
setColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Ebene auf die Farbe mit den angegebenen Rot-, Grün-
* und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setColor( int red, int green, int blue, int alpha ) {
setColor(new Color(red, green, blue, alpha));
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der in die
* angegebene Richtung verläuft.
*
* @param from Farbe am Startpunkt.
* @param to Farbe am Endpunkt.
* @param dir Richtung des Farbverlaufs.
*/
public void setGradient( Color from, Color to, Options.Direction dir ) {
double halfW = getWidth() * .5;
double halfH = getHeight() * .5;
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);
}
/**
* Setzt die Füllung der Ebene auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
this.color = from;
background = new GradientPaint(
(float) fromX, (float) fromY, from.getJavaColor(),
(float) toX, (float) toY, to.getJavaColor()
);
clear();
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, der im Zentrum beginnt.
*
* @param from Farbe im Zentrum.
* @param to Farbe am Rand.
*/
public void setGradient( Color from, Color to ) {
setGradient(getWidth() * .5, getHeight() * .5, Math.min(getWidth() * .5, getHeight() * .5), from, to);
}
/**
* Setzt die Füllung der Ebene auf einen kreisförmigen (radialen)
* Farbverlauf, mit dem Zentrum im Punkt ({@code centerX}, {@code centerY})
* und dem angegebenen Radius. Der Verlauf starte im Zentrum mit der Farbe
* {@code from} und endet am Rand des durch den Radius beschriebenen Kreises
* mit der Farbe {@code to}.
*
* @param centerX x-Koordinate des Kreismittelpunktes.
* @param centerY y-Koordinate des Kreismittelpunktes.
* @param radius Radius des Kreises.
* @param from Farbe im Zentrum des Kreises.
* @param to Farbe am Rand des Kreises.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
this.color = from;
background = new RadialGradientPaint(
(float) centerX, (float) centerY, (float) radius,
new float[]{0f, 1f},
new java.awt.Color[]{from.getJavaColor(), to.getJavaColor()});
clear();
}
/**
* Zeichnet den Hintergrund der Ebene mit der gesetzten Füllung neu.
*/
@Override
public void clear() {
drawing.setPaint(background);
drawing.fillRect(0, 0, getWidth(), getHeight());
}
}

View File

@@ -0,0 +1,106 @@
package schule.ngb.zm.layers;
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;
/**
* Ein Layer um {@link Drawable} Objekte zu zeichnen.
* <p>
* Objekte, die das {@code Drawable} Interface implementieren, können der Ebene
* hinzugefügt werden. Die Ebene sorgt dafür, dass alle {@code Drawable}s einmal
* pro Frame über ihre {@link Drawable#draw(Graphics2D)} Methode gezeichnet.
*/
@SuppressWarnings( "unused" )
public class DrawableLayer extends Layer {
/**
* Liste der {@link Drawable}s.
*/
protected final List<Drawable> drawables;
/**
* Ob die Ebene bei jedem Aufruf von {@link #draw(Graphics2D)} geleert
* werden soll.
*/
protected boolean clearBeforeDraw = true;
/**
* Erstellt eine Ebene in der Standardgröße.
*/
public DrawableLayer() {
drawables = new LinkedList<>();
}
/**
* Erstellt eine Ebene mit der angegebenen Größe.
*
* @param width Die Breite der Ebene.
* @param height Die Höhe der Ebene.
*/
public DrawableLayer( int width, int height ) {
super(width, height);
drawables = new LinkedList<>();
}
/**
* Fügt alle angegebenen {@code Drawable}s der Ebene hinzu.
*
* @param drawables Die {@code Drawable} Objekte.
*/
public void add( Drawable... drawables ) {
synchronized( this.drawables ) {
Collections.addAll(this.drawables, drawables);
}
}
/**
* Gibt eine Liste aller {@code Drawable} Objekte dieser Ebene zurück.
*
* @return Die Liste der {@code Drawable} Objekte.
*/
public java.util.List<Drawable> getDrawables() {
return drawables;
}
/**
* Ob die Ebene bei jedem Frame automatisch gelöscht wird.
*
* @return {@code true}, wenn die Ebene vorm Zeichnen gelöscht wird,
* {@code false} sonst.
*/
public boolean isClearBeforeDraw() {
return clearBeforeDraw;
}
/**
* Stellt ein, ob die Ebene vorm Zeichnen gelöscht werden soll.
*
* @param pClearBeforeDraw Ob die Ebene vorm Zeichnen gelöscht werden
* soll.
*/
public void setClearBeforeDraw( boolean pClearBeforeDraw ) {
this.clearBeforeDraw = pClearBeforeDraw;
}
@Override
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) {
if( d.isVisible() ) {
d.draw(drawing);
}
}
super.draw(graphics);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,139 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Graphics2D;
import java.awt.Image;
/**
* Eine Ebene, die ein statisches Bild anzeigt.
* <p>
* Die Ebene wird mit einem Bild initialisiert und zeigt dieses Bild als
* einzigen Inhalt an. Optional kann die Position des Bildes verändert werden,
* sodass es nicht im Ursprung der Ebene gezeichnet wird.
*/
@SuppressWarnings( "unused" )
public class ImageLayer extends Layer {
/**
* Das Bild, das angezeigt wird.
*/
protected Image image;
/**
* x-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double x = 0;
/**
* y-Koordinate der oberen linken Ecke auf der Ebene.
*/
protected double y = 0;
/**
* Interner Schalter, ob die Ebene neu gezeichnet werden muss.
*/
protected boolean redraw = true;
/**
* Erstellt eine Bildebene in der Standardgröße aus der angegebenen
* Bildquelle.
*
* @param source Eine Bildquelle.
* @see ImageLoader#loadImage(String)
*/
public ImageLayer( String source ) {
image = ImageLoader.loadImage(source);
}
/**
* Erstellt eine Bildebene in der Standardgröße aus dem angegebenen Bild.
*
* @param image Ein Bild-Objekt.
*/
public ImageLayer( Image image ) {
this.image = image;
}
/**
* Erstellt eine Bildebene in der angegebenen Größe aus dem angegebenen
* Bild.
*
* @param width Breite der Bildebene.
* @param height Höhe der Bildebene.
* @param image Ein Bild-Objekt.
*/
public ImageLayer( int width, int height, Image image ) {
super(width, height);
this.image = image;
}
/**
* Setzt das Bild der Ebene auf das angegebene Bild-Objekt.
*
* @param image Ein Bild-Objekt.
*/
public void setImage( Image image ) {
this.image = image;
redraw = true;
}
/**
* @return Die x-Koordinate des Bildes in der Ebene.
*/
public double getX() {
return x;
}
/**
* Setzt die {@code x}-Koordinate des BIldes in der Ebene auf den
* angegebenen Wert.
*
* @param pX Die x-Koordinate des Bildes.
*/
public void setX( double pX ) {
this.x = pX;
redraw = true;
}
/**
* @return Die y-Koordinate des Bildes in der Ebene.
*/
public double getY() {
return y;
}
/**
* Setzt die {@code y}-Koordinate des BIldes in der Ebene auf den
* angegebenen Wert.
*
* @param pY Die y-Koordinate des Bildes.
*/
public void setY( double pY ) {
this.y = pY;
redraw = true;
}
/**
* Löscht die Ebene und zeichnet das Bild neu.
* <p>
* In der Regel muss die Ebene nicht gelöscht werden, da sie automatisch neu
* gezeichnet wird, sobald sich das zugrundeliegende Bild ändert.
*/
@Override
public void clear() {
super.clear();
redraw = true;
}
@Override
public void draw( Graphics2D graphics ) {
if( redraw && visible ) {
drawing.drawImage(image, (int) x, (int) y, null);
redraw = false;
}
super.draw(graphics);
}
}

View File

@@ -0,0 +1,171 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Layer;
import schule.ngb.zm.anim.Animation;
import schule.ngb.zm.anim.AnimationFacade;
import schule.ngb.zm.anim.Easing;
import schule.ngb.zm.shapes.Shape;
import java.awt.Graphics2D;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
/**
* Eine Ebene um {@link Shape} Objekte zu zeichnen.
* <p>
* Ein {@code ShapesLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
*/
@SuppressWarnings( "unused" )
public class ShapesLayer extends Layer {
/**
*
*/
protected boolean clearBeforeDraw = true;
private final List<Shape> shapes;
private final List<Animation<? extends Shape>> animations;
public ShapesLayer() {
super();
shapes = new LinkedList<>();
animations = new LinkedList<>();
}
public ShapesLayer( int width, int height ) {
super(width, height);
shapes = new LinkedList<>();
animations = new LinkedList<>();
}
public Shape getShape( int index ) {
return shapes.get(index);
}
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
return shapeClass.cast(s);
}
}
return null;
}
public List<Shape> getShapes() {
return shapes;
}
public <ST extends Shape> List<ST> getShapes( Class<ST> shapeClass ) {
List<ST> result = new LinkedList<>();
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
result.add(shapeClass.cast(s));
}
}
return result;
}
public void add( Shape... shapes ) {
synchronized( this.shapes ) {
Collections.addAll(this.shapes, shapes);
}
}
public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
this.shapes.addAll(shapes);
}
}
public void remove( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
}
}
public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
this.shapes.removeAll(shapes);
}
}
public void removeAll() {
synchronized( shapes ) {
shapes.clear();
}
}
public void showAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.show();
}
}
}
public void hideAll() {
synchronized( shapes ) {
for( Shape s : shapes ) {
s.hide();
}
}
}
public <S extends Shape> void play( Animation<S> anim ) {
this.animations.add(anim);
anim.start();
}
@SafeVarargs
public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim);
anim.start();
}
}
public <S extends Shape> void play( Animation<S> anim, int runtime ) {
play(anim, runtime, Easing.DEFAULT_EASING);
}
public <S extends Shape> void play( Animation<S> anim, int runtime, DoubleUnaryOperator easing ) {
AnimationFacade<S> facade = new AnimationFacade<>(anim, runtime, easing);
play(facade);
}
@Override
public void update( double delta ) {
Iterator<Animation<? extends Shape>> it = animations.iterator();
while( it.hasNext() ) {
Animation<? extends Shape> anim = it.next();
anim.update(delta);
if( !anim.isActive() ) {
it.remove();
}
}
}
@Override
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
synchronized( shapes ) {
List<Shape> it = List.copyOf(shapes);
for( Shape s : it ) {
if( s.isVisible() ) {
s.draw(drawing);
}
}
}
super.draw(graphics);
}
}

View File

@@ -1,18 +1,25 @@
package schule.ngb.zm.turtle;
package schule.ngb.zm.layers;
import schule.ngb.zm.*;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import schule.ngb.zm.Vector;
import schule.ngb.zm.shapes.FilledShape;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Strokeable;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class TurtleLayer extends Layer {
/**
* Eine Ebene, auf der eine Turtle gesteuert werden kann, die Grafiken plottet.
* <p>
* Die Turtle verhält sich ähnlich zu ihren Entsprechungen in Logo oder
* TigerJython.
*/
@SuppressWarnings( "unused" )
public class TurtleLayer extends Layer implements Strokeable, Fillable {
// Rotating by the clock
public static final int H1 = 30;
@@ -39,7 +46,7 @@ public class TurtleLayer extends Layer {
public static final int H12 = 360;
private static Stack<Color> turtleColors;
private final static Stack<Color> turtleColors;
static {
turtleColors = new Stack<>();
@@ -51,9 +58,9 @@ public class TurtleLayer extends Layer {
turtleColors.add(Color.BLUE);
}
private Turtle mainTurtle = null;
private final Turtle mainTurtle;
private ArrayList<Turtle> turtles = new ArrayList<Turtle>(6);
private final List<Turtle> turtles = new ArrayList<>(6);
public TurtleLayer() {
super();
@@ -117,7 +124,20 @@ public class TurtleLayer extends Layer {
}
}
// Begin of delegate methods (auto-generated)
// begin of delegate methods (auto-generated)
@Override
public boolean isVisible() {
return mainTurtle.isVisible();
}
public void beginPath() {
mainTurtle.beginPath();
}
public void closePath() {
mainTurtle.closePath();
}
public void fill() {
mainTurtle.fill();
@@ -175,100 +195,196 @@ public class TurtleLayer extends Layer {
mainTurtle.moveTo(x, y);
}
public Color getFillColor() {
return mainTurtle.getFillColor();
@Override
public void setFill( Paint fill ) {
mainTurtle.setFill(fill);
}
@Override
public Paint getFill() {
return mainTurtle.getFill();
}
@Override
public void setFillColor( Color color ) {
mainTurtle.setFillColor(color);
}
public void setFillColor( int gray ) {
mainTurtle.setFillColor(gray);
}
public void noFill() {
mainTurtle.noFill();
}
public void setFillColor( int gray, int alpha ) {
mainTurtle.setFillColor(gray, alpha);
}
public void setFillColor( int red, int green, int blue ) {
mainTurtle.setFillColor(red, green, blue);
}
public void setFillColor( int red, int green, int blue, int alpha ) {
mainTurtle.setFillColor(red, green, blue, alpha);
}
public void resetFill() {
mainTurtle.resetFill();
}
public Color getStrokeColor() {
return mainTurtle.getStrokeColor();
@Override
public Color getFillColor() {
return mainTurtle.getFillColor();
}
@Override
public void setStrokeColor( Color color ) {
mainTurtle.setStrokeColor(color);
}
public void setStrokeColor( int gray ) {
mainTurtle.setStrokeColor(gray);
}
public void noStroke() {
mainTurtle.noStroke();
}
public void setStrokeColor( int gray, int alpha ) {
mainTurtle.setStrokeColor(gray, alpha);
}
public void setStrokeColor( int red, int green, int blue ) {
mainTurtle.setStrokeColor(red, green, blue);
}
public void setStrokeColor( int red, int green, int blue, int alpha ) {
mainTurtle.setStrokeColor(red, green, blue, alpha);
}
public double getStrokeWeight() {
return mainTurtle.getStrokeWeight();
}
@Override
public void setStrokeWeight( double weight ) {
mainTurtle.setStrokeWeight(weight);
}
@Override
public Options.StrokeType getStrokeType() {
return mainTurtle.getStrokeType();
}
@Override
public void setStrokeType( Options.StrokeType type ) {
mainTurtle.setStrokeType(type);
}
@Override
public void setGradient( Color from, Color to, Options.Direction dir ) {
mainTurtle.setGradient(from, to, dir);
}
@Override
public void setGradient( Color from, Color to ) {
mainTurtle.setGradient(from, to);
}
@Override
public boolean hasFill() {
return mainTurtle.hasFill();
}
@Override
public boolean hasFillColor() {
return mainTurtle.hasFillColor();
}
@Override
public boolean hasGradient() {
return mainTurtle.hasGradient();
}
@Override
public void setFillColor( Color color, int alpha ) {
mainTurtle.setFillColor(color, alpha);
}
@Override
public void setFillColor( int gray ) {
mainTurtle.setFillColor(gray);
}
@Override
public void setFillColor( int gray, int alpha ) {
mainTurtle.setFillColor(gray, alpha);
}
@Override
public void setFillColor( int red, int green, int blue ) {
mainTurtle.setFillColor(red, green, blue);
}
@Override
public void setFillColor( int red, int green, int blue, int alpha ) {
mainTurtle.setFillColor(red, green, blue, alpha);
}
@Override
public void noFill() {
mainTurtle.noFill();
}
@Override
public void resetFill() {
mainTurtle.resetFill();
}
@Override
public MultipleGradientPaint getGradient() {
return mainTurtle.getGradient();
}
@Override
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
mainTurtle.setGradient(fromX, fromY, from, toX, toY, to);
}
@Override
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
mainTurtle.setGradient(centerX, centerY, radius, from, to);
}
@Override
public void noGradient() {
mainTurtle.noGradient();
}
@Override
public void setStroke( Stroke stroke ) {
mainTurtle.setStroke(stroke);
}
@Override
public Stroke getStroke() {
return mainTurtle.getStroke();
}
@Override
public boolean hasStroke() {
return mainTurtle.hasStroke();
}
@Override
public Color getStrokeColor() {
return mainTurtle.getStrokeColor();
}
@Override
public void setStrokeColor( Color color, int alpha ) {
mainTurtle.setStrokeColor(color, alpha);
}
@Override
public void setStrokeColor( int gray ) {
mainTurtle.setStrokeColor(gray);
}
@Override
public void setStrokeColor( int gray, int alpha ) {
mainTurtle.setStrokeColor(gray, alpha);
}
@Override
public void setStrokeColor( int red, int green, int blue ) {
mainTurtle.setStrokeColor(red, green, blue);
}
@Override
public void setStrokeColor( int red, int green, int blue, int alpha ) {
mainTurtle.setStrokeColor(red, green, blue, alpha);
}
@Override
public void noStroke() {
mainTurtle.noStroke();
}
@Override
public void resetStroke() {
mainTurtle.resetStroke();
}
public void addPosToPath() {
mainTurtle.addPosToPath();
@Override
public double getStrokeWeight() {
return mainTurtle.getStrokeWeight();
}
public void closePath() {
mainTurtle.closePath();
}
// End of delegate methods (auto-generated)
// end of delegate methods (auto-generated)
public class Turtle extends FilledShape {
/**
* Die Turtle der Zeichenmaschine.
*/
public class Turtle extends BasicDrawable {
private static final int STD_SIZE = 12;
private static final int DEFAULT_SIZE = 12;
boolean penDown = true;
@@ -282,7 +398,31 @@ public class TurtleLayer extends Layer {
boolean pathOpen = false;
Turtle() {}
/**
* Path-Objekt für die Darstellung der Turtle.
*/
Path2D.Double turtlePath;
Turtle() {
}
public boolean isVisible() {
return visible;
}
public void beginPath() {
pathOpen = false;
addPosToPath();
}
public void closePath() {
if( pathOpen ) {
addPosToPath();
path.closePath();
path.trimToSize();
pathOpen = false;
}
}
private void addPosToPath() {
if( !pathOpen ) {
@@ -294,53 +434,39 @@ public class TurtleLayer extends Layer {
}
}
private void closePath() {
if( pathOpen ) {
addPosToPath();
path.closePath();
path.trimToSize();
pathOpen = false;
}
}
public void fill() {
closePath();
if( fillColor != null && fillColor.getAlpha() > 0 ) {
drawing.setColor(fillColor.getJavaColor());
if( hasFill() ) {
drawing.setPaint(getFill());
drawing.fill(path);
}
}
public boolean isVisible() {
return visible;
}
@Override
public void draw( Graphics2D graphics ) {
/*Shape shape = new RoundRectangle2D.Double(
-12, -5, 16, 10, 5, 3
);*/
Path2D path = new Path2D.Double();
path.moveTo(STD_SIZE, 0);
path.lineTo(-STD_SIZE, -STD_SIZE/2);
path.lineTo(-STD_SIZE, STD_SIZE/2);
path.lineTo(STD_SIZE, 0);
if( turtlePath == null ) {
turtlePath = new Path2D.Double();
path.moveTo(DEFAULT_SIZE, 0);
path.lineTo(-DEFAULT_SIZE, -DEFAULT_SIZE / 2.0);
path.lineTo(-DEFAULT_SIZE, DEFAULT_SIZE / 2.0);
path.lineTo(DEFAULT_SIZE, 0);
}
AffineTransform verzerrung = new AffineTransform();
verzerrung.translate(position.x, position.y);
verzerrung.rotate(Math.toRadians(direction.angle()));
Shape shape = verzerrung.createTransformedShape(path);
java.awt.Shape shape = verzerrung.createTransformedShape(turtlePath);
if( strokeColor != null ) {
if( hasStroke() ) {
graphics.setColor(strokeColor.getJavaColor());
} else {
graphics.setColor(STD_STROKECOLOR.getJavaColor());
graphics.setColor(DEFAULT_STROKECOLOR.getJavaColor());
}
graphics.fill(shape);
graphics.setColor(Color.BLACK.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.draw(shape);
}
@@ -354,9 +480,9 @@ public class TurtleLayer extends Layer {
Vector positionStart = position.copy();
position.add(Vector.setLength(direction, dist));
if( penDown && strokeColor != null ) {
if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.setStroke(getStroke());
drawing.drawLine((int) positionStart.x, (int) positionStart.y, (int) position.x, (int) position.y);
}
}
@@ -407,9 +533,9 @@ public class TurtleLayer extends Layer {
position.x = x;
position.y = y;
if( penDown && strokeColor != null ) {
if( penDown && hasStroke() ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.setStroke(getStroke());
drawing.drawLine((int) x, (int) y, (int) position.x, (int) position.y);
}
}

View File

@@ -0,0 +1,8 @@
/**
* Dieses Paket enthält implementationen der abstrakten
* {@link schule.ngb.zm.Layer} Klasse.
* <p>
* {@code Layer} sind Ebenen, die der {@link schule.ngb.zm.Zeichenleinwand}
* hinzugefügt und pro Frame gerendert werden.
*/
package schule.ngb.zm.layers;

View File

@@ -0,0 +1,117 @@
package schule.ngb.zm.media;
/**
* Schnittstelle für Audio-Medien.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/
public interface Audio {
/**
* @return Die Quelle, aus der das Medium geladen wurde.
*/
String getSource();
/**
* Prüft, ob das Medium gerade abgespielt wird.
*
* @return {@code true}, wenn das Medium abgespielt wird, {@code false}
* sonst.
*/
boolean isPlaying();
/**
* Prüft, ob das Medium gerade in einer Schleife abgespielt wird. Wenn
* {@code isLooping() == true} gilt, dann muss auch immer
* {@code isPlaying() == true} gelten.
*
* @return {@code true}, wenn das Medium in einer Schleife abgespielt wird,
* {@code false} sonst.
*/
boolean isLooping();
/**
* Legt die Lautstärke des Mediums beim Abspielen fest.
* <p>
* Die Lautstärke wird auf einer linearen Skale festgelegt, wobei 0 kein Ton
* und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
* Mediums. Negative Werte setzen die Lautstärke aud 0.
*
* @param volume Die neue Lautstärke zwischen 0 und 1.
* @see <a
* href="https://stackoverflow.com/a/40698149">https://stackoverflow.com/a/40698149</a>
*/
void setVolume( double volume );
/**
* Liefert die aktuelle Lautstärke dieses Mediums.
* <p>
* Die Lautstärke wird auf einer linearen Skale angegeben, wobei 0 kein Ton
* und 1 volle Lautstärke bedeutet. Werte über 1 verstärken den Ton des
* Mediums.
*
* @return Die Lautstärke als linear skalierter Wert.
*/
double getVolume();
/**
* Startet die Wiedergabe des Mediums. Das Audio-Medium wird einmal
* abgespielt und stoppt dann.
* <p>
* Die Methode beendet sofort und die Wiedergabe erfolgt im Hintergrund.
* Soll die Programmausführung erst nach Wiedergabe des Mediums fortgesetzt
* werden, sollte {@link #playAndWait()} verwendet werden.
* <p>
* Soll die Wiedergabe im Hintergrund ablaufen, aber dennoch auf das Ende
* reagiert werden, kann ein
* {@link #addAudioListener(AudioListener) AudioListener} verwendet werden.
*/
void play();
/**
* Startet die Wiedergabe des Mediums und blockiert das Programm, bis die
* Wiedergabe beendet ist.
*/
void playAndWait();
/**
* Spielt das Medium in einer kontinuierlichen Schleife ab. Die Methode
* startet die Wiedergabe im Hintergrund und beendet dann sofort. Um die
* Wiedergabe zu stoppen, muss {@link #stop()} aufgerufen werden.
*/
void loop();
/**
* Stoppt die Wiedergabe. Wird das Medium gerade nicht abgespielt
* ({@code isPlaying() == false}), dann passiert nichts.
*/
void stop();
/**
* Stoppt die Wiedergabe und gibt alle Ressourcen, die für das Medium
* verwendet werden, frei.
*/
void dispose();
/**
* Fügt dem Medium das angegebene Objekt als {@code AudioListener} hinzu,
* der bei Start und Stopp der Wiedergabe informiert wird.
*
* @param listener Das Listener-Objekt.
*/
void addAudioListener( AudioListener listener );
/**
* Entfernt den angegebenen {@code AudioListener} vom Medium.
*
* @param listener Das Listener-Objekt.
*/
void removeAudioListener( AudioListener listener );
}

View File

@@ -0,0 +1,34 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.events.Listener;
/**
* Interface für Klassen, die auf das starten und stoppen der Wiedergabe von
* {@link Audio}-Objekten reagieren möchten.
* <p>
* Implementierende Klassen können sich bei einem Auido-Objekt mittels
* {@link Audio#addAudioListener(AudioListener)} anmelden und werden über die
* jeweilige Methode informiert, sobald die Wiedergabe gestartet oder gestoppt
* wird.
*/
public interface AudioListener extends Listener<Audio> {
/**
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Mediums startet, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Medium, dessen Wiedergabe gestartet wurde.
*/
void playbackStarted( Audio source );
/**
* Wird aufgerufen, sobald die Wiedergabe eines Audio-Mediums stoppt, dem
* dieses Objekt mittels {@link Audio#addAudioListener(AudioListener)}
* hinzugefügt wurde.
*
* @param source Das Audio-Medium, dessen Wiedergabe gestoppt wurde.
*/
void playbackStopped( Audio source );
}

View File

@@ -0,0 +1,245 @@
package schule.ngb.zm.media;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.tasks.TaskRunner;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Ein Mixer ist eine Sammlung mehrerer {@link Audio Audio-Medien}, die
* gemeinsam kontrolliert werden können.
* <p>
* Im einfachsten Fall kann die Audio-Gruppe gemeinsam gestartet und gestoppt
* werden. Ein Mixer kann die Lautstärke der Medien in Relation zueinander
* setzen. Dazu wird jedem Medium ein Faktor mitgegeben. Ein Medium mit dem
* Faktor 0.5 ist dann halb so laut wie eines, mit dem Faktor 1.0.
* <p>
* Darüber hinaus kann ein Mixer Effekte wie einen
* {@link #fade(double, int) fadeIn} auf die Medien anwenden.
*/
@SuppressWarnings( "unused" )
public class Mixer implements Audio, AudioListener {
private List<AudioWrapper> audios;
private float volume = 0.8f;
EventDispatcher<Audio, AudioListener> eventDispatcher;
public Mixer() {
this.audios = new ArrayList<>(4);
}
public String getSource() {
return "";
}
private AudioWrapper findWrapper( Audio pAudio ) {
for( AudioWrapper aw: audios ) {
if( aw.audio == pAudio ) {
return aw;
}
}
return null;
}
public boolean contains( Audio pAudio ) {
return findWrapper(pAudio) != null;
}
public void add( Audio pAudio ) {
add(pAudio, 1);
}
/**
* Fügt ein Audio-Objekt dem Mixer mit dem angegebenen Lautstärke-Faktor
* hinzu.
* <p>
* Der Lautstärke-Faktor setzt die Lautstärke des Audio-Objektes relativ zur
* Lautstärke des Mixers. Bei einem Faktor von 1.0 wird die Lautstärke des
* Mixers übernommen. Bei einem Wert von 0.5 wird das Objekt halb so laut
* abgespielt. Auf diese Weise lässt sich die Lautstärke aller Audio-Objekte
* des Mixers gleichzeitig anpassen, während ihre relative Lautstärke
* zueinander gleich bleibt.
*
* @param pAudio Ein Audio-Objekt.
* @param pVolumeFactor Der Lautstärke-Faktor.
*/
public void add( Audio pAudio, double pVolumeFactor ) {
if( !contains(pAudio) ) {
audios.add(new AudioWrapper(pAudio, (float) pVolumeFactor));
} else {
findWrapper(pAudio).volumeFactor = (float) pVolumeFactor;
}
pAudio.setVolume(pVolumeFactor * volume);
}
/**
* Entfernt die das angegebene Audio-Objekt aus dem Mixer. Ist das Objekt
* nicht Teil des Mixers, passiert nichts.
*
* @param pAudio Ein Audio-Objekt.
*/
public void remove( Audio pAudio ) {
Iterator<AudioWrapper> it = audios.listIterator();
while( it.hasNext() ) {
AudioWrapper aw = it.next();
if( aw.audio == pAudio ) {
it.remove();
break;
}
}
}
public void removeAll() {
audios.clear();
}
@Override
public boolean isPlaying() {
return audios.stream().anyMatch(aw -> aw.audio.isPlaying());
}
@Override
public boolean isLooping() {
return audios.stream().anyMatch(aw -> aw.audio.isLooping());
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
audios.stream().forEach(aw -> aw.audio.setVolume(aw.volumeFactor * volume));
}
@Override
public double getVolume() {
return volume;
}
@Override
public void play() {
audios.stream().forEach(aw -> aw.audio.play());
}
@Override
public void playAndWait() {
audios.stream().forEach(aw -> aw.audio.play());
while( audios.stream().anyMatch(aw -> aw.audio.isPlaying()) ) {
try {
Thread.sleep(10);
} catch( InterruptedException e ) {
// Just keep waiting
}
}
}
@Override
public void loop() {
audios.stream().forEach(aw -> aw.audio.loop());
}
@Override
public void stop() {
audios.stream().forEach(aw -> aw.audio.stop());
}
@Override
public void dispose() {
if( isPlaying() ) {
stop();
}
audios.stream().forEach(aw -> aw.audio.dispose());
}
/**
* Ändert die Lautstärke aller hinzugefügten Audiomedien in der angegebenen
* Zeit schrittweise, bis die angegebene Lautstärke erreicht ist.
* <p>
* Zu beachten ist, dass die Lautstärke des Mixers angepasst wird. Das
* bedeutet, dass die Lautstärke der hinzugefügten Medien mit ihrem
* Lautstärkefaktor multipliziert werden. Die Medien haben am Ende also
* nicht unbedingt die Lautstärke {@code to}.
*
* @param to Der Zielwert für die Lautstärke.
* @param time Die Zeit, nach der die Änderung abgeschlossen sein soll.
*/
public void fade( final double to, final int time ) {
TaskRunner.run(new Runnable() {
@Override
public void run() {
final long start = System.currentTimeMillis();
double t = 0.0;
double from = volume;
do {
setVolume(Constants.interpolate(from, to, t));
t = (double) (System.currentTimeMillis() - start) / (double) time;
try {
Thread.sleep(1000 / Constants.framesPerSecond);
} catch( InterruptedException e ) {
// Keep waiting
}
} while( t < 1.0 );
setVolume(to);
}
});
}
@Override
public void playbackStarted( Audio source ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Mixer.this);
}
}
@Override
public void playbackStopped( Audio source ) {
if( !isPlaying() ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Mixer.this);
}
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich auch ein Listener registrieren möchte.
*
* @return
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", (a,l) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", (a,l) -> l.playbackStopped(a));
}
return eventDispatcher;
}
class AudioWrapper {
Audio audio;
float volumeFactor;
public AudioWrapper( Audio audio, float volumeFactor ) {
this.audio = audio;
this.volumeFactor = volumeFactor;
}
}
}

View File

@@ -0,0 +1,326 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import schule.ngb.zm.util.tasks.TaskRunner;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Eine Musik, die abgespielt werden kann.
* <p>
* Im Gegensatz zu einem {@link Sound} wird {@code Music} für längere
* Audiodateien benutzt, die zum Beispiel als Hintergrundmusik gespielt werden.
* Die Audiodaten werden daher nicht vollständig in den Speicher geladen,
* sondern direkt aus der Quelle gestreamt und direkt wiedergegeben.
* <p>
* Daher ist es nicht möglich, die länge der Musik im Vorfeld abzufragen oder zu
* einer bestimmten Stelle im Stream zu springen.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/
// TODO: Wann sollten Listener beim Loopen informiert werden? Nach jedem Loop oder erst ganz am Ende?
@SuppressWarnings( "unused" )
public class Music implements Audio {
/**
* Größe des verwendeten Input-Puffers für die Audiodaten.
*/
private static final int BUFFER_SIZE = 4096;
/**
* Ob der Sound aktuell abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound aktuell in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle der Audiodaten.
*/
private String audioSource;
/**
* Der {@link AudioInputStream}, um die Audiosdaten zu lesen. {@code null},
* falls noch kein Stream geöffnet wurde.
*/
private AudioInputStream audioStream;
/**
* Die {@link SourceDataLine} für die Ausgabe. {@code null}, falls die
* Audiodatei noch nicht geöffnet wurde.
*/
private SourceDataLine audioLine;
/**
* Die aktuelle Lautstärke des Mediums.
*/
private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher;
/**
* Erstellt eine Musik aus der angegebenen Audioquelle.
*
* @param audioSource Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/
public Music( String audioSource ) {
Validator.requireNotNull(audioSource, "audioSource");
this.audioSource = audioSource;
}
@Override
public String getSource() {
return audioSource;
}
@Override
public boolean isPlaying() {
return playing;
}
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
if( audioLine != null ) {
applyVolume();
}
}
@Override
public double getVolume() {
return volume;
}
/**
* Wendet die Lautstärke vor dem Abspielen auf den Audiostream an.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
@Override
public void play() {
if( openLine() ) {
TaskRunner.run(this::stream);
}
}
@Override
public void playAndWait() {
if( openLine() ) {
stream();
}
}
@Override
public void loop() {
looping = true;
play();
}
@Override
public void stop() {
playing = false;
looping = false;
dispose();
}
@Override
public synchronized void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
playing = false;
audioLine.stop();
}
if( audioLine.isOpen() ) {
audioLine.drain();
audioLine.close();
}
}
try {
if( audioStream != null ) {
audioStream.close();
}
} catch( IOException ex ) {
}
audioLine = null;
audioStream = null;
}
/**
* Startet den Stream der Audiodaten und damit die Wiedergabe.
* <p>
* Die {@link #audioLine} muss vorher mit {@link #openLine()} geöffnet
* werden, ansonsten passiert nichts.
*/
private synchronized void stream() {
if( audioLine == null ) {
return;
}
audioLine.start();
playing = true;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Music.this);
}
byte[] bytesBuffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
try {
while( playing && (bytesRead = audioStream.read(bytesBuffer)) != -1 ) {
audioLine.write(bytesBuffer, 0, bytesRead);
}
audioLine.drain();
audioLine.stop();
} catch( IOException ex ) {
LOG.warn(ex, "Error while playing Music source <%s>", audioSource);
}
// Wait for the remaining audio to play
/*while( audioLine.isRunning() ) {
try {
Thread.sleep(10);
} catch( InterruptedException ex ) {
// Just keep waiting ...
}
}*/
playing = false;
streamingStopped();
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Music.this);
}
}
/**
* Öffnet eine {@link SourceDataLine} für die
* {@link #audioSource Audioquelle} und bereitet die Wiedergabe vor. Es wird
* noch nichts abgespielt.
*
* @return {@code true}, wenn die Line geöffnet werden konnte, {@code false}
* sonst.
*/
private boolean openLine() {
if( audioLine != null ) {
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream inStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = inStream.getFormat();
final int ch = format.getChannels();
final float rate = format.getSampleRate();
AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch * 2, rate, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, outFormat);
audioLine = (SourceDataLine) AudioSystem.getLine(info);
audioLine.open(outFormat);
applyVolume();
audioStream = AudioSystem.getAudioInputStream(outFormat, inStream);
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
/**
* Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Aufruf von {@link #stop()} oder weil keine Audiodaten mehr vorhanden
* sind.
* <p>
* Nach dem Ende des Streams wird {@link #dispose()} aufgerufen und, falls
* das Musikstück in einer Schleife abgespielt wird, der Stream direkt
* wieder gestartet.
*/
private void streamingStopped() {
dispose();
if( looping ) {
if( openLine() ) {
stream();
} else {
playing = false;
looping = false;
}
} else {
playing = false;
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich der erste Listener anmelden möchte.
*
* @return Der {@code EventDispatcher} für dieses Objekt.
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", ( a, l ) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", ( a, l ) -> l.playbackStopped(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Music.class);
}

View File

@@ -0,0 +1,349 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import schule.ngb.zm.util.events.EventDispatcher;
import schule.ngb.zm.util.io.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.net.URL;
/**
* Ein kurzer Soundclip, der mehrmals wiederverwendet werden kann.
* <p>
* In Spielen und anderen Projekten gibt es oftmals eine Reihe kurzer
* Soundclips, die zusammen mit bestimmten Aktionen wiedergegeben werden (zum
* Beispiel, wenn die Spielfigur springt, wenn zwei Objekte kollidieren, usw.).
* Sounds werden vollständig in den Speicher geladen und können immer wieder,
* als Schleife oder auch nur Abschnittsweise, abgespielt werden.
* <p>
* Für längere Musikstücke (beispielsweise Hintergrundmusik) bietet sich eher
* die Klasse {@link Music} an.
*
* <h2>MP3-Dateien verwenden</h2>
* Java kann nativ nur Waveform ({@code .wav}) Dateien wiedergeben. Um auch
* MP3-Dateien zu nutzen, müssen die Bibliotheken <a href="#">jlayer</a>, <a
* href="#">tritonus-share</a> und <a href="#">mp3spi</a> eingebunden werden.
* Details zur Verwendung können in der <a
* href="https://zeichenmaschine.xyz/installation/#unterstutzung-fur-mp3">Dokumentation
* der Zeichenmaschine</a> nachgelesen werden.
*/
@SuppressWarnings( "unused" )
public class Sound implements Audio {
/**
* Ob der Sound aktuell abgespielt wird.
*/
private boolean playing = false;
/**
* Ob der Sound aktuell in einer Schleife abgespielt wird.
*/
private boolean looping = false;
/**
* Die Quelle der Audiodaten.
*/
private String audioSource;
/**
* Der Clip, falls er schon geladen wurde. Ansonsten {@code null}.
*/
private Clip audioClip;
/**
* Ob die Ressourcen des Clips im Speicher nach dem nächsten Abspielen
* freigegeben werden sollen.
*/
private boolean disposeAfterPlay = false;
/**
* Die aktuelle Lautstärke des Clips.
*/
private float volume = 0.8f;
/**
* Dispatcher für Audio-Events (start und stop).
*/
EventDispatcher<Audio, AudioListener> eventDispatcher;
/**
* Erstellt einen Sound aus der angegebene Quelle.
*
* @param source Quelle der Audiodaten.
* @throws NullPointerException Falls die Quelle {@code null} ist.
* @see ResourceStreamProvider#getResourceURL(String)
*/
public Sound( String source ) {
Validator.requireNotNull(source, "source");
this.audioSource = source;
}
@Override
public String getSource() {
return audioSource;
}
@Override
public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning();
return playing;
}
@Override
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
@Override
public void setVolume( double volume ) {
this.volume = volume < 0 ? 0f : (float) volume;
if( audioClip != null ) {
applyVolume();
}
}
@Override
public double getVolume() {
return volume;
}
/**
* Wendet die Lautstärke vor dem Abspielen auf den Clip an.
*/
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioClip.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
@Override
public void stop() {
looping = false;
if( audioClip.isRunning() ) {
audioClip.stop();
}
playing = false;
}
@Override
public void play() {
if( this.openClip() ) {
audioClip.start();
playing = true;
}
}
@Override
public void playAndWait() {
this.play();
long audioLen = audioClip.getMicrosecondLength();
while( playing ) {
try {
long ms = (audioLen - audioClip.getMicrosecondPosition()) / 1000L;
Thread.sleep(ms);
} catch( InterruptedException ex ) {
// Ignore
}
}
audioClip.close();
}
/**
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei.
* <p>
* Der Aufruf ist effektiv gleich zu
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
* allerdings wird der aufrufende Thread nicht blockiert und
* {@link #dispose()} automatisch am Ende aufgerufen.
*/
public void playOnce() {
disposeAfterPlay = true;
play();
}
/**
* Spielt den Sound einmal ab und gibt danach alle Ressourcen des Clips
* frei.
* <p>
* Der Aufruf entspricht
* <pre><code>
* clip.playAndWait();
* clip.dispose();
* </code></pre>
*/
public void playOnceAndWait() {
disposeAfterPlay = true;
playAndWait();
}
@Override
public void loop() {
loop(Clip.LOOP_CONTINUOUSLY);
}
/**
* Wiederholt den Sound die angegebene Anzahl an Wiederholungen und stoppt
* die Wiedergabe dann.
* <p>
* Wird {@code count} auf {@link Clip#LOOP_CONTINUOUSLY} gesetzt (-1), wird
* der Clip unendlich oft wiederholt. Der Aufruf entspricht dann
* {@link #loop()}.
*
* @param count Anzahl der Wiederholungen.
*/
public void loop( int count ) {
if( count > 0 ) {
int loopCount = count;
if( loopCount != Clip.LOOP_CONTINUOUSLY ) {
if( loopCount <= 0 ) {
return;
}
// Adjust Number of loops
loopCount -= 1;
}
if( openClip() ) {
looping = true;
audioClip.loop(loopCount);
playing = true;
}
}
}
@Override
public synchronized void dispose() {
if( audioClip != null ) {
if( audioClip.isRunning() ) {
stop();
}
audioClip.close();
audioClip = null;
}
}
/**
* Lädt, falls nötig, den {@link Clip} für die
* {@link #audioSource Audioquelle}.
*
* @return {@code true}, wenn der Clip geöffnet werden konnte, {@code false}
* sonst.
*/
private synchronized boolean openClip() {
if( audioClip != null ) {
audioClip.setFramePosition(0);
return true;
}
try {
URL url = ResourceStreamProvider.getResourceURL(audioSource);
if( url != null ) {
final AudioInputStream audioStream = AudioSystem.getAudioInputStream(url);
AudioFormat format = audioStream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, format);
audioClip = (Clip) AudioSystem.getLine(info);
audioClip.addLineListener(new LineListener() {
@Override
public void update( LineEvent event ) {
if( event.getType() == LineEvent.Type.START ) {
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("start", Sound.this);
}
} else if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped();
}
}
});
audioClip.open(audioStream);
applyVolume();
return true;
} else {
LOG.warn("Sound source <%s> could not be played: No audio source found.", audioSource);
}
} catch( UnsupportedAudioFileException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: The specified audio file is not supported.", audioSource);
} catch( LineUnavailableException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Audio line for playing back is unavailable.", audioSource);
} catch( IOException ex ) {
LOG.warn(ex, "Sound source <%s> could not be played: Error playing the audio file.", audioSource);
}
return false;
}
/*@Override
public void update( LineEvent event ) {
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START ) {
playing = true;
} else if( type == LineEvent.Type.STOP ) {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
}*/
/**
* Wird aufgerufen, wenn die Wiedergabe beendet wurde. Entweder durch einen
* Aufruf von {@link #stop()} oder, weil die Wiedergabe nach
* {@link #playOnce()} beendet wurde.
* <p>
* Falls {@link #disposeAfterPlay} gesetzt ist, wird nach dem Ende der
* Wiedergabe {@link #dispose()} aufgerufen.
*/
private void playbackStopped() {
playing = false;
if( eventDispatcher != null ) {
eventDispatcher.dispatchEvent("stop", Sound.this);
}
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
@Override
public void addAudioListener( AudioListener listener ) {
initializeEventDispatcher().addListener(listener);
}
@Override
public void removeAudioListener( AudioListener listener ) {
initializeEventDispatcher().removeListener(listener);
}
/**
* Interne Methode, um den Listener-Mechanismus zu initialisieren. Wird erst
* aufgerufen, sobald sich der erste Listener anmelden möchte.
*
* @return Der {@code EventDispatcher} für dieses Objekt.
*/
private EventDispatcher<Audio, AudioListener> initializeEventDispatcher() {
if( eventDispatcher == null ) {
eventDispatcher = new EventDispatcher<>();
eventDispatcher.registerEventType("start", ( a, l ) -> l.playbackStarted(a));
eventDispatcher.registerEventType("stop", ( a, l ) -> l.playbackStopped(a));
}
return eventDispatcher;
}
private static final Log LOG = Log.getLogger(Sound.class);
}

View File

@@ -0,0 +1,7 @@
/**
* Dieses Paket enthält Klassen zur Einbettung von Mediendateien.
* <p>
* Mit Medien sind vor allem Audio und Videodateien gemeint. Aktuell kann die
* Zeichenmaschine Audiodateien verwenden.
*/
package schule.ngb.zm.media;

View File

@@ -0,0 +1,337 @@
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);
}
@Override
public int columns() {
return columns;
}
@Override
public int rows() {
return rows;
}
int idx( int r, int c ) {
return c * rows + r;
}
@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);
}
}
@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;
}
@Override
public MLMatrix initializeRandom() {
return initializeRandom(-1.0, 1.0);
}
@Override
public MLMatrix initializeRandom( double lower, double upper ) {
applyInPlace(( d ) -> ((upper - lower) * Constants.random()) + lower);
return this;
}
@Override
public MLMatrix initializeOne() {
applyInPlace(( d ) -> 1.0);
return this;
}
@Override
public MLMatrix initializeZero() {
applyInPlace(( d ) -> 0.0);
return this;
}
@Override
public MLMatrix duplicate() {
return new DoubleMatrix(this);
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@Override
public MLMatrix scaleInPlace( final double scalar ) {
for( int i = 0; i < coefficients.length; i++ ) {
coefficients[i] *= scalar;
}
return this;
}
@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;
}
@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;
}
@Override
public MLMatrix applyInPlace( DoubleUnaryOperator op ) {
for( int i = 0; i < coefficients.length; i++ ) {
coefficients[i] = op.applyAsDouble(coefficients[i]);
}
return this;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(rows);
sb.append(" x ");
sb.append(columns);
sb.append(" Matrix");
sb.append('\n');
for( int i = 0; i < rows; i++ ) {
for( int j = 0; j < columns; j++ ) {
sb.append(get(i, j));
if( j < columns - 1 )
sb.append(' ');
}
sb.append('\n');
}
return sb.toString();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
/**
* Dieses Paekt enthält Klassen für Experimente mit Verfahren des maschinellen
* Lernens (ML).
* <p>
* Die hier implementierten Klassen sind eine prototypische Umsetzung von
* einfachen neuronalen Netzwerken, mit denen an kleinen Problemstellungen
* experimentell Modelle trainiert und angewandt werden können.
* <p>
* Die Implementierungen sind nicht optimiert und setzen auf native
* Java-Methoden. Daher sind sie nur für die Anwendung auf extrem kleine Modelle
* in Bildungskontexten gedacht.
* <p>
* Durch Einbettung wissenschaftlicher Bibliotheken mit optimierten Operationen
* lassen sich bessere Ergebnisse erreichen.
*/
package schule.ngb.zm.ml;

View File

@@ -0,0 +1,13 @@
/**
* <h2>Die Zeichenmaschine</h2>
* <p>
* Die <b>Zeichenmaschine</b> ist eine für den Informatikunterricht entwickelte
* Bibliothek, die unter anderem an <a
* href="https://processing.org/">Processing</a> angelehnt ist. Die Bibliothek
* soll einige der üblichen Anfängerschwierigkeiten mit Java vereinfachen und
* für Schülerinnen und Schüler im Unterricht nutzbar machen.
* <p>
* Eine umfassende Dokumentation ist unter <a
* href="https://zeichenmaschine.xyz">zeichenmaschine.xyz</a> verfügbar.
*/
package schule.ngb.zm;

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

@@ -0,0 +1,815 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.BasicDrawable;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Options;
import schule.ngb.zm.Strokeable;
import schule.ngb.zm.util.Validator;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
/**
* Dies ist die 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 Werte einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert:
* <pre><code>
* public Circle( double x, double y, double radius ) {
* super(x, y);
* 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 BasicDrawable {
/**
* Speichert die x-Koordinate der Form.
*/
protected double x;
/**
* Speichert die y-Koordinate der Form.
*/
protected double y;
/**
* Speichert die Rotation in Grad um den Punkt (x, y).
*/
protected double rotation = 0.0;
/**
* Speichert den Skalierungsfaktor.
*/
protected double scale = 1.0;
/**
* Speichert den Ankerpunkt.
*/
protected Options.Direction anchor = Options.Direction.CENTER;
/**
* Erstellt eine neue Form mit den Koordinaten {@code (0,0)}.
*/
public Shape() {
this(0.0, 0.0);
}
/**
* Erstellt eine Form mit den angegebenen Koordinaten.
*
* @param x Die x-Koordinate.
* @param y Die y-Koordinate.
*/
public Shape( double x, double y ) {
this.x = x;
this.y = y;
}
/**
* Liefert die aktuelle x-Koordinate der Form.
*
* @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;
}
/**
* Liefert die aktuelle y-Koordinate der Form.
*
* @return Die y-Koordinate.
*/
public double getY() {
return y;
}
/**
* Setzt die x-Koordinate der Form.
*
* @param y Die neue y-Koordinate der Form.
*/
public void setY( double y ) {
this.y = y;
}
/**
* Liefert die aktuelle Breite dieser Form.
* <p>
* Die Breite einer Form ist immer die Breite ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen.
*
* @return Die Breite der Form.
*/
public abstract double getWidth();
/**
* Liefert die aktuelle Höhe dieser Form.
* <p>
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung,
* <strong>bevor</strong> Drehungen und andere Transformationen auf sie
* angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form wird mit
* {@link #getBounds()} abgerufen.
*
* @return Die Höhe der Form.
*/
public abstract double getHeight();
/**
* Liefert die Rotation in Grad.
*
* @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;
}
/**
* Dreht die Form um den angegebenen Winkel um das angegebene Drehzentrum.
*
* @param center Das Drehzentrum der Drehung.
* @param angle Der Drehwinkel.
*/
public void rotate( Point2D center, double angle ) {
Validator.requireNotNull(center, "center");
rotate(center.getX(), center.getY(), angle);
}
/**
* Dreht die Form um den angegebenen Drehwinkel um die angegbenen
* Koordinaten als Drehzentrum.
*
* @param x x-Koordiante des Drehzentrums.
* @param y y-Koordiante des Drehzentrums.
* @param angle Drehwinkel in Grad.
*/
public void rotate( double x, double y, double angle ) {
this.rotation += angle % 360;
// 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;
}
/**
* Setzt die Drehung der Form auf den angegebenen Winkel.
*
* @param angle Drehwinkel in Grad.
*/
public void rotateTo( double angle ) {
this.rotation = angle % 360;
}
/**
* Gibt den aktuellen Skalierungsfaktor zurück.
*
* @return Der Skalierungsfaktor.
*/
public double getScale() {
return scale;
}
/**
* Setzt den Skalierungsfaktor auf den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Das Seitenverhältnis wird immer beibehalten.
*
* @param factor Der Skalierungsfaktor.
*/
public void scale( double factor ) {
scale = factor;
}
/**
* Skaliert die Form um den angegebenen Faktor.
* <p>
* Bei einem Faktor größer 0 wird die Form vergrößert, bei einem Faktor
* kleiner 0 verkleinert. Bei negativen Werten wird die Form entlang der x-
* bzw. y-Achse gespiegelt.
* <p>
* Die Skalierung wird zusätzlich zur aktuellen Skalierung angewandt. Wurde
* die Form zuvor um den Faktor 0.5 verkleinert und wird dann um 1.5
* vergrößert, dann ist die Form im Anschluss ein Drittel kleiner als zu
* Beginn ({@code 0.5 * 1.5 = 0.75}).
*
* @param factor Der Skalierungsfaktor.
*/
public void scaleBy( double factor ) {
scale(scale * factor);
}
/**
* Liefert den aktuellen Ankerpunkt der Form.
*
* @return Der Ankerpunkt.
*/
public Options.Direction getAnchor() {
return anchor;
}
/**
* Setzt den Ankerpunkt der Form auf die angegebene Richtung.
* <p>
* Jede Form hat einen Ankerpunkt, von dem aus sie gezeichnet wird. Jede
* {@link schule.ngb.zm.Options.Direction Richtung} beschreibt einen der
* Neun Ankerpunkte:
* <pre>
* NW────N────NE
* │ │
* │ │
* W C E
* │ │
* │ │
* SW────S────SE
* </pre>
* <p>
* Für den Ankerpunkt {@link #CENTER} wird die Form also ausgehend von den
* Koordinaten {@link #x} und {@link #y} um die Hälfte der Breite nach links
* und rechts, sowie um die Hälfte der Höhe nach oben und unten gezeichnet.
* Fpr den Ankerpunkt {@link #NORTHWEST} dagegen um die gesamte Breite nach
* rechts und die Höhe nach unten.
* <pre>
* setAnchor(CENTER) │ setAnchor(NORTHWEST)
* ┌───────────┐ │
* │ │ │
* │ │ │
* │ (x,y) │ │ (x,y)─────────┐
* │ │ │ │ │
* │ │ │ │ │
* └───────────┘ │ │ │
* │ │ │
* │ │ │
* │ └───────────┘
* </pre>
* <p>
* Der Ankerpunkt der Form bestimmt bei Transformationen auch die Position
* des Drehzentrums und anderer relativer Koordinaten bezüglich der Form.
*
* @param anchor Der Ankerpunkt.
*/
public void setAnchor( Options.Direction anchor ) {
Validator.requireNotNull(anchor, "anchor");
this.anchor = anchor;
}
/**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkts basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p>
* Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt. Der Ankerpunkt {@link #NORTHWEST} hat daher immer das Ergebnis
* {@code (0,0)} und {@link #SOUTHEAST} {@code (width, height)}.
* <pre>
* (0,0)───(w/2,0)───(w,0)
* │ │
* │ │
* │ │
* (0,h/2) (w/2,h/2) (w,h/2)
* │ │
* │ │
* │ │
* (0,h)───(w/2,h)───(w,h)
* </pre>
*
* @param width Breite des umschließenden Rechtecks.
* @param height Höhe des umschließenden Rechtecks.
* @param anchor Gesuchter Ankerpunkt.
* @return Ein {@link Point2D} mit den relativen Koordinaten.
*/
public static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5;
return new Point2D.Double(
wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y
);
}
/**
* Bestimmt die Koordinaten des angegebenen Ankers der Form relativ zum
* aktuellen {@link #setAnchor(Options.Direction) Ankerpunkt}.
*
* @param anchor Die Richtung des Ankers.
* @return Der relative Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/
public Point2D.Double getAnchorPoint( Options.Direction anchor ) {
double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER
return new Point2D.Double(
wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y)
);
}
/**
* Ermittelt die absoluten Koordinaten des angegebenen
* {@link #setAnchor(Options.Direction) Ankers}.
* <p>
* Die absoluten Koordinaten werden bestimmt durch die Position der Form
* {@code (x,y)} plus die
* {@link #getAnchorPoint(Options.Direction) relativen Koordinaten} des
* Ankers.
*
* <b>Wichtig:</b> Die Berechnung berücksichtigt derzeit keine Rotationen
* und Transformationen der Form.
*
* @param anchor Die Richtung des Ankers.
* @return Der absolute Ankerpunkt.
* @see #getAnchorPoint(double, double, Options.Direction)
*/
public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) {
// TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen.
Point2D.Double ap = getAnchorPoint(anchor);
ap.x += getX();
ap.y += getY();
return ap;
}
/**
* Kopiert die Eigenschaften der angegebenen Form in diese.
* <p>
* Unterklassen überschreiben diese Methode, um weitere Eigenschaften zu
* kopieren (zum Beispiel den Radius eines Kreises). Überschreibende
* Methoden sollten immer mit dem Aufruf {@code super.copyFrom(shape)} die
* Basiseigenschaften kopieren.
* <p>
* Die Methode kopiert so viele Eigenschaften wie möglich von der
* angegebenen Form in diese. Wenn die andere Form einen anderen Typ hat,
* dann werden trotzdem die Basiseigenschaften (Konturlinie, Füllung,
* Position, Rotation, Skalierung, Sichtbarkeit und Ankerpunkt) in diese
* Form kopiert. Soweit sinnvoll übernehmen implementierende Unterklassen
* auch andere Werte. Eine {@link Ellipse} kopiert beispielsweise auch die
* Breite und Höhe eines {@link Rectangle}.
* <p>
* Wird {@code null} übergeben, dann passiert nichts.
*
* @param shape Die Originalform, von der kopiert wird.
*/
public void copyFrom( Shape shape ) {
if( shape != null ) {
moveTo(shape.x, shape.y);
setFillColor(shape.getFillColor());
setStrokeColor(shape.getStrokeColor());
setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType());
visible = shape.isVisible();
rotation = shape.getRotation();
scale(shape.getScale());
setAnchor(shape.getAnchor());
}
}
/**
* 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 besitzt aber jede Form einen Konstruktor,
* der alle Werte einer andern Form übernimmt. Die gezeigte Implementierung
* ist daher im Regelfall ausreichend.
*
* @return Eine genaue Kopie dieser Form.
*/
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>
* Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, liefert die
* Methode {@code null}.
*
* @return Eine Java-AWT {@code Shape} die diese Form repräsentiert oder
* {@code null}.
*/
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);
}
/**
* Verschiebt die Form um die angegebenen Werte entlang der
* Koordinatenachsen.
*
* @param dx Verschiebung entlang der x-Achse.
* @param dy Verschiebung entlang der y-Achse.
*/
public void move( double dx, double dy ) {
x += dx;
y += dy;
}
/**
* Bewegt die Form an die angegebenen Koordinaten.
*
* @param x Die neue x-Koordinate.
* @param y Die neue y-Koordinate.
*/
public void moveTo( double x, double y ) {
this.x = x;
this.y = y;
}
/**
* Bewegt die Form an dieselben Koordinaten wie die angegebene Form.
*
* @param shape Eine andere Form.
*/
public void moveTo( Shape shape ) {
moveTo(shape.getX(), shape.getY());
}
/**
* Bewegt die Form zum angegebenen Ankerpunkt der angegebenen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #moveTo(Shape, Options.Direction, double)
*/
public void moveTo( Shape shape, Options.Direction dir ) {
moveTo(shape, dir, 0.0);
}
/**
* Bewegt den Ankerpunkt dieser Form zu einem Ankerpunkt einer anderen
* Form.
* <p>
* Mit {@code buff} wird ein zusätzlicher Abstand angegeben, um den die Form
* entlang des Ankerpunktes {@code anchor} verschoben wird.
* <p>
* Ist der Anker zum Beispiel {@code NORTH}, dann wird die Form um
* {@code buff} oberhalb der oberen Kante der zweiten Form verschoben.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────────┐
* │ │
* W B │
* ┌─────┐ │ │
* │ │ └─────────┘
* W A │
* │ │
* └─────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.moveTo(A, DOWN, 0)} in diese Ausrichtung:
* <pre>
* B.moveTo(A, WEST, 0) │ B.moveTo(A, WEST, 10)
* │
* ┌─────┬───┐ │ ┌┬────┬────┐
* │ │ │ │ ││ │ │
* │ A B│ │ │ ││ A B │
* │ │ │ │ ││ │ │
* └─────┴───┘ │ └┴────┴────┘
* </pre>
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @param buff Der Abstand zum angegebenen Ankerpunkt.
*/
public void moveTo( Shape shape, Options.Direction dir, double buff ) {
Point2D ap = shape.getAbsAnchorPoint(dir);
this.x = ap.getX() + dir.x * buff;
this.y = ap.getY() + dir.y * buff;
}
/**
* Bewegt die Form an den Rand der Zeichenfläche in der angegebenen
* Richtung.
*
* @param dir Die Richtung.
*/
public void alignTo( Options.Direction dir ) {
alignTo(dir, 0.0);
}
/**
* Bewegt die Form mit dem angegebenen Abstand an den Rand der Zeichenfläche
* in der angegebenen Richtung aus.
*
* @param dir Die Richtung.
* @param buff Der Abstand zum Rand.
*/
public void alignTo( Options.Direction dir, double buff ) {
Point2D anchorShape = Shape.getAnchorPoint(canvasWidth, canvasHeight, dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
/**
* Bewegt den Ankerpunkt dieser Form in der angegebenen Richtung zum
* Gleichen Ankerpunkt der anderen Form.
*
* @param shape Die andere Form.
* @param dir Die Richtung des Ankerpunktes.
* @see #alignTo(Shape, Options.Direction, double)
*/
public void alignTo( Shape shape, Options.Direction dir ) {
alignTo(shape, dir, 0.0);
}
/**
* Richtet die Form entlang der angegebenen Richtung an einer anderen Form
* aus.
* <p>
* {@code buff} gibt einen Abstand ab, um den diese From versetzt
* ausgerichtet wird.
* <p>
* Für {@link #DOWN} wird beispielsweise die y-Koordinate der unteren Kante
* dieser Form an der unteren Kante von {@code shape} ausgerichtet. Die
* x-Koordinate wird in dem Fall nicht verändert.
* <p>
* Befinden sich die Formen beispielsweise in folgender Position:
* <pre>
* ┌─────┐
* │ │
* │ B │
* ┌─────┐ │ │
* │ │ └──D──┘
* │ A │
* │ │
* └──D──┘
* </pre>
* <p>
* <p>
* werden sie durch {@code alignTo} so positioniert:
* <pre>
* B.alignTo(A, EAST, 0) │ B.alignTo(A, EAST, 10)
* │
* ┌─────┐ ┌─────┐ │ ┌─────┐
* │ │ │ │ │ │ │ ┌─────┐
* │ A │ │ B │ │ │ A │ │ │
* │ │ │ │ │ │ │ │ B │
* └──D──┘ └──D──┘ │ └──D──┘ │ │
* │ └──D──┘
* │
* </pre>
*
* @param shape Die andere Form.
* @param dir Die Richtung.
* @param buff Der Abstand.
*/
public void alignTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
/**
* @param shape
* @param dir
* @see #nextTo(Shape, Options.Direction, double)
*/
public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, DEFAULT_BUFFER);
}
/**
* Bewegt die Form neben eine andere in Richtung des angegebenen
* Ankerpunktes.
* <p>
* Im Gegensatz zu {@link #moveTo(Shape, Options.Direction, double)} wird
* die Breite bzw. Höhe der Formen berücksichtigt und die Formen so
* platziert, dass keine Überlappungen vorhanden sind.
* <p>
* Befinden sich die Formen zuvor in folgender Ausrichtung:
* <pre>
* ┌─────┐
* │ │
* W B │
* ┌──────┐ │ │
* │ │ └─────┘
* │ A E
* │ │
* └──────┘
* </pre>
* <p>
* bringt sie der Aufruf {@code B.nextTo(A, EAST, 0)} in diese Ausrichtung:
* <pre>
* B.nextTo(A, EAST, 0) │ B.nextTo(A, EAST, 10)
* │
* ┌─────┬─────┐ │ ┌─────┐ ┌─────┐
* │ │ │ │ │ │ │ │
* │ A │ B │ │ │ A │ │ B │
* │ │ │ │ │ │ │ │
* └─────┴─────┘ │ └─────┘ └─────┘
* │
* </pre>
*
* @param shape
* @param dir
* @param buff
*/
public void nextTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir.inverse());
this.x += (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
@Override
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
/*public void shear( double dx, double dy ) {
verzerrung.shear(dx, dy);
}*/
public AffineTransform getTransform() {
// Point2D.Double anchorPoint = getAnchorPoint();
Point2D.Double basePoint = getAnchorPoint(Options.Direction.NORTHWEST);
AffineTransform transform = new AffineTransform();
transform.translate(x, y);
transform.rotate(Math.toRadians(rotation));
//transform.scale(scale, scale);
//transform.translate(-anchorPoint.x, -anchorPoint.y);
transform.translate(basePoint.x, basePoint.y);
return transform;
}
/**
* Zeichnet die Form.
*
* @param graphics
*/
@Override
public final void draw( Graphics2D graphics ) {
draw(graphics, getTransform());
}
/**
* Zeichnet die Form, aber wendet zuvor noch eine zusätzliche
* Transformations- matrix an. Wird u.A. von der {@link ShapeGroup}
* verwendet.
*
* @param graphics
* @param transform
*/
public void draw( Graphics2D graphics, AffineTransform transform ) {
if( !visible ) {
return;
}
java.awt.Shape shape = getShape();
if( shape != null ) {
if( transform != null ) {
shape = transform.createTransformedShape(shape);
}
java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
}
}
/**
* Vergleicht die Form mit einem anderen Objekt. Handelt es sich bei dem
* Objekt um eine andere Form, werden Position, Drehwinkel und Skalierung
* verglichen. Unterklassen überschreiben die Methode, um weitere
* Eigenschaften zu berücksichtigen.
* <p>
* Die Eigenschaften, die durch {@link Fillable} und {@link Strokeable}
* impliziert werden, werden nicht verglichen.
*
* @param o Ein anderes Objekt.
* @return
*/
@Override
public boolean equals( Object o ) {
if( this == o ) return true;
if( o == null || getClass() != o.getClass() ) return false;
Shape pShape = (Shape) o;
return Double.compare(pShape.x, x) == 0 &&
Double.compare(pShape.y, y) == 0 &&
Double.compare(pShape.rotation, rotation) == 0 &&
Double.compare(pShape.scale, scale) == 0;
}
/**
* 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(getStroke());
graphics.draw(shape);
}
}
/**
* 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

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

View File

@@ -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,15 +22,16 @@ 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);
Font userfont = FontLoader.loadFont(fontname);
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;
@@ -60,29 +61,58 @@ public class Text extends Shape {
return height;
}
public void setFont( String fontname ) {
Font newFont = FontLoader.loadFont(fontname);
public Font getFont() {
return font;
}
public void setFont( Font newFont ) {
//font = newFont.deriveFont(font.getStyle(), font.getSize2D());
font = new Font(newFont.getFontName(), newFont.getStyle(), newFont.getSize());
//font = newFont;
calculateBounds();
}
public void setFont( String fontName ) {
Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
}
public void setFont( Font newFont ) {
font = newFont.deriveFont(font.getSize2D());
public void setFont( String fontName, double fontSize ) {
Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
font = newFont.deriveFont((float) fontSize);
calculateBounds();
}
public Font getFont() {
return font;
public void setFont( String fontName, double fontSize, int style ) {
Font newFont = FontLoader.loadFont(fontName);
if( newFont != null ) {
setFont(newFont);
}
font = newFont.deriveFont(style, (float) fontSize);
calculateBounds();
}
public void setFontsize( double size ) {
public double getFontSize() {
return font.getSize2D();
}
public void setFontSize( double size ) {
font = font.deriveFont((float) size);
calculateBounds();
}
public double getFontsize() {
return font.getSize2D();
public int getFontStyle() {
return font.getStyle();
}
public void setFontStyle( int fontStyle ) {
font = font.deriveFont(fontStyle);
calculateBounds();
}
public String getText() {
@@ -151,19 +181,19 @@ public class Text extends Shape {
@Override
public void copyFrom( Shape shape ) {
super.copyFrom(shape);
if( shape instanceof Text ) {
Text pText = (Text) shape;
this.text = pText.getText();
this.font = pText.getFont();
calculateBounds();
}
super.copyFrom(shape);
calculateBounds();
}
@Override
public void scale( double factor ) {
super.scale(factor);
setFontsize(font.getSize2D() * factor);
setFontSize(font.getSize2D() * factor);
}
@Override
@@ -191,7 +221,7 @@ public class Text extends Shape {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawRect(0, 0, width, height);
}

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;
@@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@SuppressWarnings("unused")
public class BarChart extends Rectangle {
public static String DEFAULT_LABEL = "%.2f";
@@ -105,10 +106,10 @@ public class BarChart extends Rectangle {
}
public void addValue( double pValue, Color pColor ) {
addValue(pValue, pColor, String.format(DEFAULT_LABEL, pValue));
addValue(pValue, String.format(DEFAULT_LABEL, pValue), pColor);
}
public void addValue( double pValue, Color pColor, String pLabel ) {
public void addValue( double pValue, String pLabel, Color pColor ) {
addValue(new BasicChartValue(pValue, pLabel, pColor));
}
@@ -288,7 +289,7 @@ public class BarChart extends Rectangle {
barY += barHeight + gap;
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
if( inverted ) {
graphics.drawLine((int) width, 0, (int) width, (int) height);
} else {
@@ -360,7 +361,7 @@ public class BarChart extends Rectangle {
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
if( inverted ) {
graphics.drawLine(0, 0, (int) width, 0);
} else {

View File

@@ -1,4 +1,4 @@
package schule.ngb.zm.charts;
package schule.ngb.zm.shapes.charts;
import schule.ngb.zm.Color;
@@ -136,57 +136,36 @@ public class BasicChartValue implements ChartValue {
this.color = color;
}
/**
* {@inheritDoc}
*/
@Override
public double getX() {
return xValue;
}
/**
* {@inheritDoc}
*/
@Override
public double getValue() {
return value;
}
/**
* {@inheritDoc}
*/
@Override
public void setValue( double pValue ) {
this.value = pValue;
}
/**
* {@inheritDoc}
*/
@Override
public String getLabel() {
return label;
}
/**
* {@inheritDoc}
*/
@Override
public void setLabel( String pLabel ) {
this.label = pLabel;
}
/**
* {@inheritDoc}
*/
@Override
public Color getColor() {
return color;
}
/**
* {@inheritDoc}
*/
@Override
public void setColor( Color pColor ) {
this.color = pColor;

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;
@@ -53,7 +52,7 @@ public class ChartAxes extends Rectangle {
}
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawLine(0, (int)(height), (int)((xMax-xMin) * xUnit), (int)(height));
graphics.drawLine(0, (int)(height), 0, (int)(height - (yMax-yMin) * yUnit));
if( showArrows ) {

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;
@@ -92,7 +91,7 @@ public class LineChart extends Rectangle {
for( ChartValue lcv : val ) {
if( drawLines && lastLcv != null ) {
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawLine((int)(lastLcv.getX()*xUnit), (int)(height - lastLcv.getValue()*yUnit), (int)(lcv.getX()*xUnit), (int)(height - lcv.getValue()*yUnit));
drawDot(graphics, lastLcv, xUnit, yUnit);
}

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;
@@ -326,7 +326,7 @@ public class PieChart extends Circle {
}
graphics.setColor(getStrokeColor().getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.drawOval(0, 0, (int) (radius * 2), (int) (radius * 2));
graphics.setTransform(originalTransform);

View File

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

View File

@@ -0,0 +1,4 @@
/**
* Diese Paket enthält Formen, die Diagramme darstellen.
*/
package schule.ngb.zm.shapes.charts;

View File

@@ -0,0 +1,13 @@
/**
* Dieses Paket enthält Implementationen der abstrakten
* {@link schule.ngb.zm.shapes.Shape} Klasse.
*
* Jede Unterklasse von {@code Shape} stellt eine konkrete Form wie ein
* {@link schule.ngb.zm.shapes.Rectangle Rechteck}, ein
* {@link schule.ngb.zm.shapes.Circle Kreis} oder ein
* {@link schule.ngb.zm.shapes.Picture Bild} dar.
*
* Mit {@link schule.ngb.zm.shapes.ShapeGroup} können Formen gruppiert
* und gemeinsam transformiert werden.
*/
package schule.ngb.zm.shapes;

View File

@@ -0,0 +1,204 @@
package schule.ngb.zm.util;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Ein Cache ist ein {@link Map} Implementation, die Inhaltsobjekte in einer
* {@link Reference} speichert und als Zwischenspeicher für Objekte dienen kann,
* deren Erstellung aufwendig ist.
* <p>
* Für einen Cache ist nicht garantiert, dass ein eingefügtes Objekt beim
* nächsten Aufruf noch vorhanden ist, da die Referenz inzwischen vom Garbage
* Collector gelöscht worden sein kann.
* <p>
* Als interne Map wird einen {@link ConcurrentHashMap} verwendet.
* <p>
* Ein passender Cache wird mittels der Fabrikmethoden {@link #newSoftCache()}
* und {@link #newWeakCache()} erstellt.
* <pre><code>
* Cache&lt;String, Image&gt; imageCache = Cache.newSoftCache();
* </code></pre>
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
*/
public final class Cache<K, V> implements Map<K, V> {
/**
* Erstellt einen Cache mit {@link SoftReference} Referenzen.
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
* @return Ein Cache.
*/
public static <K, V> Cache<K, V> newSoftCache() {
return new Cache<>(SoftReference::new);
}
/**
* Erstellt einen Cache mit {@link WeakReference} Referenzen.
*
* @param <K> Der Typ der Schlüssel.
* @param <V> Der Typ der Objekte.
* @return Ein Cache.
*/
public static <K, V> Cache<K, V> newWeakCache() {
return new Cache<>(WeakReference::new);
}
private final Map<K, Reference<V>> cache = new ConcurrentHashMap<>();
private final Reference<V> NOCACHE;
private final Function<V, Reference<V>> refSupplier;
private Cache( Function<V, Reference<V>> refSupplier ) {
this.refSupplier = refSupplier;
NOCACHE = refSupplier.apply(null);
}
@Override
public int size() {
return cache.size();
}
@Override
public boolean isEmpty() {
return cache.isEmpty();
}
@Override
public boolean containsKey( Object key ) {
return cache.containsKey(key) && cache.get(key).get() != null;
}
@Override
public boolean containsValue( Object value ) {
return cache.values().stream()
.anyMatch(( ref ) -> ref.get() != null && ref.get() == value);
}
@Override
public V get( Object key ) {
if( cache.containsKey(key) ) {
return cache.get(key).get();
}
return null;
}
/**
* Deaktiviert das Caching für den angegebenen Schlüssel.
* <p>
* Folgende Aufrufe von {@link #put(Object, Object)} mit demselben Schlüssel
* haben keinen Effekt. Um das Caching wieder zu aktivieren, muss
* {@link #remove(Object)} mit dem Schlüssel aufgerufen werden,
*
* @param key Der Schlüssel.
*/
public void disableCache( K key ) {
cache.put(key, NOCACHE);
}
/**
* Prüft, ob der für den angegebenen Schlüssel zuvor
* {@link #disableCache(Object)} aufgerufen wurde.
*
* @param key Der Schlüssel.
* @return {@code true}, wenn der Schlüssel nicht gespeichert wird.
*/
public boolean isCachingDisabled( K key ) {
return cache.get(key) == NOCACHE;
}
@Override
public V put( K key, V value ) {
if( !isCachingDisabled(key) ) {
V prev = remove(key);
cache.put(key, refSupplier.apply(value));
return prev;
} else {
return null;
}
}
@Override
public V remove( Object key ) {
Reference<V> ref = cache.get(key);
cache.remove(key);
V prev = null;
if( ref != null ) {
prev = ref.get();
ref.clear();
}
return prev;
}
@Override
public void putAll( Map<? extends K, ? extends V> m ) {
for( Entry<? extends K, ? extends V> e : m.entrySet() ) {
put(e.getKey(), e.getValue());
}
}
@Override
public void clear() {
cache.clear();
}
@Override
public Set<K> keySet() {
return cache.keySet();
}
@Override
public Collection<V> values() {
return cache.values().stream()
.filter(( ref ) -> ref.get() != null)
.map(( ref ) -> ref.get())
.collect(Collectors.toList());
}
@Override
public Set<Entry<K, V>> entrySet() {
return cache.entrySet().stream()
.filter(( e ) -> e.getValue() != null && e.getValue().get() != null)
.map(( e ) -> new SoftCacheEntryView(e.getKey()))
.collect(Collectors.toSet());
}
private final class SoftCacheEntryView implements Map.Entry<K, V> {
private K key;
public SoftCacheEntryView( K key ) {
this.key = key;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return Cache.this.get(key);
}
@Override
public V setValue( V value ) {
return Cache.this.put(key, value);
}
}
}

View File

@@ -0,0 +1,314 @@
package schule.ngb.zm.util;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Eine Hilfsklasse, um Dinge zu zählen.
* <p>
* Im einfachsten Fall kann der Zähler als geteilte Zählvariable genutzt werden,
* die mit {@link #inc()} und {@link #dec()} aus verschiedenen Objekten oder
* Methoden verändert werden kann.
* <p>
* Der Zähler kann aber auch Objekte zählen, indem die Instanzen an
* {@link #count(Object)} übergeben werden. Am Ende kann mit {@link #getCount()}
* die Anzahl der Objekte abgerufen werden.
* <p>
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein
* {@code Counter} auch das Maximum, das Minimum, die Summe und den Durchschnitt
* der gezählten Werte.
* <p>
* Ein Zähler kann auch komplette Arrays oder Listen von Zahlen zählen und die
* obigen Statistiken auswerten.
*/
@SuppressWarnings( "unused" )
public final class Counter {
/**
* Erstellt einen neuen {@code Counter}, der alle Integer im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( int[] values ) {
return new Counter().countAll(values);
}
/**
* Erstellt einen neuen {@code Counter}, der alle Doubles im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( double[] values ) {
return new Counter().countAll(values);
}
/**
* Erstellt einen neuen {@code Counter}, der alle Zahlen im angegebenen
* Array gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromArray( Number[] values ) {
return new Counter().countAll(values);
}
/**
* Erstellt einen neuen {@code Counter}, der alle Zahlen in der angegebenen
* Liste gezählt hat.
*
* @param values Die zu zählenden Werte.
* @return Ein neuer {@code Counter}.
*/
public static Counter fromList( List<Number> values ) {
return new Counter().countAll(values);
}
/**
* Aktuelle Anzahl gezählter Werte.
*/
private final AtomicInteger count = new AtomicInteger(0);
/**
* Statistiken zu den gezählten Werten.
*/
private double min = Double.NaN, max = Double.NaN, sum = Double.NaN;
/**
* Erstellt einen neuen, leeren {@code Counter}.
*/
public Counter() {
}
/**
* Ertstellt einen neuen {@code Counter}, der mit dem angegebenen Wert
* initialisiert ist.
*
* @param initial Wert des Zählers zu Beginn.
*/
public Counter( int initial ) {
count.set(initial);
}
/**
* @return Die aktuelle Anzahl.
*/
public int getCount() {
return count.get();
}
/**
* @return Das Maxium der bisher gezählten Werte.
*/
public double getMax() {
synchronized( count ) {
return max;
}
}
/**
* @return Das Minimum der bisher gezählten Werte.
*/
public double getMin() {
synchronized( count ) {
return min;
}
}
/**
* @return Die Summe der bisher gezählten Werte.
*/
public double getSum() {
synchronized( count ) {
return sum;
}
}
/**
* @return Der Mittelwert der bisher gezählten Werte.
*/
public double getAvg() {
if( Double.isNaN(sum) ) {
return Double.NaN;
} else {
return sum / count.doubleValue();
}
}
/**
* Setzt den Zähler auf den angegebenen Wert.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @param count Der neue Wert des Zählers.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter setCount( int count ) {
this.count.set(count);
return this;
}
/**
* Setzt den Zähler auf Null.
* <p>
* Die Statistiken werden auf {@link Double#NaN} gesetzt.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter reset() {
count.set(0);
synchronized( count ) {
min = Double.NaN;
max = Double.NaN;
sum = Double.NaN;
}
return this;
}
/**
* Erhöht den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter inc() {
this.count.incrementAndGet();
return this;
}
/**
* Verringert den Zähler um Eins.
* <p>
* Die anderen Statistiken werden nicht verändert.
*
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter dec() {
this.count.decrementAndGet();
return this;
}
/**
* Zählt den angegebenen Wert.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param value Der neue Wert.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( double value ) {
inc();
// Update stats
synchronized( count ) {
sum = Double.isNaN(sum) ? value : sum + value;
if( Double.isNaN(max) || max < value )
max = value;
if( Double.isNaN(min) || min > value )
min = value;
}
return this;
}
/**
* Zählt die angegebene Zahl.
* <p>
* Erhöht den Zähler um Eins und aktualisiert die Statistiken.
*
* @param num Die neue Zahl.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Number num ) {
return count(num.doubleValue());
}
/**
* Zählt das angegebenen Objekt.
* <p>
* Erhöht den Zähler um Eins.
*
* @param obj Ein beliebiges Objekt.
* @return Dieser Zähler selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Counter count( Object obj ) {
if( obj instanceof Number ) {
return count((Number) obj);
} else {
inc();
return this;
}
}
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) {
for( double value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Werte im angegebenen Array.
*
* @param values Das Array der neuen Werte.
* @return Dieser Zähler selbst (method chaining).
* @see #count(double)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( int[] values ) {
for( double value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Zahlen im angegebenen Array.
*
* @param values Das Array der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) {
for( Number value : values ) {
count(value);
}
return this;
}
/**
* Zöhlt alle Zahlen in der angegebenen Sammlung.
*
* @param values Die Sammlung der neuen Zahlen.
* @return Dieser Zähler selbst (method chaining).
* @see #count(Number)
*/
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) {
for( Number value : values ) {
count(value);
}
return this;
}
}

View File

@@ -0,0 +1,400 @@
package schule.ngb.zm.util;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.font.LineMetrics;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Math.exp;
import static java.lang.Math.log;
import static schule.ngb.zm.Constants.random;
/**
* Eine Hilfsklasse, um zufällige Beispieldaten zu erzeugen.
* <p>
* Die Klasse kann verschiedene Arten realistischer Beispieldaten erzeugen.
* Unter anderem Namen, E-Mail-Adressen, Passwörter oder Platzhalter-Bilder.
*/
@SuppressWarnings( "unused" )
public final class Faker {
/**
* URL, von der extern generierte Fake-Bilder geladen werden können.
* <p>
* Die URL wird als Format-String definiert mit zwei {@code %d}
* Platzhaltern, die durch die Breite und Höhe des gewünschten Bildes
* ersetzt werden.
*/
public static final String FAKE_IMG_URL = "https://loremflickr.com/%d/%d";
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Benutzerdaten.
* <p>
* Jeder Datensatz besteht aus einem String-Array mit den folgenden Daten
* <ul>
* <li><code>fakeUser[i][0]</code>: Vorname</li>
* <li><code>fakeUser[i][1]</code>: Nachname</li>
* <li><code>fakeUser[i][2]</code>: Geschlecht</li>
* <li><code>fakeUser[i][3]</code>: Nutzername</li>
* <li><code>fakeUser[i][4]</code>: Passwort</li>
* <li><code>fakeUser[i][5]</code>: E-Mail</li>
* <li><code>fakeUser[i][6]</code>: Geburtsdatum</li>
* </ul>
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[][] fakeUsers( int count ) {
return randomSample("users", count, ( line ) -> line.split(","), String[].class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Vornamen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeNames( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[0], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Namen im Format
* "Vorname Nachname".
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeFullnames( int count ) {
return randomSample("users", count, ( line ) -> {
String[] parts = line.split(",");
return parts[0] + " " + parts[1];
}, String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeUsernames( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[3], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Passwörter.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakePasswords( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[4], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger E-Mail-Adressen.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeEmails( int count ) {
return randomSample("users", count, ( line ) -> line.split(",")[5], String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger deutscher
* Wörter.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeStrings( int count ) {
return randomSample("words", count, ( line ) -> line, String.class);
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger
* {@code LocalDate}-Objekte, die ein Datum ohne Uhrzeit beschreiben.
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static LocalDate[] fakeDates( int count ) {
long nowEpoch = LocalDate.now().toEpochDay();
long from = LocalDate.ofEpochDay(nowEpoch - 18 * 365).toEpochDay();
long to = LocalDate.ofEpochDay(nowEpoch - 14 * 365).toEpochDay();
LocalDate[] result = new LocalDate[count];
for( int i = 0; i < count; i++ ) {
result[i] = LocalDate.ofEpochDay((int) Constants.interpolate(from, to, random()));
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger
* {@code LocalDateTime}-Objekte, die einen Zeitpunkt mit Dateum und Uhrzeit
* beschreiben,
*
* @param count Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static LocalDateTime[] fakeDatetimes( int count ) {
long nowEpoch = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
long from = LocalDateTime.ofEpochSecond(nowEpoch - 18 * 365, 0, ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC);
long to = LocalDateTime.ofEpochSecond(nowEpoch - 14 * 365, 0, ZoneOffset.UTC).toEpochSecond(ZoneOffset.UTC);
LocalDateTime[] result = new LocalDateTime[count];
for( int i = 0; i < count; i++ ) {
result[i] = LocalDateTime.ofEpochSecond((int) Constants.interpolate(from, to, random()), 0, ZoneOffset.UTC);
}
return result;
}
/**
* Erzeugt einen Blindtext mit der angegebenen Anzahl Worten, aufgeteilt in
* die angegebene Anzahl Absätze.
* <p>
* Abssätze werden duch einen doppelten Zeilenumbruch {@code \n\n}
* getrennt.
*
* @param words Anzahl Wörter im Text insgesamt.
* @param paragraphs Anzahl Absätze.
* @return Ein zufälliger Blindtext.
*/
public static String fakeText( int words, int paragraphs ) {
String basetext = "";
try(
InputStream in = Faker.class.getResourceAsStream("mock-text.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))
) {
basetext = reader.readLine();
} catch( IOException ex ) {
LOG.error(ex, "Error generating fake blindtext: " + ex.getMessage());
}
String result = basetext.repeat(words / 283);
int w = (words % 283);
if( w > 0 ) {
Matcher m = Pattern.compile("([a-zA-Z_0-9\\u00C0-\\u017F]+\\W+){" + w + "}").matcher(basetext);
if( m.find() ) {
result += m.group().stripTrailing();
}
}
if( paragraphs > 1 ) {
int half = words / paragraphs;
final int maxLength = result.length();
Matcher m = Pattern.compile("([a-zA-Z_0-9\\u00C0-\\u017F]+\\W+){" + (half - 10) + "," + (half + 10) + "}([a-zA-Z_0-9\\u00C0-\\u017F]+)\\.").matcher(result);
if( m.find() ) {
result = m.replaceAll(( mr ) -> mr.end() == maxLength ? mr.group().trim() : mr.group().trim() + "\n\n");
}
}
return result;
}
/**
* Erzeugt ein Array mit der angegebenen Anzahl zufällig erzeugter Integer
* im angegebenen Bereich.
*
* @param count Anzahl der Zahlen im Array.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @return Ein Array mit Zufallszahlen.
* @see Constants#random(int, int)
*/
public static int[] fakeIntArray( int count, int min, int max ) {
int[] arr = new int[count];
for( int i = 0; i < count; i++ ) {
arr[i] = random(min, max);
}
return arr;
}
/**
* Erzeugt eine Liste mit der angegebenen Anzahl zufällig erzeugter Integer
* im angegebenen Bereich.
* <p>
* Ist {@code list} ein Listenobjekt, werden dei Zahlen an diese angehängt.
* Wird {@code null} übergeben, wird eine neue {@link ArrayList} erzeugt.
*
* @param count Anzahl der erzeugten Zahlen.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @param list Eine Liste, die befüllt werden soll, oder {@code null}.
* @return Ein Array mit Zufallszahlen.
* @see Constants#random(int, int)
*/
public static List<Integer> fakeIntegerList( int count, int min, int max, List<Integer> list ) {
List<Integer> result = (list == null) ? new ArrayList<>(count) : list;
fakeIntegers(count, min, max, result::add);
return result;
}
/**
* Erzeugt die angegebene Anzahl Zufallszahlen im angegebenen Bereich und
* übergibt sie an den angegebenen {@code Consumer}.
*
* Ein typischer Aufruf, um eine {@code #LinkedList} mit 100 Zufallszahlen
* zu erzeugen könnte so aussehen:
* <pre><code>
* List&lt;Integer&gt; l = new LinkedList&lt;&gt;();
* Faker.fakeIntegers(100, 0, 100, l::add);
* </code></pre>
*
* @param count Anzahl der erzeugten Zahlen.
* @param min Untere Grenze der Zufallszahlen.
* @param max Obere Grenze der Zufallszahlen.
* @param con {@code Consumer} für die Zahlen.
*/
public static void fakeIntegers( int count, int min, int max, Consumer<Integer> con ) {
for( int i = 0; i < count; i++ ) {
con.accept(random(min, max));
}
}
public static <L> L fakeIntegers( int count, int min, int max, Supplier<L> sup, BiConsumer<L, Integer> con ) {
L result = sup.get();
for( int i = 0; i < count; i++ ) {
con.accept(result, random(min, max));
}
return result;
}
@SuppressWarnings( "unchecked" )
private static <T> T[] randomSample( String filename, int count, Function<String, T> transformer, Class<T> type ) {
T[] result = (T[]) Array.newInstance(type, count);
int i = 0;
double k = count; // cast to double
double W = exp(log(random()) / k);
try(
InputStream in = Faker.class.getResourceAsStream("mock-" + filename + ".csv");
BufferedReader reader = new BufferedReader(new InputStreamReader(in))
) {
String line;
while( (line = reader.readLine()) != null ) {
if( i < count ) {
result[i] = transformer.apply(line);
i += 1;
} else {
int j = (int) (log(random()) / log(1 - W)) + 1;
while( j > 0 ) {
line = reader.readLine();
j -= 1;
}
if( line != null ) {
result[random(0, count - 1)] = transformer.apply(line);
i += 1;
} else {
break;
}
W *= exp(log(random()) / k);
}
}
// Fill remaining array
while( i < count ) {
result[i] = result[random(0, i - 1)];
i += 1;
}
} catch( IOException ex ) {
LOG.error(ex, "Error loading mock data file: " + ex.getMessage());
}
return result;
}
/**
* Erzeugt ein Platzhalter-Bild in der angegebenen Größe.
* <p>
* Das Bild ist ein aus dem Internet geladenes, zufälliges Motiv, dass unter
* einer freien Lizenz (Creative Commons) steht.
*
* @param width Breite des Bildes.
* @param height Höhe des Bildes.
* @return Ein zufälliges Bild in der angegebenen Größe.
*/
public static BufferedImage fakeImage( int width, int height ) {
return fakeImage(width, height, true);
}
/**
* Erzeugt ein Platzhalter-Bild in der angegebenen Größe.
* <p>
* Wenn {@code fromWeb} auf {@code true} gesetzt ist, wird ein zufälliges
* Motiv, das unter einer freien Lizenz (Creative Commons) steht, geladen.
* Bei {@code false} wird das Bild lokal generiert.
*
* @param width Breite des Bildes.
* @param height Höhe des Bildes.
* @param fromWeb Bei {@code true} wird das Bild aus dem Internet geladen,
* bei {@code false} wird das Bild lokal erzeugt.
* @return Ein zufälliges Bild in der angegebenen Größe.
*/
public static BufferedImage fakeImage( int width, int height, boolean fromWeb ) {
if( !fromWeb ) {
BufferedImage img = ImageLoader.createImage(width, height);
Graphics2D graphics = (Graphics2D) img.getGraphics().create();
String text = width + " x " + height;
Color clr = Constants.randomNiceColor();
graphics.setBackground(clr.getJavaColor());
graphics.clearRect(0, 0, width, height);
graphics.setColor(clr.textcolor().getJavaColor());
graphics.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, (int) ((width + height) * 0.05)));
FontMetrics fontMerics = graphics.getFontMetrics();
LineMetrics lineMetrics = fontMerics.getLineMetrics(text, graphics);
graphics.drawString(text,
(width - fontMerics.stringWidth(text)) / 2,
(int) (height / 2 - lineMetrics.getDescent() + lineMetrics.getAscent() / 2)
);
graphics.dispose();
return img;
} else {
return ImageLoader.loadImage(String.format(FAKE_IMG_URL, width, height), false);
}
}
private Faker() {
}
private static final Log LOG = Log.getLogger(Faker.class);
}

View File

@@ -1,6 +1,9 @@
package schule.ngb.zm.util;
import java.util.Enumeration;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.function.Supplier;
import java.util.logging.*;
@@ -23,12 +26,17 @@ import static java.util.logging.Level.*;
* Klasse nur genau ein {@code Log}-Objekt erstellt. Mehrere {@code Log}s nutzen
* dann aber denselben {@code Logger}.
* <p>
* Die API orientiert sich lose an <a href="#">Log4j</a> und vereinfacht die
* Die API orientiert sich lose an <a
* href="https://logging.apache.org/log4j/2.x/">Log4j</a> und vereinfacht die
* Nutzung der Java logging API für die häufigsten Anwendungsfälle.
*/
public final class Log {
private static final String ROOT_LOGGER = "schule.ngb.zm";
private static final String ROOT_LOGGER = "schule";
private static final String DEFAULT_LOG_FORMAT = "[%1$tT] [%4$11s] %5$s %6$s%n";
private static final String DEFAULT_DEBUG_FORMAT = "[%1$tT] [%4$11s] (%2$s) %5$s %6$s%n";
private static boolean LOGGING_INIT = false;
@@ -43,7 +51,7 @@ public final class Log {
*
* @see #enableGlobalLevel(Level)
*/
public static final void enableGlobalDebugging() {
public static void enableGlobalDebugging() {
enableGlobalLevel(ALL);
}
@@ -68,32 +76,18 @@ public final class Log {
* @param level Das Level, auf das alle {@code Logger} und {@code Handler}
* mindestens herabgesenkt werden sollen.
*/
public static final void xenableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
public static void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level, "level").intValue();
ensureRootLoggerInitialized();
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(level);
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
}
}
}
}
public static final void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
// Decrease level of root level ConsoleHandlers for outpu
// Decrease level of root level ConsoleHandlers for output
Logger rootLogger = Logger.getLogger("");
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
handler.setFormatter(new LogFormatter());
}
}
}
@@ -117,16 +111,41 @@ public final class Log {
}
}
}
Logger LOG = Logger.getLogger(ROOT_LOGGER);
LOG.fine("Debug logging enabled.");
}
public static final Log getLogger( Class<?> clazz ) {
if( !LOGGING_INIT ) {
Logger.getLogger(ROOT_LOGGER);
LOGGING_INIT = true;
}
public static Log getLogger( Class<?> clazz ) {
ensureRootLoggerInitialized();
return new Log(clazz);
}
private static void ensureRootLoggerInitialized() {
if( LOGGING_INIT ) {
return;
}
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null ) {
System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
}
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(Level.INFO);
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null
&& LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) {
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) {
@Override
public synchronized void publish( final LogRecord record ) {
super.publish(record);
flush();
}
});
}
LOGGING_INIT = true;
}
private final Logger LOGGER;
private final Class<?> sourceClass;
@@ -180,26 +199,15 @@ public final class Log {
}
}
private String inferCallerName() {
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
/// if( trace[i].getClassName().equals(sourceClass.getName()) ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
return trace[i].getMethodName();
}
}
return "unknown";
}
private void doLog( Level level, final Throwable throwable, final Supplier<String> msgSupplier ) {
String clazz = sourceClass.getName();
String method = "unknown";
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
clazz = trace[i].getClassName();
method = trace[i].getMethodName();
for( StackTraceElement stackTraceElement : trace ) {
if( !stackTraceElement.getClassName().equals(Log.class.getName()) ) {
clazz = stackTraceElement.getClassName();
method = stackTraceElement.getMethodName();
break;
}
}
@@ -315,4 +323,40 @@ public final class Log {
this.log(FINER, throwable, msgSupplier);
}
static final class LogFormatter extends Formatter {
@Override
public String format( LogRecord record ) {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if( record.getSourceClassName() != null ) {
source = record.getSourceClassName();
if( record.getSourceMethodName() != null ) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
String message = formatMessage(record);
String throwable = "";
if( record.getThrown() != null ) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return String.format(DEFAULT_DEBUG_FORMAT,
zdt,
source,
record.getLoggerName(),
record.getLevel().getLocalizedName(),
message,
throwable);
}
}
}

View File

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

View File

@@ -0,0 +1,193 @@
package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit;
/**
* Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p>
* Mit einem {@code Timer} kann zum Beispiel die Laufzeit eines Algorithmus
* gemessen werden. Wie eine echte Stoppuhr läuft der {@code Timer} weiter, wenn
* nach einem {@link #stop()} wieder {@link #start()} aufgerufen wird. Soll die
* Zeitmessung wieder bei null beginnen, muss vorher {@link #reset()} genutzt
* werden.
* <p>
* Die gemessene Zeit kann in {@link #getMillis() Millisekunden} oder
* {@link #getSeconds() Sekunden} abgerufen werden. Wird eine noch größere
* Genauigkeit benötigt, können mit {@link #getTime(TimeUnit)} beliebige
* Zeiteinheiten (zum Beispiel {@link TimeUnit#NANOSECONDS Nanosekunden})
* abgerufen werden.
* <p>
* Die Zeit kann auch bei laufender Uhr abgefragt werden. In dem Fall wird die
* bis zu diesem Zeitpunkt gemessene Zeit zurückgegeben.
*/
@SuppressWarnings( "unused" )
public final class Timer {
/**
* Die Basiseinheit für die Zeitmessung.
*/
private final TimeUnit baseUnit;
/**
* Ob die Zeitmessung gerade läuft.
*/
private boolean running = false;
/**
* Startzeit der Zeitmessung. -1, wenn noch keine Messung gestartet wurde.
*/
private long start = -1;
/**
* Zeit, die bisher bei allen Zeitmessungen, die gestoppt wurden, insgesamt
* vergangen ist.
*/
private long elapsed = 0;
/**
* Erstellt einen neuen {@code Timer} mit Millisekunden als Basiseinheit.
*/
public Timer() {
this(TimeUnit.MILLISECONDS);
}
/**
* Erstellt einen {@code Timer}, der die angegebene Einheit als Basiseinheit
* für {@link #getTime()} benutzt.
* <p>
* Um eine Zeitmessung in Nanosekunden durchzuführen, kann der {@code Timer}
* beispielsweise so instanziiert werden:
* <pre><code>
* Timer clock = new Timer(TimeUnit.NANOSECONDS);
* </code></pre>
*
* @param baseUnit Die Basiseinheit für die Zeitmessung.
*/
public Timer( TimeUnit baseUnit ) {
this.baseUnit = baseUnit;
}
/**
* Ob die Zeitmessung gerade läuft.
*
* @return {@code true}, wenn die Zeitmessung mit {@link #start()} gestartet
* wurde.
*/
public boolean isRunning() {
return running;
}
/**
* Startet die Zeitmessung.
* <p>
* Wenn zuvor schon eine Zeitmessung gestartet wurde, wird die neue Messung
* zur Summe aller Messungen hinzuaddiert. Soll die Messung bei null
* starten, muss vorher {@link #reset()} verwendet werden:
*
* <pre><code>
* // Timer auf null stellen und sofort starten
* timer.reset().start();
* </code></pre>
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer start() {
start = System.nanoTime();
running = true;
return this;
}
/**
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Dauer wird
* zur Summe aller gemessenen Zeiten hinzuaddiert.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer stop() {
if( running ) {
running = false;
elapsed += System.nanoTime() - start;
}
return this;
}
/**
* Setzt den {@code Timer} auf den Startzustand und löscht alle bisher
* gemessenen Zeiten. Falls die Zeitmessung gerade läuft, stoppt sie nicht,
* sondern startet vom Zeitpunkt des Aufrufs neu.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer reset() {
elapsed = 0;
start = -1;
if( running ) {
start = System.nanoTime();
}
return this;
}
/**
* Gibt die Zeit in der Basiseinheit zurück.
*
* @return Die bisher insgesamt gemessene Zeit.
*/
public long getTime() {
return getTime(baseUnit);
}
/**
* Gibt die Zeit in der angegebenen Einheit zurück.
* <p>
* Größere Zeiteinheiten werden gerundet und verlieren daher an Genauigkeit.
* Eine Zeitmessung von 999 Millisekunden wird als 0 Sekunden
* zurückgegeben.
* <p>
* Um genauere Ergebnisse zu erhalten, kann mit {@link #getMillis()} und
* {@link #getSeconds()} die gemessene Zeit in Minuten beziehungsweise
* Sekunden als Kommazahl abgefragt werden.
*
* @param unit Zeiteinheit
* @return Die bisher insgesamt gemessene Zeit in der gewählten Zeiteinheit.
*/
public long getTime( TimeUnit unit ) {
if( running ) {
return unit.convert(System.nanoTime() - start + elapsed, TimeUnit.NANOSECONDS);
} else {
return unit.convert(elapsed, TimeUnit.NANOSECONDS);
}
}
/**
* Gibt die gemessene Zeit in Millisekunden (gerundet) zurück.
*
* @return Die gemessene Zeit in ms.
*/
public int getMillis() {
if( running ) {
return (int) ((System.nanoTime() - start + elapsed) / 1000000);
} else {
return (int) (elapsed / 1000000);
}
}
/**
* Gibt die gemessene Zeit in Sekunden zurück.
*
* @return Die gemessene Zeit in s.
*/
public double getSeconds() {
if( running ) {
return (System.nanoTime() - start + elapsed) / 1000000000.0;
} else {
return elapsed / 1000000000.0;
}
}
}

View File

@@ -1,39 +1,62 @@
package schule.ngb.zm.util;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* Statische Methoden, um Methodenparameter auf Gültigkeit zu prüfen.
*/
@SuppressWarnings( {"unused", "UnusedReturnValue"} )
public class Validator {
public static final <T> T requireNotNull( T obj ) {
return Objects.requireNonNull(obj);
public static final <T> T requireNotNull( T obj, CharSequence paramName ) {
return requireNotNull(obj, () -> "Parameter <%s> may not be null.".format(paramName.toString()));
}
public static final <T> T requireNotNull( T obj, CharSequence msg ) {
return Objects.requireNonNull(obj, msg::toString);
public static final <T> T requireNotNull( T obj, CharSequence paramName, CharSequence msg ) {
return requireNotNull(obj, () -> msg.toString().format(paramName.toString()));
}
public static final <T> T requireNotNull( T obj, Supplier<String> msg ) {
return Objects.requireNonNull(obj, msg);
if( obj == null ) {
throw new NullPointerException(msg == null ? "Parameter may nut be null." : msg.get());
}
return obj;
}
public static final String requireNotEmpty( String str ) {
return requireNotEmpty(str, (Supplier<String>)null);
public static final String requireNotEmpty( String str, CharSequence paramName ) {
return requireNotEmpty(str, () -> String.format("Parameter <%s> may not be empty string (<%s> provided)", paramName, str));
}
public static final String requireNotEmpty( String str, CharSequence msg ) {
return requireNotEmpty(str, msg::toString);
public static final String requireNotEmpty( String str, CharSequence paramName, CharSequence msg ) {
return requireNotEmpty(str, () -> msg.toString().format(paramName.toString()));
}
public static final String requireNotEmpty( String str, Supplier<String> msg ) {
if( str.isEmpty() )
if( str.isEmpty() ) {
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be empty string (<%s> provided)", str) : msg.get());
}
return str;
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName ) {
return requireNotEmpty(arr, () -> String.format("Parameter <%s> may not be empty", paramName));
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence paramName, CharSequence msg ) {
return requireNotEmpty(arr, () -> msg.toString().format(paramName.toString()));
}
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
if( arr.length == 0 )
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
return arr;
}
public static final int requirePositive( int i ) {
return requirePositive(i, (Supplier<String>)null);
return requirePositive(i, (Supplier<String>) null);
}
public static final int requirePositive( int i, CharSequence msg ) {
@@ -47,7 +70,7 @@ public class Validator {
}
public static final int requireNotNegative( int i ) {
return requireNotNegative(i, (Supplier<String>)null);
return requireNotNegative(i, (Supplier<String>) null);
}
public static final int requireNotNegative( int i, CharSequence msg ) {
@@ -61,7 +84,7 @@ public class Validator {
}
public static final int requireInRange( int i, int min, int max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
return requireInRange(i, min, max, (Supplier<String>) null);
}
public static final int requireInRange( int i, int min, int max, CharSequence msg ) {
@@ -75,7 +98,7 @@ public class Validator {
}
public static final double requirePositive( double i ) {
return requirePositive(i, (Supplier<String>)null);
return requirePositive(i, (Supplier<String>) null);
}
public static final double requirePositive( double i, CharSequence msg ) {
@@ -89,7 +112,7 @@ public class Validator {
}
public static final double requireNotNegative( double i ) {
return requireNotNegative(i, (Supplier<String>)null);
return requireNotNegative(i, (Supplier<String>) null);
}
public static final double requireNotNegative( double i, CharSequence msg ) {
@@ -103,7 +126,7 @@ public class Validator {
}
public static final double requireInRange( double i, double min, double max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
return requireInRange(i, min, max, (Supplier<String>) null);
}
public static final double requireInRange( double i, double min, double max, CharSequence msg ) {
@@ -136,13 +159,17 @@ public class Validator {
}
*/
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
return requireSize(arr, 0, ()->"Parameter array may not be empty");
public static final <T> T[] requireSize( T[] arr, int size ) {
return requireSize(arr, size, (Supplier<String>) null);
}
public static final <T> T[] requireSize( T[] arr, int size, CharSequence msg ) {
return requireSize(arr, size, msg::toString);
}
public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) {
if( arr.length != size )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array must have <%d> elements (<%d> provided)", size) : msg.get());
throw new IllegalArgumentException(msg == null ? String.format("Parameter array must have <%d> elements (<%d> provided)", size, arr.length) : msg.get());
return arr;
}

View File

@@ -0,0 +1,114 @@
package schule.ngb.zm.util.events;
import schule.ngb.zm.util.Validator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiConsumer;
/**
* Implementierung einer EventListener-API basierend auf dem Artikel
* <a href="https://org.coloradomesa.edu/~wmacevoy/listen/paper.html">Skilled
* Listening in Java</a> von Dr. Warren D. MacEvoy.
* <p>
* Der {@code EventDispatcher} verwaltet eine Liste von angemeldeten Listenern
* in einem {@link CopyOnWriteArraySet}, dass besonders für Situationen geeignet
* ist, in denen in der Regel wenige Objekte verwaltet werden müssen, auf die
* schnell zugegriffen werden soll und die sich selten ändern. Das trifft in den
* meisten Fällen auf die Listener eines Objekts zu.
* <p>
* Um einen {@code EventDispatcher} zu nutzen, muss eine Schnittstelle als
* Unterinterface von {@link Listener} erstellt werden, die Methoden für die
* einzelnen Events definiert. In der Regel wird eine Methode pro Event
* angelegt, aber dies ist nicht unbedingt notwendig.
* <p>
* Ein Objekt, dass Events für diese Art von Listener erzeugt, erstellt einen
* {@code EventDispatcher} und
* {@link #registerEventType(String, BiConsumer) registriert} die notwendigen
* Events.
*
* <pre><code>
* EventDispatcher&lt;MyEvent, MyEventListener&gt; dispatcher = new EventDispatcher&lt;&gt;();
* dispatcher.registerEventType("start", (evt, listener) -> listener.started(evt));
* dispatcher.registerEventType("stop", (evt, listener) -> listener.stopped(evt));
* </code></pre>
* <p>
* Hier werden zwei Events registriert "start" und "stop". Die Bezeichnung der
* Events wird nut intern verwendet. Jedes Event registriert eine Funktion, die
* den Aufruf der Listener-Schnittstelle umsetzt. In der Regel ist dies ein
* Lambda-Ausdruck von zwei Parametern: dem Event-Objekt vom Typ {@code E} und
* der Listener, der aufgerufen werden soll, vom Typ {@code L}.
* <p>
* Nun können {@code MyEventListener} angemeldet und Events ausgelöst werden.
*
* <pre><code>
* public void addEventListener( MyEventListener listener ) {
* dispatcher.addListener(listener);
* }
*
* public void removeEventListener( MyEventListener listener ) {
* dispatcher.removeListener(listener);
* }
*
* public void methodThatCreatesEvents() {
* dispatcher.dispatchEvent("start", new MyEvent());
* // Do something
* dispatcher.dispatchEvent("stop", new MyEvent());
* }
* </code></pre>
* <p>
* Siehe {@link schule.ngb.zm.media.AudioListener} und
* {@link schule.ngb.zm.media.Music} für ein Beispiel der Verwendung.
*
* @param <E> Typ der Event-Objekte.
* @param <L> Typ der verwendeten Listener-Schnittstelle.
*/
public class EventDispatcher<E, L extends Listener<E>> {
private final CopyOnWriteArraySet<L> listeners;
private final ConcurrentMap<String, BiConsumer<E, L>> eventRegistry;
public EventDispatcher() {
listeners = new CopyOnWriteArraySet<>();
eventRegistry = new ConcurrentHashMap<>();
}
public void registerEventType( String eventKey, BiConsumer<E, L> dispatcher ) {
Validator.requireNotNull(eventKey, "eventKey");
Validator.requireNotNull(dispatcher, "dispatcher");
if( !eventRegistered(eventKey) ) {
eventRegistry.put(eventKey, dispatcher);
}
}
public void addListener( L listener ) {
listeners.add(listener);
}
public void removeListener( L listener ) {
listeners.remove(listener);
}
@SuppressWarnings( "unused" )
public boolean hasListeners() {
return !listeners.isEmpty();
}
public boolean eventRegistered( String eventKey ) {
return eventRegistry.containsKey(eventKey);
}
public void dispatchEvent( String eventKey, final E event ) {
Validator.requireNotNull(eventKey, "eventKey");
Validator.requireNotNull(event, "event");
if( eventRegistered(eventKey) ) {
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
listeners.forEach(( listener ) -> dispatcher.accept(event, listener));
}
}
}

View File

@@ -0,0 +1,11 @@
package schule.ngb.zm.util.events;
/**
* Generische Basisschnittstelle für die Implementierung von EventListenern.
*
* @param <E> Typ der Event-Objekte, die von diesem Listener empfangen werden.
* @see EventDispatcher
*/
public interface Listener<E> {
}

View File

@@ -0,0 +1,5 @@
/**
* Dieses Paket enthält Hilfsklassen, die das Listener-Entwurfsmuster
* umsetzen.
*/
package schule.ngb.zm.util.events;

View File

@@ -0,0 +1,214 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
/**
* Hilfsklasse, um Textdateien in verschiedenen Formaten einzulesen.
*/
@SuppressWarnings( "unused" )
public final class FileLoader {
/**
* Charset: ASCII
*/
public static final Charset ASCII = StandardCharsets.US_ASCII;
/**
* Charset: UTF-8
*/
public static final Charset UTF8 = StandardCharsets.UTF_8;
/**
* Charset: UTF-16
*/
public static final Charset UTF16 = StandardCharsets.UTF_16;
/**
* Charset: ISO-8859-1
*/
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
/**
* Lädt die angegebene Textdatei zeilenweise in eine Liste.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Die Quelle der Textdatei.
* @return Eine Liste mit den Zeilen der Textdatei.
* @see #loadLines(String, Charset)
*/
public static List<String> loadLines( String source ) {
return loadLines(source, UTF8);
}
/**
* Lädt die angegebene Textdatei mit dem angegebenen Charset zeilenweise in
* eine Liste.
* <p>
* Am Ende jeder Zeile wird das Symbol für einen Zeilenumbruch ({@code \n})
* entfernt.
*
* @param source Die Quelle der Textdatei.
* @param charset Das zu verwendene {@code Charset}.
* @return Eine Liste mit den Zeilen der Textdatei.
*/
public static List<String> loadLines( String source, Charset charset ) {
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
List<String> result = new ArrayList<>();
String line;
while( (line = reader.readLine()) != null ) {
result.add(line);
}
return result;
} catch( IOException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source);
return Collections.emptyList();
}
}
/**
* Lädt den Inhalt der angegebenen Textdatei vollständig in einen String.
* <p>
* Als {@link Charset} wird {@link #UTF8} verwendet.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source ) {
return loadText(source, UTF8);
}
/**
* Lädt den Inhalt der angegebenen Textdatei mit dem angegebenen
* {@code Charset} vollständig in einen String.
*
* @param source Eine Quelle für die Textdatei (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @param charset Der {@code Charset} der Textdatei.
* @return Der Inhalt der Textdatei.
*/
public static String loadText( String source, Charset charset ) {
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
StringBuilder result = new StringBuilder();
String line;
while( (line = reader.readLine()) != null ) {
result.append(line).append('\n');
}
return result.toString();
} catch( IOException ex ) {
LOG.error(ex, "Error while loading string from source <%s>", source);
return "";
}
}
/**
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
* String-Array.
* <p>
* Der Aufruf entspricht
* <pre><code>
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
* </code></pre>
*
* @param source Eine Quelle für die CSV-Datei (Absoluter Dateipfad,
* * Dateipfad im Classpath oder Netzwerkressource)
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @return Ein Array mit den Daten als {@code String}s.
* @see #loadCsv(String, char, boolean, Charset)
*/
public static String[][] loadCsv( String source, boolean skipFirst ) {
return loadCsv(source, ',', skipFirst, UTF8);
}
/**
* Lädt die Daten aus einer CSV-Datei in ein zweidimensionales
* String-Array.
* <p>
* Die Methode ist nicht in der Lage, komplexe CSV-Dateien zu verarbeiten.
* Insbesondere werden Inhalte, die das Trennzeichen {@code separator}
* enthalten, nicht korrekt erkannt. Das Trennzeichen wird unabhängig
* vom Kontext immer als Zelltrenner erkannt. (Im Normalfall kann das
* Trennzeichen durch die Verwendung doppelter Anführungszeichen in der Art
* {@code Inhalt,"Inhalt, der Komma enthält",Inhalt} maskiert werden.)
* <p>
* Es wird auch keine erweiterte Inhaltserkennung ausgeführt, sondern alle
* Inhalte als {@code String} gelesen. Die weitere Verarbeitung mit den
* passenden Parser-Methoden (beispielsweise
* {@link Double#parseDouble(String)}) obligt dem Nutzer.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static String[][] loadCsv( String source, char separator, boolean skipFirst, Charset charset ) {
int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map(
//( line ) -> line.split(Character.toString(separator))
( line ) -> line.split("\\s*" + separator + "\\s*")
).toArray(String[][]::new);
}
public static double[][] loadValues( String source, char separator, boolean skipFirst ) {
return loadValues(source, separator, skipFirst, UTF8);
}
/**
* Lädt Double-Werte aus einer CSV Datei in ein zweidimensionales Array.
* <p>
* Die gelesenen Strings werden mit {@link Double#parseDouble(String)} in
* {@code double} umgeformt. Es leigt in der Verantwortung des Nutzers
* sicherzustellen, dass die CSV-Datei auch nur Zahlen enthält, die korrekt
* in {@code double} umgewandelt werden können. Zellen für die die
* Umwandlung fehlschlägt werden mit 0.0 befüllt.
* <p>
* Die Methode unterliegt denselben Einschränkungen wie
* {@link #loadCsv(String, char, boolean, Charset)}.
*
* @param source Die Quelle der CSV-Daten.
* @param separator Das verwendete Trennzeichen.
* @param skipFirst Ob die erste Zeile übersprungen werden soll.
* @param charset Die zu verwendende Zeichenkodierung.
* @return Ein Array mit den Daten als {@code String}s.
*/
public static double[][] loadValues( String source, char separator, boolean skipFirst, Charset charset ) {
int n = skipFirst ? 1 : 0;
List<String> lines = loadLines(source, charset);
return lines.stream().skip(n).map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).toArray(double[][]::new);
}
private FileLoader() {
}
private static final Log LOG = Log.getLogger(FileLoader.class);
}

View File

@@ -0,0 +1,141 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.Image;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Eine Hilfsklasse mit Klassenmethoden, um Schriftarten zu laden.
* <p>
* Schriftarten können von verschiedenen Quellen geladen werden. Schriftarten,
* die aus Dateien geladen wurden, werden in einem internen Cache gespeichert
* und nachfolgende Versuche, dieselbe Schriftart zu laden, werden aus dem Cache
* bedient.
*/
public class FontLoader {
private static final Cache<String, Font> fontCache = Cache.newSoftCache();
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Methode kann eine Liste von Schriften bekommen und probiert diese
* nacheinander zu laden. Die erste Schrift, die Fehlerfrei geladen werden
* kann, wird zurückgegeben. Kann keine der Schriften geladen werden, ist
* das Ergebnis {@code null}.
* <p>
* Die gefundene Schrift wird unter ihrem Dateinamen in den
* Schriftenspeicher geladen und kann danach in der Zeichenmaschine benutzt
* werden.
* <p>
* Eine Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden.
*
* @param source
* @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/
public static Font loadFont( String source ) {
String name = source;
// Dateipfad entfernen
int lastIndex = source.lastIndexOf(File.separatorChar);
if( lastIndex > -1 ) {
source = 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 ) {
Validator.requireNotNull(source, "Font source may not be null");
Validator.requireNotEmpty(source, "Font source may not be empty.");
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( source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(name, font);
fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source);
return font;
} else {
font = null;
}
// Load userfonts
try( InputStream in = ResourceStreamProvider.getInputStream(source) ) {
font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(Font.PLAIN);
if( font != null ) {
fontCache.put(name, font);
fontCache.put(source, font);
//ge.registerFont(font);
}
LOG.debug("Loaded custom font from source <%s>.", source);
} catch( IOException ioex ) {
LOG.error(ioex, "Error loading custom font file from source <%s>.", source);
} catch( FontFormatException ffex ) {
LOG.error(ffex, "Error creating custom font from source <%s>.", source);
}
return font;
}
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Methode kann eine Liste von Schriften bekommen und probiert diese
* nacheinander zu laden. Die erste Schrift, die Fehlerfrei geladen werden
* kann, wird zurückgegeben. Kann keine der Schriften geladen werden, ist
* das Ergebnis {@code null}.
* <p>
* Die gefundene Schrift wird unter ihrem Dateinamen in den
* Schriftenspeicher geladen und kann danach in der Zeichenmaschine benutzt
* werden.
* <p>
* Eine Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* "Font-Name" geladen und kann danach zum Beispiel in einem
* {@link schule.ngb.zm.shapes.Text} mit {@code text.setFont("Font-Name");}
* verwendet werden.
*
* @param name
* @param sources
* @return Die erste geladene Schrift oder {@code null}.
* @see #loadFont(String, String)
*/
public static Font loadFonts( String name, String... sources ) {
for( String fontSource : sources ) {
// TODO: Ignore exceptions in this case and throw own at end?
Font font = loadFont(name, fontSource);
if( font != null ) {
return font;
}
}
return null;
}
private FontLoader() {
}
private static final Log LOG = Log.getLogger(FontLoader.class);
}

View File

@@ -1,4 +1,8 @@
package schule.ngb.zm.util;
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Cache;
import schule.ngb.zm.util.Validator;
import javax.imageio.ImageIO;
import java.awt.Color;
@@ -12,24 +16,25 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class ImageLoader {
/**
* Eine Hilfsklasse mit Klassenmethoden, um Bilder zu laden.
* <p>
* Bilder können von verschiedenen Quellen als {@link Image} geladen werden. Die
* Objekte werden in einem internen Cache gespeichert und nachfolgende Versuche,
* dieselbe Quelle zu laden, werden aus dem Cache bedient.
*/
public final class ImageLoader {
public static boolean caching = true;
private static Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
private static final Cache<String, BufferedImage> imageCache = Cache.newSoftCache();
/**
* Lädt ein Bild von der angegebenen Quelle {@code source}.
* <p>
* Die Bilddatei wird nach den Regeln von
* {@link ResourceStreamProvider#getResourceStream(String)} gesucht und
* {@link ResourceStreamProvider#getInputStream(String)} gesucht und
* geöffnet. Tritt dabei ein Fehler auf oder konnte keine passende Datei
* gefunden werden, wird {@code null} zurückgegeben.
* <p>
@@ -47,12 +52,12 @@ public class ImageLoader {
/**
* Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* Bild zurück oder <code>null</code>, wenn das Bild nicht geladen werden
* konnte. Ist ein Bild mit der angegebenen Quelle im Cache, wird das
* gespeicherte Bild zurückgegeben. Dies kann mit {@code caching = false}
* verhindert werden.
* Bild zurück oder {@code null}, wenn das Bild nicht geladen werden konnte.
* Ist ein Bild mit der angegebenen Quelle im Cache, wird das gespeicherte
* Bild zurückgegeben. Dies kann mit {@code caching = false} verhindert
* werden.
* <p>
* Wurde das Chaching global deaktiviert, kann mit <code>caching =
* Wurde das Caching global deaktiviert, kann mit <code>caching =
* true</code> das Bild trotzdem aus dem Cache geladen werden, wenn es
* vorhanden ist.
*
@@ -65,19 +70,19 @@ public class ImageLoader {
Validator.requireNotNull(source, "Image source may not be null");
Validator.requireNotEmpty(source, "Image source may not be empty.");
if( caching && isCached(source) ) {
return getCache(source);
if( caching && imageCache.containsKey(source) ) {
return imageCache.get(source);
}
BufferedImage img = null;
try( InputStream in = ResourceStreamProvider.getResourceStream(source) ) {
try( InputStream in = ResourceStreamProvider.getInputStream(source) ) {
//URL url = ResourceStreamProvider.getResourceURL(source);
//BufferedImage img = ImageIO.read(url);
img = ImageIO.read(in);
if( caching && img != null ) {
putCache(source, img);
imageCache.put(source, img);
}
} catch( IOException ioex ) {
LOG.error(ioex, "Error loading image file from source <%s>.", source);
@@ -105,16 +110,16 @@ public class ImageLoader {
public static boolean preloadImage( String name, String source ) {
BufferedImage img = loadImage(source, true);
if( img != null ) {
putCache(name, img);
imageCache.put(name, img);
return true;
}
return false;
}
/**
* Speiechert das angegebene Bild unter dem angegebenen Namen im Cache.
* Speichert das angegebene Bild unter dem angegebenen Namen im Cache.
* <p>
* Exisitert zu {@code name} schon ein Bild im Zwischenspeicher
* Existiert zu {@code name} schon ein Bild im Zwischenspeicher
* ({@code ImageLoader.isCached(name) == true}), dann wird dieses
* überschrieben.
* <p>
@@ -126,7 +131,7 @@ public class ImageLoader {
* @param img ZU speicherndes Bild.
*/
public static void preloadImage( String name, BufferedImage img ) {
putCache(name, img);
imageCache.put(name, img);
}
/**
@@ -137,8 +142,7 @@ public class ImageLoader {
* {@code false}.
*/
public static boolean isCached( String name ) {
SoftReference<BufferedImage> imgRef = imageCache.get(name);
return imgRef != null && imgRef != NOCACHE && imgRef.get() != null;
return imageCache.containsKey(name);
}
/**
@@ -148,33 +152,7 @@ public class ImageLoader {
* @param name Name des Bildes im Cache.
*/
public static void invalidateCache( final String name ) {
if( imageCache.containsKey(name) ) {
imageCache.remove(name);
}
}
/**
* Speichert ein Bild als {@link SoftReference} im Cache.
*
* @param name Name des Bildes im Zwischenspeicher.
* @param img Das zu speichernde Bild.
*/
private static void putCache( final String name, final BufferedImage img ) {
imageCache.put(name, new SoftReference<>(img));
}
/**
* Holt ein Bild aus dem Cache.
* <p>
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
* werden kann.
*
* @param name
* @return
*/
private static BufferedImage getCache( final String name ) {
return imageCache.get(name).get();
imageCache.remove(name);
}
/**
@@ -186,14 +164,14 @@ public class ImageLoader {
*
* @param name Die Quelle des Bildes.
*/
public static void preventCache( final String name ) {
imageCache.put(name, NOCACHE);
public static void disableCache( final String name ) {
imageCache.disableCache(name);
}
/**
* Leer den Cache und löschte alle bisher gespeicherten Bilder.
* <p>
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden
* Auch vorher mit {@link #disableCache(String)} verhinderte Caches werden
* gelöscht und müssen neu gesetzt werden.
*/
public static void clearCache() {
@@ -210,7 +188,7 @@ public class ImageLoader {
* Bild zurückgegeben und nicht komplett neu geladen.
* <p>
* <b>Wichtig:</b> Bildreferenzen, die aus dem Cache geladen werden,
* verweisen alle auf dasselbe Objekt. Änderungen schalgen sich daher in
* verweisen alle auf dasselbe Objekt. Änderungen schlagen sich daher in
* allen anderen Versionen des Bildes nieder (inklusive dem Bild im
* Zwischenspeicher). Für Änderungen sollte daher immer
* {@link #copyImage(BufferedImage) eine Kopie} des Bildes erstellt werden:
@@ -301,7 +279,7 @@ public class ImageLoader {
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert.
* wenn die Datei schon existiert.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
@@ -334,7 +312,7 @@ public class ImageLoader {
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert.
* wenn die Datei schon existiert.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.

View File

@@ -0,0 +1,213 @@
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.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Hilfsklasse, um {@link InputStream}s für Ressourcen zu erhalten.
*/
@SuppressWarnings("unused")
public class ResourceStreamProvider {
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Ressource wird wie folgt
* ermittelt:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* ({@code new File(source}.isFile() == true})?</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt ({@code getResource(source) != null})?</li>
* <li>Ist {@code source} im Classpath enthalten ({@code getClassLoader().getResource(source) != null})?</li>
* <li>Ansonsten erstelle ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* <p>
* Auftretende Exceptions werden als {@link IOException} geworfen.
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link java.net.MalformedURLException} beim Konstruieren des {@code URL}
* zu einer Datei verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkressource)
* @return Ein {@code InputStream} für die Ressource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim Erzeugen einer URL zu
* einer bestehenden Ressource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
// Ist source ein valider Dateipfad?
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
// Ist source im Classpath vorhanden?
URL url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
// Dann versuchen aus source direkt eine URL zu machen.
return new URL(source);
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
* ab, welche Art an Ressource gefunden wird:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* {@code new File(source).isFile() == true}, dann wird ein
* {@link FileInputStream} erstellt.</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt, wird ein passender
* Stream mit {@link Class#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} im Classpath enthalten, wird ein passender Stream
* mit {@link ClassLoader#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} eine gültige {@link URL}, dann wird ein
* {@link URL#openStream() Netwerkstream} geöffnet.</li>
* </ol>
* <p>
* Die Möglichen Ressourcen werden in der gelisteten Reihenfolge durchprobiert
* und der erste gefundene Stream zurückgegeben. Auftretende Exceptions
* werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source}
* keine gültige Ressource gefunden werden kann, wird am Ende die
* von {@link URL#openStream()} erzeugte {@code IOException} geworfen.
* <p>
* Bei einer Exception werden die nachfolgenden Quellen nicht mehr abgefragt.
* Eine {@link SecurityException} beim Öffnen des {@code FileInputStream}
* (zum Beispiel, weil auf eine existierende Datei keine Leserechte bestehen)
* verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code InputStream} für die Ressource.
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim Öffnen des Streams zu
* einer bestehenden Ressource oder falls
* keine passende Ressource gefunden
* wurde.
*/
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
return getResourceURL(source).openStream();
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* passenden {@link OutputStream}.
* <p>
* Es wird davon ausgegangen, dass die Quelle auf eine Datei verweist, auf
* die Schreibrechte bestehen. Ist dies nicht der Fall, wird eine
* {@link IOException} geworfen.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath).
* @return {@code OutputStream} für die Ressource.
* @throws IOException Falls die Quelle kein gültiges Ziel beschreibt.
*/
public static OutputStream getOutputStream( String source ) throws IOException {
try {
return Files.newOutputStream(Path.of(getResourceURL(source).toURI()));
} catch( URISyntaxException ex ) {
throw new IOException(ex);
}
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* {@link BufferedReader}.
* <p>
* Die Suche erfolgt, wie bei {@link #getInputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source ) throws IOException {
return getReader(source, StandardCharsets.UTF_8);
}
/**
* Sucht eine zur angegebenen Quelle passende Ressource und öffnet einen
* {@link BufferedReader} mit dem angegebenen Charset.
* <p>
* Die Suche erfolgt wie bei {@link #getInputStream(String)} beschrieben.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @param charset Der zu verwendende Charset.
* @return Ein {@code BufferedReader} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getInputStream(String)
*/
public static BufferedReader getReader( String source, Charset charset ) throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(source), charset.newDecoder()));
}
/**
* Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source ) throws IOException {
return getWriter(source, StandardCharsets.UTF_8);
}
/**
* Sucht eine zur angegebenen Quelle passende Datei und öffnet einen
* {@link BufferedWriter}.
* <p>
* Die Suche erfolgt wie bei {@link #getOutputStream(String)} beschrieben.
* <p>
* Als {@link Charset} wird {@link StandardCharsets#UTF_8 UTF-8} verwendet.
*
* @param source Eine Quelle für die Ressource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource).
* @return Ein {@code BufferedWriter} für die Quelle.
* @param charset Der zu verwendende Charset.
* @throws IOException Falls die Quelle nicht exisitert oder nicht geöffnet
* werden kann.
* @see #getOutputStream(String)
*/
public static BufferedWriter getWriter( String source, Charset charset ) throws IOException {
return new BufferedWriter(new OutputStreamWriter(getOutputStream(source), charset.newEncoder()));
}
// Privater C'tor
private ResourceStreamProvider() {
}
private static final Log LOG = Log.getLogger(ResourceStreamProvider.class);
}

View File

@@ -0,0 +1,5 @@
/**
* Dieses Paket enthält Hilfsklassen, um Ressourcen aus verschiedenen Quellen
* zu laden.
*/
package schule.ngb.zm.util.io;

View File

@@ -0,0 +1,4 @@
/**
* Dieses Paket enthält Hilfsklassen für verschiedene Einsatzzwecke.
*/
package schule.ngb.zm.util;

View File

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

View File

@@ -0,0 +1,44 @@
package schule.ngb.zm.util.tasks;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;
public abstract class FrameSynchronizedTask extends Task {
@Override
public void run() {
initialize();
running = true;
final Object lock = Zeichenmaschine.globalSyncLock;
// current time in ns
long beforeTime = System.nanoTime();
// internal counters for tick and delta time
int _tick;
double delta;
while( running ) {
// delta in seconds
delta = (System.nanoTime() - beforeTime) / 1000000000.0;
beforeTime = System.nanoTime();
_tick = Constants.tick;
this.update(delta);
synchronized( lock ) {
while( _tick >= Constants.tick ) {
try {
lock.wait();
} catch( InterruptedException e ) {
// We got interrupted ...
}
}
}
}
done = true;
finish();
}
}

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