import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.application.Platform; /** * A MosaicPanel object represents a grid containing rows * and columns of colored rectangles. There can be "grouting" * between the rectangles. (The grouting is just drawn as a * one-pixel outline around each rectangle.) The rectangles can * optionally be drawn as raised 3D-style rectangles; this effect * works much better with some colors than with others. Methods are * provided for getting and setting the colors of the rectangles. */ public class MosaicCanvas extends Canvas { //------------------ private instance variables -------------------- private int rows; // The number of rows of rectangles in the grid. private int columns; // The number of columns of rectangles in the grid. private Color defaultColor; // Color used for any rectangle whose color // has not been set explicitly. This // can never be null. private Color groutingColor; // The color for "grouting" between // rectangles. If this is null, no // grouting is drawn. private boolean alwaysDrawGrouting; // Grouting is drawn around default- // colored rects if this is true. private boolean use3D = true; // If true, 3D rects are used; if false, // flat rects are used to draw the rectangles. private boolean autopaint = true; // If true, then when a square's color is set, // repaint is called automatically. private Color[][] grid; // An array that contains the rectangles' colors. // If a null occurs in this array, the rectangle // is drawn in the default color, and "grouting" // will be drawn around that rectangle only if // alwaysDrawGrouting is true. Also, the // rectangle is drawn as a flat rectangle rather // than as a 3D rectangle. private GraphicsContext g; // The graphics context for drawing on this canvas. //------------------------ constructors ----------------------------- /** * Construct a MosaicPanel with 42 rows and 42 columns of rectangles, * and with preferred rectangle height and width both set to 16. */ public MosaicCanvas() { this(42,42); } /** * Construct a MosaicPanel with specified numbers of rows and columns of rectangles, * and with preferred rectangle height and width both set to 16. */ public MosaicCanvas(int rows, int columns) { this(rows,columns,16,16); } /** * Construct a MosaicPanel with the specified number of rows and * columns of rectangles, and with a specified preferred size for the * rectangle. The default rectangle color is black, the * grouting color is gray, and alwaysDrawGrouting is set to false. * If a non-null border color is specified, then a border of that color is added * to the panel, and its width is taken into account in the computation of the preferred * size of the panel. * @param rows the mosaic will have this many rows of rectangles. This must be a positive number. * @param columns the mosaic will have this many columns of rectangles. This must be a positive number. * @param preferredBlockHeight the preferred height of the mosaic will be set to this value times the number of * rows. The actual height is set by the component that contains the mosaic, and so might not be * equal to the preferred height. Size is measured in pixels. The value should not be less than about 5, * and any smaller value will be increased to 5. * @param preferredBlockWidth the preferred width of the mosaic will be set to this value times the number of * columns. The actual width is set by the component that contains the mosaic, and so might not be * equal to the preferred width. Size is measured in pixels. The value should not be less than about 5, * and any smaller value will be increased to 5. */ public MosaicCanvas(int rows, int columns, int preferredBlockHeight, int preferredBlockWidth) { this.rows = rows; this.columns = columns; if (rows <= 0 || columns <= 0) throw new IllegalArgumentException("Rows and Columns must be greater than zero."); preferredBlockHeight = Math.max( preferredBlockHeight, 5); preferredBlockWidth = Math.max( preferredBlockWidth, 5); grid = new Color[rows][columns]; defaultColor = Color.BLACK; groutingColor = Color.GRAY; alwaysDrawGrouting = false; setWidth(preferredBlockWidth*columns); setHeight(preferredBlockHeight*rows); g = getGraphicsContext2D(); } //--------- methods for getting and setting grid properties ---------- /** * Set the defaultColor. If c is null, the color will be set to black. * When a mosaic is first created, the defaultColor is black. * This is the color that is used for rectangles whose color * value is null. Such rectangles are always drawn as flat rather * than 3D rectangles. */ public void setDefaultColor(Color c) { if (c == null) c = Color.BLACK; if (! c.equals(defaultColor)) { defaultColor = c; forceRedraw(); } } /** * Return the defaultColor, which cannot be null. */ public Color getDefaultColor() { return defaultColor; } /** * Set the color of the "grouting" that is drawn between rectangles. * If the value is null, no grouting is drawn and the rectangles * fill the entire grid. When a mosaic is first created, the * groutingColor is gray. */ public void setGroutingColor(Color c) { if (c == null || ! c.equals(groutingColor)) { groutingColor = c; forceRedraw(); } } /** * Get the current groutingColor, which can be null. */ public Color getGroutingColor(Color c) { return groutingColor; } /** * Set the value of alwaysDrawGrouting. If this is false, then * no grouting is drawn around rectangles whose color value is null. * When a mosaic is first created, the value is false. */ public void setAlwaysDrawGrouting(boolean always) { if (alwaysDrawGrouting != always) { alwaysDrawGrouting = always; forceRedraw(); } } /** * Get the value of the use3D property. */ public boolean getUse3D() { return use3D; } /** * Set the use3D property. When this property is true, the rectangles are * drawn as "3D" rects, which are supposed appear to be raised. When use3D * is false, they are drawn as regular "flat" rects. Note that flat rects * are always used for background squares that have not been assigned a color. * The default value of use3D is true; */ public void setUse3D(boolean use3D) { this.use3D = use3D; } /** * Get the value of the alwaysDrawGrouting property. */ public boolean getAlwaysDrawGrouting() { return alwaysDrawGrouting; } /** * Set the number of rows and columns in the grid. If the value of * the preserveData parameter is false, then the color values of all * the rectangles in the new grid are set to null. If it is true, * then as much color data as will fit is copied from the old grid. * The number of rows and number of columns must be positive. */ public void setGridSize(int rows, int columns, boolean preserveData) { if (rows <= 0 && columns <= 0) throw new IllegalArgumentException("Rows and columns must be positive."); Color[][] newGrid = new Color[rows][columns]; if (preserveData) { int rowMax = Math.min(rows,this.rows); int colMax = Math.min(columns,this.columns); for (int r = 0; r < rowMax; r++) for (int c = 0; c < colMax; c++) newGrid[r][c] = grid[r][c]; } grid = newGrid; this.rows = rows; this.columns = columns; forceRedraw(); } /** * Return the number of rows of rectangles in the grid. */ public int getRowCount() { return rows; } /** * Return the number of columns of rectangles in the grid. */ public int getColumnCount() { return columns; } //------------------ other useful public methods --------------------- /** * Get the color which has been set for the rectangle in the specified * row and column of the grid. This value can be null if no * color has been set for that rectangle. (Such rectangles are * actually displayed using the defaultColor.) If the specified * rectangle is outside the grid, then null is returned. */ public Color getColor(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns) return grid[row][col]; else return null; } /** * Return the red component of color of the rectangle in the * specified row and column, as a double value in the range * 0.0 to 1.0. If the specified rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the red component of the defaultColor is returned. */ public double getRed(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getRed(); else return defaultColor.getRed(); } /** * Return the green component of color of the rectangle in the * specified row and column, as a double value in the range * 0.0 to 1.0. If the specified rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the green component of the defaultColor is returned. */ public double getGreen(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getGreen(); else return defaultColor.getGreen(); } /** * Return the blue component of color of the rectangle in the * specified row and column, as a double value in the range * 0.0 to 1.0. If the specified rectangle lies outside * the grid or if no color has been specified for the rectangle, * then the blue component of the defaultColor is returned. */ public double getBlue(int row, int col) { if (row >=0 && row < rows && col >= 0 && col < columns && grid[row][col] != null) return grid[row][col].getBlue(); else return defaultColor.getBlue(); } /** * Set the color of the rectangle in the specified row and column. * If the rectangle lies outside the grid, this is simply ignored. * The color can be null. Rectangles for which the color is null * will be displayed in the defaultColor, and they will always be shown * as flat rather than 3D rects. */ public void setColor(int row, int col, Color c) { if (row >=0 && row < rows && col >= 0 && col < columns) { grid[row][col] = c; drawSquare(row,col); } } /** * Set the color of the rectangle in the specified row and column, * where the RGB color components are given as double values in * the range 0.0 to 1.0. Values are clamped to lie in that range. * If the rectangle lies outside the grid, this is simply ignored. */ public void setColor(int row, int col, double red, double green, double blue) { if (row >=0 && row < rows && col >= 0 && col < columns) { red = (red < 0)? 0 : ( (red > 1)? 1 : red ); green = (green < 0)? 0 : ( (green > 1)? 1 : green ); blue = (blue < 0)? 0 : ( (blue > 1)? 1 : blue ); grid[row][col] = Color.color(red,green,blue); drawSquare(row,col); } } /** * Set the color of the rectangle in the specified row and column. * The color is specified by giving hue, saturation, and brightness * components of the color. The hue should be in the range 0.0 to 360.0, * and the saturation and brightness should be in the range 0.0. to 1.0. * Their values are clamped to lie in those ranges. * If the rectangle lies outside the grid, this is simply ignored. */ public void setHSBColor(int row, int col, double hue, double saturation, double brightness) { if (row >=0 && row < rows && col >= 0 && col < columns) { hue = (hue < 0)? 0 : ( (hue > 360)? 360 : hue ); saturation = (saturation < 0)? 0 : ( (saturation > 1)? 1 : saturation ); brightness = (brightness < 0)? 0 : ( (brightness > 1)? 1 : brightness ); grid[row][col] = Color.hsb(hue,saturation,brightness); drawSquare(row,col); } } /** * Set all rectangles of the grid to have the specified color. * The color can be null. In that case, the rectangles are * drawn as flat rather than 3D rects in the defaultColor. */ public void fill(Color c) { for (int i = 0; i < rows; i++) for (int j = 0; j < columns; j++) grid[i][j] = c; forceRedraw(); } /** * Set all rectangles of the grid to have the color specified by * the given red, green, and blue components. These components * should be double values in the range 0 to 1 and are clamped to lie * in that range. */ public void fill(double red, double green, double blue) { red = (red < 0)? 0 : ( (red > 1)? 1 : red ); green = (green < 0)? 0 : ( (green > 1)? 1 : green ); blue = (blue < 0)? 0 : ( (blue > 1)? 1 : blue ); fill(Color.color(red,green,blue)); } /** * Fill all the rectangles with randomly selected colors. */ public void fillRandomly() { for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { grid[i][j] = Color.color(Math.random(),Math.random(),Math.random()); } } forceRedraw(); } /** * Clear the mosaic by setting all the colors to null. */ public void clear() { fill(null); } /** * Returns the current value of the autopaint property. * @see #setAutopaint(boolean) */ public boolean getAutopaint() { return autopaint; } /** * Sets the value of the autopaint property. When this property is true, * then every call to one of the setColor methods automatically results in * repainting that square on the screen. When it is desired to avoid this * immediate repaint -- for example, during a long sequence of setColors * that will all show up at once -- then the value of the autopaint property * can be set to false. When the value is false, color changes are recorded * in the data for the mosaic but are not made on the screen. When the * autopaint property is reset to true, the changes are applied and the * entire mosaic is repainted. The default value of this property is * true. *
Note that clearing or filling the mosaic will cause an immediate * screen update, even if autopaint is false. */ public void setAutopaint(boolean autopaint) { if (this.autopaint == autopaint) return; this.autopaint = autopaint; if (autopaint) forceRedraw(); } /** * This method can be called to force redrawing of the entire mosaic. The only * time it might be necessary for users of this class to call this method is * while the autopaint property is set to false, and it is desired to show * all the changes that have been made to the mosaic, without resetting * the autopaint property to true. * @see #setAutopaint(boolean) */ final public void forceRedraw() { drawAllSquares(); } /** * Return an object that contains the color data that * is needed to redraw the mosaic. This includes the * defaultColor, the groutingColor, the number of rows and * columns, the color of each rectangle, and the * value of alwaysDrawGrouting. */ public Object copyColorData() { // Note: This is a fudge. The data about defaultColor, // groutingColor, alwaysDrawGrouting, and use3D are added to the // last row of the grid. boolean values are represented by a // null for false and a non-null value for true. Color[][] copy = new Color[rows][columns]; // Replace the last row with a longer row. copy[rows-1] = new Color[columns+4]; for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) copy[r][c] = grid[r][c]; copy[rows-1][columns] = defaultColor; copy[rows-1][columns+1] = groutingColor; copy[rows-1][columns+2] = (alwaysDrawGrouting? Color.BLACK : null); copy[rows-1][columns+3] = (use3D? Color.BLACK : null); return copy; } /** * The parameter to this method should be an Object that * was created by the copyColorData() method. This method * will restore the data in the object to the grid. This * can change the size of the grid, the colors in the grid, * the defaultColor, the groutingColor, and the value of * alwaysDrawGrouting. If the object is of the proper * form, then the return value is true. If not, the return * value is false and no changes are made to the current data. */ public boolean restoreColorData(Object data) { if (data == null || !(data instanceof Color[][])) return false; Color[][] newGrid = (Color[][])data; int newRows = newGrid.length; if (newRows == 0 || newGrid[0].length == 0) return false; int newColumns = newGrid[0].length; for (int r = 1; r < newRows-1; r++) if (newGrid[r].length != newColumns) return false; if (newGrid[newRows-1].length != newColumns+4) return false; if (newGrid[newRows-1][newColumns] == null) return false; rows = newRows; columns = newColumns; grid = new Color[rows][columns]; for (int i = 0; i < rows; i++) for (int j = 0; j < columns; j++) grid[i][j] = newGrid[i][j]; defaultColor = newGrid[newRows-1][newColumns]; groutingColor = newGrid[newRows-1][newColumns+1]; alwaysDrawGrouting = newGrid[newRows-1][newColumns+2] != null; use3D = newGrid[newRows-1][newColumns+3] != null; forceRedraw(); return true; } /** * Given an x-coordinate of a pixel in the MosaicPanel, this method returns * the row number of the mosaic rectangle that contains that pixel. If * the x-coordinate does not lie within the bounds of the mosaic, the return * value is -1 or is equal to the number of columns, depending on whether * x is to the left or to the right of the mosaic. */ public int xCoordToColumnNumber(double x) { if (x < 0) return -1; double colWidth = getWidth() / columns; int col = (int)( x / colWidth); if (col >= columns) return columns; else return col; } /** * Given a y-coordinate of a pixel in the MosaicPanel, this method returns * the column number of the mosaic rectangle that contains that pixel. If * the y-coordinate does not lie within the bounds of the mosaic, the return * value is -1 or is equal to the number of rows, depending on whether * y is above or below the mosaic. */ public int yCoordToRowNumber(double y) { if (y < 0) return -1; double rowHeight = getHeight() / rows; int row = (int)(y / rowHeight); if (row >= rows) return rows; else return row; } // private implementation section -- the only part that actually draws squares private void drawSquare(int row, int col) { if ( autopaint ) { if (Platform.isFxApplicationThread()) { drawOneSquare(row,col); } else { Platform.runLater( () -> drawOneSquare(row,col) ); } try { // to avoid overwhelming the application thread with draw operations... Thread.sleep(1); } catch (InterruptedException e) { } } } private void drawAllSquares() { if (Platform.isFxApplicationThread()) { for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) drawOneSquare(r,c); } else { Platform.runLater( () -> { for (int r = 0; r < rows; r++) for (int c = 0; c < columns; c++) drawOneSquare(r,c); } ); } try { // to avoid overwhelming the application thread with draw operations... Thread.sleep(1); } catch (InterruptedException e) { } } private void drawOneSquare(int row, int col) { // only called from two previous methods double rowHeight = getHeight() / rows; double colWidth = getWidth() / columns; int y = (int)Math.round(rowHeight*row); int h = Math.max(1, (int)Math.round(rowHeight*(row+1)) - y); int x = (int)Math.round(colWidth*col); int w = Math.max(1, (int)Math.round(colWidth*(col+1)) - x); Color c = grid[row][col]; g.setFill( (c == null)? defaultColor : c ); if (groutingColor == null || (c == null && !alwaysDrawGrouting)) { if (!use3D || c == null) g.fillRect(x,y,w,h); else fill3DRect(c,x,y,w,h); } else { if (!use3D || c == null) g.fillRect(x+1,y+1,w-2,h-2); else fill3DRect(c,x+1,y+1,w-2,h-2); g.setStroke(groutingColor); g.strokeRect(x+0.5,y+0.5,w-1,h-1); } } private void fill3DRect(Color color, int x, int y, int width, int height) { double h = color.getHue(); double b = color.getBrightness(); double s = color.getSaturation(); if (b > 0.8) { b = 0.8; g.setFill(Color.hsb(h,s,b)); } else if (b < 0.2) { b = 0.2; g.setFill(Color.hsb(h,s,b)); } g.fillRect(x,y,width,height); g.setStroke(Color.hsb(h,s,b+0.2)); g.strokeLine(x+0.5,y+0.5,x+width-0.5,y+0.5); g.strokeLine(x+0.5,y+0.5,x+0.5,y+height-0.5); g.setStroke(Color.hsb(h,s,b-0.2)); g.strokeLine(x+width-0.5,y+1.5,x+width-0.5,y+height-0.5); g.strokeLine(x+1.5,y+height-0.5,x+width-0.5,y+height-0.5); } } // end class MosaicCanvas