diff --git a/README.md b/README.md index 105bbaf..3d9a668 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,6 @@ Mit der Zechenmaschine können Schüler:innen ihre Programme von Anfang an mit Visualisierungen und Interaktionen ausstatten, ohne sich mit der GUI Programmierung mit Java Swing auseinandersetzen zu müssen. -Die Bibliothek wurde sich stark on [Processing](https://processing.org), +Die Bibliothek wurde stark on [Processing](https://processing.org), [Engine Alpha](https://engine-alpha.org) und [TigerJython](https://www.tigerjython.ch/de) inspiriert. diff --git a/src/schule/ngb/zm/ColorLayer.java b/src/schule/ngb/zm/ColorLayer.java index 2cdecd0..07e95d0 100644 --- a/src/schule/ngb/zm/ColorLayer.java +++ b/src/schule/ngb/zm/ColorLayer.java @@ -9,6 +9,12 @@ public class ColorLayer extends Layer { clear(); } + public ColorLayer( int width, int height, Color color ) { + super(width, height); + this.background = color; + clear(); + } + @Override public void setSize( int width, int height ) { super.setSize(width, height); diff --git a/src/schule/ngb/zm/Constants.java b/src/schule/ngb/zm/Constants.java index a8f0d71..b626028 100644 --- a/src/schule/ngb/zm/Constants.java +++ b/src/schule/ngb/zm/Constants.java @@ -8,8 +8,8 @@ public class Constants { public static final String APP_NAME = "Zeichenmaschine"; public static final int APP_VERSION_MAJ = 0; - public static final int APP_VERSION_MIN = 1; - public static final int APP_VERSION_REV = 5; + public static final int APP_VERSION_MIN = 0; + public static final int APP_VERSION_REV = 8; public static final String APP_VERSION = APP_VERSION_MAJ + "." + APP_VERSION_MIN + "." + APP_VERSION_REV; diff --git a/src/schule/ngb/zm/ImageLayer.java b/src/schule/ngb/zm/ImageLayer.java index e64c87d..bdf5abf 100644 --- a/src/schule/ngb/zm/ImageLayer.java +++ b/src/schule/ngb/zm/ImageLayer.java @@ -16,11 +16,6 @@ public class ImageLayer extends Layer { protected boolean redraw = true; - public ImageLayer() { - - } - - public ImageLayer( String source ) { image = ImageLoader.loadImage(source); } diff --git a/src/schule/ngb/zm/Zeichenleinwand.java b/src/schule/ngb/zm/Zeichenleinwand.java index 491bfc6..92803b3 100644 --- a/src/schule/ngb/zm/Zeichenleinwand.java +++ b/src/schule/ngb/zm/Zeichenleinwand.java @@ -30,9 +30,9 @@ public class Zeichenleinwand extends Canvas { // Liste der Ebenen initialisieren und die Standardebenen einfügen layers = new LinkedList<>(); synchronized( layers ) { - layers.add(new ColorLayer(Constants.STD_BACKGROUND)); - layers.add(new DrawingLayer()); - layers.add(new ShapesLayer()); + layers.add(new ColorLayer(width, height, Constants.STD_BACKGROUND)); + layers.add(new DrawingLayer(width, height)); + layers.add(new ShapesLayer(width, height)); } } @@ -160,6 +160,16 @@ public class Zeichenleinwand extends Canvas { render(); } + public void draw( Graphics graphics ) { + Graphics2D g2d = (Graphics2D) graphics.create(); + synchronized( layers ) { + for( Layer layer : layers ) { + layer.draw(g2d); + } + } + g2d.dispose(); + } + public void render() { if( getBufferStrategy() == null ) { allocateBuffer(); @@ -192,16 +202,6 @@ public class Zeichenleinwand extends Canvas { // Repeat the rendering if the drawing buffer was lost } while( strategy.contentsLost() ); } - - /* - Graphics2D g2d = (Graphics2D) g.create(); - - for( Layer layer : layers ) { - layer.draw(g2d); - } - - g2d.dispose(); - */ } } } diff --git a/src/schule/ngb/zm/Zeichenmaschine.java b/src/schule/ngb/zm/Zeichenmaschine.java index 66bf867..c7fc0d1 100644 --- a/src/schule/ngb/zm/Zeichenmaschine.java +++ b/src/schule/ngb/zm/Zeichenmaschine.java @@ -1,15 +1,21 @@ package schule.ngb.zm; import schule.ngb.zm.shapes.ShapesLayer; +import schule.ngb.zm.util.ImageLoader; import javax.swing.*; import javax.swing.event.MouseInputListener; import java.awt.*; +import java.awt.Color; import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.awt.image.RescaleOp; +import java.io.File; +import java.io.IOException; /** * Hauptklasse der Zeichenmaschine. - * + *

* Projekte der Zeichenmaschine sollten als Unterklasse implementiert werden. * Die Klasse übernimmt die Initialisierung eines Programmfensters und der * nötigen Komponenten. @@ -25,6 +31,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke /* * Attributes to be accessed by subclasses. */ + protected Zeichenleinwand canvas; protected ColorLayer background; @@ -41,18 +48,24 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke protected double mouseX = 0.0, mouseY = 0.0, pmouseX = 0.0, pmouseY = 0.0; - protected int width = STD_WIDTH, height = STD_HEIGHT; + protected int width, height; + + protected int screenWidth, screenHeight; + + /* + * Interne Attribute zur Steuerung der Zeichenmaschine. + */ private Object mouseLock = new Object(); private Object keyboardLock = new Object(); - /* - * Interne Attribute zur Steuerung der Zeichenamschine. - */ - // private JFrame frame; + private GraphicsEnvironment environment; + + private GraphicsDevice displayDevice; + private boolean running = false, isDrawing = false, isUpdating = false; private int framesPerSecond; @@ -61,6 +74,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke private boolean quitAfterTeardown = false, initialized = false; + public Zeichenmaschine() { this(APP_NAME + " " + APP_VERSION); } @@ -73,20 +87,37 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch( Exception e ) { - System.err.println("Error setting the look and feel."); + System.err.println("Error setting the look and feel: " + e.getMessage()); } - GraphicsEnvironment environment = - GraphicsEnvironment.getLocalGraphicsEnvironment(); - GraphicsDevice displayDevice = environment.getDefaultScreenDevice(); + + // Looking for the screen currently holding the mouse pointer + // that will be used as the screen device for this Zeichenmaschine + java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation(); + environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] devices = environment.getScreenDevices(); + for( GraphicsDevice gd: devices ) { + if( gd.getDefaultConfiguration().getBounds().contains(mouseLoc) ) { + displayDevice = gd; + break; + } + } + if( displayDevice == null ) { + displayDevice = environment.getDefaultScreenDevice(); + } + + + this.width = width; + this.height = height; + java.awt.Rectangle displayBounds = displayDevice.getDefaultConfiguration().getBounds(); + this.screenWidth = (int)displayBounds.getWidth(); + this.screenHeight = (int)displayBounds.getHeight(); + frame = new JFrame(displayDevice.getDefaultConfiguration()); frame.setTitle(title); frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); - this.width = width; - this.height = height; - canvas = new Zeichenleinwand(width, height); frame.add(canvas); @@ -96,6 +127,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke drawing = getDrawingLayer(); shapes = getShapesLayer(); + // TODO: When to call settings? settings(); canvas.addMouseListener(this); @@ -114,9 +146,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke frame.pack(); frame.setResizable(false); - // TODO: Center on current display (not main display by default) - // TODO: Position at current BlueJ windows if IN_BLUEJ - frame.setLocationRelativeTo(null); + centerFrame(); frame.setVisible(true); canvas.allocateBuffer(); @@ -148,6 +178,19 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke } } + public final void centerFrame() { + // TODO: Center on current display (not main display by default) + // TODO: Position at current BlueJ windows if IN_BLUEJ + //frame.setLocationRelativeTo(null); + //frame.setLocationRelativeTo(displayDevice.getFullScreenWindow()); + + java.awt.Rectangle bounds = displayDevice.getDefaultConfiguration().getBounds(); + frame.setLocation( + (int)(bounds.x + (screenWidth-frame.getWidth())/2.0), + (int)(bounds.y + (screenHeight-frame.getHeight())/2.0) + ); + } + public final void redraw() { canvas.render(); //canvas.invalidate(); @@ -242,6 +285,64 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke framesPerSecond = pFramesPerSecond; } + public void saveImage() { + JFileChooser jfc = new JFileChooser(); + jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + jfc.setMultiSelectionEnabled(false); + + int status = jfc.showSaveDialog(frame); + if( status == JFileChooser.APPROVE_OPTION ) { + File outfile = jfc.getSelectedFile(); + if( outfile.isDirectory() ) { + outfile = new File(outfile.getAbsolutePath() + File.separator + "zeichenmaschine.png"); + } + saveImage(outfile.getAbsolutePath()); + } + } + + public void saveImage( String filepath ) { + BufferedImage img = new BufferedImage(canvas.getWidth(),canvas.getHeight(), BufferedImage.TYPE_INT_RGB); + + Graphics2D g = img.createGraphics(); + g.setColor(STD_BACKGROUND.getColor()); + g.fillRect(0, 0, img.getWidth(), img.getHeight()); + canvas.draw(g); + g.dispose(); + + try { + ImageLoader.saveImage(img, new File(filepath), true); + } catch ( IOException ex ) { + ex.printStackTrace(); + } + } + + public ImageLayer snapshot() { + BufferedImage img = new BufferedImage(canvas.getWidth(),canvas.getHeight(), BufferedImage.TYPE_INT_RGB); + + Graphics2D g = img.createGraphics(); + g.setColor(STD_BACKGROUND.getColor()); + g.fillRect(0, 0, img.getWidth(), img.getHeight()); + canvas.draw(g); + g.dispose(); + + /* + float factor = 0.8f; + float base = 255f * (1f - factor); + RescaleOp op = new RescaleOp(factor, base, null); + BufferedImage filteredImage + = new BufferedImage(img.getWidth(), img.getHeight(), img.getType()); + op.filter(img, filteredImage); + */ + + ImageLayer imgLayer = new ImageLayer(img); + if( canvas.getLayer(0) instanceof ColorLayer ) { + canvas.addLayer(1, imgLayer); + } else { + canvas.addLayer(0, imgLayer); + } + return imgLayer; + } + /* * Methoden, die von Unterklassen überschrieben werden können / sollen. */ @@ -366,6 +467,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke pmouseX = mouseX; pmouseY = mouseY; + // TODO: Seems not right ... java.awt.Point mouseLoc = MouseInfo.getPointerInfo().getLocation(); java.awt.Point compLoc = canvas.getLocationOnScreen(); mouseX = mouseLoc.x - compLoc.x; diff --git a/src/schule/ngb/zm/shapes/Shape.java b/src/schule/ngb/zm/shapes/Shape.java index f080d6c..90d36d8 100644 --- a/src/schule/ngb/zm/shapes/Shape.java +++ b/src/schule/ngb/zm/shapes/Shape.java @@ -121,7 +121,7 @@ public abstract class Shape extends FilledShape { * Bestimmt den Ankerpunkt der Form relativ zur oberen linken Ecke und * abhängig vom gesetzten {@link #setAnchor(Options.Direction) Anker}. */ - public Point2D.Double getAnchorPoint() { + public Point2D.Double getAnchorPoint( Options.Direction anchor ) { Point2D.Double anchorpoint = new Point2D.Double(0, 0); double bHalf = getWidth() * .5, hHalf = getHeight() * .5; @@ -203,18 +203,36 @@ public abstract class Shape extends FilledShape { this.rotation = angle % 360; } + public void rotate( Point2D center, double angle ) { + rotate(center.getX(), center.getY(), angle); + } + + public void rotate( double x, double y, double angle ) { + this.rotation += angle % 360; + + // Rotate x/y position + double x1 = this.x-x, y1 = this.y-y; + + double rad = Math.toRadians(angle); + double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad); + double y2 = x1 * Math.sin(rad) + y1 * Math.cos(rad); + + this.x = x2 + x; + this.y = y2 + y; + } + /*public void shear( double dx, double dy ) { verzerrung.shear(dx, dy); }*/ public AffineTransform getTransform() { - Point2D.Double anchor = getAnchorPoint(); + Point2D.Double anchorPoint = getAnchorPoint(this.anchor); AffineTransform transform = new AffineTransform(); transform.translate(x, y); transform.rotate(Math.toRadians(rotation)); //transform.scale(scale, scale); - transform.translate(-anchor.x, -anchor.y); + transform.translate(-anchorPoint.x, -anchorPoint.y); return transform; } diff --git a/src/schule/ngb/zm/util/ImageLoader.java b/src/schule/ngb/zm/util/ImageLoader.java index 42e23d7..73716c8 100644 --- a/src/schule/ngb/zm/util/ImageLoader.java +++ b/src/schule/ngb/zm/util/ImageLoader.java @@ -1,7 +1,11 @@ package schule.ngb.zm.util; import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.net.URL; @@ -138,4 +142,45 @@ public class ImageLoader { cacheing = false; } + + public static void saveImage( Image image, File file ) throws IOException { + saveImage(image, file, false); + } + + public static void saveImage( Image image, File file, boolean overwriteIfExists ) throws IOException { + BufferedImage outimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB); + + Graphics2D g = outimage.createGraphics(); + g.setColor(Color.WHITE); + g.fillRect(0, 0, outimage.getWidth(), outimage.getHeight()); + g.drawImage(image, 0, 0, null); + g.dispose(); + + saveImage(outimage, file, overwriteIfExists); + } + + public static void saveImage( BufferedImage image, File file ) throws IOException { + saveImage(image, file, false); + } + + public static void saveImage( BufferedImage image, File file, boolean overwriteIfExists ) throws IOException { + if( file.isFile() ) { + if( !overwriteIfExists ) { + throw new IOException("File already exists. Delete target file before saving image."); + } else if( !file.canWrite() ) { + throw new IOException("File already exists and is not writeable. Change permissions before saving again."); + } + } + + String filename = file.getName(); + String formatName = "png"; + if( filename.lastIndexOf('.') >= 0 ) { + formatName = filename.substring(filename.lastIndexOf('.') + 1); + } else { + file = new File(file.getAbsolutePath() + ".png"); + } + + ImageIO.write(image, formatName, file); + } + }