import java.awt.image.BufferedImage; import java.io.File; import javax.imageio.ImageIO; import javafx.application.Application; import javafx.embed.swing.SwingFXUtils; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; import javafx.scene.Scene; import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.scene.control.MenuBar; import javafx.scene.control.Alert; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.Toggle; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.ToggleGroup; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.input.MouseEvent; import javafx.scene.image.WritableImage; import javafx.scene.image.Image; import javafx.scene.image.PixelReader; import javafx.scene.image.PixelWriter; import javafx.scene.SnapshotParameters; import javafx.geometry.Rectangle2D; /** * A simple paint program that lets the user paint with several * different tools, including a "smudge" tool that uses fairly * sophisticated pixel manipulation. The program also demonstrates * using a transparent "overlay" canvas to implement some of the * tools. The window for this program is not resizable. * The program also requires the file SimpleDialogs.java. */ public class ToolPaint extends Application { public static void main(String[] args) { launch(args); } private Canvas canvas; // The canvas on which the image is drawn. private GraphicsContext canvasGraphics; // The graphics context for the canvas. private Canvas overlay; // A transparent canvas that lies on top of // the image canvas, used for temporarily // drawing some shapes during a mouse drag. private GraphicsContext overlayGraphics; // Graphics context for overlay. /** * The possible drawing tools in this program. (The CURVE tool allows the * user to sketch a free-hand curve, while the LINE tool draws a line * between two points. The SMUDGE tool lets the user "spread paint around" * with the mouse. The ERASE tool erases with a 10-by-10 pixel rectangle.) */ private enum Tool { LINE, RECT, OVAL, FILLED_RECT, FILLED_OVAL, CURVE, SMUDGE, ERASE } private Tool currentTool = Tool.CURVE; // The current drawing tool. private Color currentColor = Color.BLACK; // The current drawing color. private Color backgroundColor = Color.WHITE; // The current background color. /* Some variables that are used during dragging. */ private boolean dragging; // is a drag in progress? private int startX, startY; // start point of drag private int prevX, prevY; // previous mouse location during a drag private int currentX, currentY; // current mouse position during a drag /* Some variables used to implement the smudge tool. */ private double[][] smudgeRed, smudgeGreen, smudgeBlue; private WritableImage pixels; // a 9-by-9 block of pixels from the canvas private PixelReader pixelReader; // for reading colors from pixels private SnapshotParameters snapshotParams; // used for snapshotting the canvas private PixelWriter pixelWriter; // for writing pixels to the canvas private Stage window; // The program's window. /** * Create the canvas and the overlayCanvas, and set up mouse handling, * configure and show the window. */ public void start(Stage stage) { window = stage; int width = 800; // size of canvas; can be changed here int height = 600; canvas = new Canvas(width,height); canvasGraphics = canvas.getGraphicsContext2D(); canvasGraphics.setFill(backgroundColor); canvasGraphics.fillRect(0,0,width,height); overlay = new Canvas(width,height); overlayGraphics = overlay.getGraphicsContext2D(); overlay.setOnMousePressed( e -> mousePressed(e) ); overlay.setOnMouseDragged( e -> mouseDragged(e) ); overlay.setOnMouseReleased( e -> mouseReleased(e) ); canvasGraphics.setLineWidth(2); overlayGraphics.setLineWidth(2); StackPane canvasHolder = new StackPane(canvas,overlay); BorderPane root = new BorderPane(canvasHolder); root.setTop( makeMenuBar() ); stage.setScene( new Scene(root) ); stage.setTitle("A Simple Paint Program"); stage.setResizable(false); stage.show(); } // end start() /** * A utility method to draw the current shape in a given graphics context. * It draws the correct shape for the current tool in a rectangle whose * corners are given by the starting position of the mouse and the current * position of the mouse. This method is not used when the current tool * is Tool.CURVE or Tool.ERASE, or Tool.SMUDGE. For other tools, it is * used to draw the shape to the overlay canvas during a drag operation; * then, when the drag ends, it is used to draw the shape to the main * canvas. The shape is defined by the tool and by the two points * (startX,startY) and (currentX,currentY). */ private void putCurrentShape(GraphicsContext g) { switch (currentTool) { case LINE: g.strokeLine(startX, startY, currentX, currentY); break; case OVAL: putOval(g,false,startX, startY, currentX, currentY); break; case RECT: putRect(g,false,startX, startY, currentX, currentY); break; case FILLED_OVAL: putOval(g,true,startX, startY, currentX, currentY); break; case FILLED_RECT: putRect(g,true,startX, startY, currentX, currentY); break; default: // not called in other cases break; } } /** * Draws a filled or unfilled rectangle with corners at the points (x1,y1) * and (x2,y2). Nothing is drawn if x1 == x2 or y1 == y2. * (This method translates from an opposite-corners definition of the rectangle * to the upper-left-corner-width-and-height definition used for drawing.) * @param g the graphics context where the rectangle is drawn * @param filled tells whether to draw a filled or unfilled rectangle. */ private void putRect(GraphicsContext g, boolean filled, int x1, int y1, int x2, int y2) { if (x1 == x2 || y1 == y2) return; int x = Math.min(x1,x2); // get upper left corner, (x,y) int y = Math.min(y1,y2); int w = Math.abs(x1 - x2); // get width and height int h = Math.abs(y1 - y2); if (filled) g.fillRect(x,y,w,h); else g.strokeRect(x,y,w,h); } /** * Draws a filled or unfilled oval in the rectangle with corners at the * points (x1,y1) and (x2,y2). Nothing is drawn if x1 == x2 or y1 == y2. * @param g the graphics context where the oval is drawn * @param filled tells whether to draw a filled or unfilled oval. */ private void putOval(GraphicsContext g, boolean filled, int x1, int y1, int x2, int y2) { if (x1 == x2 || y1 == y2) return; int x = Math.min(x1,x2); // get upper left corner, (x,y) int y = Math.min(y1,y2); int w = Math.abs(x1 - x2); // get width and height int h = Math.abs(y1 - y2); if (filled) g.fillOval(x,y,w,h); else g.strokeOval(x,y,w,h); } /** * Creates a menu bar for use with this program, with "Color" * and "Tool" menus. */ private MenuBar makeMenuBar() { MenuBar menubar = new MenuBar(); Menu fileMenu = new Menu("File"); Menu colorMenu = new Menu("Color"); Menu toolMenu = new Menu("Tool"); menubar.getMenus().add(fileMenu); menubar.getMenus().add(colorMenu); menubar.getMenus().add(toolMenu); MenuItem openImage = new MenuItem("Load Image..."); openImage.setOnAction( e -> doOpenImage() ); fileMenu.getItems().add(openImage); MenuItem saveImage = new MenuItem("Save PNG Image..."); saveImage.setOnAction( e -> doSaveImage() ); fileMenu.getItems().add(saveImage); fileMenu.getItems().add( new SeparatorMenuItem() ); MenuItem quit = new MenuItem("Quit"); quit.setOnAction( e -> System.exit(0) ); fileMenu.getItems().add(quit); /* Color choices are given by RadioMenuItems, controlled by * a ToggleGroup. Each choice corresponds to a standard color, * except for a "Custom Drawing Color" item that calls up * a color choice dialog box. */ Color[] colors = { // Standard colors available in the menu. Color.BLACK, Color.WHITE, Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.CYAN, Color.PURPLE, Color.GRAY}; String[] colorNames = { // Names for the colors, used to construct menu items. "Black", "White", "Red", "Green", "Blue", "Yellow", "Cyan", "Purple", "Gray"}; ToggleGroup colorGroup = new ToggleGroup(); for (int i = 0; i < colors.length; i++) { RadioMenuItem item = new RadioMenuItem("Draw with " + colorNames[i]); item.setUserData(colors[i]); // Stash the actual color in the menu item's UserData item.setToggleGroup(colorGroup); colorMenu.getItems().add(item); if (i == 0) item.setSelected(true); // Initially selected color is black. } RadioMenuItem customColor = new RadioMenuItem("Custom Drawing Color..."); customColor.setToggleGroup(colorGroup); customColor.setOnAction( e -> { // For the custom color selection, use a dialog box to get an // arbitrary color from the user. This has to be done in an // ActionEvent handler, since it needs to happen even if the user selects // Custom Drawing Color when it is already selected. (In that case, // the selected toggle does not change. customColor.setSelected(true); Color c = SimpleDialogs.colorChooser(currentColor, "Select a Color to Use For Drawing"); if (c != null) // c is null if user cancels the dialog currentColor = c; }); colorMenu.getItems().add(customColor); colorGroup.selectedToggleProperty().addListener( e -> { // Sets the color, when one of the standard colors is selected. // This does not handle the Custom Color option. Note that // when the user chooses a new radio menu item, the selected toggle // first changes to null as the old menu item is deselected, then // changes to the newly selected menu item. Toggle t = colorGroup.getSelectedToggle(); // the selected RadioMenuItem if (t != null && t != customColor) { // The color associated with this menu item was stashed // in the UserData of the menu item, t. currentColor = (Color)t.getUserData(); } }); colorMenu.getItems().add( new SeparatorMenuItem() ); MenuItem clear = new MenuItem("Clear to Background Color"); clear.setOnAction( e -> { // Fill main canvas with current background color. canvasGraphics.setFill(backgroundColor); canvasGraphics.fillRect(0,0,canvas.getWidth(),canvas.getHeight()); }); colorMenu.getItems().add(clear); MenuItem fill = new MenuItem("Fill with Drawing Color"); fill.setOnAction( e -> { // Fill main canvas with current drawing color, but // don't change the background color. (The erase // tool will still erase to the (old) background color.) canvasGraphics.setFill(currentColor); canvasGraphics.fillRect(0,0,canvas.getWidth(),canvas.getHeight()); }); colorMenu.getItems().add(fill); colorMenu.getItems().add(new SeparatorMenuItem()); MenuItem setBG = new MenuItem("Fill and Set Background..."); setBG.setOnAction( e -> { // User can select a new background color from a dialog box. If the // dialog box is not canceled, the selected color becomes the background // color, and the canvas is filled with that background color. Color c = SimpleDialogs.colorChooser(backgroundColor, "Select a Background Color"); if (c != null) { backgroundColor = c; canvasGraphics.setFill(c); canvasGraphics.fillRect(0,0,canvas.getWidth(),canvas.getHeight()); } }); colorMenu.getItems().add(setBG); /* The User selects a drawing tool from the tool menu. The menu contains * an entry for each available tool. Tools are represented by RadioMenuItems, * controlled by a ToggleGroup. */ Tool[] tools = { // The available tools in the order they appear in the menu. Tool.CURVE, Tool.LINE, Tool.RECT, Tool.OVAL, Tool.FILLED_RECT, Tool.FILLED_OVAL, Tool.SMUDGE, Tool.ERASE }; String[] toolNames = { // Names for the tools, used as text in the menu items. "Curve", "Line", "Rectangle", "Oval", "Filled Rectangle", "Filled Oval", "Smudge", "Erase", }; ToggleGroup toolGroup = new ToggleGroup(); for (int i = 0; i < tools.length; i++) { RadioMenuItem item = new RadioMenuItem(toolNames[i]); item.setUserData(tools[i]); // Stash the actual tool in the menu items' UserData item.setToggleGroup(toolGroup); toolMenu.getItems().add(item); if (i == 0) item.setSelected(true); // Curve tool is initially selected if (i == 0 || i == 5) // Separators before and after the shape tools. toolMenu.getItems().add(new SeparatorMenuItem() ); } toolGroup.selectedToggleProperty().addListener( e -> { Toggle t = toolGroup.getSelectedToggle(); // The selected RadioMenuItem if (t != null) currentTool = (Tool)t.getUserData(); // the actual tool was stashed in the UserData. }); return menubar; } // end makeMenuBar /** * When the ERASE or SMUDGE tools are used and the mouse jumps * from (x1,y1) to (x2,y2), the tool has to be applied to a * line of pixel positions between the two points in order to * be sure to cover the entire line that the mouse moves along. */ private void applyToolAlongLine(int x1, int y1, int x2, int y2) { int w = (int)canvas.getWidth(); // (for SMUDGE only) int h = (int)canvas.getHeight(); // (for SMUDGE only) int dist = Math.max(Math.abs(x2-x1),Math.abs(y2-y1)); // dist is the number of points along the line from // (x1,y1) to (x2,y2) at which the tool will be applied. double dx = (double)(x2-x1)/dist; double dy = (double)(y2-y1)/dist; for (int d = 1; d <= dist; d++) { // Apply the tool at one of the points (x,y) along the // line from (x1,y1) to (x2,y2). int x = (int)Math.round(x1 + dx*d); int y = (int)Math.round(y1 + dy*d); if (currentTool == Tool.ERASE) { // Erase a 10-by-10 block of pixels around (x,y) canvasGraphics.fillRect(x-5,y-5,10,10); } else { // For the SMUDGE tool, blend some of the color from // the smudgeRed, smudgeGreen, and smudgeBlue arrays // into the pixels in a 9-by-9 block around (x,y), and // vice versa. The effect is to smear out the color // of pixels that are visited by the tool. snapshotParams.setViewport(new Rectangle2D(x-4,y-4,9,9)); canvas.snapshot(snapshotParams, pixels); for (int j = 0; j < 9; j++) { int c = x - 4 + j; for (int i = 0; i < 9; i++) { int r = y - 4 + i; if ( r >= 0 && r < h && c >= 0 && c < w && smudgeRed[i][j] != -1) { Color oldColor = pixelReader.getColor(j,i); double newRed = (oldColor.getRed()*0.8 + smudgeRed[i][j]*0.2); double newGreen = (oldColor.getGreen()*0.8 + smudgeGreen[i][j]*0.2); double newBlue = (oldColor.getBlue()*0.8 + smudgeBlue[i][j]*0.2); pixelWriter.setColor(c, r,Color.color(newRed,newGreen,newBlue)); smudgeRed[i][j] = oldColor.getRed()*0.2 + smudgeRed[i][j]*0.8; smudgeGreen[i][j] = oldColor.getGreen()*0.2 + smudgeGreen[i][j]*0.8; smudgeBlue[i][j] = oldColor.getBlue()*0.2 + smudgeBlue[i][j]*0.8; } } } } } } // end applyToolAlongLine /** * Start a drag operation. */ private void mousePressed(MouseEvent evt) { startX = prevX = currentX = (int)evt.getX(); startY = prevY = currentY = (int)evt.getY(); dragging = true; canvasGraphics.setStroke(currentColor); // Make sure we are drawing with the right color. canvasGraphics.setFill(currentColor); overlayGraphics.setStroke(currentColor); overlayGraphics.setFill(currentColor); if (currentTool == Tool.ERASE) { // Erase a 10-by-10 block around the starting mouse position. canvasGraphics.setFill(backgroundColor); // Change the color when using erase. canvasGraphics.fillRect(startX-5,startY-5,10,10); } else if (currentTool == Tool.SMUDGE) { // Record the colors in a 9-by-9 block of pixels around the // starting mouse position into the arrays smudgeRed, // smudgeGreen, and smudgeBlue. These arrays hold the // red, green, and blue components of the colors. if (smudgeRed == null) { // Create all variables needed for smudge, if not already done. pixels = new WritableImage(9,9); pixelReader = pixels.getPixelReader(); snapshotParams = new SnapshotParameters(); smudgeRed = new double[9][9]; smudgeGreen = new double[9][9]; smudgeBlue = new double[9][9]; pixelWriter = canvasGraphics.getPixelWriter(); } snapshotParams.setViewport(new Rectangle2D(startX-4,startY-4,9,9)); canvas.snapshot(snapshotParams, pixels); int h = (int)canvas.getHeight(); int w = (int)canvas.getWidth(); for (int j = 0; j < 9; j++) { // row in the snapshot int r = startY + j - 4; // the corresponding row in the canvas for (int i = 0; i < 9; i++) { // column in the snapshot int c = startX + i - 4; // the corresponding column in canvas if (r < 0 || r >= h || c < 0 || c >= w) { // The point (i,j) is outside the canvas. // A -1 in the smudgeRed array indicates that the // corresponding pixel was outside the canvas. smudgeRed[j][i] = -1; } else { Color color = pixelReader.getColor(i, j); // get color from snapshot smudgeRed[j][i] = color.getRed(); smudgeGreen[j][i] = color.getGreen(); smudgeBlue[j][i] = color.getBlue(); } } } } } /** * Continue a drag operation when the user drags the mouse. * For the CURVE tool, a line is drawn from the previous mouse * position to the current mouse position in the main canvas. * For shape tools like LINE and FILLED_RECT, the shape is drawn * to the overlay canvas after first clearing the overlay canvas. * For the SMUDGE and ERASE tools, the tool is applied along a * line from the previous mouse position to the current position, * on the main canvas. */ private void mouseDragged(MouseEvent evt) { if (!dragging) return; currentX = (int)evt.getX(); currentY = (int)evt.getY(); if (currentTool == Tool.CURVE) { canvasGraphics.strokeLine(prevX,prevY,currentX,currentY); } else if (currentTool == Tool.ERASE || currentTool == Tool.SMUDGE) { applyToolAlongLine(prevX,prevY,currentX,currentY); } else { // tool is a shape that has to be drawn to overlay canvas overlayGraphics.clearRect(0,0,overlay.getWidth(),overlay.getHeight()); putCurrentShape(overlayGraphics); } prevX = currentX; prevY = currentY; } /** * Finish a mouse drag operation. Nothing is done unless the current tool * is a shape tool. For shape tools, the user's shape is drawn to the * main canvas, making it a permanent part of the picture, and * the overlay canvas, which was used for the shape during dragging, * is cleared. */ private void mouseReleased(MouseEvent evt) { dragging = false; if (currentTool != Tool.CURVE && currentTool != Tool.ERASE && currentTool != Tool.SMUDGE) { putCurrentShape(canvasGraphics); overlayGraphics.clearRect(0,0,overlay.getWidth(),overlay.getHeight()); } } /** * Reads an image from a file and draws it to the canvas, * scaling it so it fills the canvas. */ private void doOpenImage() { FileChooser fileDialog = new FileChooser(); fileDialog.setInitialFileName(""); fileDialog.setInitialDirectory( new File( System.getProperty("user.home") ) ); fileDialog.setTitle("Select Image File to Load"); File selectedFile = fileDialog.showOpenDialog(window); if ( selectedFile == null ) return; // User did not select a file. Image image = new Image("file:" + selectedFile); if (image.isError()) { Alert errorAlert = new Alert(Alert.AlertType.ERROR, "Sorry, an error occurred while\ntrying to load the file:\n" + image.getException().getMessage()); errorAlert.showAndWait(); return; } canvasGraphics.drawImage(image,0,0,canvas.getWidth(),canvas.getHeight()); } /** * Saves the user's sketch as an image file in PNG format. */ private void doSaveImage() { FileChooser fileDialog = new FileChooser(); fileDialog.setInitialFileName("imagefile.png"); fileDialog.setInitialDirectory( new File( System.getProperty("user.home") ) ); fileDialog.setTitle("Select File to Save. Name MUST end with .png!"); File selectedFile = fileDialog.showSaveDialog(window); if ( selectedFile == null ) return; // User did not select a file. try { Image canvasImage = canvas.snapshot(null,null); BufferedImage image = SwingFXUtils.fromFXImage(canvasImage,null); String filename = selectedFile.getName().toLowerCase(); if ( ! filename.endsWith(".png")) { throw new Exception("The file name must end with \".png\"."); } boolean hasFormat = ImageIO.write(image,"PNG",selectedFile); if ( ! hasFormat ) { // (this should never happen) throw new Exception( "PNG format not available."); } } catch (Exception e) { Alert errorAlert = new Alert(Alert.AlertType.ERROR, "Sorry, an error occurred while\ntrying to save the image:\n" + e.getMessage()); errorAlert.showAndWait(); } } } // end class ToolPaint