/* * @(#)ColorPickerPanel.java * * $Date: 2012-07-03 01:10:05 -0500 (Tue, 03 Jul 2012) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * http://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package ale.util.colors.bric.swing; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.awt.image.BufferedImage; import java.util.Vector; import javax.swing.JPanel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import javax.swing.event.MouseInputListener; import ale.util.colors.bric.plaf.PlafPaintUtils; /** * This is the large graphic element in the <code>ColorPicker</code> that depicts a wide range of colors. * <P> * This panel can operate in 6 different modes. In each mode a different property is held constant: hue, saturation, * brightness, red, green, or blue. (Each property is identified with a constant in the <code>ColorPicker</code> class, * such as: <code>ColorPicker.HUE</code> or <code>ColorPicker.GREEN</code>.) * <P> * In saturation and brightness mode, a wheel is used. Although it doesn't use as many pixels as a square does: it is a * very aesthetic model since the hue can wrap around in a complete circle. (Also, on top of looks, this is how most * people learn to think the color spectrum, so it has that advantage, too). In all other modes a square is used. * <P> * The user can click in this panel to select a new color. The selected color is highlighted with a circle drawn around * it. Also once this component has the keyboard focus, the user can use the arrow keys to traverse the available * colors. * <P> * Note this component is public and exists independently of the <code>ColorPicker</code> class. The only way this class * is dependent on the <code>ColorPicker</code> class is when the constants for the modes are used. * <P> * The graphic in this panel will be based on either the width or the height of this component: depending on which is * smaller. * */ public class ColorPickerPanel extends JPanel { private static final long serialVersionUID = 1L; /** * The maximum size the graphic will be. No matter how big the panel becomes, the graphic will not exceed this * length. * <P> * (This is enforced because only 1 BufferedImage is used to render the graphic. This image is created once at a * fixed size and is never replaced.) */ public static final int MAX_SIZE = 325; /** This controls how the colors are displayed. */ private int mode = ColorPicker.BRI; /** The point used to indicate the selected color. */ private Point point = new Point(0, 0); private Vector<ChangeListener> changeListeners = new Vector<ChangeListener>(); /* * Floats from [0,1]. They must be kept distinct, because when you convert them to RGB coordinates HSB(0,0,0) and * HSB (.5,0,0) and then convert them back to HSB coordinates, the hue always shifts back to zero. */ float hue = -1, sat = -1, bri = -1; int red = -1, green = -1, blue = -1; MouseInputListener mouseListener = new MouseInputAdapter() { @Override public void mousePressed(MouseEvent e) { requestFocus(); Point p = e.getPoint(); if ((ColorPickerPanel.this.mode == ColorPicker.BRI) || (ColorPickerPanel.this.mode == ColorPicker.SAT) || (ColorPickerPanel.this.mode == ColorPicker.HUE)) { float[] hsb = getHSB(p); setHSB(hsb[0], hsb[1], hsb[2]); } else { int[] rgb = getRGB(p); setRGB(rgb[0], rgb[1], rgb[2]); } } @Override public void mouseDragged(MouseEvent e) { mousePressed(e); } }; KeyListener keyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int dx = 0; int dy = 0; if (e.getKeyCode() == KeyEvent.VK_LEFT) { dx = -1; } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { dx = 1; } else if (e.getKeyCode() == KeyEvent.VK_UP) { dy = -1; } else if (e.getKeyCode() == KeyEvent.VK_DOWN) { dy = 1; } int multiplier = 1; if (e.isShiftDown() && e.isAltDown()) { multiplier = 10; } else if (e.isShiftDown() || e.isAltDown()) { multiplier = 5; } if ((dx != 0) || (dy != 0)) { int size = Math.min(MAX_SIZE, Math.min(getWidth() - ColorPickerPanel.this.imagePadding.left - ColorPickerPanel.this.imagePadding.right, getHeight() - ColorPickerPanel.this.imagePadding.top - ColorPickerPanel.this.imagePadding.bottom)); int offsetX = (getWidth() / 2) - (size / 2); int offsetY = (getHeight() / 2) - (size / 2); ColorPickerPanel.this.mouseListener.mousePressed(new MouseEvent(ColorPickerPanel.this, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), 0, ColorPickerPanel.this.point.x + (multiplier * dx) + offsetX, ColorPickerPanel.this.point.y + (multiplier * dy) + offsetY, 1, false )); } } }; FocusListener focusListener = new FocusListener() { @Override public void focusGained(FocusEvent e) { repaint(); } @Override public void focusLost(FocusEvent e) { repaint(); } }; ComponentListener componentListener = new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { regeneratePoint(); regenerateImage(); } }; BufferedImage image = new BufferedImage(MAX_SIZE, MAX_SIZE, BufferedImage.TYPE_INT_ARGB); /** Creates a new <code>ColorPickerPanel</code> */ public ColorPickerPanel() { setMaximumSize(new Dimension(MAX_SIZE + this.imagePadding.left + this.imagePadding.right, MAX_SIZE + this.imagePadding.top + this.imagePadding.bottom)); setPreferredSize(new Dimension((int) (MAX_SIZE * .75), (int) (MAX_SIZE * .75))); setRGB(0, 0, 0); addMouseListener(this.mouseListener); addMouseMotionListener(this.mouseListener); setFocusable(true); addKeyListener(this.keyListener); addFocusListener(this.focusListener); setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); addComponentListener(this.componentListener); } /** * This listener will be notified when the current HSB or RGB values change. */ public void addChangeListener(ChangeListener l) { if (this.changeListeners.contains(l)) { return; } this.changeListeners.add(l); } /** * Remove a <code>ChangeListener</code> so it is no longer notified when the selected color changes. */ public void removeChangeListener(ChangeListener l) { this.changeListeners.remove(l); } protected void fireChangeListeners() { if (this.changeListeners == null) { return; } for (int a = 0; a < this.changeListeners.size(); a++) { ChangeListener l = this.changeListeners.get(a); try { l.stateChanged(new ChangeEvent(this)); } catch (RuntimeException e) { e.printStackTrace(); } } } Insets imagePadding = new Insets(6, 6, 6, 6); @Override public void paint(Graphics g) { super.paint(g); Graphics2D g2 = (Graphics2D) g; int size = Math.min( MAX_SIZE, Math.min(getWidth() - this.imagePadding.left - this.imagePadding.right, getHeight() - this.imagePadding.top - this.imagePadding.bottom)); g2.translate((getWidth() / 2) - (size / 2), (getHeight() / 2) - (size / 2)); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Shape shape; if ((this.mode == ColorPicker.SAT) || (this.mode == ColorPicker.BRI)) { shape = new Ellipse2D.Float(0, 0, size, size); } else { Rectangle r = new Rectangle(0, 0, size, size); shape = r; } if (hasFocus()) { PlafPaintUtils.paintFocus(g2, shape, 3); } if (!(shape instanceof Rectangle)) { // paint a circular shadow g2.translate(2, 2); g2.setColor(new Color(0, 0, 0, 20)); g2.fill(new Ellipse2D.Float(-2, -2, size + 4, size + 4)); g2.setColor(new Color(0, 0, 0, 40)); g2.fill(new Ellipse2D.Float(-1, -1, size + 2, size + 2)); g2.setColor(new Color(0, 0, 0, 80)); g2.fill(new Ellipse2D.Float(0, 0, size, size)); g2.translate(-2, -2); } g2.drawImage(this.image, 0, 0, size, size, 0, 0, size, size, null); g2.setStroke(new BasicStroke(1)); if (shape instanceof Rectangle) { Rectangle r = (Rectangle) shape; PlafPaintUtils.drawBevel(g2, r); } else { g2.setColor(new Color(0, 0, 0, 120)); g2.draw(shape); } g2.setColor(Color.white); g2.setStroke(new BasicStroke(1)); g2.draw(new Ellipse2D.Float(this.point.x - 3, this.point.y - 3, 6, 6)); g2.setColor(Color.black); g2.draw(new Ellipse2D.Float(this.point.x - 4, this.point.y - 4, 8, 8)); g.translate(-this.imagePadding.left, -this.imagePadding.top); } /** * Set the mode of this panel. * * @param mode * This must be one of the following constants from the <code>ColorPicker</code> class: <code>HUE</code>, * <code>SAT</code>, <code>BRI</code>, <code>RED</code>, <code>GREEN</code>, or <code>BLUE</code> */ public void setMode(int mode) { if (!((mode == ColorPicker.HUE) || (mode == ColorPicker.SAT) || (mode == ColorPicker.BRI) || (mode == ColorPicker.RED) || (mode == ColorPicker.GREEN) || (mode == ColorPicker.BLUE))) { throw new IllegalArgumentException("The mode must be HUE, SAT, BRI, RED, GREEN, or BLUE."); } if (this.mode == mode) { return; } this.mode = mode; regenerateImage(); regeneratePoint(); } /** * Sets the selected color of this panel. * <P> * If this panel is in HUE, SAT, or BRI mode, then this method converts these values to HSB coordinates and calls * <code>setHSB</code>. * <P> * This method may regenerate the graphic if necessary. * * @param r * the red value of the selected color. * @param g * the green value of the selected color. * @param b * the blue value of the selected color. */ public void setRGB(int r, int g, int b) { if ((r < 0) || (r > 255)) { throw new IllegalArgumentException("The red value (" + r + ") must be between [0,255]."); } if ((g < 0) || (g > 255)) { throw new IllegalArgumentException("The green value (" + g + ") must be between [0,255]."); } if ((b < 0) || (b > 255)) { throw new IllegalArgumentException("The blue value (" + b + ") must be between [0,255]."); } if ((this.red != r) || (this.green != g) || (this.blue != b)) { if ((this.mode == ColorPicker.RED) || (this.mode == ColorPicker.GREEN) || (this.mode == ColorPicker.BLUE)) { int lastR = this.red; int lastG = this.green; int lastB = this.blue; this.red = r; this.green = g; this.blue = b; if (this.mode == ColorPicker.RED) { if (lastR != r) { regenerateImage(); } } else if (this.mode == ColorPicker.GREEN) { if (lastG != g) { regenerateImage(); } } else if (this.mode == ColorPicker.BLUE) { if (lastB != b) { regenerateImage(); } } } else { float[] hsb = new float[3]; Color.RGBtoHSB(r, g, b, hsb); setHSB(hsb[0], hsb[1], hsb[2]); return; } regeneratePoint(); repaint(); fireChangeListeners(); } } /** * @return the HSB values of the selected color. Each value is between [0,1]. */ public float[] getHSB() { return new float[] { this.hue, this.sat, this.bri }; } /** * @return the RGB values of the selected color. Each value is between [0,255]. */ public int[] getRGB() { return new int[] { this.red, this.green, this.blue }; } /** * Returns the color at the indicated point in HSB values. * * @param p * a point relative to this panel. * @return the HSB values at the point provided. */ public float[] getHSB(Point p) { if ((this.mode == ColorPicker.RED) || (this.mode == ColorPicker.GREEN) || (this.mode == ColorPicker.BLUE)) { int[] rgb = getRGB(p); float[] hsb = Color.RGBtoHSB(rgb[0], rgb[1], rgb[2], null); return hsb; } int size = Math.min( MAX_SIZE, Math.min(getWidth() - this.imagePadding.left - this.imagePadding.right, getHeight() - this.imagePadding.top - this.imagePadding.bottom)); p.translate(-((getWidth() / 2) - (size / 2)), -((getHeight() / 2) - (size / 2))); if ((this.mode == ColorPicker.BRI) || (this.mode == ColorPicker.SAT)) { // the two circular views: double radius = (size) / 2.0; double x = p.getX() - (size / 2.0); double y = p.getY() - (size / 2.0); double r = Math.sqrt((x * x) + (y * y)) / radius; double theta = Math.atan2(y, x) / (Math.PI * 2.0); if (r > 1) { r = 1; } if (this.mode == ColorPicker.BRI) { return new float[] { (float) (theta + .25f), (float) (r), this.bri }; } else { return new float[] { (float) (theta + .25f), this.sat, (float) (r) }; } } else { float s = ((float) p.x) / ((float) size); float b = ((float) p.y) / ((float) size); if (s < 0) { s = 0; } if (s > 1) { s = 1; } if (b < 0) { b = 0; } if (b > 1) { b = 1; } return new float[] { this.hue, s, b }; } } /** * Returns the color at the indicated point in RGB values. * * @param p * a point relative to this panel. * @return the RGB values at the point provided. */ public int[] getRGB(Point p) { if ((this.mode == ColorPicker.BRI) || (this.mode == ColorPicker.SAT) || (this.mode == ColorPicker.HUE)) { float[] hsb = getHSB(p); int rgb = Color.HSBtoRGB(hsb[0], hsb[1], hsb[2]); int r = (rgb & 0xff0000) >> 16; int g = (rgb & 0xff00) >> 8; int b = (rgb & 0xff); return new int[] { r, g, b }; } int size = Math.min( MAX_SIZE, Math.min(getWidth() - this.imagePadding.left - this.imagePadding.right, getHeight() - this.imagePadding.top - this.imagePadding.bottom)); p.translate(-((getWidth() / 2) - (size / 2)), -((getHeight() / 2) - (size / 2))); int x2 = (p.x * 255) / size; int y2 = (p.y * 255) / size; if (x2 < 0) { x2 = 0; } if (x2 > 255) { x2 = 255; } if (y2 < 0) { y2 = 0; } if (y2 > 255) { y2 = 255; } if (this.mode == ColorPicker.RED) { return new int[] { this.red, x2, y2 }; } else if (this.mode == ColorPicker.GREEN) { return new int[] { x2, this.green, y2 }; } else { return new int[] { x2, y2, this.blue }; } } /** * Sets the selected color of this panel. * <P> * If this panel is in RED, GREEN, or BLUE mode, then this method converts these values to RGB coordinates and calls * <code>setRGB</code>. * <P> * This method may regenerate the graphic if necessary. * * @param h * the hue value of the selected color. * @param s * the saturation value of the selected color. * @param b * the brightness value of the selected color. */ public void setHSB(float h, float s, float b) { // hue is cyclic: it can be any value h = (float) (h - Math.floor(h)); if ((s < 0) || (s > 1)) { throw new IllegalArgumentException("The saturation value (" + s + ") must be between [0,1]"); } if ((b < 0) || (b > 1)) { throw new IllegalArgumentException("The brightness value (" + b + ") must be between [0,1]"); } if ((this.hue != h) || (this.sat != s) || (this.bri != b)) { if ((this.mode == ColorPicker.HUE) || (this.mode == ColorPicker.BRI) || (this.mode == ColorPicker.SAT)) { float lastHue = this.hue; float lastBri = this.bri; float lastSat = this.sat; this.hue = h; this.sat = s; this.bri = b; if (this.mode == ColorPicker.HUE) { if (lastHue != this.hue) { regenerateImage(); } } else if (this.mode == ColorPicker.SAT) { if (lastSat != this.sat) { regenerateImage(); } } else if (this.mode == ColorPicker.BRI) { if (lastBri != this.bri) { regenerateImage(); } } } else { Color c = new Color(Color.HSBtoRGB(h, s, b)); setRGB(c.getRed(), c.getGreen(), c.getBlue()); return; } Color c = new Color(Color.HSBtoRGB(this.hue, this.sat, this.bri)); this.red = c.getRed(); this.green = c.getGreen(); this.blue = c.getBlue(); regeneratePoint(); repaint(); fireChangeListeners(); } } /** Recalculates the (x,y) point used to indicate the selected color. */ private void regeneratePoint() { int size = Math.min( MAX_SIZE, Math.min(getWidth() - this.imagePadding.left - this.imagePadding.right, getHeight() - this.imagePadding.top - this.imagePadding.bottom)); if ((this.mode == ColorPicker.HUE) || (this.mode == ColorPicker.SAT) || (this.mode == ColorPicker.BRI)) { if (this.mode == ColorPicker.HUE) { this.point = new Point((int) ((this.sat * size) + .5), (int) ((this.bri * size) + .5)); } else if (this.mode == ColorPicker.SAT) { double theta = (this.hue * 2 * Math.PI) - (Math.PI / 2); if (theta < 0) { theta += 2 * Math.PI; } double r = (this.bri * size) / 2; this.point = new Point((int) ((r * Math.cos(theta)) + .5 + (size / 2.0)), (int) ((r * Math.sin(theta)) + .5 + (size / 2.0))); } else if (this.mode == ColorPicker.BRI) { double theta = (this.hue * 2 * Math.PI) - (Math.PI / 2); if (theta < 0) { theta += 2 * Math.PI; } double r = (this.sat * size) / 2; this.point = new Point((int) ((r * Math.cos(theta)) + .5 + (size / 2.0)), (int) ((r * Math.sin(theta)) + .5 + (size / 2.0))); } } else if (this.mode == ColorPicker.RED) { this.point = new Point((int) (((this.green * size) / 255f) + .49f), (int) (((this.blue * size) / 255f) + .49f)); } else if (this.mode == ColorPicker.GREEN) { this.point = new Point((int) (((this.red * size) / 255f) + .49f), (int) (((this.blue * size) / 255f) + .49f)); } else if (this.mode == ColorPicker.BLUE) { this.point = new Point((int) (((this.red * size) / 255f) + .49f), (int) (((this.green * size) / 255f) + .49f)); } } /** A row of pixel data we recycle every time we regenerate this image. */ private int[] row = new int[MAX_SIZE]; /** Regenerates the image. */ private synchronized void regenerateImage() { int size = Math.min( MAX_SIZE, Math.min(getWidth() - this.imagePadding.left - this.imagePadding.right, getHeight() - this.imagePadding.top - this.imagePadding.bottom)); if ((this.mode == ColorPicker.BRI) || (this.mode == ColorPicker.SAT)) { float bri2 = this.bri; float sat2 = this.sat; float radius = (size) / 2f; float hue2; float k = 1.2f; // the number of pixels to antialias for (int y = 0; y < size; y++) { float y2 = (y - (size / 2f)); for (int x = 0; x < size; x++) { float x2 = (x - (size / 2f)); double theta = Math.atan2(y2, x2) - ((3 * Math.PI) / 2.0); if (theta < 0) { theta += 2 * Math.PI; } double r = Math.sqrt((x2 * x2) + (y2 * y2)); if (r <= radius) { if (this.mode == ColorPicker.BRI) { hue2 = (float) (theta / (2 * Math.PI)); sat2 = (float) (r / radius); } else { // SAT hue2 = (float) (theta / (2 * Math.PI)); bri2 = (float) (r / radius); } this.row[x] = Color.HSBtoRGB(hue2, sat2, bri2); if (r > (radius - k)) { int alpha = (int) (255 - ((255 * ((r - radius) + k)) / k)); if (alpha < 0) { alpha = 0; } if (alpha > 255) { alpha = 255; } this.row[x] = this.row[x] & (0xffffff + (alpha << 24)); } } else { this.row[x] = 0x00000000; } } this.image.getRaster().setDataElements(0, y, size, 1, this.row); } } else if (this.mode == ColorPicker.HUE) { float hue2 = this.hue; for (int y = 0; y < size; y++) { float y2 = ((float) y) / ((float) size); for (int x = 0; x < size; x++) { float x2 = ((float) x) / ((float) size); this.row[x] = Color.HSBtoRGB(hue2, x2, y2); } this.image.getRaster().setDataElements(0, y, this.image.getWidth(), 1, this.row); } } else { // mode is RED, GREEN, or BLUE int red2 = this.red; int green2 = this.green; int blue2 = this.blue; for (int y = 0; y < size; y++) { float y2 = ((float) y) / ((float) size); for (int x = 0; x < size; x++) { float x2 = ((float) x) / ((float) size); if (this.mode == ColorPicker.RED) { green2 = (int) ((x2 * 255) + .49); blue2 = (int) ((y2 * 255) + .49); } else if (this.mode == ColorPicker.GREEN) { red2 = (int) ((x2 * 255) + .49); blue2 = (int) ((y2 * 255) + .49); } else { red2 = (int) ((x2 * 255) + .49); green2 = (int) ((y2 * 255) + .49); } this.row[x] = 0xFF000000 + (red2 << 16) + (green2 << 8) + blue2; } this.image.getRaster().setDataElements(0, y, size, 1, this.row); } } repaint(); } }