001 /* 002 * @(#)ColorPickerPanel.java 1.0 2008-03-01 003 * 004 * Copyright (c) 2008 Jeremy Wood 005 * E-mail: mickleness@gmail.com 006 * All rights reserved. 007 * 008 * The copyright of this software is owned by Jeremy Wood. 009 * You may not use, copy or modify this software, except in 010 * accordance with the license agreement you entered into with 011 * Jeremy Wood. For details see accompanying license terms. 012 */ 013 014 package com.colorpicker.swing; 015 016 import java.util.*; 017 import javax.swing.event.*; 018 import java.awt.*; 019 import javax.swing.*; 020 import java.awt.event.*; 021 import java.awt.geom.*; 022 import java.awt.image.*; 023 import com.colorpicker.awt.*; 024 025 /** This is the large graphic element in the <code>ColorPicker</code> 026 * that depicts a wide range of colors. 027 * <P>This panel can operate in 6 different modes. In each mode a different 028 * property is held constant: hue, saturation, brightness, red, green, or blue. 029 * (Each property is identified with a constant in the <code>ColorPicker</code> class, 030 * such as: <code>ColorPicker.HUE</code> or <code>ColorPicker.GREEN</code>.) 031 * <P>In saturation and brightness mode, a wheel is used. Although it doesn't 032 * use as many pixels as a square does: it is a very aesthetic model since the hue can 033 * wrap around in a complete circle. (Also, on top of looks, this is how most 034 * people learn to think the color spectrum, so it has that advantage, too). 035 * In all other modes a square is used. 036 * <P>The user can click in this panel to select a new color. The selected color is 037 * highlighted with a circle drawn around it. Also once this 038 * component has the keyboard focus, the user can use the arrow keys to 039 * traverse the available colors. 040 * <P>Note this component is public and exists independently of the 041 * <code>ColorPicker</code> class. The only way this class is dependent 042 * on the <code>ColorPicker</code> class is when the constants for the modes 043 * are used. 044 * <P>The graphic in this panel will be based on either the width or 045 * the height of this component: depending on which is smaller. 046 * 047 * @version 1.0 048 * @author Jeremy Wood 049 */ 050 public class ColorPickerPanel extends JPanel { 051 private static final long serialVersionUID = 1L; 052 053 /** The maximum size the graphic will be. No matter 054 * how big the panel becomes, the graphic will not exceed 055 * this length. 056 * <P>(This is enforced because only 1 BufferedImage is used 057 * to render the graphic. This image is created once at a fixed 058 * size and is never replaced.) 059 */ 060 public static int MAX_SIZE = 325; 061 private int mode = ColorPicker.BRI; 062 private Point point = new Point(0,0); 063 private Vector changeListeners; 064 065 /* Floats from [0,1]. They must be kept distinct, because 066 * when you convert them to RGB coordinates HSB(0,0,0) and HSB (.5,0,0) 067 * and then convert them back to HSB coordinates, the hue always shifts back to zero. 068 */ 069 float hue = -1, sat = -1, bri = -1; 070 int red = -1, green = -1, blue = -1; 071 072 MouseInputListener mouseListener = new MouseInputAdapter() { 073 public void mousePressed(MouseEvent e) { 074 requestFocus(); 075 Point p = e.getPoint(); 076 int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom)); 077 p.translate(-(getWidth()/2-size/2), -(getHeight()/2-size/2)); 078 if(mode==ColorPicker.BRI || mode==ColorPicker.SAT) { 079 //the two circular views: 080 double radius = ((double)size)/2.0; 081 double x = p.getX()-size/2.0; 082 double y = p.getY()-size/2.0; 083 double r = Math.sqrt(x*x+y*y)/radius; 084 double theta = Math.atan2(y,x)/(Math.PI*2.0); 085 086 if(r>1) r = 1; 087 088 if(mode==ColorPicker.BRI) { 089 setHSB((float)(theta+.25f), 090 (float)(r), 091 bri); 092 } else { 093 setHSB((float)(theta+.25f), 094 sat, 095 (float)(r) ); 096 } 097 } else if(mode==ColorPicker.HUE) { 098 float s = ((float)p.x)/((float)size); 099 float b = ((float)p.y)/((float)size); 100 if(s<0) s = 0; 101 if(s>1) s = 1; 102 if(b<0) b = 0; 103 if(b>1) b = 1; 104 setHSB( hue, 105 s, 106 b ); 107 } else { 108 int x2 = p.x*255/size; 109 int y2 = p.y*255/size; 110 if(x2<0) x2 = 0; 111 if(x2>255) x2 = 255; 112 if(y2<0) y2 = 0; 113 if(y2>255) y2 = 255; 114 115 if(mode==ColorPicker.RED) { 116 setRGB(red,x2,y2); 117 } else if(mode==ColorPicker.GREEN) { 118 setRGB(x2,green,y2); 119 } else { 120 setRGB(x2,y2,blue); 121 } 122 } 123 } 124 125 public void mouseDragged(MouseEvent e) { 126 mousePressed(e); 127 } 128 }; 129 130 KeyListener keyListener = new KeyAdapter() { 131 public void keyPressed(KeyEvent e) { 132 int dx = 0; 133 int dy = 0; 134 if(e.getKeyCode()==KeyEvent.VK_LEFT) { 135 dx = -1; 136 } else if(e.getKeyCode()==KeyEvent.VK_RIGHT) { 137 dx = 1; 138 } else if(e.getKeyCode()==KeyEvent.VK_UP) { 139 dy = -1; 140 } else if(e.getKeyCode()==KeyEvent.VK_DOWN) { 141 dy = 1; 142 } 143 int multiplier = 1; 144 if(e.isShiftDown() && e.isAltDown()) { 145 multiplier = 10; 146 } else if(e.isShiftDown() || e.isAltDown()) { 147 multiplier = 5; 148 } 149 if(dx!=0 || dy!=0) { 150 int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom)); 151 152 int offsetX = getWidth()/2-size/2; 153 int offsetY = getHeight()/2-size/2; 154 mouseListener.mousePressed(new MouseEvent(ColorPickerPanel.this, 155 MouseEvent.MOUSE_PRESSED, 156 System.currentTimeMillis(), 0, 157 point.x+multiplier*dx+offsetX, 158 point.y+multiplier*dy+offsetY, 159 1, false 160 )); 161 } 162 } 163 }; 164 165 FocusListener focusListener = new FocusListener() { 166 public void focusGained(FocusEvent e) { 167 repaint(); 168 } 169 public void focusLost(FocusEvent e) { 170 repaint(); 171 } 172 }; 173 174 ComponentListener componentListener = new ComponentAdapter() { 175 176 public void componentResized(ComponentEvent e) { 177 178 regeneratePoint(); 179 regenerateImage(); 180 } 181 182 }; 183 184 BufferedImage image = new BufferedImage(MAX_SIZE, MAX_SIZE, BufferedImage.TYPE_INT_ARGB); 185 186 /** Creates a new <code>ColorPickerPanel</code> */ 187 public ColorPickerPanel() { 188 setMaximumSize(new Dimension(MAX_SIZE+imagePadding.left+imagePadding.right, 189 MAX_SIZE+imagePadding.top+imagePadding.bottom)); 190 setPreferredSize(new Dimension( (int)(MAX_SIZE*.75), (int)(MAX_SIZE*.75))); 191 192 setRGB(0,0,0); 193 addMouseListener(mouseListener); 194 addMouseMotionListener(mouseListener); 195 196 setFocusable(true); 197 addKeyListener(keyListener); 198 addFocusListener(focusListener); 199 200 setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); 201 addComponentListener(componentListener); 202 } 203 204 /** This listener will be notified when the current HSB or RGB values 205 * change, depending on what mode the user is in. 206 */ 207 public void addChangeListener(ChangeListener l) { 208 if(changeListeners==null) 209 changeListeners = new Vector(); 210 if(changeListeners.contains(l)) 211 return; 212 changeListeners.add(l); 213 } 214 215 /** Remove a <code>ChangeListener</code> so it is no longer 216 * notified when the selected color changes. 217 */ 218 public void removeChangeListener(ChangeListener l) { 219 if(changeListeners==null) 220 return; 221 changeListeners.remove(l); 222 } 223 224 protected void fireChangeListeners() { 225 if(changeListeners==null) 226 return; 227 for(int a = 0; a<changeListeners.size(); a++) { 228 ChangeListener l = (ChangeListener)changeListeners.get(a); 229 try { 230 l.stateChanged(new ChangeEvent(this)); 231 } catch(RuntimeException e) { 232 e.printStackTrace(); 233 } 234 } 235 } 236 237 Insets imagePadding = new Insets(6,6,6,6); 238 239 public void paint(Graphics g) { 240 super.paint(g); 241 242 Graphics2D g2 = (Graphics2D)g; 243 int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom)); 244 245 g2.translate(getWidth()/2-size/2, getHeight()/2-size/2); 246 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 247 248 Shape shape; 249 250 if(mode==ColorPicker.SAT || mode==ColorPicker.BRI) { 251 shape = new Ellipse2D.Float(0,0,size,size); 252 } else { 253 Rectangle r = new Rectangle(0,0,size,size); 254 shape = r; 255 } 256 257 if(hasFocus()) { 258 PaintUtils.paintFocus(g2,shape,5); 259 } 260 261 if(!(shape instanceof Rectangle)) { 262 //paint a circular shadow 263 g2.translate(2,2); 264 g2.setColor(new Color(0,0,0,20)); 265 g2.fill(new Ellipse2D.Float(-2,-2,size+4,size+4)); 266 g2.setColor(new Color(0,0,0,40)); 267 g2.fill(new Ellipse2D.Float(-1,-1,size+2,size+2)); 268 g2.setColor(new Color(0,0,0,80)); 269 g2.fill(new Ellipse2D.Float(0,0,size,size)); 270 g2.translate(-2,-2); 271 } 272 273 g2.drawImage(image, 0, 0, size, size, 0, 0, size, size, null); 274 275 if(shape instanceof Rectangle) { 276 Rectangle r = (Rectangle)shape; 277 PaintUtils.drawBevel(g2,r); 278 } else { 279 g2.setColor(new Color(0,0,0,120)); 280 g2.draw(shape); 281 } 282 283 g2.setColor(Color.white); 284 g2.setStroke(new BasicStroke(1)); 285 g2.draw(new Ellipse2D.Float(point.x-3,point.y-3,6,6)); 286 g2.setColor(Color.black); 287 g2.draw(new Ellipse2D.Float(point.x-4,point.y-4,8,8)); 288 289 g.translate(-imagePadding.left, -imagePadding.top); 290 } 291 292 /** Set the mode of this panel. 293 * @param mode This must be one of the following constants from the <code>ColorPicker</code> class: 294 * <code>HUE</code>, <code>SAT</code>, <code>BRI</code>, <code>RED</code>, <code>GREEN</code>, or <code>BLUE</code> 295 */ 296 public void setMode(int mode) { 297 if(!(mode==ColorPicker.HUE || mode==ColorPicker.SAT || mode==ColorPicker.BRI || 298 mode==ColorPicker.RED || mode==ColorPicker.GREEN || mode==ColorPicker.BLUE)) 299 throw new IllegalArgumentException("The mode must be HUE, SAT, BRI, RED, GREEN, or BLUE."); 300 301 if(this.mode==mode) 302 return; 303 this.mode = mode; 304 regenerateImage(); 305 regeneratePoint(); 306 } 307 308 /** Sets the selected color of this panel. 309 * <P>If this panel is in HUE, SAT, or BRI mode, then 310 * this method converts these values to HSB coordinates 311 * and calls <code>setHSB</code>. 312 * <P>This method may regenerate the graphic if necessary. 313 * 314 * @param r the red value of the selected color. 315 * @param g the green value of the selected color. 316 * @param b the blue value of the selected color. 317 */ 318 public void setRGB(int r,int g,int b) { 319 320 if(r<0 || r>255) 321 throw new IllegalArgumentException("The red value ("+r+") must be between [0,255]."); 322 if(g<0 || g>255) 323 throw new IllegalArgumentException("The green value ("+g+") must be between [0,255]."); 324 if(b<0 || b>255) 325 throw new IllegalArgumentException("The blue value ("+b+") must be between [0,255]."); 326 327 if(red!=r || green!=g || blue!=b) { 328 if(mode==ColorPicker.RED || 329 mode==ColorPicker.GREEN || 330 mode==ColorPicker.BLUE) { 331 int lastR = red; 332 int lastG = green; 333 int lastB = blue; 334 red = r; 335 green = g; 336 blue = b; 337 338 if(mode==ColorPicker.RED) { 339 if(lastR!=r) { 340 regenerateImage(); 341 } 342 } else if(mode==ColorPicker.GREEN) { 343 if(lastG!=g) { 344 regenerateImage(); 345 } 346 } else if(mode==ColorPicker.BLUE) { 347 if(lastB!=b) { 348 regenerateImage(); 349 } 350 } 351 } else { 352 float[] hsb = new float[3]; 353 Color.RGBtoHSB(r, g, b, hsb); 354 setHSB(hsb[0],hsb[1],hsb[2]); 355 return; 356 } 357 regeneratePoint(); 358 repaint(); 359 fireChangeListeners(); 360 } 361 } 362 363 /** @return the HSB values of the selected color. 364 * Each value is between [0,1]. 365 */ 366 public float[] getHSB() { 367 return new float[] {hue, sat, bri}; 368 } 369 370 /** @return the RGB values of the selected color. 371 * Each value is between [0,255]. 372 */ 373 public int[] getRGB() { 374 return new int[] {red, green, blue}; 375 } 376 377 /** Sets the selected color of this panel. 378 * <P>If this panel is in RED, GREEN, or BLUE mode, then 379 * this method converts these values to RGB coordinates 380 * and calls <code>setRGB</code>. 381 * <P>This method may regenerate the graphic if necessary. 382 * 383 * @param h the hue value of the selected color. 384 * @param s the saturation value of the selected color. 385 * @param b the brightness value of the selected color. 386 */ 387 public void setHSB(float h,float s,float b) { 388 if(Float.isInfinite(h) || Float.isNaN(h)) 389 throw new IllegalArgumentException("The hue value ("+h+") is not a valid number."); 390 //hue is cyclic, so it can be any value: 391 while(h<0) h++; 392 while(h>1) h--; 393 394 if(s<0 || s>1) 395 throw new IllegalArgumentException("The saturation value ("+s+") must be between [0,1]"); 396 if(b<0 || b>1) 397 throw new IllegalArgumentException("The brightness value ("+b+") must be between [0,1]"); 398 399 if(hue!=h || sat!=s || bri!=b) { 400 if(mode==ColorPicker.HUE || 401 mode==ColorPicker.BRI || 402 mode==ColorPicker.SAT) { 403 float lastHue = hue; 404 float lastBri = bri; 405 float lastSat = sat; 406 hue = h; 407 sat = s; 408 bri = b; 409 if(mode==ColorPicker.HUE) { 410 if(lastHue!=hue) { 411 regenerateImage(); 412 } 413 } else if(mode==ColorPicker.SAT) { 414 if(lastSat!=sat) { 415 regenerateImage(); 416 } 417 } else if(mode==ColorPicker.BRI) { 418 if(lastBri!=bri) { 419 regenerateImage(); 420 } 421 } 422 } else { 423 424 Color c = new Color(Color.HSBtoRGB(h, s, b)); 425 setRGB(c.getRed(), c.getGreen(), c.getBlue()); 426 return; 427 } 428 429 430 Color c = new Color(Color.HSBtoRGB(hue, sat, bri)); 431 red = c.getRed(); 432 green = c.getGreen(); 433 blue = c.getBlue(); 434 435 regeneratePoint(); 436 repaint(); 437 fireChangeListeners(); 438 } 439 } 440 441 /** Recalculates the (x,y) point used to indicate the selected color. */ 442 private void regeneratePoint() { 443 int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom)); 444 if(mode==ColorPicker.HUE || mode==ColorPicker.SAT || mode==ColorPicker.BRI) { 445 if(mode==ColorPicker.HUE) { 446 point = new Point((int)(sat*size),(int)(bri*size)); 447 } else if(mode==ColorPicker.SAT) { 448 double theta = hue*2*Math.PI-Math.PI/2; 449 if(theta<0) theta+=2*Math.PI; 450 451 double r = bri*size/2; 452 point = new Point((int)(r*Math.cos(theta)+.5+size/2.0),(int)(r*Math.sin(theta)+.5+size/2.0)); 453 } else if(mode==ColorPicker.BRI) { 454 double theta = hue*2*Math.PI-Math.PI/2; 455 if(theta<0) theta+=2*Math.PI; 456 double r = sat*size/2; 457 point = new Point((int)(r*Math.cos(theta)+.5+size/2.0),(int)(r*Math.sin(theta)+.5+size/2.0)); 458 } 459 } else if(mode==ColorPicker.RED) { 460 point = new Point((int)(green*size/255f+.49f), 461 (int)(blue*size/255f+.49f) ); 462 } else if(mode==ColorPicker.GREEN) { 463 point = new Point((int)(red*size/255f+.49f), 464 (int)(blue*size/255f+.49f) ); 465 } else if(mode==ColorPicker.BLUE) { 466 point = new Point((int)(red*size/255f+.49f), 467 (int)(green*size/255f+.49f) ); 468 } 469 } 470 471 /** A row of pixel data we recycle every time we regenerate this image. */ 472 private int[] row = new int[MAX_SIZE]; 473 /** Regenerates the image. */ 474 private synchronized void regenerateImage() { 475 int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom)); 476 477 if(mode==ColorPicker.BRI || mode==ColorPicker.SAT) { 478 float bri2 = this.bri; 479 float sat2 = this.sat; 480 float radius = ((float)size)/2f; 481 float hue2; 482 float k = 1.2f; //the number of pixels to antialias 483 for(int y = 0; y<size; y++) { 484 float y2 = (y-size/2f); 485 for(int x = 0; x<size; x++) { 486 float x2 = (x-size/2f); 487 double theta = Math.atan2(y2,x2)-3*Math.PI/2.0; 488 if(theta<0) theta+=2*Math.PI; 489 490 double r = Math.sqrt(x2*x2+y2*y2); 491 if(r<=radius) { 492 if(mode==ColorPicker.BRI) { 493 hue2 = (float)(theta/(2*Math.PI)); 494 sat2 = (float)(r/radius); 495 } else { //SAT 496 hue2 = (float)(theta/(2*Math.PI)); 497 bri2 = (float)(r/radius); 498 } 499 row[x] = Color.HSBtoRGB(hue2, sat2, bri2); 500 if(r>radius-k) { 501 int alpha = (int)(255-255*(r-radius+k)/k); 502 if(alpha<0) alpha = 0; 503 if(alpha>255) alpha = 255; 504 row[x] = row[x] & 0xffffff+(alpha << 24); 505 } 506 } else { 507 row[x] = 0x00000000; 508 } 509 } 510 image.getRaster().setDataElements(0, y, size, 1, row); 511 } 512 } else if(mode==ColorPicker.HUE) { 513 float hue2 = this.hue; 514 for(int y = 0; y<size; y++) { 515 float y2 = ((float)y)/((float)size); 516 for(int x = 0; x<size; x++) { 517 float x2 = ((float)x)/((float)size); 518 row[x] = Color.HSBtoRGB(hue2, x2, y2); 519 } 520 image.getRaster().setDataElements(0, y, image.getWidth(), 1, row); 521 } 522 } else { //mode is RED, GREEN, or BLUE 523 int red2 = red; 524 int green2 = green; 525 int blue2 = blue; 526 for(int y = 0; y<size; y++) { 527 float y2 = ((float)y)/((float)size); 528 for(int x = 0; x<size; x++) { 529 float x2 = ((float)x)/((float)size); 530 if(mode==ColorPicker.RED) { 531 green2 = (int)(x2*255+.49); 532 blue2 = (int)(y2*255+.49); 533 } else if(mode==ColorPicker.GREEN) { 534 red2 = (int)(x2*255+.49); 535 blue2 = (int)(y2*255+.49); 536 } else { 537 red2 = (int)(x2*255+.49); 538 green2 = (int)(y2*255+.49); 539 } 540 row[x] = 0xFF000000 + (red2 << 16) + (green2 << 8) + blue2; 541 } 542 image.getRaster().setDataElements(0, y, size, 1, row); 543 } 544 } 545 repaint(); 546 } 547 }