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}