32 Commits

Author SHA1 Message Date
ec30afd441 Fixed icon loading on windows 2022-11-28 09:26:45 +01:00
d3bdbdbffb Konstanten für Schriften und kleinere fixes 2022-11-28 09:11:37 +01:00
8cc7167d7e Formatierung und Doku 2022-11-28 09:11:13 +01:00
9e4271c304 Laden alternativer Schriften möglich 2022-11-28 09:10:59 +01:00
4f13f5177d Mausposition merken wenn pausiert 2022-11-28 09:10:42 +01:00
6321a7d421 render Method added 2022-11-28 09:10:31 +01:00
135af10729 Python files added 2022-11-28 09:09:58 +01:00
912f68c58f Javadoc 2022-08-01 20:50:23 +02:00
7f1d9012e9 Unter macOS auf Cmd+Q reagieren 2022-08-01 20:50:16 +02:00
60ed045986 Javadoc 2022-08-01 20:49:49 +02:00
dc16608333 Javadoc 2022-08-01 14:48:17 +02:00
7b6398fe52 Einfache Faker-Klasse, um Zufallsdaten zu erzeugen 2022-08-01 14:42:20 +02:00
8f98ddc56d Changelog 2022-08-01 10:08:15 +02:00
782ce33540 GradientPaint durch MultipleGradientPaint ersetzt 2022-08-01 10:08:11 +02:00
537527e525 Versuch den Interrupt von dispose() zu verhindern 2022-08-01 10:07:53 +02:00
8e93866b5e Interfaces verschoben 2022-07-31 10:03:28 +02:00
fcb536ff96 copyFrom angepasst 2022-07-31 10:02:11 +02:00
70c607f2e8 java.io -> java.nio 2022-07-31 10:00:22 +02:00
6126ed3c15 Vereinheitlichung der APIs für Füllungen und Konturen 2022-07-31 09:59:36 +02:00
b0353c53a0 Refactorings 2022-07-28 12:25:56 +02:00
c93a203ab9 DrawingLayer delegiert nun zu einer Shape
Macht Weniger doppelte Implementierungen nötig
2022-07-28 12:25:35 +02:00
f1d32685b4 KeyListener wieder zur Canvas bewegt 2022-07-28 12:24:48 +02:00
91842b511f Refactorings und Javadoc 2022-07-28 12:24:30 +02:00
52b480b46b Refactorings 2022-07-27 20:37:13 +02:00
4d2ade899d Refactorings und Javadoc 2022-07-27 20:37:01 +02:00
dcdca893b7 Refactorings und Javadoc 2022-07-27 20:36:34 +02:00
ebf0135486 Versionsnummer erhöht 2022-07-27 13:57:06 +02:00
fea1083926 Javadoc 2022-07-27 13:56:58 +02:00
250d9d17d3 Neuer Zustand QUITING 2022-07-27 13:56:09 +02:00
2a71243fc6 SuppressWarnings eingefügt 2022-07-27 13:55:28 +02:00
03d37222bf Neue choice() Methoden 2022-07-27 13:55:11 +02:00
687d7d35b7 Verbesserter Vollbildmodus und Trennung GUI / Controller 2022-07-27 13:54:55 +02:00
49 changed files with 5807 additions and 1275 deletions

8
.gitignore vendored
View File

@@ -48,3 +48,11 @@ gradle-app.setting
# Cache of project
.gradletasknamecache
# Python mkdocs
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

View File

@@ -6,6 +6,14 @@ und diese Projekt folgt [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Changed
- `FilledShape` und `StrokedShape` durch `Fillable` und `Strokeable` Interfaces ersetzt.
- `Shape` erweitert nun `BasisDrawable` als abstrakte Grundlage.
- `io` Klassen nutzen nun mehr der `java.nio` Funktionen.
- Package-Struktur angepasst.
## Version 0.0.23
### Added
- System für EventListener.
- `AudioListener` und `AnimationListener` als erste Anwendungsfälle.

View File

@@ -7,13 +7,14 @@ plugins {
zmVersion {
major = 0;
minor = 0;
rev = 21;
rev = 32;
}
}*/
}
version '{$zmVersion.major}.{$zmVersion.minor}.{$zmVersion.rev}-SNAPSHOT'
*/
group 'schule.ngb'
version '0.0.22-SNAPSHOT'
//version '{$zmVersion.major}.{$zmVersion.minor}.{$zmVersion.rev}-SNAPSHOT'
version '0.0.32-SNAPSHOT'
compileJava {
options.release = 11

0
docs/quickstart.md Normal file
View File

View File

@@ -0,0 +1,236 @@
package schule.ngb.zm;
import java.awt.*;
/**
* 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
/**
* {@inheritDoc}
*/
@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;
}
}
/**
* {@inheritDoc}
*/
@Override
public Paint getFill() {
if( fill != null ) {
return fill;
} else if( fillColor != null && fillColor.getAlpha() > 0 ) {
return fillColor;
} else {
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasFillColor() {
return fillColor != null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasGradient() {
return fill != null;
}
/**
* {@inheritDoc}
*/
@Override
public Color getFillColor() {
return fillColor;
}
/**
* {@inheritDoc}
*/
@Override
public void setFillColor( Color color ) {
fillColor = color;
fill = null;
}
/**
* {@inheritDoc}
*/
@Override
public MultipleGradientPaint getGradient() {
return fill;
}
// Implementierung Strokeable Interface
/**
* {@inheritDoc}
*/
@Override
public void setStroke( Stroke stroke ) {
this.stroke = stroke;
}
/**
* {@inheritDoc}
*/
@Override
public Stroke getStroke() {
if( stroke == null ) {
stroke = Strokeable.createStroke(strokeType, strokeWeight);
}
return stroke;
}
/**
* {@inheritDoc}
*/
@Override
public Color getStrokeColor() {
return strokeColor;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeColor( Color color ) {
strokeColor = color;
}
/**
* {@inheritDoc}
*/
@Override
public double getStrokeWeight() {
return strokeWeight;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeWeight( double weight ) {
strokeWeight = weight;
this.stroke = null;
}
/**
* {@inheritDoc}
*/
@Override
public Options.StrokeType getStrokeType() {
return strokeType;
}
/**
* {@inheritDoc}
*/
@Override
public void setStrokeType( Options.StrokeType type ) {
strokeType = type;
this.stroke = null;
}
}

View File

@@ -14,6 +14,7 @@ import java.awt.image.ColorModel;
* Eine Farbe hat außerdem einen Transparenzwert zwischen 0 (unsichtbar) und 255
* (deckend).
*/
@SuppressWarnings( "unused" )
public class Color implements Paint {
@@ -151,7 +152,7 @@ public class Color implements Paint {
* @param alpha Transparentwert zwischen 0 und 255.
*/
public Color( int red, int green, int blue, int alpha ) {
rgba = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | ((blue & 0xFF) << 0);
rgba = ((alpha & 0xFF) << 24) | ((red & 0xFF) << 16) | ((green & 0xFF) << 8) | (blue & 0xFF);
}
/**
@@ -160,7 +161,7 @@ public class Color implements Paint {
* @param color
*/
public Color( Color color ) {
this(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha());
this(color.getRGBA(), true);
}
/**
@@ -208,8 +209,7 @@ public class Color implements Paint {
* @return
*/
public static Color getRGBColor( int rgba ) {
Color c = new Color(rgba, true);
return c;
return new Color(rgba, true);
}
public static Color getHSBColor( double h, double s, double b ) {
@@ -259,8 +259,6 @@ public class Color implements Paint {
} 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));
@@ -307,12 +305,12 @@ public class Color implements Paint {
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_;
@@ -484,7 +482,6 @@ public class Color implements Paint {
}
}
@Override
/**
* Prüft, ob ein anderes Objekt zu diesem gleich ist.
*
@@ -494,6 +491,7 @@ public class Color implements Paint {
* @param obj Das zu vergleichende Objekt.
* @return {@code true}, wenn die Objekte gleich sind, sonst {@code false}.
*/
@Override
public boolean equals( Object obj ) {
if( obj == null ) {
return false;

View File

@@ -5,19 +5,22 @@ import schule.ngb.zm.util.io.ImageLoader;
import schule.ngb.zm.util.Noise;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.Collections;
import java.util.Random;
import java.util.function.DoubleUnaryOperator;
/**
* Basisklasse für die meisten Objekte der Zeichemaschine, die von Nutzern
* Basisklasse für die meisten Objekte der Zeichenmaschine, die von Nutzern
* erweitert werden können.
* <p>
* Die Konstanten stellen viele Funktionen zur einfachen Programmierung bereit
* und enthält auch einige dynamische Werte, die von der Zeichenmaschine laufend
* aktuell gehalten werden (beispielsweise {@link #runtime}.
* aktuell gehalten werden (beispielsweise {@link #runtime}).
* <p>
* Für die Implementierung eigener Klassen ist es oft hilfreich von
* {@code Constants} zu erben, um die Methoden und Konstanten einfach im
@@ -64,13 +67,49 @@ public class Constants {
/**
* Patchversion der Zeichenmaschine.
*/
public static final int APP_VERSION_REV = 22;
public static final int APP_VERSION_REV = 32;
/**
* Version der Zeichenmaschine als Text-String.
*/
public static final String APP_VERSION = APP_VERSION_MAJ + "." + APP_VERSION_MIN + "." + APP_VERSION_REV;
/**
* Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde.
*/
public static final boolean MACOS;
/**
* Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde.
*/
public static final boolean WINDOWS;
/**
* Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde.
*/
public static final boolean LINUX;
static {
final String name = System.getProperty("os.name");
if( name.contains("Mac") ) {
MACOS = true;
WINDOWS = false;
LINUX = false;
} else if( name.contains("Windows") ) {
MACOS = false;
WINDOWS = true;
LINUX = false;
} else if( name.equals("Linux") ) { // true for the ibm vm
MACOS = false;
WINDOWS = false;
LINUX = true;
} else {
MACOS = false;
WINDOWS = false;
LINUX = false;
}
}
/**
* Standardbreite eines Zeichenfensters.
@@ -405,6 +444,21 @@ public class Constants {
*/
public static final double TWO_PI = Math.PI * 2.0;
/**
* Konstante für fette Schrift.
*/
public static final int BOLD = Font.BOLD;
/**
* Konstante für kursive Schrift.
*/
public static final int ITALIC = Font.ITALIC;
/**
* Konstante für normale Schrift.
*/
public static final int PLAIN = Font.PLAIN;
/*
* Globale Variablen, die von allen Klassen genutzt werden dürfen. Änderungen
* wirken sich auf die aktuelle Zeichenmaschine aus und sollten nur von der
@@ -1280,6 +1334,26 @@ public class Constants {
return getRandom().nextGaussian();
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
public static final int choice( int[] values ) {
return values[random(0, values.length - 1)];
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @return Ein zufälliges Element aus dem Array.
*/
public static final double choice( double[] values ) {
return values[random(0, values.length - 1)];
}
/**
* Wählt ein zufälliges Element aus dem Array aus.
*
@@ -1296,15 +1370,149 @@ public class Constants {
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/
public static final int[] choice( int[] values, int n, boolean unique ) {
if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
int[] result = new int[n];
int[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) {
int j = random(0, valuesCopy.length - 1);
int l = valuesCopy.length - 1;
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[l];
valuesCopy[l] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, l);
}
return result;
}
/**
* Wählt die angegebene Anzahl Elemente aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/
public static final double[] choice( double[] values, int n, boolean unique ) {
if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
double[] result = new double[n];
double[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) {
int j = random(0, valuesCopy.length - 1);
int l = valuesCopy.length - 1;
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[l];
valuesCopy[l] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, l);
}
return result;
}
/**
* Wählt die angegebene Anzahl Elemente aus dem Array aus.
*
* @param values Ein Array mit Werten, die zur Auswahl stehen.
* @param n Anzahl der auszuwählenden Elemente.
* @param unique Bei {@code true} werden Elemente im Array nur maximal
* einmal ausgewählt (Ziehen ohne Zurücklegen).
* @param <T> Datentyp der Elemente.
* @return Ein zufälliges Element aus dem Array.
* @throws IllegalArgumentException Wenn {@code unique == true} und
* {@code values.length < n}, also nicht
* genug Werte zur Wahl stehen.
*/
public static final <T> T[] choice( T[] values, int n ) {
Object[] result = new Object[n];
public static final <T> T[] choice( T[] values, int n, boolean unique ) {
if( unique && values.length < n )
throw new IllegalArgumentException(
String.format("Need at least <%d> values to choose <%d> unique values (<%d> given).", n, n, values.length)
);
T[] result = Arrays.copyOf(values, n);
T[] valuesCopy = Arrays.copyOf(values, values.length);
for( int i = 0; i < n; i++ ) {
result[i] = choice(values);
int last = valuesCopy.length - 1;
int j = random(0, last);
result[i] = valuesCopy[j];
valuesCopy[j] = valuesCopy[last];
valuesCopy[last] = result[i];
if( unique )
valuesCopy = Arrays.copyOf(valuesCopy, last);
}
return (T[]) result;
return result;
}
/**
* Bringt die Zahlen im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Zahlen, die gemischt werden sollen.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final int[] shuffle( int[] values ) {
for( int i = 0; i < values.length - 1; i++ ) {
int j = random(i, values.length - 1);
int tmp = values[i];
values[i] = values[j];
values[j] = tmp;
}
return values;
}
/**
* Bringt die Zahlen im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Zahlen, die gemischt werden sollen.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final double[] shuffle( double[] values ) {
for( int i = 0; i < values.length - 1; i++ ) {
int j = random(i, values.length - 1);
double tmp = values[i];
values[i] = values[j];
values[j] = tmp;
}
return values;
}
/**
* Bringt die Werte im Array in eine zufällige Reihenfolge.
*
* @param values Ein Array mit Werte, die gemischt werden sollen.
* @param <T> Datentyp der Elemente.
* @return Das Array in zufälliger Reihenfolge.
*/
public static final <T> T[] shuffle( T[] values ) {
java.util.List<T> valueList = Arrays.asList(values);
Collections.shuffle(valueList, random);
return valueList.toArray(values);
}
/**
@@ -1474,7 +1682,7 @@ public class Constants {
}
public static final double asDouble( char value ) {
return (double) value;
return value;
}
public static final double asDouble( byte value ) {

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

@@ -77,7 +77,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Gibt die Resourcen der Ebene frei.
* Gibt die Ressourcen der Ebene frei.
*/
public void dispose() {
drawing.dispose();
@@ -139,14 +139,14 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Zeichnet den Puffer auf die Grafik-Instanz.
* Zeichnet den Puffer auf den Grafikkontext.
*
* @param pGraphics
* @param graphics Der Grafikkontext, auf den gezeichnet wird.
*/
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( visible ) {
pGraphics.drawImage(buffer, 0, 0, null);
graphics.drawImage(buffer, 0, 0, null);
}
}

View File

@@ -36,12 +36,13 @@ public final class Options {
INITIALIZING,
INITIALIZED,
RUNNING,
UPDATING,
DRAWING,
PAUSED,
STOPPED,
TERMINATED,
QUITING,
IDLE,
UPDATING,
DRAWING,
DELAYED,
DISPATCHING
}

View File

@@ -108,7 +108,7 @@ public class Spielemaschine extends Zeichenmaschine {
}
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
clear();
List<Drawable> it = List.copyOf(drawables);
for( Drawable d: it ) {
@@ -116,7 +116,7 @@ public class Spielemaschine extends Zeichenmaschine {
d.draw(drawing);
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -1,51 +1,68 @@
package schule.ngb.zm.shapes;
package schule.ngb.zm;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Drawable;
import schule.ngb.zm.Options;
import schule.ngb.zm.shapes.Shape;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Stroke;
/**
* Basisklasse für Formen, die eine Konturlinie besitzen.
* {@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 erfü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 abstract class StrokedShape extends Constants implements Drawable {
public interface Strokeable extends Drawable {
/**
* Aktuelle Farbe der Konturlinie oder {@code null}, wenn die Form ohne
* kontur dargestellt werden soll.
* Setzt den {@code Stroke} für die Konturlinie direkt.
*
* @param stroke Ein {@code Stroke}-Objekt.
*/
protected Color strokeColor = DEFAULT_STROKECOLOR;
void setStroke( Stroke stroke );
/**
* Die Dicke der Konturlinie. Wird nicht kleiner als 0.
* Gibt ein {@code Stroke}-Objekt mit den aktuell gesetzten Eigenschaften
* zurück.
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
Stroke getStroke();
/**
* Die Art der Konturlinie.
* 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.
*/
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;
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}.
*/
public Color getStrokeColor() {
return strokeColor;
}
Color getStrokeColor();
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe.
@@ -53,9 +70,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param color Die neue Farbe der Konturlinie.
* @see Color
*/
public void setStrokeColor( Color color ) {
this.strokeColor = color;
}
void setStrokeColor( Color color );
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die
@@ -66,7 +81,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
public void setStrokeColor( Color color, int alpha ) {
default void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha));
}
@@ -77,7 +92,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setStrokeColor( int gray ) {
default void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
@@ -90,7 +105,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setStrokeColor( int gray, int alpha ) {
default void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
@@ -105,7 +120,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue ) {
default void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
@@ -121,14 +136,14 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue, int alpha ) {
default void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new Color(red, green, blue, alpha));
}
/**
* Entfernt die Kontur der Form.
*/
public void noStroke() {
default void noStroke() {
setStrokeColor(null);
}
@@ -139,10 +154,10 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT
* @see schule.ngb.zm.Constants#SOLID
*/
public void resetStroke() {
setStrokeColor(DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID);
default void resetStroke() {
setStrokeColor(Constants.DEFAULT_STROKECOLOR);
setStrokeWeight(Constants.DEFAULT_STROKEWEIGHT);
setStrokeType(Constants.SOLID);
}
/**
@@ -150,9 +165,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
*
* @return Die aktuelle Dicke der Linie.
*/
public double getStrokeWeight() {
return strokeWeight;
}
double getStrokeWeight();
/**
* Setzt die Dicke der Konturlinie. Die Dicke muss größer 0 sein. Wird 0
@@ -160,9 +173,8 @@ public abstract class StrokedShape extends Constants implements Drawable {
*
* @param weight Die Dicke der Konturlinie.
*/
public void setStrokeWeight( double weight ) {
this.strokeWeight = max(0.0, weight);
this.stroke = null;
default void setStrokeWeight( double weight ) {
setStroke(createStroke(getStrokeType(), weight));
}
/**
@@ -171,25 +183,19 @@ public abstract class StrokedShape extends Constants implements Drawable {
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
public Options.StrokeType getStrokeType() {
return strokeType;
}
Options.StrokeType getStrokeType();
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
* 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
*/
public void setStrokeType( Options.StrokeType type ) {
this.strokeType = type;
this.stroke = null;
default void setStrokeType( Options.StrokeType type ) {
setStroke(createStroke(type, getStrokeWeight()));
}
@Override
public abstract void draw( Graphics2D graphics );
/**
* Hilfsmethode, um ein {@link Stroke} Objekt mit den aktuellen
* Kontureigenschaften zu erstellen. Der aktuelle {@code Stroke} wird
@@ -197,52 +203,26 @@ public abstract class StrokedShape extends Constants implements Drawable {
*
* @return Ein {@code Stroke} mit den passenden Kontureigenschaften.
*/
protected Stroke createStroke() {
// TODO: Used global cached Stroke Objects?
if( stroke == null ) {
switch( strokeType ) {
case DOTTED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{1.0f, 5.0f}, 0.0f);
break;
case DASHED:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f, new float[]{5.0f}, 0.0f);
break;
case SOLID:
default:
stroke = new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
break;
}
}
return stroke;
}
/**
* Hilfsmethode für Unterklassen, um die angegebene Form mit den aktuellen
* Kontureigenschaften auf den Grafik-Kontext zu zeichnen. Die Methode
* verändert gegebenenfalls die aktuelle Farbe des Grafikobjekts und setzt
* sie nicht auf den Ursprungswert zurück, wie von {@link #draw(Graphics2D)}
* gefordert. Dies sollte die aufrufende Unterklasse übernehmen.
*
* @param shape Die zu zeichnende Java-AWT Form
* @param graphics Das Grafikobjekt.
*/
protected void strokeShape( java.awt.Shape shape, Graphics2D graphics ) {
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.draw(shape);
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

@@ -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.");
@@ -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

@@ -11,6 +11,7 @@ import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
@@ -60,8 +61,8 @@ public class Zeichenfenster extends JFrame {
private GraphicsDevice displayDevice;
/**
* Bevorzugte Abmessungen der Zeichenleinwand. Für das Zeichenfenster hat
* es Priorität die Leinwand auf dieser Größe zu halten. Gegebenenfalls unter
* 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.
*/
@@ -78,12 +79,13 @@ public class Zeichenfenster extends JFrame {
* verlassen. Wird von {@link #setFullscreen(boolean)} automatisch
* hinzugefügt und entfernt.
*/
private KeyListener fullscreenExitListener = new KeyAdapter() {
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();
}
}
};
@@ -110,12 +112,11 @@ public class Zeichenfenster extends JFrame {
this.canvasPreferredWidth = canvas.getWidth();
this.canvasPreferredHeight = canvas.getHeight();
//this.add(canvas, BorderLayout.CENTER);
this.add(canvas);
this.add(canvas, BorderLayout.CENTER);
this.canvas = canvas;
// Konfiguration des Frames
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION: title);
this.setTitle(title == null ? "Zeichenfenster " + Constants.APP_VERSION : title);
// Kann vom Aufrufenden überschrieben werden
this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
@@ -129,7 +130,7 @@ public class Zeichenfenster extends JFrame {
taskbar.setIconImage(icon);
} else {
ArrayList<Image> icons = new ArrayList<>(4);
for( int size = 32; size <= 512; size *= size ) {
for( int size: new int[]{32, 64, 128, 512} ) {
icons.add(ImageIO.read(new File("icon_" + size + ".png")));
}
@@ -144,6 +145,7 @@ public class Zeichenfenster extends JFrame {
// Fenster zusammenbauen, auf dem Bildschirm positionieren ...
this.pack();
this.setResizable(false);
this.setFocusable(true);
this.setLocationByPlatform(true);
// this.centerFrame();
}
@@ -153,6 +155,7 @@ public class Zeichenfenster extends JFrame {
}
public Rectangle getScreenBounds() {
// return GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
return displayDevice.getDefaultConfiguration().getBounds();
}
@@ -163,6 +166,7 @@ public class Zeichenfenster extends JFrame {
public Rectangle getCanvasBounds() {
return canvas.getBounds();
}
/**
* Zentriert das Zeichenfenster auf dem aktuellen Bildschirm.
*/
@@ -176,7 +180,7 @@ public class Zeichenfenster extends JFrame {
}
public void setCanvasSize( int newWidth, int newHeight ) {
// TODO: (ngb) Put constains on max/min frame/canvas size
// TODO: (ngb) Put constrains on max/min frame/canvas size
if( fullscreen ) {
canvasPreferredWidth = newWidth;
canvasPreferredHeight = newHeight;
@@ -205,27 +209,53 @@ public class Zeichenfenster extends JFrame {
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 ) {
// frame.setUndecorated(true);
this.setResizable(false); // Should be set anyway
// Activate fullscreen
dispose();
setUndecorated(true);
setResizable(false);
displayDevice.setFullScreenWindow(this);
java.awt.Rectangle bounds = getScreenBounds();
// TODO: (ngb) We need to switch layouts to allow the LayoutManger to maximize the canvas
canvas.setSize(bounds.width, bounds.height);
// Register ESC to exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
// 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 ) {
fullscreen = false;
displayDevice.setFullScreenWindow(null);
dispose();
setUndecorated(false);
setResizable(false);
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
canvas.setSize(canvasPreferredWidth, canvasPreferredHeight);
this.pack();
// frame.setUndecorated(false);
setVisible(true);
pack();
//canvas.requestFocus();
canvas.requestFocus();
fullscreen = false;
}
// Resume rendering
canvas.resumeRendering();
}
}

View File

@@ -1,16 +1,14 @@
package schule.ngb.zm;
import schule.ngb.zm.layers.ColorLayer;
import schule.ngb.zm.layers.ShapesLayer;
import schule.ngb.zm.util.Log;
import java.awt.*;
import java.awt.Canvas;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.*;
/**
* Eine Leinwand ist die Hauptkomponente einer Zeichenmaschine. Sie besteht aus
@@ -24,10 +22,14 @@ import java.util.concurrent.LinkedBlockingQueue;
*/
public class Zeichenleinwand extends Canvas {
private final Object[] renderLock = new Object[0];
/**
* Liste der hinzugefügten Ebenen.
*/
private LinkedList<Layer> layers;
private final List<Layer> layers;
private boolean rendering = false, suspended = false;
/**
* Erstellt eine neue Zeichenleinwand mit einer festen Größe.
@@ -41,13 +43,47 @@ public class Zeichenleinwand extends Canvas {
this.setMinimumSize(getSize());
this.setBackground(Constants.DEFAULT_BACKGROUND.getJavaColor());
// Liste der Ebenen initialisieren und die Standardebenen einfügen
layers = new LinkedList<>();
synchronized( layers ) {
layers.add(new ColorLayer(width, height, Constants.DEFAULT_BACKGROUND));
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;
}
}
public void resumeRendering() {
suspended = false;
}
/**
* Ändert die Größe der Zeichenleinwand auf die angegebene Größe in Pixeln.
* <p>
@@ -78,32 +114,28 @@ public class Zeichenleinwand extends Canvas {
*/
public void addLayer( Layer layer ) {
if( layer != null ) {
synchronized( layers ) {
layer.setSize(getWidth(), getHeight());
layers.add(layer);
}
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
* {@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 i Index der Ebene beginnend mit 0.
* @param layer Die neue Ebene.
*/
public void addLayer( int i, Layer layer ) {
if( layer != null ) {
synchronized( layers ) {
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
layer.setSize(getWidth(), getHeight());
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
}
}
@@ -118,19 +150,19 @@ public class Zeichenleinwand extends Canvas {
}
/**
* Gibt die Liste der bisher hinzugefügten Ebenen zurück.
* Gibt eine Kopie der Liste der bisher hinzugefügten Ebenen zurück.
*
* @return Liste der Ebenen.
*/
public java.util.List<Layer> getLayers() {
return layers;
public List<Layer> getLayers() {
return List.copyOf(layers);
}
/**
* Holt die Ebene am Index <var>i</var> (beginnend bei 0).
* Holt die Ebene am Index {@code i} (beginnend bei 0).
*
* @param i Index der Ebene (beginnend bei 0).
* @return Die Ebene am Index <var>i</var> oder {@code null}.
* @return Die Ebene am Index {@code i} oder {@code null}.
* @throws IndexOutOfBoundsException Falls der Index nicht existiert.
*/
public Layer getLayer( int i ) {
@@ -145,14 +177,16 @@ public class Zeichenleinwand extends Canvas {
* Sucht die erste Ebene des angegebenen Typs aus der Liste der Ebenen.
* Existiert keine solche Ebene, wird {@code null} zurückgegeben.
*
* @param clazz Typ der Ebene.
* @param <L>
* @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> clazz ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(clazz) ) {
return clazz.cast(layer);
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;
@@ -163,44 +197,41 @@ public class Zeichenleinwand extends Canvas {
* gibt diese als Liste zurück. Die Reihenfolge in der Liste entspricht der
* Reihenfolge der Ebenen in der Leinwand (von unten nach oben).
*
* @param pClazz
* @param <L>
* @return
* @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.
*/
public <L extends Layer> java.util.List<L> getLayers( Class<L> pClazz ) {
public <L extends Layer> List<L> getLayers( Class<L> type ) {
ArrayList<L> result = new ArrayList<>(layers.size());
for( Layer layer : layers ) {
if( layer.getClass().equals(pClazz) ) {
result.add(pClazz.cast(layer));
synchronized( layers ) {
for( Layer layer : layers ) {
if( layer.getClass().equals(type) ) {
result.add(type.cast(layer));
}
}
}
return result;
}
public boolean removeLayer( Layer pLayer ) {
synchronized( layers ) {
return layers.remove(pLayer);
}
return layers.remove(pLayer);
}
public void removeLayers( Layer... pLayers ) {
public void removeLayers( Layer... removeLayers ) {
synchronized( layers ) {
for( Layer layer : pLayers ) {
for( Layer layer : removeLayers ) {
layers.remove(layer);
}
}
}
public void clearLayers() {
synchronized( layers ) {
layers.clear();
}
layers.clear();
}
public void updateLayers( double delta ) {
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
for( Layer layer : List.copyOf(layers) ) {
layer.update(delta);
}
}
@@ -237,8 +268,7 @@ public class Zeichenleinwand extends Canvas {
public void draw( Graphics graphics ) {
Graphics2D g2d = (Graphics2D) graphics.create();
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
for( Layer layer : layers ) {
layer.draw(g2d);
}
}
@@ -249,37 +279,41 @@ public class Zeichenleinwand extends Canvas {
* Zeigt den aktuellen Inhalt der Zeichenleinwand an.
*/
public void render() {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
if( !suspended && isDisplayable() ) {
if( getBufferStrategy() == null ) {
allocateBuffer();
}
if( isDisplayable() ) {
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
synchronized( renderLock ) {
rendering = true;
BufferStrategy strategy = this.getBufferStrategy();
if( strategy != null ) {
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
do {
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.clearRect(0, 0, getWidth(), getHeight());
synchronized( layers ) {
List<Layer> it = List.copyOf(layers);
for( Layer layer : it ) {
layer.draw(g2d);
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();
}
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() );
// Repeat the rendering if the drawing buffer was lost
} while( strategy.contentsLost() );
}
rendering = false;
renderLock.notifyAll();
}
}
}

View File

@@ -17,6 +17,7 @@ import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Hauptklasse der Zeichenmaschine.
@@ -37,43 +38,6 @@ public class Zeichenmaschine extends Constants {
IN_BLUEJ = System.getProperty("java.class.path").contains("bluej");
}
/**
* Gibt an, ob die Zeichenmaschine unter macOS gestartet wurde.
*/
public static final boolean MACOS;
/**
* Gibt an, ob die Zeichenmaschine unter Windows gestartet wurde.
*/
public static final boolean WINDOWS;
/**
* Gibt an, ob die Zeichenmaschine unter Linux gestartet wurde.
*/
public static final boolean LINUX;
static {
final String name = System.getProperty("os.name");
if( name.contains("Mac") ) {
MACOS = true;
WINDOWS = false;
LINUX = false;
} else if( name.contains("Windows") ) {
MACOS = false;
WINDOWS = true;
LINUX = false;
} else if( name.equals("Linux") ) { // true for the ibm vm
MACOS = false;
WINDOWS = false;
LINUX = true;
} else {
MACOS = false;
WINDOWS = false;
LINUX = false;
}
}
/*
* Objektvariablen, die von Unterklassen benutzt werden können.
*/
@@ -111,14 +75,14 @@ public class Zeichenmaschine extends Constants {
/**
* Das Zeichenfenster der Zeichenmaschine
*/
private Zeichenfenster frame;
protected Zeichenfenster frame;
// Aktueller Zustand der Zeichenmaschine.
/**
* Zustand der Zeichenmaschine insgesamt
*/
private Options.AppState state = Options.AppState.INITIALIZING;
private Options.AppState state;
/**
* Zustand des update/draw Threads
@@ -128,7 +92,7 @@ public class Zeichenmaschine extends Constants {
/**
* Ob der Zeichenthread noch laufen soll, oder beendet.
*/
private boolean running = false;
private boolean running;
private boolean terminateImediately = false;
@@ -141,7 +105,7 @@ public class Zeichenmaschine extends Constants {
* Ob die ZM bei nicht überschriebener update() Methode stoppen soll,
* oder trotzdem weiterläuft.
*/
private boolean run_once = true;
private final boolean run_once;
/**
* Aktuelle Frames pro Sekunde der Zeichenmaschine.
@@ -151,17 +115,17 @@ public class Zeichenmaschine extends Constants {
/**
* Hauptthread der Zeichenmaschine.
*/
private Thread mainThread;
private final Thread mainThread;
/**
* Queue für geplante Aufgaben
*/
private DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
private final DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
/**
* Queue für abgefangene InputEvents
*/
private BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
private final BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
/**
* Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
@@ -274,9 +238,15 @@ public class Zeichenmaschine extends Constants {
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}.
*/
@SuppressWarnings("static-access")
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
// Register Cmd+Q on macOS
if( Constants.MACOS ) {
System.setProperty("apple.eawt.quitStrategy", "CLOSE_ALL_WINDOWS");
}
// Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height);
@@ -312,8 +282,15 @@ public class Zeichenmaschine extends Constants {
canvas.addMouseWheelListener(inputListener);
canvas.addKeyListener(inputListener);
/*KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent( KeyEvent e ) {
enqueueEvent(e);
return false;
}
});*/
// Programm beenden, wenn Fenster geschlossen wird
// TODO: (ngb) Der Listener hat zu viel FUnktionalität -> nach quit() / exit() auslagern
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
@@ -350,17 +327,15 @@ public class Zeichenmaschine extends Constants {
*
* @param title
*/
private final Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
private Zeichenfenster createFrame( Zeichenleinwand c, String title ) {
while( frame == null ) {
try {
TaskRunner.invokeLater(new Runnable() {
@Override
public void run() {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}
TaskRunner.invokeLater(() -> {
Zeichenfenster.setLookAndFeel();
frame = new Zeichenfenster(canvas, title);
}).get();
} catch( InterruptedException e ) {
// Keep waiting
} catch( ExecutionException e ) {
LOG.error(e, "Error initializing application frame: %s", e.getMessage());
throw new RuntimeException(e);
@@ -392,10 +367,18 @@ public class Zeichenmaschine extends Constants {
public final void setFullscreen( boolean pEnable ) {
if( pEnable && !frame.isFullscreen() ) {
frame.setFullscreen(true);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( frame.isFullscreen() )
fullscreenChanged();
} else if( !pEnable && frame.isFullscreen() ) {
frame.setFullscreen(false);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
if( !frame.isFullscreen() )
fullscreenChanged();
}
@@ -444,9 +427,18 @@ public class Zeichenmaschine extends Constants {
* im Zeichenfenster an.
*/
public final void redraw() {
if( state == Options.AppState.PAUSED ) {
if( state == Options.AppState.PAUSED
|| state == Options.AppState.TERMINATED ) {
draw();
}
render();
}
/**
* Zeigt den aktuellen Inhalt der {@link Zeichenleinwand}
* im Zeichenfenster an, ohne vorher {@link #draw()} aufzurufen.
*/
public final void render() {
canvas.render();
// canvas.invalidate();
// frame.repaint();
@@ -546,6 +538,11 @@ public class Zeichenmaschine extends Constants {
}
public final void exitNow() {
// Do nothing, when already quitting
if( state == Options.AppState.QUITING ) {
return;
}
if( running ) {
running = false;
terminateImediately = true;
@@ -584,13 +581,16 @@ public class Zeichenmaschine extends Constants {
* @see System#exit(int)
*/
public final void quit( boolean exit ) {
frame.setVisible(false);
canvas.dispose();
frame.dispose();
state = Options.AppState.QUITING;
TaskRunner.invokeLater(() -> {
frame.setVisible(false);
canvas.dispose();
frame.dispose();
if( exit ) {
System.exit(0);
}
if( exit ) {
System.exit(0);
}
});
}
/**
@@ -601,11 +601,11 @@ public class Zeichenmaschine extends Constants {
* @see Zeichenleinwand#setSize(int, int)
*/
public final void setSize( int width, int height ) {
frame.setSize(width, height);
frame.setCanvasSize(width, height);
java.awt.Rectangle canvasBounds = frame.getCanvasBounds();
canvasWidth = (int) canvasBounds.getWidth();
canvasHeight = (int) canvasBounds.getHeight();
canvasWidth = canvasBounds.width;
canvasHeight = canvasBounds.height;
}
/**
@@ -1100,6 +1100,9 @@ public class Zeichenmaschine extends Constants {
}
if( isPaused() || isTerminated() ) {
if( MouseEvent.class.isInstance(evt) ) {
saveMousePosition((MouseEvent)evt);
}
dispatchEvents();
}
}
@@ -1327,7 +1330,7 @@ public class Zeichenmaschine extends Constants {
long overslept = 0L;
// Interne Zähler für tick und runtime
int _tick = 0;
long _runtime = 0;
long _runtime;
// Öffentliche Zähler für Unterklassen
tick = 0;
runtime = 0;
@@ -1390,7 +1393,7 @@ public class Zeichenmaschine extends Constants {
}
// Display the current buffer content
if( canvas != null ) {
if( canvas != null && frame.isDisplayable() ) {
canvas.render();
// canvas.invalidate();
// frame.repaint();
@@ -1458,7 +1461,7 @@ public class Zeichenmaschine extends Constants {
}
// TODO: Remove
class DelayedTask implements Delayed {
static class DelayedTask implements Delayed {
long startTime; // in ms
@@ -1547,6 +1550,7 @@ public class Zeichenmaschine extends Constants {
}
// TODO: (ngb) exception handling when update/draw throws ex
class UpdateThreadExecutor extends ThreadPoolExecutor {
private Thread updateThread;
@@ -1555,7 +1559,18 @@ public class Zeichenmaschine extends Constants {
public UpdateThreadExecutor() {
super(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread( Runnable r ) {
Thread t = new Thread(mainThread.getThreadGroup(), r,
"updateThread-" + threadNumber.getAndIncrement(),
0);
t.setDaemon(true);
return t;
}
});
updateState = Options.AppState.IDLE;
}

View File

@@ -11,9 +11,13 @@ 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
* Hintergundfarbe für eine Szene dienen, oder als halbtransparente "Abdeckung",
* wenn ein {@code ColorLayer} über den anderen Ebenen eingefügt wird.
* 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 {

View File

@@ -8,55 +8,102 @@ 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 {
protected List<Drawable> drawables = new LinkedList<>();
/**
* 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( drawables ) {
for( Drawable d : drawables ) {
this.drawables.add(d);
}
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;
}
/**
* {@inheritDoc}
*/
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
synchronized( drawables ) {
List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) {
if( d.isVisible() ) {
d.draw(drawing);
}
List<Drawable> it = List.copyOf(drawables);
for( Drawable d : it ) {
if( d.isVisible() ) {
d.draw(drawing);
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -1,161 +1,424 @@
package schule.ngb.zm.layers;
import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import schule.ngb.zm.Fillable;
import schule.ngb.zm.Strokeable;
import schule.ngb.zm.shapes.Text;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.*;
import java.awt.geom.*;
import java.util.Stack;
public class DrawingLayer extends Layer {
protected schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
private Options.Direction default_anchor = CENTER;
/**
* Eine Ebene auf der direkt gezeichnet werden kann.
* <p>
* Ein {@code DrawingLayer} ist eine der drei Standardebenen der
* {@link schule.ngb.zm.Zeichenmaschine}.
*/
@SuppressWarnings( "unused" )
public class DrawingLayer extends Layer implements Strokeable, Fillable {
/**
* Wiederverwendbarer Speicher für eine Linie.
*/
protected Line2D.Double line = new Line2D.Double();
/**
* Wiederverwendbarer Speicher für eine Ellipse.
*/
protected Ellipse2D.Double ellipse = new Ellipse2D.Double();
/**
* Wiederverwendbarer Speicher für ein Rechteck.
*/
protected Rectangle2D.Double rect = new Rectangle2D.Double();
/**
* Wiederverwendbarer Speicher für einen Kreisbogen.
*/
protected Arc2D.Double arc = new Arc2D.Double();
/**
* Wiederverwendbarer Speicher für einen Pfad.
*/
protected Path2D.Double path = new Path2D.Double();
/**
* Ob ein individueller Pfad gestartet wurde.
*/
private boolean pathStarted = false;
private Stack<AffineTransform> transformStack = new Stack<>();
/**
* Stapel für zwischengespeicherte Transformationsmatrizen.
*/
private final Stack<AffineTransform> transformStack;
// private FontMetrics fontMetrics;
private FontMetrics fontMetrics = null;
/**
* Eine Shape, an die die Methoden für Farben und Konturlinien delegiert
* werden.
*/
private schule.ngb.zm.shapes.Text shapeDelegate;
private final Stack<schule.ngb.zm.shapes.Text> styleStack;
/**
* Erstellt eine Ebene in der Standardgröße.
*/
public DrawingLayer() {
super();
transformStack = new Stack<>();
transformStack.push(new AffineTransform());
fontMetrics = drawing.getFontMetrics();
// fontMetrics = drawing.getFontMetrics();
styleStack = new Stack<>();
resetStyle();
}
/**
* Erstellt eine Ebene mit der angegebenen Größe.
*
* @param width Die Breite der Ebene.
* @param height Die Höhe der Ebene.
*/
public DrawingLayer( int width, int height ) {
super(width, height);
transformStack = new Stack<>();
transformStack.push(new AffineTransform());
fontMetrics = drawing.getFontMetrics();
// fontMetrics = drawing.getFontMetrics();
styleStack = new Stack<>();
resetStyle();
}
public schule.ngb.zm.Color getColor() {
return fillColor;
/**
* Gibt die aktuelle Füllfarbe zurück.
*
* @return Die aktuelle Füllfarbe.
*/
public Color getFillColor() {
return shapeDelegate.getFillColor();
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @see Color
*/
public void setFillColor( Color color ) {
shapeDelegate.setFillColor(color);
drawing.setPaint(color);
}
/**
* Setzt die Füllfarbe auf die angegebene Farbe und setzt die Transparenz
* auf den angegebenen Wert. 0 is komplett durchsichtig und 255 komplett
* deckend.
*
* @param color Die neue Füllfarbe oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
public void setFillColor( Color color, int alpha ) {
setFillColor(new Color(color, alpha));
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität. 0
* entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setFillColor( int gray ) {
setFillColor(gray, gray, gray, 255);
}
public void setFillColor( schule.ngb.zm.Color color ) {
fillColor = color;
drawing.setColor(color.getJavaColor());
}
public void noFill() {
fillColor = null;
}
/**
* Setzt die Füllfarbe auf einen Grauwert mit der angegebenen Intensität und
* dem angegebenen Transparenzwert. Der Grauwert 0 entspricht schwarz, 255
* entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setFillColor( int gray, int alpha ) {
setFillColor(gray, gray, gray, alpha);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setFillColor( int red, int green, int blue ) {
setFillColor(red, green, blue, 255);
}
/**
* Setzt die Füllfarbe auf die Farbe mit den angegebenen Rot-, Grün- und
* Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setFillColor( int red, int green, int blue, int alpha ) {
setFillColor(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public schule.ngb.zm.Color getStrokeColor() {
return strokeColor;
/**
* Entfernt die Füllung der Form.
*/
public void noFill() {
shapeDelegate.noFill();
}
/**
* Setzt die Füllfarbe auf den Standardwert zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_FILLCOLOR
*/
public void resetFill() {
shapeDelegate.resetFill();
}
@Override
public void setFill( Paint fill ) {
shapeDelegate.setFill(fill);
}
@Override
public Paint getFill() {
return shapeDelegate.getFill();
}
/**
* Setzt die Füllung auf einen linearen Farbverlauf, der am Punkt
* ({@code fromX}, {@code fromY}) mit der Farbe {@code from} startet und am
* Punkt (({@code toX}, {@code toY}) mit der Farbe {@code to} endet.
*
* @param fromX x-Koordinate des Startpunktes.
* @param fromY y-Koordinate des Startpunktes.
* @param from Farbe am Startpunkt.
* @param toX x-Koordinate des Endpunktes.
* @param toY y-Koordinate des Endpunktes.
* @param to Farbe am Endpunkt.
*/
public void setGradient( double fromX, double fromY, Color from, double toX, double toY, Color to ) {
shapeDelegate.setGradient(fromX, fromY, from, toX, toY, 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.
*/
public void setGradient( double centerX, double centerY, double radius, Color from, Color to ) {
shapeDelegate.setGradient(centerX, centerY, radius, from, to);
}
@Override
public void setGradient( Color from, Color to, Options.Direction dir ) {
Point2D.Double anchorPoint = schule.ngb.zm.shapes.Shape.getAnchorPoint(getWidth(), getHeight(), dir);
Point2D.Double inversePoint = schule.ngb.zm.shapes.Shape.getAnchorPoint(getWidth(), getHeight(), dir.inverse());
shapeDelegate.setGradient(inversePoint.x, inversePoint.y, from, anchorPoint.x, anchorPoint.y, to);
}
@Override
public void setGradient( Color from, Color to ) {
shapeDelegate.setGradient(getWidth() / 2.0, getHeight() / 2.0, min(getWidth(), getHeight()) / 2.0, from, to);
}
/**
* Entfernt den Farbverlauf von der Form.
*/
public void noGradient() {
shapeDelegate.noGradient();
}
@Override
public void setStroke( Stroke stroke ) {
shapeDelegate.setStroke(stroke);
}
/**
* Gibt die aktuelle Farbe der Konturlinie zurück.
*
* @return Die Konturfarbe oder {@code null}.
*/
public Color getStrokeColor() {
return shapeDelegate.getStrokeColor();
}
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe.
*
* @param color Die neue Farbe der Konturlinie.
* @see Color
*/
public void setStrokeColor( Color color ) {
shapeDelegate.setStrokeColor(color);
drawing.setPaint(color);
}
/**
* Setzt die Farbe der Konturlinie auf die angegebene Farbe und setzt die
* Transparenz auf den angegebenen Wert. 0 is komplett durchsichtig und 255
* komplett deckend.
*
* @param color Die neue Farbe der Konturlinie oder {@code null}.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(Color, int)
*/
public void setStrokeColor( Color color, int alpha ) {
setStrokeColor(new Color(color, alpha));
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität. 0 entspricht schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @see Color#Color(int)
*/
public void setStrokeColor( int gray ) {
setStrokeColor(gray, gray, gray, 255);
}
public void setStrokeColor( schule.ngb.zm.Color color ) {
strokeColor = color;
drawing.setColor(color.getJavaColor());
}
public void noStroke() {
strokeColor = null;
}
/**
* Setzt die Farbe der Konturlinie auf einen Grauwert mit der angegebenen
* Intensität und dem angegebenen Transparenzwert. Der Grauwert 0 entspricht
* schwarz, 255 entspricht weiß.
*
* @param gray Ein Grauwert zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 255.
* @see Color#Color(int, int)
*/
public void setStrokeColor( int gray, int alpha ) {
setStrokeColor(gray, gray, gray, alpha);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @see Color#Color(int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue ) {
setStrokeColor(red, green, blue, 255);
}
/**
* Setzt die Farbe der Konturlinie auf die Farbe mit den angegebenen Rot-,
* Grün- und Blauanteilen und dem angegebenen Transparenzwert.
*
* @param red Der Rotanteil der Farbe zwischen 0 und 255.
* @param green Der Grünanteil der Farbe zwischen 0 und 255.
* @param blue Der Blauanteil der Farbe zwischen 0 und 255.
* @param alpha Ein Transparenzwert zwischen 0 und 25
* @see Color#Color(int, int, int, int)
* @see <a
* href="https://de.wikipedia.org/wiki/RGB-Farbraum">https://de.wikipedia.org/wiki/RGB-Farbraum</a>
*/
public void setStrokeColor( int red, int green, int blue, int alpha ) {
setStrokeColor(new schule.ngb.zm.Color(red, green, blue, alpha));
}
public void setStrokeWeight( double pWeight ) {
strokeWeight = pWeight;
drawing.setStroke(createStroke());
}
protected Stroke createStroke() {
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);
default:
return new BasicStroke(
(float) strokeWeight,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND);
}
}
public Options.StrokeType getStrokeType() {
return strokeType;
}
public void setStrokeType( Options.StrokeType type ) {
switch( type ) {
case DASHED:
this.strokeType = DASHED;
break;
case DOTTED:
this.strokeType = DOTTED;
break;
default:
this.strokeType = SOLID;
break;
}
/**
* Entfernt die Kontur der Form.
*/
public void noStroke() {
shapeDelegate.noStroke();
}
/**
* Setzt die Farbe der Konturlinie auf die Standardwerte zurück.
*
* @see schule.ngb.zm.Constants#DEFAULT_STROKECOLOR
* @see schule.ngb.zm.Constants#DEFAULT_STROKEWEIGHT
* @see schule.ngb.zm.Constants#SOLID
*/
public void resetStroke() {
setStrokeColor(DEFAULT_STROKECOLOR);
setStrokeWeight(DEFAULT_STROKEWEIGHT);
setStrokeType(SOLID);
shapeDelegate.resetStroke();
}
/**
* Gibt die Dicke der Konturlinie zurück.
*
* @return Die aktuelle Dicke der Linie.
*/
public double getStrokeWeight() {
return shapeDelegate.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.
*/
public void setStrokeWeight( double weight ) {
shapeDelegate.setStrokeWeight(weight);
drawing.setStroke(shapeDelegate.getStroke());
}
/**
* Gibt die Art der Konturlinie zurück.
*
* @return Die aktuelle Art der Konturlinie.
* @see Options.StrokeType
*/
public Options.StrokeType getStrokeType() {
return shapeDelegate.getStrokeType();
}
/**
* Setzt den Typ der Kontur. Erlaubte Werte sind {@link #DASHED},
* {@link #DOTTED} und {@link #SOLID}.
*
* @param type Eine der möglichen Konturarten.
* @see Options.StrokeType
*/
public void setStrokeType( Options.StrokeType type ) {
shapeDelegate.setStrokeType(type);
}
@Override
public Stroke getStroke() {
return shapeDelegate.getStroke();
}
public void setAnchor( Options.Direction anchor ) {
default_anchor = anchor;
shapeDelegate.setAnchor(anchor);
}
public void clear( int gray ) {
@@ -187,80 +450,122 @@ public class DrawingLayer extends Layer {
}
public void line( double x1, double y1, double x2, double y2 ) {
//Shape line = new Line2D.Double(x1, y1, x2, y2);
line.setLine(x1, y1, x2, y2);
drawShape(line);
}
public void pixel( double x, double y ) {
// square(x, y, 1);
buffer.setRGB((int)x, (int)y, fillColor.getRGBA());
buffer.setRGB((int) x, (int) y, shapeDelegate.getFontColor().getRGBA());
}
public void square( double x, double y, double w ) {
rect(x, y, w, w);
rect(x, y, w, w, shapeDelegate.getAnchor());
}
public void square( double x, double y, double w, Options.Direction anchor ) {
rect(x, y, w, w, anchor);
}
/**
* Zeichnet ein Rechteck in die Zeichenebene.
*
* @param x x-Koordinate des Rechtecks.
* @param y y-Koordinate des Rechtecks.
* @param w Breite des Rechtecks.
* @param h Höhe des Rechtecks.
*/
public void rect( double x, double y, double w, double h ) {
rect(x, y, w, h, default_anchor);
rect(x, y, w, h, shapeDelegate.getAnchor());
}
/**
* Zeichnet ein Rechteck mit in die Zeichenebene.
*
* @param x x-Koordinate des Rechtecks.
* @param y y-Koordinate des Rechtecks.
* @param w Breite des Rechtecks.
* @param h Höhe des Rechtecks.
* @param anchor Ankerpunkt de Rechtecks.
*/
public void rect( double x, double y, double w, double h, Options.Direction anchor ) {
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, anchor);
// Shape rect = new Rectangle2D.Double(anchorPoint.getX(), anchorPoint.getY(), w, h);
Point2D.Double anchorPoint = this.getOriginPoint(x, y, w, h, anchor);
rect.setRect(anchorPoint.getX(), anchorPoint.getY(), w, h);
fillShape(rect);
drawShape(rect);
}
/**
* Zeichnet ein Rechteck mit abgerundeten Ecken in die Zeichenebene.
* <p>
* Jede Ecke wird als Viertelkreis eines Kreises mit dem Radius
* {@code radius} gezeichnet.
*
* @param x x-Koordinate des Rechtecks.
* @param y y-Koordinate des Rechtecks.
* @param w Breite des Rechtecks.
* @param h Höhe des Rechtecks.
* @param radius Radius der Eckenkreise.
*/
public void roundedRect( double x, double y, double w, double h, double radius ) {
roundedRect(x, y, w, h, radius, shapeDelegate.getAnchor());
}
/**
* Zeichnet ein Rechteck mit abgerundeten Ecken in die Zeichenebene.
* <p>
* Jede Ecke wird als Viertelkreis eines Kreises mit dem Radius
* {@code radius} gezeichnet.
*
* @param x x-Koordinate des Rechtecks.
* @param y y-Koordinate des Rechtecks.
* @param w Breite des Rechtecks.
* @param h Höhe des Rechtecks.
* @param radius Radius der Eckenkreise.
* @param anchor Ankerpunkt de Rechtecks.
*/
public void roundedRect( double x, double y, double w, double h, double radius, Options.Direction anchor ) {
Point2D.Double anchorPoint = this.getOriginPoint(x, y, w, h, anchor);
Shape rect = new RoundRectangle2D.Double(anchorPoint.getX(), anchorPoint.getY(), w, h, radius, radius);
fillShape(rect);
drawShape(rect);
}
public void point( double x, double y ) {
circle(x - 1, y - 1, 2);
ellipse(x - 1, y - 1, 2, 2, shapeDelegate.getAnchor());
}
public void circle( double x, double y, double r ) {
ellipse(x, y, r+r, r+r, default_anchor);
ellipse(x, y, r + r, r + r, shapeDelegate.getAnchor());
}
public void circle( double x, double y, double r, Options.Direction anchor ) {
ellipse(x, y, r+r, r+r, anchor);
ellipse(x, y, r + r, r + r, anchor);
}
public void ellipse( double x, double y, double w, double h ) {
ellipse(x, y, w, h, default_anchor);
ellipse(x, y, w, h, shapeDelegate.getAnchor());
}
public void ellipse( double x, double y, double w, double h, Options.Direction anchor ) {
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, anchor);
// Shape ellipse = new Ellipse2D.Double(anchorPoint.x, anchorPoint.y, w, h);
Point2D.Double anchorPoint = getOriginPoint(x, y, w, h, anchor);
ellipse.setFrame(anchorPoint.x, anchorPoint.y, w, h);
fillShape(ellipse);
drawShape(ellipse);
}
public void arc( double x, double y, double r, double angle1, double angle2 ) {
arc(x, y, r+r, r+r, angle1, angle2);
arc(x, y, r + r, r + r, angle1, angle2);
}
public void arc( double x, double y, double w, double h, double angle1, double angle2 ) {
while( angle2 < angle1 ) angle2 += 360.0;
Point2D.Double anchorPoint = getAnchorPoint(x, y, w, h, CENTER);
/*Shape arc = new Arc2D.Double(
anchorPoint.x,
anchorPoint.y,
d, d,
angle1, angle2 - angle1,
Arc2D.OPEN
);*/
Point2D.Double anchorPoint = getOriginPoint(x, y, w, h, CENTER);
arc.setArc(
anchorPoint.x, anchorPoint.y,
w, h,
//Math.toRadians(angle1), Math.toRadians(angle2 - angle1),
angle1, angle2-angle1,
angle1, angle2 - angle1,
Arc2D.OPEN
);
@@ -270,16 +575,9 @@ public class DrawingLayer extends Layer {
public void pie( double x, double y, double r, double angle1, double angle2 ) {
while( angle2 < angle1 ) angle2 += 360.0;
double d = r+r;
double d = r + r;
Point2D.Double anchorPoint = getAnchorPoint(x, y, d, d, CENTER);
/*Shape arc = new Arc2D.Double(
anchorPoint.x,
anchorPoint.y,
d, d,
angle1, angle2 - angle1,
Arc2D.PIE
);*/
Point2D.Double anchorPoint = getOriginPoint(x, y, d, d, CENTER);
arc.setArc(
anchorPoint.x, anchorPoint.y,
d, d,
@@ -313,12 +611,12 @@ public class DrawingLayer extends Layer {
}
public void rhombus( double x, double y, double width, double height ) {
rhombus(x, y, width, height, default_anchor);
rhombus(x, y, width, height, shapeDelegate.getAnchor());
}
public void rhombus( double x, double y, double width, double height, Options.Direction anchor ) {
double whalf = width / 2.0, hhalf = height / 2.0;
Point2D.Double anchorPoint = getAnchorPoint(x, y, width, height, anchor);
Point2D.Double anchorPoint = getOriginPoint(x, y, width, height, anchor);
polygon(anchorPoint.x + whalf, anchorPoint.y, anchorPoint.x + width, anchorPoint.y + hhalf, anchorPoint.x + whalf, anchorPoint.y + height, anchorPoint.x, anchorPoint.y + hhalf);
}
@@ -364,8 +662,21 @@ public class DrawingLayer extends Layer {
drawShape(path);
}
// TODO: General shape drawing
/*
protected void fillShape( Shape shape ) {
if( shapeDelegate.getFill() != null ) {
drawing.setPaint(shapeDelegate.getFill());
drawing.fill(shape);
}
}
protected void drawShape( Shape shape ) {
if( shapeDelegate.hasStroke() ) {
drawing.setColor(shapeDelegate.getStrokeColor().getJavaColor());
drawing.setStroke(shapeDelegate.getStroke());
drawing.draw(shape);
}
}
public void beginShape() {
path.reset();
pathStarted = false;
@@ -388,10 +699,9 @@ public class DrawingLayer extends Layer {
fillShape(path);
drawShape(path);
}
*/
public void image( String imageSource, double x, double y ) {
image(ImageLoader.loadImage(imageSource), x, y, 1.0, default_anchor);
image(ImageLoader.loadImage(imageSource), x, y, 1.0, shapeDelegate.getAnchor());
}
public void image( String imageSource, double x, double y, Options.Direction anchor ) {
@@ -399,7 +709,7 @@ public class DrawingLayer extends Layer {
}
public void image( String imageSource, double x, double y, double scale ) {
image(ImageLoader.loadImage(imageSource), x, y, scale, default_anchor);
image(ImageLoader.loadImage(imageSource), x, y, scale, shapeDelegate.getAnchor());
}
public void image( String imageSource, double x, double y, double scale, Options.Direction anchor ) {
@@ -407,120 +717,110 @@ public class DrawingLayer extends Layer {
}
public void image( Image image, double x, double y ) {
image(image, x, y, 1.0, default_anchor);
image(image, x, y, 1.0, shapeDelegate.getAnchor());
}
public void image( Image image, double x, double y, double scale ) {
image(image, x, y, scale, default_anchor);
image(image, x, y, scale, shapeDelegate.getAnchor());
}
public void image( Image image, double x, double y, double scale, Options.Direction anchor ) {
if( image != null ) {
double neww = image.getWidth(null) * scale;
double newh = image.getHeight(null) * scale;
Point2D.Double anchorPoint = getAnchorPoint(x, y, neww, newh, anchor);
Point2D.Double anchorPoint = getOriginPoint(x, y, neww, newh, anchor);
drawing.drawImage(image, (int) anchorPoint.x, (int) anchorPoint.y, (int) neww, (int) newh, null);
}
}
public Font getFont() {
return shapeDelegate.getFont();
}
public void setFont( Font newFont ) {
shapeDelegate.setFont(newFont);
// fontMetrics = drawing.getFontMetrics();
}
public void setFont( String fontName ) {
shapeDelegate.setFont(fontName);
}
public void setFont( String fontName, int size ) {
shapeDelegate.setFont(fontName, size);
}
public void setFont( String fontName, int size, int style ) {
shapeDelegate.setFont(fontName, size, style);
}
public double getFontSize() {
return shapeDelegate.getFontSize();
}
public void setFontSize( double size ) {
shapeDelegate.setFontSize(size);
}
public void text( String text, double x, double y ) {
text(text, x, y, default_anchor);
text(text, x, y, shapeDelegate.getAnchor());
}
public void text( String text, double x, double y, Options.Direction anchor ) {
drawing.setFont(shapeDelegate.getFont());
FontMetrics fm = drawing.getFontMetrics();
Point2D.Double anchorPoint = getAnchorPoint(x, y + fm.getAscent(), fm.stringWidth(text), fm.getHeight(), anchor);
Point2D.Double anchorPoint = getOriginPoint(x, y + fm.getAscent(), fm.stringWidth(text), fm.getHeight(), anchor);
drawing.drawString(text, (float) anchorPoint.x, (float) anchorPoint.y);
}
public void setFontSize( int size ) {
setFont(drawing.getFont().deriveFont((float)size));
/**
* Berechnet den Ursprung einer zu zeichnenden Form, wenn der angegebene
* Ankerpunkt zugrunde gelegt wird.
* <p>
* Der Ursprung einer Form liegt immer oben links. Eine Form an den
* Koordinaten {@code (300, 300)} und den Abmessungen {@code (100, 50)}
* würde für den Anker {@link Options.Direction#NORTHWEST} also den Ursprung
* {@code (300, 300)} haben. Für den Anker {@link Options.Direction#CENTER}
* wäre der Ursprung zu den Koordinaten um die hälfte der Abmessungen nach
* links und oben verschoben bei {@code (250, 275)}. Beim Zeichnen liegen
* die Koordinaten {@code (300, 300)} dann in der Mitte der Form.
*
* @param x x-Koordinate des umgebenden Rechtecks.
* @param y y-Koordinate des umgebenden Rechtecks.
* @param w Breite des umgebenden Rechtecks.
* @param h Höhe des umgebenden Rechtecks.
* @param anchor Zu verendender Anker.
* @return Der Ursprung des umgebenden Rechtecks.
*/
protected Point2D.Double getOriginPoint( double x, double y, double w, double h, Options.Direction anchor ) {
Point2D.Double ap = schule.ngb.zm.shapes.Shape.getAnchorPoint(w, h, anchor);
ap.x = x - ap.x;
ap.y = y - ap.y;
return ap;
}
public void setFont( String fontName ) {
Font font = new Font(fontName, drawing.getFont().getStyle(), drawing.getFont().getSize());
setFont(font);
public void pushStyle() {
styleStack.push(shapeDelegate);
shapeDelegate = new Text(shapeDelegate);
}
public void setFont( String fontName, int size ) {
Font font = new Font(fontName, drawing.getFont().getStyle(), size);
setFont(font);
}
public void setFont( String fontName, int size, int style ) {
Font font = new Font(fontName, style, size);
setFont(font);
}
public void setFont( Font font ) {
drawing.setFont(font);
fontMetrics = drawing.getFontMetrics();
}
private Point2D.Double transformToCanvas( double x, double y ) {
return transformToCanvas(new Point2D.Double(x,y));
}
private Point2D.Double transformToCanvas( Point2D.Double pPoint ) {
AffineTransform matrix = getMatrix();
matrix.transform(pPoint, pPoint);
return pPoint;
}
private Point2D.Double transformToUser( double x, double y ) {
return transformToUser(new Point2D.Double(x,y));
}
private Point2D.Double transformToUser( Point2D.Double pPoint ) {
AffineTransform matrix = getMatrix();
try {
matrix.inverseTransform(pPoint, pPoint);
} catch( NoninvertibleTransformException e ) {
e.printStackTrace();
}
return pPoint;
}
private Point2D.Double getAnchorPoint( double x, double y, double w, double h, Options.Direction anchor ) {
double whalf = w * .5, hhalf = h * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(x - whalf, y - hhalf);
if( NORTH.in(anchor) ) {
anchorPoint.y += hhalf;
}
if( SOUTH.in(anchor) ) {
anchorPoint.y -= hhalf;
}
if( WEST.in(anchor) ) {
anchorPoint.x += whalf;
}
if( EAST.in(anchor) ) {
anchorPoint.x -= whalf;
}
return anchorPoint;
}
private void fillShape( Shape pShape ) {
if( fillColor != null && fillColor.getAlpha() > 0.0 ) {
drawing.setColor(fillColor.getJavaColor());
drawing.fill(pShape);
public void popStyle() {
if( styleStack.isEmpty() ) {
resetStyle();
} else {
shapeDelegate = styleStack.pop();
}
}
private void drawShape( Shape pShape ) {
if( strokeColor != null && strokeColor.getAlpha() > 0.0
&& strokeWeight > 0.0 ) {
drawing.setColor(strokeColor.getJavaColor());
drawing.setStroke(createStroke());
drawing.draw(pShape);
public void resetStyle() {
Text newDelegate = new Text(0, 0, "");
newDelegate.setFillColor(DEFAULT_FILLCOLOR);
newDelegate.setStrokeColor(DEFAULT_STROKECOLOR);
if( shapeDelegate != null ) {
newDelegate.setAnchor(shapeDelegate.getAnchor());
}
shapeDelegate = newDelegate;
}
public void translate( double dx, double dy ) {
@@ -535,6 +835,10 @@ public class DrawingLayer extends Layer {
drawing.rotate(Math.toRadians(pAngle));
}
public void rotate( double pAngle, double centerX, double centerY ) {
drawing.rotate(Math.toRadians(pAngle), centerX, centerY);
}
public void shear( double dx, double dy ) {
drawing.shear(dx, dy);
}

View File

@@ -1,11 +1,12 @@
package schule.ngb.zm.layers;
import java.awt.Graphics2D;
import java.awt.Image;
import schule.ngb.zm.Layer;
import schule.ngb.zm.util.io.ImageLoader;
import java.awt.Graphics2D;
import java.awt.Image;
@SuppressWarnings( "unused" )
public class ImageLayer extends Layer {
protected Image image;
@@ -16,20 +17,20 @@ public class ImageLayer extends Layer {
protected boolean redraw = true;
public ImageLayer(String source) {
public ImageLayer( String source ) {
image = ImageLoader.loadImage(source);
}
public ImageLayer(Image image) {
public ImageLayer( Image image ) {
this.image = image;
}
public ImageLayer(int width, int height, Image image) {
public ImageLayer( int width, int height, Image image ) {
super(width, height);
this.image = image;
}
public void setImage(Image image) {
public void setImage( Image image ) {
this.image = image;
redraw = true;
}
@@ -38,7 +39,7 @@ public class ImageLayer extends Layer {
return x;
}
public void setX(double pX) {
public void setX( double pX ) {
this.x = pX;
redraw = true;
}
@@ -47,7 +48,7 @@ public class ImageLayer extends Layer {
return y;
}
public void setY(double pY) {
public void setY( double pY ) {
this.y = pY;
redraw = true;
}
@@ -59,8 +60,8 @@ public class ImageLayer extends Layer {
}
@Override
public void draw(Graphics2D graphics) {
if (redraw && visible) {
public void draw( Graphics2D graphics ) {
if( redraw && visible ) {
drawing.drawImage(image, (int) x, (int) y, null);
redraw = false;
}

View File

@@ -4,42 +4,46 @@ import schule.ngb.zm.Color;
import schule.ngb.zm.Layer;
import schule.ngb.zm.Options;
import java.awt.*;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.util.LinkedList;
@SuppressWarnings( "unused" )
public final class Shape2DLayer extends Layer {
protected schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
private schule.ngb.zm.Color strokeColor = DEFAULT_STROKECOLOR;
protected schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
private schule.ngb.zm.Color fillColor = DEFAULT_FILLCOLOR;
protected double strokeWeight = DEFAULT_STROKEWEIGHT;
private double strokeWeight = DEFAULT_STROKEWEIGHT;
protected Options.StrokeType strokeType = SOLID;
private Options.StrokeType strokeType = SOLID;
private LinkedList<java.awt.Shape> shapes;
private final LinkedList<java.awt.Shape> shapes;
private boolean instantDraw = false;
public Shape2DLayer() {
super();
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
}
public Shape2DLayer( boolean instantDraw ) {
super();
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
this.instantDraw = instantDraw;
}
public Shape2DLayer( int width, int height ) {
super(width, height);
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
}
public Shape2DLayer( int width, int height, boolean instantDraw ) {
super(width, height);
shapes = new LinkedList<java.awt.Shape>();
shapes = new LinkedList<>();
this.instantDraw = instantDraw;
}
@@ -106,7 +110,7 @@ public final class Shape2DLayer extends Layer {
drawing.setStroke(createStroke());
}
protected Stroke createStroke() {
private Stroke createStroke() {
switch( strokeType ) {
case DOTTED:
return new BasicStroke(
@@ -153,7 +157,7 @@ public final class Shape2DLayer extends Layer {
}
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( !instantDraw ) {
for( Shape shape : shapes ) {
drawing.setColor(fillColor.getJavaColor());
@@ -164,7 +168,7 @@ public final class Shape2DLayer extends Layer {
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -10,13 +10,20 @@ import java.awt.Graphics2D;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
/**
* Ein Layer 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 List<Shape> shapes;
private final List<Shape> shapes;
private List<Animation<? extends Shape>> animations;
private final List<Animation<? extends Shape>> animations;
public ShapesLayer() {
super();
@@ -37,7 +44,7 @@ public class ShapesLayer extends Layer {
public <ST extends Shape> ST getShape( Class<ST> shapeClass ) {
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
return (ST) s;
return shapeClass.cast(s);
}
}
return null;
@@ -51,7 +58,7 @@ public class ShapesLayer extends Layer {
List<ST> result = new LinkedList<>();
for( Shape s : shapes ) {
if( shapeClass.isInstance(s) ) {
result.add((ST) s);
result.add(shapeClass.cast(s));
}
}
return result;
@@ -59,17 +66,13 @@ public class ShapesLayer extends Layer {
public void add( Shape... shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
Collections.addAll(this.shapes, shapes);
}
}
public void add( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.add(s);
}
this.shapes.addAll(shapes);
}
}
@@ -83,9 +86,7 @@ public class ShapesLayer extends Layer {
public void remove( Collection<Shape> shapes ) {
synchronized( this.shapes ) {
for( Shape s : shapes ) {
this.shapes.remove(s);
}
this.shapes.removeAll(shapes);
}
}
@@ -116,9 +117,9 @@ public class ShapesLayer extends Layer {
anim.start();
}
public void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim: anims ) {
@SafeVarargs
public final void play( Animation<? extends Shape>... anims ) {
for( Animation<? extends Shape> anim : anims ) {
this.animations.add(anim);
anim.start();
}
@@ -141,13 +142,13 @@ public class ShapesLayer extends Layer {
anim.update(delta);
if( !anim.isActive() ) {
animations.remove(anim);
it.remove();
}
}
}
@Override
public void draw( Graphics2D pGraphics ) {
public void draw( Graphics2D graphics ) {
if( clearBeforeDraw ) {
clear();
}
@@ -161,7 +162,7 @@ public class ShapesLayer extends Layer {
}
}
super.draw(pGraphics);
super.draw(graphics);
}
}

View File

@@ -1,18 +1,19 @@
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 {
@SuppressWarnings( "unused" )
public class TurtleLayer extends Layer implements Strokeable, Fillable {
// Rotating by the clock
public static final int H1 = 30;
@@ -39,7 +40,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 +52,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 +118,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 +189,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 +392,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 +428,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(DEFAULT_STROKECOLOR.getJavaColor());
}
graphics.fill(shape);
graphics.setColor(Color.BLACK.getJavaColor());
graphics.setStroke(createStroke());
graphics.setStroke(getStroke());
graphics.draw(shape);
}
@@ -354,9 +474,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 +527,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

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

View File

@@ -1,8 +1,7 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Options;
import schule.ngb.zm.*;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
@@ -14,7 +13,7 @@ import java.awt.geom.Point2D;
* <p>
* Neben den abstrakten Methoden implementieren Unterklassen mindestens zwei
* Konstruktoren. Einen Konstruktor, der die Form mit vom Nutzer gegebenen
* Parametern initialisiert und einen, der die Werten einer anderen Form
* Parametern initialisiert und einen, der die Werte einer anderen Form
* desselben Typs übernimmt. In der Klasse {@link Circle} sind die Konstruktoren
* beispielsweise so implementiert:
*
@@ -34,7 +33,7 @@ import java.awt.geom.Point2D;
* eine {@link #equals(Object)} Methode.
*/
@SuppressWarnings( "unused" )
public abstract class Shape extends FilledShape {
public abstract class Shape extends BasicDrawable {
/**
* x-Koordinate der Form.
@@ -56,11 +55,6 @@ public abstract class Shape extends FilledShape {
*/
protected double scale = 1.0;
/**
* Ob die Form angezeigt werden soll.
*/
protected boolean visible = true;
/**
* Ankerpunkt der Form.
*/
@@ -84,38 +78,6 @@ public abstract class Shape extends FilledShape {
this.y = y;
}
/**
* Ob die Form angezeigt wird oder nicht.
*
* @return {@code true}, wenn die From angezeigt werden soll, {@code false}
* sonst.
*/
public boolean isVisible() {
return visible;
}
/**
* Versteckt die Form.
*/
public void hide() {
visible = false;
}
/**
* Zeigt die Form an.
*/
public void show() {
visible = true;
}
/**
* Versteckt die Form, wenn sie derzeit angezeigt wird und zeigt sie
* andernfalls an.
*/
public void toggle() {
visible = !visible;
}
/**
* Gibt die x-Koordinate der Form zurück.
*
@@ -180,6 +142,25 @@ public abstract class Shape extends FilledShape {
*/
public abstract double getHeight();
/**
* {@inheritDoc}
*/
@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);
}
/**
* {@inheritDoc}
*/
@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);
}
/**
* Gibt die Rotation in Grad zurück.
*
@@ -191,6 +172,7 @@ public abstract class Shape extends FilledShape {
/**
* Dreht die Form um den angegebenen Winkel um ihren Ankerpunkt.
*
* @param angle Drehwinkel in Grad.
*/
public void rotate( double angle ) {
@@ -227,6 +209,7 @@ public abstract class Shape extends FilledShape {
public double getScale() {
return scale;
}
public void scale( double factor ) {
scale = factor;
}
@@ -235,17 +218,6 @@ public abstract class Shape extends FilledShape {
scale(scale * factor);
}
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to, Options.Direction dir ) {
Point2D apDir = getAbsAnchorPoint(dir);
Point2D apInv = getAbsAnchorPoint(dir.inverse());
setGradient(apInv.getX(), apInv.getY(), from, apDir.getX(), apDir.getY(), to);
}
public void setGradient( schule.ngb.zm.Color from, schule.ngb.zm.Color to ) {
Point2D ap = getAbsAnchorPoint(CENTER);
setGradient(ap.getX(), ap.getY(), Math.min(ap.getX(), ap.getY()), from, to);
}
public Options.Direction getAnchor() {
return anchor;
}
@@ -268,28 +240,25 @@ public abstract class Shape extends FilledShape {
}
/**
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkt basierend
* Bestimmt die relativen Koordinaten des angegebenen Ankerpunkts basierend
* auf der angegebenen Breite und Höhe des umschließenden Rechtecks.
* <p>
* Die Koordinaten des Ankerpunkt werden relativ zur oberen linken Ecke des
* Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* Die Koordinaten des Ankerpunktes werden relativ zur oberen linken Ecke
* des Rechtecks mit der Breite {@code width} und der Höhe {@code height}
* bestimmt.
*
* @param width Breite des umschließdenden Rechtecks.
* @param height Höhe des umschließdenden Rechtecks.
* @param 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.
*/
protected static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
public static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(
return new Point2D.Double(
wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y
);
return anchorPoint;
}
/**
@@ -303,12 +272,10 @@ public abstract class Shape extends FilledShape {
double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(
return new Point2D.Double(
wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y)
);
return anchorPoint;
}
/**
@@ -352,8 +319,8 @@ public abstract class Shape extends FilledShape {
setStrokeWeight(shape.getStrokeWeight());
setStrokeType(shape.getStrokeType());
visible = shape.isVisible();
rotation = shape.rotation;
scale(shape.scale);
rotation = shape.getRotation();
scale(shape.getScale());
setAnchor(shape.getAnchor());
}
}
@@ -384,19 +351,10 @@ public abstract class Shape extends FilledShape {
* zurück. Intern werden die AWT Shapes benutzt, um sie auf den
* {@link Graphics2D Grafikkontext} zu zeichnen.
* <p>
* Da die AWT-Shape bei jedem Zeichnen (also mindestens einmal pro Frame)
* benötigt wird, wird das aktuelle Shape-Objekt in {@link #awtShape}
* zwischengespeichert. Bei Änderungen der Objekteigenschaften muss daher
* intern {@link #invalidate()} aufgerufen werden, damit beim nächsten
* Aufruf von {@link #draw(Graphics2D)} die Shape mit einem Aufurf von
* {@code getShape()} neu erzeugt wird. Unterklassen können aber auch die
* zwischengespeicherte Shape direkt modifizieren, um eine Neugenerierung zu
* vermeiden.
* <p>
* Wenn diese Form nicht durch eine AWT-Shape dargstellt wird, kann die
* Wenn diese Form nicht durch eine AWT-Shape dargestellt wird, kann die
* Methode {@code null} zurückgeben.
*
* @return Eine Java-AWT {@code Shape} die diess Form repräsentiert oder
* @return Eine Java-AWT {@code Shape} die diese Form repräsentiert oder
* {@code null}.
*/
public abstract java.awt.Shape getShape();
@@ -560,7 +518,7 @@ public abstract class Shape extends FilledShape {
shape = transform.createTransformedShape(shape);
}
Color currentColor = graphics.getColor();
java.awt.Color currentColor = graphics.getColor();
fillShape(shape, graphics);
strokeShape(shape, graphics);
graphics.setColor(currentColor);
@@ -573,8 +531,8 @@ public abstract class Shape extends FilledShape {
* verglichen. Unterklassen überschreiben die Methode, um weitere
* Eigenschaften zu berücksichtigen.
* <p>
* Die Eigenschaften von {@link FilledShape} und {@link StrokedShape} werden
* nicht verglichen.
* Die Eigenschaften, die durch {@link Fillable} und {@link Strokeable}
* impliziert werden, werden nicht verglichen.
*
* @param o Ein anderes Objekt.
* @return
@@ -590,4 +548,43 @@ public abstract class Shape extends FilledShape {
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

@@ -7,6 +7,7 @@ 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;
@@ -35,7 +36,7 @@ import java.util.List;
* {@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)}.
* wie z.B. {@link #alignTo(Shape, Options.Direction)}).
*/
public class ShapeGroup extends Shape {
@@ -43,7 +44,7 @@ public class ShapeGroup extends Shape {
public static final int ARRANGE_COLS = 1;
private List<Shape> shapes;
private final List<Shape> shapes;
private double groupWidth = -1.0;
@@ -51,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 ) {
@@ -62,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() {
@@ -99,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.isInstance(s) ) {
list.add((ShapeType) s);
synchronized( shapes ) {
for( Shape shape : shapes ) {
if( typeClass.isInstance(shape) ) {
list.add(typeClass.cast(shape));
}
}
}
return list;
@@ -170,19 +171,15 @@ public class ShapeGroup extends Shape {
int rows, cols;
if( mode == ARRANGE_ROWS ) {
rows = n;
cols = (int) ceil(shapes.size() / n);
cols = (int) ceil(shapes.size() / (double)n);
} else {
cols = n;
rows = (int) ceil(shapes.size() / n);
rows = (int) ceil(shapes.size() / (double)n);
}
// Calculate grid cell size
double maxHeight = shapes.stream().mapToDouble(
( s ) -> s.getHeight()
).reduce(0.0, Double::max);
double maxWidth = shapes.stream().mapToDouble(
( s ) -> s.getWidth()
).reduce(0.0, Double::max);
double 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;

View File

@@ -24,6 +24,7 @@ public class Text extends Shape {
public Text( double x, double y, String text ) {
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);
@@ -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

@@ -288,7 +288,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 +360,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

@@ -52,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

@@ -91,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

@@ -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

@@ -4,33 +4,101 @@ 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 Objkete abgerufen werden.
* <p>
* Handelt es sich bei den Objekten um Zahlen, dann merkt sich ein Zähler 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);
}
private AtomicInteger count = new AtomicInteger(0);
/**
* 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;
public void Counter() {
/**
* Erstellt einen neuen, leeren {@code Counter}.
*/
public Counter() {
}
public void Counter( int initial ) {
/**
* 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);
}
/**
* Gibt die aktuelle Anzahl zurück.
* @return Die aktuelle Anzahl.
*/
public int getCount() {
return count.get();
}
@@ -61,6 +129,8 @@ public final class Counter {
}
}
@SuppressWarnings( "UnusedReturnValue" )
public Counter setCount( int count ) {
this.count.set(count);
return this;
@@ -94,10 +164,10 @@ public final class Counter {
inc();
// Update stats
synchronized( count ) {
sum = Double.isNaN(sum) ? value : sum + value;
sum = Double.isNaN(sum) ? value : sum + value;
if( Double.isNaN(max) || max < value )
max = value;
if( Double.isNaN(min) ||min > value )
if( Double.isNaN(min) || min > value )
min = value;
}
return this;
@@ -120,7 +190,15 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( double[] values ) {
for( double value: values ) {
for( double value : values ) {
count(value);
}
return this;
}
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( int[] values ) {
for( double value : values ) {
count(value);
}
return this;
@@ -128,7 +206,7 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Number[] values ) {
for( Number value: values ) {
for( Number value : values ) {
count(value);
}
return this;
@@ -136,7 +214,7 @@ public final class Counter {
@SuppressWarnings( "UnusedReturnValue" )
public synchronized Counter countAll( Collection<Number> values ) {
for( Number value: values ) {
for( Number value : values ) {
count(value);
}
return this;

View File

@@ -0,0 +1,190 @@
package schule.ngb.zm.util;
import schule.ngb.zm.Color;
import schule.ngb.zm.Constants;
import schule.ngb.zm.Zeichenmaschine;
import schule.ngb.zm.util.io.FileLoader;
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;
/**
* 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 Platzhalterbilder.
*/
public final class Faker {
public static final String FAKE_IMG_URL = "https://loremflickr.com/%d/%d";
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[][] fakeUsers( int n ) {
String[][] data = loadMockfile("users");
String[][] result = new String[n][];
for( int i = 0; i < n; i++ ) {
result[i] = Constants.choice(data);
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Vornamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeNames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[0];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Namen (Vor- und
* Nachname).
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeFullnames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[0] + " " + row[1];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Nutzernamen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeUsernames( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[3];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger Passwörter.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakePasswords( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[4];
}
return result;
}
/**
* Erzeugt ein Array mit den angegebenen Anzahl zufälliger E-Mail-Adressen.
*
* @param n Anzahl der Beispieldaten.
* @return Ein Array mit den Beispieldaten.
*/
public static String[] fakeEmails( int n ) {
String[][] data = loadMockfile("users");
String[] result = new String[n];
for( int i = 0; i < n; i++ ) {
String[] row = Constants.choice(data);
result[i] = row[5];
}
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,
(int) ((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);
}
}
public static String fakeText( int words, int paragraphs ) {
return "";
}
private static String[][] loadMockfile( String name ) {
return FileLoader.loadCsv("schule/ngb/zm/util/mock-" + name + ".csv", true);
}
private Faker() {
}
}

View File

@@ -26,7 +26,8 @@ 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 {
@@ -133,23 +134,13 @@ public final class Log {
if( System.getProperty("java.util.logging.SimpleFormatter.format") == null
&& LogManager.getLogManager().getProperty("java.util.logging.SimpleFormatter.format") == null ) {
// System.setProperty("java.util.logging.SimpleFormatter.format", DEFAULT_LOG_FORMAT);
rootLogger.addHandler(new StreamHandler(System.err, new LogFormatter()) {
@Override
public synchronized void publish(final LogRecord record) {
public synchronized void publish( final LogRecord record ) {
super.publish(record);
flush();
}
});
// rootLogger.setUseParentHandlers(false);
}
if( rootLogger.getUseParentHandlers() ) {
// This logger was not configured somewhere else
// Add a Handler and Formatter
//StreamHandler rootHandler = new StreamHandler(System.out, new SimpleFormatter());
//rootLogger.addHandler(rootHandler);
//rootLogger.setUseParentHandlers(false);
}
LOGGING_INIT = true;
@@ -208,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;
}
}
@@ -350,9 +330,9 @@ public final class Log {
ZonedDateTime zdt = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
if( record.getSourceClassName() != null ) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
if( record.getSourceMethodName() != null ) {
source += " " + record.getSourceMethodName();
}
} else {
@@ -360,7 +340,7 @@ public final class Log {
}
String message = formatMessage(record);
String throwable = "";
if (record.getThrown() != null) {
if( record.getThrown() != null ) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println();
@@ -368,7 +348,7 @@ public final class Log {
pw.close();
throwable = sw.toString();
}
return String.format(DEFAULT_LOG_FORMAT,
return String.format(DEFAULT_DEBUG_FORMAT,
zdt,
source,
record.getLoggerName(),

View File

@@ -10,6 +10,7 @@ import java.util.Random;
* 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;
@@ -42,7 +43,7 @@ public class Noise {
/**
* Initialisiert diesen Perlin-Noise mit dem angegebenen Zufallsgenerator.
*
* @param rand
* @param rand Ein Zufallsgenerator-Objekt.
*/
public Noise( Random rand ) {
init(rand);

View File

@@ -2,77 +2,189 @@ package schule.ngb.zm.util;
import java.util.concurrent.TimeUnit;
/**
* Eine Hilfsklasse zur Zeitmessung im Nanosekundenbereich.
* <p>
* Die Klasse kann zum Beispiel genutzt werden, um die Laufzeit eines
* Quelltextes zu messen. 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;
private long starttime = -1;
/**
* 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() {
starttime = System.nanoTime();
start = System.nanoTime();
running = true;
return this;
}
/**
* Stoppt den {@code Timer}, wenn er derzeit läuft. Die gemessene Zeit wird
* zur Summe aller gemessenen Zeiten hinzuaddiert.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer stop() {
running = false;
elapsed += System.nanoTime() - starttime;
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, wird sie nicht
* gestoppt, sondern läuft vom Zeitpunkt des Aufrufs weiter.
*
* @return Dieser {@code Timer} selbst (method chaining).
*/
@SuppressWarnings( "UnusedReturnValue" )
public Timer reset() {
running = false;
starttime = -1;
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() - starttime + elapsed, TimeUnit.NANOSECONDS);
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() - starttime + elapsed) / 1000000);
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() - starttime + elapsed) / 1000000000.0;
return (System.nanoTime() - start + elapsed) / 1000000000.0;
} else {
return elapsed / 1000000000.0;
}

View File

@@ -1,9 +1,9 @@
package schule.ngb.zm.util;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
@SuppressWarnings( "UnusedReturnValue" )
public class Validator {
public static final <T> T requireNotNull( T obj ) {
@@ -19,7 +19,7 @@ public class Validator {
}
public static final String requireNotEmpty( String str ) {
return requireNotEmpty(str, (Supplier<String>)null);
return requireNotEmpty(str, (Supplier<String>) null);
}
public static final String requireNotEmpty( String str, CharSequence msg ) {
@@ -33,7 +33,7 @@ public class Validator {
}
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 +47,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 +61,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 +75,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 +89,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 +103,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 ) {
@@ -138,7 +138,7 @@ public class Validator {
public static final <T> T[] requireNotEmpty( T[] arr ) {
return requireNotEmpty(arr, (Supplier<String>)null);
return requireNotEmpty(arr, (Supplier<String>) null);
}
public static final <T> T[] requireNotEmpty( T[] arr, CharSequence msg ) {
@@ -147,12 +147,12 @@ public class Validator {
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
if( arr.length == 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array may not be empty") : msg.get());
throw new IllegalArgumentException(msg == null ? "Parameter array may not be empty" : msg.get());
return arr;
}
public static final <T> T[] requireSize( T[] arr, int size ) {
return requireSize(arr, size, (Supplier<String>)null);
return requireSize(arr, size, (Supplier<String>) null);
}
public static final <T> T[] requireSize( T[] arr, int size, CharSequence msg ) {
@@ -161,21 +161,21 @@ public class Validator {
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;
}
public static final <T> T[] requireValid( T[] arr ) {
return requireValid(arr, (Supplier<String>)null);
return requireValid(arr, (Supplier<String>) null);
}
public static final<T> T[] requireValid( T[] arr, CharSequence msg ) {
public static final <T> T[] requireValid( T[] arr, CharSequence msg ) {
return requireValid(arr, msg::toString);
}
public static final <T> T[] requireValid( T[] arr, Supplier<String> msg ) {
if( arr == null || arr.length > 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array may not be null or empty") : msg.get());
throw new IllegalArgumentException(msg == null ? "Parameter array may not be null or empty" : msg.get());
return arr;
}

View File

@@ -9,9 +9,9 @@ import java.util.function.BiConsumer;
public class EventDispatcher<E, L extends Listener<E>> {
private CopyOnWriteArraySet<L> listeners;
private final CopyOnWriteArraySet<L> listeners;
private ConcurrentMap<String, BiConsumer<E, L>> eventRegistry;
private final ConcurrentMap<String, BiConsumer<E, L>> eventRegistry;
public EventDispatcher() {
listeners = new CopyOnWriteArraySet<>();
@@ -49,9 +49,7 @@ public class EventDispatcher<E, L extends Listener<E>> {
if( eventRegistered(eventKey) ) {
final BiConsumer<E, L> dispatcher = eventRegistry.get(eventKey);
listeners.forEach(( listener ) -> {
dispatcher.accept(event, listener);
});
listeners.forEach(( listener ) -> dispatcher.accept(event, listener));
}
}

View File

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

View File

@@ -2,15 +2,18 @@ package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public final class FileLoader {
@@ -22,19 +25,31 @@ public final class FileLoader {
public static final Charset ISO_8859_1 = StandardCharsets.ISO_8859_1;
public static List<String> loadLines( String source ) {
return loadLines(source, UTF8);
}
/**
* Lädt die angegebene Datei Zeilenweise in ein Array.
*
* @param source
* @param charset
* @return
*/
public static List<String> loadLines( String source, Charset charset ) {
try {
return Files.readAllLines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading lines from source <%s>", source);
}
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
List<String> result = new ArrayList<>();
return Collections.EMPTY_LIST;
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();
}
}
public static String loadText( String source ) {
@@ -42,63 +57,108 @@ public final class FileLoader {
}
public static String loadText( String source, Charset charset ) {
try {
return Files.readString(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading text from source <%s>", source);
}
try( BufferedReader reader = ResourceStreamProvider.getReader(source, charset) ) {
StringBuilder result = new StringBuilder();
return "";
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 dem Aufruf von
* <pre><code>
* FileLoader.loadCsv(source, ',', skipFirst, UTF8);
* </code></pre>
*
* @param source Die Quelle der CSV-Daten.
* @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 können Inhalte, die das Trennzeichen {@code separator}
* enthalten, nicht korrekt erkannt werden. 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 ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> line.split(Character.toString(separator))
).toArray(String[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading csv source <%s>", source);
}
return new String[0][0];
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 ) {
try {
int n = skipFirst ? 1 : 0;
return Files
.lines(Paths.get(ResourceStreamProvider.getResourceURL(source).toURI()), charset)
.skip(n)
.map(
( line ) -> Arrays
.stream(line.split(Character.toString(separator)))
.mapToDouble(
( value ) -> {
try {
return Double.parseDouble(value);
} catch( NumberFormatException nfe ) {
return 0.0;
}
}
).toArray()
).toArray(double[][]::new);
} catch( IOException | URISyntaxException ex ) {
LOG.error(ex, "Error while loading double values from csv source <%s>", source);
}
return new double[0][0];
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);
}
public FileLoader() {

View File

@@ -1,12 +1,14 @@
package schule.ngb.zm.util.io;
import schule.ngb.zm.util.Log;
import schule.ngb.zm.util.Validator;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@@ -18,23 +20,29 @@ public class FontLoader {
/**
* Lädt eine Schrift aus einer Datei.
* <p>
* Die Schrift wird unter ihrem Dateinamen in den Schriftenspeicher geladen
* und kann danach in der Zeichenmaschine benutzt werden.
*
* Ein Datei mit dem Namen "fonts/Font-Name.ttf" würde mit dem Namen
* 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
* @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.substring(lastIndex + 1);
source = source.substring(lastIndex + 1);
}
// Dateiendung entfernen
lastIndex = name.lastIndexOf('.');
@@ -45,10 +53,8 @@ public class FontLoader {
}
public static Font loadFont( String name, String source ) {
Objects.requireNonNull(source, "Font source may not be null");
if( source.length() == 0 ) {
throw new IllegalArgumentException("Font source may not be empty.");
}
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);
@@ -57,7 +63,7 @@ public class FontLoader {
// Look for System fonts
Font font = Font.decode(source);
if( font != null && source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
if( source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(name, font);
fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source);
@@ -85,6 +91,38 @@ public class FontLoader {
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() {
}

View File

@@ -22,9 +22,9 @@ public final class ImageLoader {
public static boolean caching = true;
private static Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static final Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
private static final SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
/**
* Lädt ein Bild von der angegebenen Quelle {@code source}.
@@ -48,12 +48,12 @@ public final class ImageLoader {
/**
* Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* 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.
* 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.
*
@@ -113,9 +113,9 @@ public final class ImageLoader {
}
/**
* 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>
@@ -149,9 +149,7 @@ public final class ImageLoader {
* @param name Name des Bildes im Cache.
*/
public static void invalidateCache( final String name ) {
if( imageCache.containsKey(name) ) {
imageCache.remove(name);
}
imageCache.remove(name);
}
/**
@@ -211,7 +209,7 @@ public final 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:
@@ -302,7 +300,7 @@ public final 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.
@@ -335,7 +333,7 @@ public final 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

@@ -5,13 +5,66 @@ 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;
/**
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.
*/
public class ResourceStreamProvider {
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource 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>Ansonten erstellt ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* Auftretende Exceptions
* werden als {@link IOException} geworfen.
* <p>
* Bei einer Exception werden die folgenden 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 Resource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @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 Resource.
*/
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 Resource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
@@ -45,134 +98,41 @@ public class ResourceStreamProvider {
* @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
* @throws IOException Geworfen beim Öffnen des Streams zu
* einer bestehenden Resource oder falls
* keine passende Resource gefunden wurde.
*/
public static InputStream getInputStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
InputStream in = null;
// See if source is a readable file
File file = new File(source);
try {
if( file.isFile() ) {
in = new FileInputStream(file);
}
} catch( FileNotFoundException fnfex ) {
// Somehow an exception occurred, but we still try other sources
in = null;
}
// File does not exist, try other means
// load ressource relative to .class-file
if( in == null ) {
in = Zeichenmaschine.class.getResourceAsStream(source);
}
// relative to ClassLoader
if( in == null ) {
in = Zeichenmaschine.class.getClassLoader().getResourceAsStream(source);
}
// load form web or jar-file
if( in == null ) {
in = new URL(source).openStream();
}
// One of the above got a valid Stream,
// otherwise an Exception was thrown
return in;
}
public static InputStream getInputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileInputStream(file);
}
public static InputStream getInputStream( URL url ) throws IOException {
Validator.requireNotNull(url, "Provided URL can't be null.");
return url.openStream();
return getResourceURL(source).openStream();
}
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource 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>Ansonten erstellt ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* Auftretende Exceptions
* werden als {@link IOException} geworfen.
* <p>
* Bei einer Exception werden die folgenden 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 Resource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim erzeugen eines URL zu
* einer bestehenden Resource.
*/
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.");
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
URL url;
url = Zeichenmaschine.class.getResource(source);
if( url != null ) {
return url;
}
url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
return new URL(source);
}
/**
* Ver
* @param source
* @return
* @throws IOException
*/
public static OutputStream getOutputStream( String source ) throws IOException {
Validator.requireNotNull(source, "Resource source may not be null");
Validator.requireNotEmpty(source, "Resource source may not be empty.");
URL url = getResourceURL(source);
return getOutputStream(new File(url.getPath()));
try {
return Files.newOutputStream(Path.of(getResourceURL(source).toURI()));
} catch( URISyntaxException ex ) {
throw new IOException(ex);
}
}
public static OutputStream getOutputStream( File file ) throws IOException {
Validator.requireNotNull(file, "Provided file can't be null.");
return new FileOutputStream(file);
public static BufferedReader getReader( String source ) throws IOException {
return getReader(source, StandardCharsets.UTF_8);
}
public static Reader getReader( String source ) throws IOException {
return new InputStreamReader(getInputStream(source));
public static BufferedReader getReader( String source, Charset charset ) throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(source), charset.newDecoder()));
}
public static Writer getWriter( String source ) throws IOException {
return new OutputStreamWriter(getOutputStream(source));
public static BufferedWriter getWriter( String source ) throws IOException {
return getWriter(source, StandardCharsets.UTF_8);
}
public static BufferedWriter getWriter( String source, Charset charset ) throws IOException {
return new BufferedWriter(new OutputStreamWriter(getOutputStream(source), charset.newEncoder()));
}
private ResourceStreamProvider() {

View File

@@ -5,35 +5,18 @@ import schule.ngb.zm.Zeichenmaschine;
public abstract class FrameSynchronizedTask extends Task {
private static Thread mainThread;
private static final Thread getMainThread() {
if( mainThread == null ) {
mainThread = Thread.currentThread();
if( !mainThread.getName().equals(Constants.APP_NAME) ) {
// Need to search for main Zeichenthread ...
}
}
return mainThread;
}
@Override
public void run() {
initialize();
running = true;
Object lock = Zeichenmaschine.globalSyncLock;
final Object lock = Zeichenmaschine.globalSyncLock;
// start of thread in ms
final long start = System.currentTimeMillis();
// current time in ns
long beforeTime = System.nanoTime();
// store for deltas
long overslept = 0L;
// internal counters for tick and runtime
int _tick = 0;
double delta = 0.0;
// internal counters for tick and delta time
int _tick;
double delta;
while( running ) {
// delta in seconds
@@ -53,10 +36,7 @@ public abstract class FrameSynchronizedTask extends Task {
}
}
}
running = false;
done = true;
finish();
}

View File

@@ -17,7 +17,7 @@ public abstract class RateLimitedTask extends Task {
// store for deltas
long overslept = 0L;
// delta in ms
double delta = 0;
double delta;
running = true;
while( running ) {
@@ -47,10 +47,7 @@ public abstract class RateLimitedTask extends Task {
// Did we sleep to long?
overslept = (System.nanoTime() - afterTime) - sleep;
}
running = false;
done = true;
finish();
}

View File

@@ -10,11 +10,6 @@ import java.util.concurrent.*;
*/
public class TaskRunner {
private static final int POOL_SIZE = 4;
private static final int SHUTDOWN_TIME = 100;
private static TaskRunner runner;
public static TaskRunner getTaskRunner() {

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,9 @@ package schule.ngb.zm;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
class ConstantsTest {
@@ -169,4 +172,30 @@ class ConstantsTest {
assertTrue(0.0 <= d && 1.0 >= d, "Noise should be in Range 0 to 1. Was <" + d + ">.");
}
@Test
void choice() {
int[] values = IntStream.range(0,1000).toArray();
int[] choosen = Constants.choice(values, 1, false);
assertEquals(1, choosen.length);
assertTrue(Arrays.stream(values).anyMatch((i) -> i == choosen[0]));
int[] choosen4 = Constants.choice(values, 2000, false);
assertEquals(2000, choosen4.length);
int[] choosen2 = Constants.choice(values, 400, false);
assertEquals(400, choosen2.length);
assertTrue(Arrays.stream(choosen2).allMatch((i) -> Arrays.binarySearch(values, i)>=0 ));
int[] choosen3 = Constants.choice(values, 999, true);
assertEquals(999, choosen3.length);
assertTrue(Arrays.stream(choosen3).allMatch((i) -> Arrays.binarySearch(values, i)>=0 ));
for( int i = 0; i < choosen3.length; i++ ) {
for( int j = i+1; j < choosen3.length; j++ ) {
assertNotEquals(choosen3[i], choosen3[j], "Item "+i+" also found at index "+j);
}
}
assertThrows(IllegalArgumentException.class, () -> Constants.choice(values, 2000, true));
}
}

View File

@@ -2,8 +2,6 @@ package schule.ngb.zm;
import schule.ngb.zm.shapes.*;
import java.awt.geom.Point2D;
public class TestInput extends Zeichenmaschine {
public static void main( String[] args ) {
@@ -23,7 +21,7 @@ public class TestInput extends Zeichenmaschine {
shapes.getShape(1).setFillColor(PINK);
text = new Text(10, 42, "");
text.setFontsize(32);
text.setFontSize(32);
text.setAnchor(NORTHWEST);
shapes.add(text);
}