001/* 002 * $Id: PaintUtils.java 4193 2012-06-27 19:42:05Z kschaefe $ 003 * 004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 022package org.jdesktop.swingx.util; 023 024import java.awt.Color; 025import java.awt.GradientPaint; 026import java.awt.Graphics2D; 027import java.awt.LinearGradientPaint; 028import java.awt.Paint; 029import java.awt.Rectangle; 030import java.awt.TexturePaint; 031import java.awt.geom.Point2D; 032import java.awt.image.BufferedImage; 033import java.lang.reflect.Constructor; 034import java.lang.reflect.InvocationTargetException; 035import java.lang.reflect.Method; 036 037/** 038 * A collection of utilities for working with Paints and Colors. 039 * 040 * @author Mark Davidson 041 * @author joshua.marinacci@sun.com 042 * @author Karl George Schaefer 043 */ 044@SuppressWarnings("nls") 045public class PaintUtils { 046 public static final GradientPaint BLUE_EXPERIENCE = new GradientPaint( 047 new Point2D.Double(0, 0), 048 new Color(168, 204, 241), 049 new Point2D.Double(0, 1), 050 new Color(44, 61, 146)); 051 public static final GradientPaint MAC_OSX_SELECTED = new GradientPaint( 052 new Point2D.Double(0, 0), 053 new Color(81, 141, 236), 054 new Point2D.Double(0, 1), 055 new Color(36, 96, 192)); 056 public static final GradientPaint MAC_OSX = new GradientPaint( 057 new Point2D.Double(0, 0), 058 new Color(167, 210, 250), 059 new Point2D.Double(0, 1), 060 new Color(99, 147, 206)); 061 public static final GradientPaint AERITH = new GradientPaint( 062 new Point2D.Double(0, 0), 063 Color.WHITE, 064 new Point2D.Double(0, 1), 065 new Color(64, 110, 161)); 066 public static final GradientPaint GRAY = new GradientPaint( 067 new Point2D.Double(0, 0), 068 new Color(226, 226, 226), 069 new Point2D.Double(0, 1), 070 new Color(250, 248, 248)); 071 public static final GradientPaint RED_XP = new GradientPaint( 072 new Point2D.Double(0, 0), 073 new Color(236, 81, 81), 074 new Point2D.Double(0, 1), 075 new Color(192, 36, 36)); 076 public static final GradientPaint NIGHT_GRAY = new GradientPaint( 077 new Point2D.Double(0, 0), 078 new Color(102, 111, 127), 079 new Point2D.Double(0, 1), 080 new Color(38, 45, 61)); 081 public static final GradientPaint NIGHT_GRAY_LIGHT = new GradientPaint( 082 new Point2D.Double(0, 0), 083 new Color(129, 138, 155), 084 new Point2D.Double(0, 1), 085 new Color(58, 66, 82)); 086 087 088 //originally included in LinearGradientPainter 089 public static final Paint ORANGE_DELIGHT = new LinearGradientPaint( 090 new Point2D.Double(0, 0), 091 new Point2D.Double(1, 0), 092 new float[] {0f, .5f, .51f, 1f}, 093 new Color[] { 094 new Color(248, 192, 75), 095 new Color(253, 152, 6), 096 new Color(243, 133, 0), 097 new Color(254, 124, 0)}); 098 099 //originally included in LinearGradientPainter 100 public static final Paint BLACK_STAR = new LinearGradientPaint( 101 new Point2D.Double(0, 0), 102 new Point2D.Double(1, 0), 103 new float[] {0f, .5f, .51f, 1f}, 104 new Color[] { 105 new Color(54, 62, 78), 106 new Color(32, 39, 55), 107 new Color(74, 82, 96), 108 new Color(123, 132, 145)}); 109 110 private PaintUtils() { 111 } 112 113 /** Resizes a gradient to fill the width and height available. If the 114 * gradient is left to right it will be resized to fill the entire width. 115 * If the gradient is top to bottom it will be resized to fill the entire 116 * height. If the gradient is on an angle it will be resized to go from 117 * one corner to the other of the rectangle formed by (0,0 -> width,height). 118 * 119 * This method can resize java.awt.GradientPaint, java.awt.LinearGradientPaint, 120 * and the LinearGradientPaint implementation from Apache's Batik project. Note, 121 * this method does not require the MultipleGradientPaint.jar from Apache to 122 * compile or to run. MultipleGradientPaint.jar *is* required if you want 123 * to resize the LinearGradientPaint from that jar. 124 * 125 * Any paint passed into this method which is not a kind of gradient paint (like 126 * a Color or TexturePaint) will be returned unmodified. It will not throw 127 * an exception. If the gradient cannot be resized due to other errors the 128 * original paint will be returned unmodified. It will not throw an 129 * exception. 130 * 131 */ 132 public static Paint resizeGradient(Paint p, int width, int height) { 133 if(p == null) return p; 134 135 if(p instanceof GradientPaint) { 136 GradientPaint gp = (GradientPaint)p; 137 Point2D[] pts = new Point2D[2]; 138 pts[0] = gp.getPoint1(); 139 pts[1] = gp.getPoint2(); 140 pts = adjustPoints(pts, width, height); 141 return new GradientPaint(pts[0], gp.getColor1(), pts[1], gp.getColor2(), gp.isCyclic()); 142 } 143 144 if("java.awt.LinearGradientPaint".equals(p.getClass().getName()) || 145 "org.apache.batik.ext.awt.LinearGradientPaint".equals(p.getClass().getName())) { 146 return resizeLinearGradient(p,width,height); 147 } 148 return p; 149 } 150 151 152 private static Paint resizeLinearGradient(Paint p, int width, int height) { 153 try { 154 Point2D[] pts = new Point2D[2]; 155 pts[0] = (Point2D) invokeMethod(p,"getStartPoint"); 156 pts[1] = (Point2D) invokeMethod(p,"getEndPoint"); 157 pts = adjustPoints(pts, width, height); 158 float[] fractions = (float[]) invokeMethod(p,"getFractions"); 159 Color[] colors = (Color[]) invokeMethod(p,"getColors"); 160 161 Constructor<?> con = p.getClass().getDeclaredConstructor( 162 Point2D.class, Point2D.class, 163 new float[0].getClass(), 164 new Color[0].getClass()); 165 return (Paint) con.newInstance(pts[0],pts[1],fractions, colors); 166 } catch (Exception ex) { 167 ex.printStackTrace(); 168 } 169 return p; 170 } 171 172 private static Object invokeMethod(final Object p, final String methodName) 173 throws NoSuchMethodException, InvocationTargetException, IllegalArgumentException, SecurityException, IllegalAccessException { 174 Method meth = p.getClass().getMethod(methodName); 175 return meth.invoke(p); 176 } 177 178 179 private static Point2D[] adjustPoints(Point2D[] pts, int width, int height) { 180 Point2D start = pts[0]; 181 Point2D end = pts[1]; 182 183 double angle = calcAngle(start,end); 184 double a2 = Math.toDegrees(angle); 185 double e = 1; 186 187 // if it is near 0 degrees 188 if(Math.abs(angle) < Math.toRadians(e) || 189 Math.abs(angle) > Math.toRadians(360 - e)) { 190 start = new Point2D.Float(0, 0); 191 end = new Point2D.Float(normalize(end.getX(), width), 0); 192 } 193 194 // near 45 195 if (isNear(a2, 45, e)) { 196 start = new Point2D.Float(0, 0); 197 end = new Point2D.Float(normalize(end.getX(), width), normalize(end.getY(), height)); 198 } 199 200 // near 90 201 if (isNear(a2, 90, e)) { 202 start = new Point2D.Float(0, 0); 203 end = new Point2D.Float(0, normalize(end.getY(), height)); 204 } 205 206 // near 135 207 if (isNear(a2, 135, e)) { 208 start = new Point2D.Float(normalize(start.getX(), width), 0); 209 end = new Point2D.Float(0, normalize(end.getY(), height)); 210 } 211 212 // near 180 213 if (isNear(a2, 180, e)) { 214 start = new Point2D.Float(normalize(start.getX(), width), 0); 215 end = new Point2D.Float(0, 0); 216 } 217 218 // near 225 219 if (isNear(a2, 225, e)) { 220 start = new Point2D.Float(normalize(start.getX(), width), normalize(start.getY(), height)); 221 end = new Point2D.Float(0, 0); 222 } 223 224 // near 270 225 if (isNear(a2, 270, e)) { 226 start = new Point2D.Float(0, normalize(start.getY(), height)); 227 end = new Point2D.Float(0, 0); 228 } 229 230 // near 315 231 if (isNear(a2, 315, e)) { 232 start = new Point2D.Float(0, normalize(start.getY(), height)); 233 end = new Point2D.Float(normalize(end.getX(), width), 0); 234 } 235 236 return new Point2D[] { start, end }; 237 } 238 239 private static boolean isNear(double angle, double target, double error) { 240 return Math.abs(target - Math.abs(angle)) < error; 241 } 242 243 private static float normalize(double original, float target) { 244 if (original < 1f) { 245 return target * (float) original; 246 } 247 248 return target; 249 } 250 251 private static double calcAngle(Point2D p1, Point2D p2) { 252 double x_off = p2.getX() - p1.getX(); 253 double y_off = p2.getY() - p1.getY(); 254 double angle = Math.atan(y_off / x_off); 255 if (x_off < 0) { 256 angle = angle + Math.PI; 257 } 258 259 if(angle < 0) { angle+= 2*Math.PI; } 260 if(angle > 2*Math.PI) { angle -= 2*Math.PI; } 261 return angle; 262 } 263/* 264 public static void main(String ... args) { 265 LinearGradientPaint in = new LinearGradientPaint( 266 new Point(0,0), new Point(10,0), 267 new float[] {0f, 0.5f, 1f}, 268 new Color[] {Color.RED, Color.GREEN, Color.BLUE}); 269 log.fine("in = " + toString(in)); 270 Paint out = resizeGradient(in,100,100); 271 log.fine(("out = " + toString((MultipleGradientPaint) out)); 272 }*/ 273 /* 274 private static String toString(MultipleGradientPaint paint) { 275 StringBuffer buffer = new StringBuffer(); 276 buffer.append(paint.getClass().getName()); 277 Color[] colors = paint.getColors(); 278 float[] values = paint.getFractions(); 279 buffer.append("["); 280 for(int i=0; i<colors.length; i++) { 281 buffer.append("#").append(Integer.toHexString(colors[i].getRGB())); 282 buffer.append(":"); 283 buffer.append(values[i]); 284 buffer.append(", "); 285 } 286 buffer.append("]"); 287 if(paint instanceof LinearGradientPaint) { 288 LinearGradientPaint lgp = (LinearGradientPaint) paint; 289 buffer.append(", "); 290 buffer.append(""+lgp.getStartPoint().getX() + ", " + lgp.getStartPoint().getY()); 291 buffer.append("->"); 292 buffer.append(""+lgp.getEndPoint().getX() + ", " + lgp.getEndPoint().getY()); 293 } 294 295 return buffer.toString(); 296 }*/ 297 298 /** 299 * Creates a new {@code Paint} that is a checkered effect using the colors {@link Color#GRAY 300 * gray} and {@link Color#WHITE}. 301 * 302 * @return a the checkered paint 303 */ 304 public static Paint getCheckerPaint() { 305 return getCheckerPaint(Color.WHITE, Color.GRAY, 20); 306 } 307 308 /** 309 * Creates a new {@code Paint} that is a checkered effect using the specified colors. 310 * <p> 311 * While this method supports transparent colors, this implementation performs painting 312 * operations using the second color after it performs operations using the first color. This 313 * means that to create a checkered paint with a fully-transparent color, you MUST specify that 314 * color first. 315 * 316 * @param c1 317 * the first color 318 * @param c2 319 * the second color 320 * @param size 321 * the size of the paint 322 * @return a new {@code Paint} checkering the supplied colors 323 */ 324 public static Paint getCheckerPaint(Paint c1, Paint c2, int size) { 325 BufferedImage img = GraphicsUtilities.createCompatibleTranslucentImage(size, size); 326 Graphics2D g = img.createGraphics(); 327 328 try { 329 g.setPaint(c1); 330 g.fillRect(0, 0, size, size); 331 g.setPaint(c2); 332 g.fillRect(0, 0, size / 2, size / 2); 333 g.fillRect(size / 2, size / 2, size / 2, size / 2); 334 } finally { 335 g.dispose(); 336 } 337 338 return new TexturePaint(img,new Rectangle(0,0,size,size)); 339 } 340 341 /** 342 * Creates a {@code String} that represents the supplied color as a 343 * hex-value RGB triplet, including the "#". The return value is suitable 344 * for use in HTML. The alpha (transparency) channel is neither include nor 345 * used in producing the string. 346 * 347 * @param color 348 * the color to convert 349 * @return the hex {@code String} 350 */ 351 public static String toHexString(Color color) { 352 return "#" + Integer.toHexString(color.getRGB() | 0xFF000000).substring(2); 353 } 354 355 /** 356 * Returns a new color equal to the old one, except that there is no alpha 357 * (transparency) channel. 358 * <p> 359 * This method is a convenience and has the same effect as {@code 360 * setAlpha(color, 255)}. 361 * 362 * @param color 363 * the color to remove the alpha (transparency) from 364 * @return a new non-transparent {@code Color} 365 * @throws NullPointerException 366 * if {@code color} is {@code null} 367 */ 368 public static Color removeAlpha(Color color) { 369 return setAlpha(color, 255); 370 } 371 372 /** 373 * Returns a new color equal to the old one, except alpha (transparency) 374 * channel is set to the new value. 375 * 376 * @param color 377 * the color to modify 378 * @param alpha 379 * the new alpha (transparency) level. Must be an int between 0 380 * and 255 381 * @return a new alpha-applied {@code Color} 382 * @throws IllegalArgumentException 383 * if {@code alpha} is not between 0 and 255 inclusive 384 * @throws NullPointerException 385 * if {@code color} is {@code null} 386 */ 387 public static Color setAlpha(Color color, int alpha) { 388 if (alpha < 0 || alpha > 255) { 389 throw new IllegalArgumentException("invalid alpha value"); 390 } 391 392 return new Color( 393 color.getRed(), color.getGreen(), color.getBlue(), alpha); 394 } 395 396 /** 397 * Returns a new color equal to the old one, except the saturation is set to 398 * the new value. The new color will have the same alpha (transparency) as 399 * the original color. 400 * <p> 401 * The color is modified using HSB calculations. The saturation must be a 402 * float between 0 and 1. If 0 the resulting color will be gray. If 1 the 403 * resulting color will be the most saturated possible form of the passed in 404 * color. 405 * 406 * @param color 407 * the color to modify 408 * @param saturation 409 * the saturation to use in the new color 410 * @return a new saturation-applied {@code Color} 411 * @throws IllegalArgumentException 412 * if {@code saturation} is not between 0 and 1 inclusive 413 * @throws NullPointerException 414 * if {@code color} is {@code null} 415 */ 416 public static Color setSaturation(Color color, float saturation) { 417 if (saturation < 0f || saturation > 1f) { 418 throw new IllegalArgumentException("invalid saturation value"); 419 } 420 421 int alpha = color.getAlpha(); 422 423 float[] hsb = Color.RGBtoHSB( 424 color.getRed(), color.getGreen(), color.getBlue(), null); 425 Color c = Color.getHSBColor(hsb[0], saturation, hsb[2]); 426 427 return setAlpha(c, alpha); 428 } 429 430 /** 431 * Returns a new color equal to the old one, except the brightness is set to 432 * the new value. The new color will have the same alpha (transparency) as 433 * the original color. 434 * <p> 435 * The color is modified using HSB calculations. The brightness must be a 436 * float between 0 and 1. If 0 the resulting color will be black. If 1 the 437 * resulting color will be the brightest possible form of the passed in 438 * color. 439 * 440 * @param color 441 * the color to modify 442 * @param brightness 443 * the brightness to use in the new color 444 * @return a new brightness-applied {@code Color} 445 * @throws IllegalArgumentException 446 * if {@code brightness} is not between 0 and 1 inclusive 447 * @throws NullPointerException 448 * if {@code color} is {@code null} 449 */ 450 public static Color setBrightness(Color color, float brightness) { 451 if (brightness < 0f || brightness > 1f) { 452 throw new IllegalArgumentException("invalid brightness value"); 453 } 454 455 int alpha = color.getAlpha(); 456 457 float[] hsb = Color.RGBtoHSB( 458 color.getRed(), color.getGreen(), color.getBlue(), null); 459 Color c = Color.getHSBColor(hsb[0], hsb[1], brightness); 460 461 return setAlpha(c, alpha); 462 } 463 464 /** 465 * Blends two colors to create a new color. The {@code origin} color is the 466 * base for the new color and regardless of its alpha component, it is 467 * treated as fully opaque (alpha 255). 468 * 469 * @param origin 470 * the base of the new color 471 * @param over 472 * the alpha-enabled color to add to the {@code origin} color 473 * @return a new color comprised of the {@code origin} and {@code over} 474 * colors 475 */ 476 public static Color blend(Color origin, Color over) { 477 if (over == null) { 478 return origin; 479 } 480 481 if (origin == null) { 482 return over; 483 } 484 485 int a = over.getAlpha(); 486 487 int rb = (((over.getRGB() & 0x00ff00ff) * (a + 1)) 488 + ((origin.getRGB() & 0x00ff00ff) * (0xff - a))) & 0xff00ff00; 489 int g = (((over.getRGB() & 0x0000ff00) * (a + 1)) 490 + ((origin.getRGB() & 0x0000ff00) * (0xff - a))) & 0x00ff0000; 491 492 return new Color((over.getRGB() & 0xff000000) | ((rb | g) >> 8)); 493 } 494 495 /** 496 * Interpolates a color. 497 * 498 * @param b 499 * the first color 500 * @param a 501 * the second color 502 * @param t 503 * the amount to interpolate 504 * @return a new color 505 */ 506 public static Color interpolate(Color b, Color a, float t) { 507 float[] acomp = a.getRGBComponents(null); 508 float[] bcomp = b.getRGBComponents(null); 509 float[] ccomp = new float[4]; 510 511 // log.fine(("a comp "); 512 // for(float f : acomp) { 513 // log.fine((f); 514 // } 515 // for(float f : bcomp) { 516 // log.fine((f); 517 // } 518 for(int i=0; i<4; i++) { 519 ccomp[i] = acomp[i] + (bcomp[i]-acomp[i])*t; 520 } 521 // for(float f : ccomp) { 522 // log.fine((f); 523 // } 524 525 return new Color(ccomp[0],ccomp[1],ccomp[2],ccomp[3]); 526 } 527 528 /** 529 * Computes an appropriate foreground color (either white or black) for the 530 * given background color. 531 * 532 * @param bg 533 * the background color 534 * @return {@code Color.WHITE} or {@code Color.BLACK} 535 * @throws NullPointerException 536 * if {@code bg} is {@code null} 537 */ 538 public static Color computeForeground(Color bg) { 539 float[] rgb = bg.getRGBColorComponents(null); 540 float y = .3f * rgb[0] + .59f * rgb[1] + .11f * rgb[2]; 541 542 return y > .5f ? Color.BLACK : Color.WHITE; 543 } 544}