001    /*
002     * $Id: ReflectionRenderer.java 3256 2009-02-10 20:09:41Z kschaefe $
003     *
004     * Dual-licensed under LGPL (Sun and Romain Guy) and BSD (Romain Guy).
005     *
006     * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
007     * Santa Clara, California 95054, U.S.A. All rights reserved.
008     *
009     * Copyright (c) 2006 Romain Guy <romain.guy@mac.com>
010     * All rights reserved.
011     *
012     * Redistribution and use in source and binary forms, with or without
013     * modification, are permitted provided that the following conditions
014     * are met:
015     * 1. Redistributions of source code must retain the above copyright
016     *    notice, this list of conditions and the following disclaimer.
017     * 2. Redistributions in binary form must reproduce the above copyright
018     *    notice, this list of conditions and the following disclaimer in the
019     *    documentation and/or other materials provided with the distribution.
020     * 3. The name of the author may not be used to endorse or promote products
021     *    derived from this software without specific prior written permission.
022     *
023     * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
024     * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
025     * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
026     * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
027     * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
028     * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029     * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
030     * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
031     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
032     * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
033     */
034    package org.jdesktop.swingx.graphics;
035    
036    import java.awt.AlphaComposite;
037    import java.awt.Color;
038    import java.awt.GradientPaint;
039    import java.awt.Graphics2D;
040    import java.awt.image.BufferedImage;
041    import java.beans.PropertyChangeListener;
042    import java.beans.PropertyChangeSupport;
043    
044    import org.jdesktop.swingx.image.StackBlurFilter;
045    
046    /**
047     * <p>A reflection renderer generates the reflection of a given picture. The
048     * result can be either the reflection itself, or an image containing both the
049     * source image and its reflection.</p>
050     * <h2>Reflection Properties</h2>
051     * <p>A reflection is defined by three properties:
052     * <ul>
053     *   <li><i>opacity</i>: the opacity of the reflection. You will usually
054     *   change this valued according to the background color.</li>
055     *   <li><i>length</i>: the length of the reflection. The length is a fraction
056     *   of the height of the source image.</li>
057     *   <li><i>blur enabled</i>: perfect reflections are hardly natural. You can
058     *    blur the reflection to make it look a bit more natural.</li>
059     * </ul>
060     * You can set these properties using the provided mutaters or the appropriate
061     * constructor. Here are two ways of creating a blurred reflection, with an
062     * opacity of 50% and a length of 30% the height of the original image:
063     * <pre>
064     * ReflectionRenderer renderer = new ReflectionRenderer(0.5f, 0.3f, true);
065     * // ..
066     * renderer = new ReflectionRenderer();
067     * renderer.setOpacity(0.5f);
068     * renderer.setLength(0.3f);
069     * renderer.setBlurEnabled(true);
070     * </pre>
071     * The default constructor provides the following default values:
072     * <ul>
073     *   <li><i>opacity</i>: 35%</li>
074     *   <li><i>length</i>: 40%</li>
075     *   <li><i>blur enabled</i>: false</li>
076     * </ul></p>
077     * <h2>Generating Reflections</h2>
078     * <p>A reflection is generated as a <code>BufferedImage</code> from another
079     * <code>BufferedImage</code>. Once the renderer is set up, you must call
080     * {@link #createReflection(java.awt.image.BufferedImage)} to actually generate
081     * the reflection:
082     * <pre>
083     * ReflectionRenderer renderer = new ReflectionRenderer();
084     * // renderer setup
085     * BufferedImage reflection = renderer.createReflection(bufferedImage);
086     * </pre></p>
087     * <p>The returned image contains only the reflection. You will have to append
088     * it to the source image at painting time to get a realistic results. You can
089     * also asks the rendered to return a picture composed of both the source image
090     * and its reflection:
091     * <pre>
092     * ReflectionRenderer renderer = new ReflectionRenderer();
093     * // renderer setup
094     * BufferedImage reflection = renderer.appendReflection(bufferedImage);
095     * </pre></p>
096     * <h2>Properties Changes</h2>
097     * <p>This renderer allows to register property change listeners with
098     * {@link #addPropertyChangeListener}. Listening to properties changes is very
099     * useful when you emebed the renderer in a graphical component and give the API
100     * user the ability to access the renderer. By listening to properties changes,
101     * you can easily repaint the component when needed.</p>
102     * <h2>Threading Issues</h2>
103     * <p><code>ReflectionRenderer</code> is not guaranteed to be thread-safe.</p>
104     *
105     * @author Romain Guy <romain.guy@mac.com>
106     */
107    public class ReflectionRenderer {
108        /**
109         * <p>Identifies a change to the opacity used to render the reflection.</p>
110         */
111        public static final String OPACITY_CHANGED_PROPERTY = "reflection_opacity";
112    
113        /**
114         * <p>Identifies a change to the length of the rendered reflection.</p>
115         */
116        public static final String LENGTH_CHANGED_PROPERTY = "reflection_length";
117    
118        /**
119         * <p>Identifies a change to the blurring of the rendered reflection.</p>
120         */
121        public static final String BLUR_ENABLED_CHANGED_PROPERTY = "reflection_blur";
122    
123        // opacity of the reflection
124        private float opacity;
125    
126        // length of the reflection
127        private float length;
128    
129        // should the reflection be blurred?
130        private boolean blurEnabled;
131    
132        // notifies listeners of properties changes
133        private PropertyChangeSupport changeSupport;
134        private StackBlurFilter stackBlurFilter;
135    
136        /**
137         * <p>Creates a default good looking reflections generator.
138         * The default reflection renderer provides the following default values:
139         * <ul>
140         *   <li><i>opacity</i>: 35%</li>
141         *   <li><i>length</i>: 40%</li>
142         *   <li><i>blurring</i>: disabled with a radius of 1 pixel</li>
143         * </ul></p>
144         * <p>These properties provide a regular, good looking reflection.</p>
145         *
146         * @see #getOpacity()
147         * @see #setOpacity(float)
148         * @see #getLength()
149         * @see #setLength(float)
150         * @see #isBlurEnabled()
151         * @see #setBlurEnabled(boolean)
152         * @see #getBlurRadius()
153         * @see #setBlurRadius(int)
154         */
155        public ReflectionRenderer() {
156            this(0.35f, 0.4f, false);
157        }
158    
159        /**
160         * <p>Creates a default good looking reflections generator with the
161         * specified opacity. The default reflection renderer provides the following
162         * default values:
163         * <ul>
164         *   <li><i>length</i>: 40%</li>
165         *   <li><i>blurring</i>: disabled with a radius of 1 pixel</li>
166         * </ul></p>
167         *
168         * @param opacity the opacity of the reflection, between 0.0 and 1.0
169         * @see #getOpacity()
170         * @see #setOpacity(float)
171         * @see #getLength()
172         * @see #setLength(float)
173         * @see #isBlurEnabled()
174         * @see #setBlurEnabled(boolean)
175         * @see #getBlurRadius()
176         * @see #setBlurRadius(int)
177         */
178        public ReflectionRenderer(float opacity) {
179            this(opacity, 0.4f, false);
180        }
181    
182        /**
183         * <p>Creates a reflections generator with the specified properties. Both
184         * opacity and length are numbers between 0.0 (0%) and 1.0 (100%). If the
185         * provided numbers are outside this range, they are clamped.</p>
186         * <p>Enabling the blur generates a different kind of reflections that might
187         * look more natural. The default blur radius is 1 pixel</p>
188         *
189         * @param opacity the opacity of the reflection
190         * @param length the length of the reflection
191         * @param blurEnabled if true, the reflection is blurred
192         * @see #getOpacity()
193         * @see #setOpacity(float)
194         * @see #getLength()
195         * @see #setLength(float)
196         * @see #isBlurEnabled()
197         * @see #setBlurEnabled(boolean)
198         * @see #getBlurRadius()
199         * @see #setBlurRadius(int)
200         */
201        public ReflectionRenderer(float opacity, float length, boolean blurEnabled) {
202            //noinspection ThisEscapedInObjectConstruction
203            this.changeSupport = new PropertyChangeSupport(this);
204            this.stackBlurFilter = new StackBlurFilter(1);
205    
206            setOpacity(opacity);
207            setLength(length);
208            setBlurEnabled(blurEnabled);
209        }
210    
211        /**
212         * <p>Add a PropertyChangeListener to the listener list. The listener is
213         * registered for all properties. The same listener object may be added
214         * more than once, and will be called as many times as it is added. If
215         * <code>listener</code> is null, no exception is thrown and no action
216         * is taken.</p>
217         *
218         * @param listener the PropertyChangeListener to be added
219         */
220        public void addPropertyChangeListener(PropertyChangeListener listener) {
221            changeSupport.addPropertyChangeListener(listener);
222        }
223    
224        /**
225         * <p>Remove a PropertyChangeListener from the listener list. This removes
226         * a PropertyChangeListener that was registered for all properties. If
227         * <code>listener</code> was added more than once to the same event source,
228         * it will be notified one less time after being removed. If
229         * <code>listener</code> is null, or was never added, no exception is thrown
230         * and no action is taken.</p>
231         *
232         * @param listener the PropertyChangeListener to be removed
233         */
234        public void removePropertyChangeListener(PropertyChangeListener listener) {
235            changeSupport.removePropertyChangeListener(listener);
236        }
237    
238        /**
239         * <p>Gets the opacity used by the factory to generate reflections.</p>
240         * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
241         * transparent and 1.0f fully opaque.</p>
242         *
243         * @return this factory's shadow opacity
244         * @see #getOpacity()
245         * @see #createReflection(java.awt.image.BufferedImage)
246         * @see #appendReflection(java.awt.image.BufferedImage)
247         */
248        public float getOpacity() {
249            return opacity;
250        }
251    
252        /**
253         * <p>Sets the opacity used by the factory to generate reflections.</p>
254         * <p>Consecutive calls to {@link #createReflection} will all use this
255         * opacity until it is set again.</p>
256         * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
257         * transparent and 1.0f fully opaque. If you provide a value out of these
258         * boundaries, it will be restrained to the closest boundary.</p>
259         *
260         * @param opacity the generated reflection opacity
261         * @see #setOpacity(float)
262         * @see #createReflection(java.awt.image.BufferedImage)
263         * @see #appendReflection(java.awt.image.BufferedImage)
264         */
265        public void setOpacity(float opacity) {
266            float oldOpacity = this.opacity;
267    
268            if (opacity < 0.0f) {
269                opacity = 0.0f;
270            } else if (opacity > 1.0f) {
271                opacity = 1.0f;
272            }
273    
274            if (oldOpacity != opacity) {
275                this.opacity = opacity;
276                changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY,
277                                                 oldOpacity,
278                                                 this.opacity);
279            }
280        }
281    
282        /**
283         * <p>Returns the length of the reflection. The result is a number between
284         * 0.0 and 1.0. This number is the fraction of the height of the source
285         * image that is used to compute the size of the reflection.</p>
286         *
287         * @return the length of the reflection, as a fraction of the source image
288         *   height
289         * @see #setLength(float)
290         * @see #createReflection(java.awt.image.BufferedImage)
291         * @see #appendReflection(java.awt.image.BufferedImage)
292         */
293        public float getLength() {
294            return length;
295        }
296    
297        /**
298         * <p>Sets the length of the reflection, as a fraction of the height of the
299         * source image.</p>
300         * <p>Consecutive calls to {@link #createReflection} will all use this
301         * opacity until it is set again.</p>
302         * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
303         * transparent and 1.0f fully opaque. If you provide a value out of these
304         * boundaries, it will be restrained to the closest boundary.</p>
305         *
306         * @param length the length of the reflection, as a fraction of the source
307         *   image height
308         * @see #getLength()
309         * @see #createReflection(java.awt.image.BufferedImage)
310         * @see #appendReflection(java.awt.image.BufferedImage)
311         */
312        public void setLength(float length) {
313            float oldLength = this.length;
314    
315            if (length < 0.0f) {
316                length = 0.0f;
317            } else if (length > 1.0f) {
318                length = 1.0f;
319            }
320    
321            if (oldLength != length) {
322                this.length = length;
323                changeSupport.firePropertyChange(LENGTH_CHANGED_PROPERTY,
324                                                 oldLength,
325                                                 this.length);
326            }
327        }
328    
329        /**
330         * <p>Returns true if the blurring of the reflection is enabled, false
331         * otherwise. When blurring is enabled, the reflection is blurred to look
332         * more natural.</p>
333         *
334         * @return true if blur is enabled, false otherwise
335         * @see #setBlurEnabled(boolean)
336         * @see #createReflection(java.awt.image.BufferedImage)
337         * @see #appendReflection(java.awt.image.BufferedImage)
338         */
339        public boolean isBlurEnabled() {
340            return blurEnabled;
341        }
342    
343        /**
344         * <p>Setting the blur to true will enable the blurring of the reflection
345         * when {@link #createReflection} is invoked.</p>
346         * <p>Enabling the blurring of the reflection can yield to more natural
347         * results which may or may not be better looking, depending on the source
348         * picture.</p>
349         * <p>Consecutive calls to {@link #createReflection} will all use this
350         * opacity until it is set again.</p>
351         *
352         * @param blurEnabled true to enable the blur, false otherwise
353         * @see #isBlurEnabled()
354         * @see #createReflection(java.awt.image.BufferedImage)
355         * @see #appendReflection(java.awt.image.BufferedImage)
356         */
357        public void setBlurEnabled(boolean blurEnabled) {
358            if (blurEnabled != this.blurEnabled) {
359                boolean oldBlur = this.blurEnabled;
360                this.blurEnabled= blurEnabled;
361    
362                changeSupport.firePropertyChange(BLUR_ENABLED_CHANGED_PROPERTY,
363                                                 oldBlur,
364                                                 this.blurEnabled);
365            }
366        }
367    
368        /**
369         * <p>Returns the effective radius, in pixels, of the blur used by this
370         * renderer when {@link #isBlurEnabled()} is true.</p>
371         *
372         * @return the effective radius of the blur used when
373         *   <code>isBlurEnabled</code> is true
374         * @see #isBlurEnabled()
375         * @see #setBlurEnabled(boolean)
376         * @see #setBlurRadius(int)
377         * @see #getBlurRadius()
378         */
379        public int getEffectiveBlurRadius() {
380            return stackBlurFilter.getEffectiveRadius();
381        }
382    
383        /**
384         * <p>Returns the radius, in pixels, of the blur used by this renderer when
385         * {@link #isBlurEnabled()} is true.</p>
386         *
387         * @return the radius of the blur used when <code>isBlurEnabled</code>
388         *         is true
389         * @see #isBlurEnabled()
390         * @see #setBlurEnabled(boolean)
391         * @see #setBlurRadius(int)
392         * @see #getEffectiveBlurRadius()
393         */
394        public int getBlurRadius() {
395            return stackBlurFilter.getRadius();
396        }
397    
398        /**
399         * <p>Sets the radius, in pixels, of the blur used by this renderer when
400         * {@link #isBlurEnabled()} is true. This radius changes the size of the
401         * generated image when blurring is applied.</p>
402         *
403         * @param radius the radius, in pixels, of the blur
404         * @see #isBlurEnabled()
405         * @see #setBlurEnabled(boolean)
406         * @see #getBlurRadius()
407         */
408        public void setBlurRadius(int radius) {
409            this.stackBlurFilter = new StackBlurFilter(radius);
410        }
411    
412        /**
413         * <p>Returns the source image and its reflection. The appearance of the
414         * reflection is defined by the opacity, the length and the blur
415         * properties.</p>
416         * * <p>The width of the generated image will be augmented when
417         * {@link #isBlurEnabled()} is true. The generated image will have the width
418         * of the source image plus twice the effective blur radius (see
419         * {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the
420         * width will be augmented by 6. You might need to take this into account
421         * at drawing time.</p>
422         * <p>The returned image height depends on the value returned by
423         * {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance,
424         * if the length is 0.5 (or 50%) and the source image is 480 pixels high,
425         * then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.</p>
426         * <p>You can create only the reflection by calling
427         * {@link #createReflection(java.awt.image.BufferedImage)}.</p>
428         *
429         * @param image the source image
430         * @return the source image with its reflection below
431         * @see #createReflection(java.awt.image.BufferedImage)
432         */
433        public BufferedImage appendReflection(BufferedImage image) {
434            BufferedImage reflection = createReflection(image);
435            BufferedImage buffer = GraphicsUtilities.createCompatibleTranslucentImage(
436                    reflection.getWidth(), image.getHeight() + reflection.getHeight());
437            Graphics2D g2 = buffer.createGraphics();
438    
439            try {
440                int effectiveRadius = isBlurEnabled() ? stackBlurFilter
441                        .getEffectiveRadius() : 0;
442                g2.drawImage(image, effectiveRadius, 0, null);
443                g2.drawImage(reflection, 0, image.getHeight() - effectiveRadius,
444                        null);
445            } finally {
446                g2.dispose();
447            }
448            
449            reflection.flush();
450    
451            return buffer;
452        }
453    
454        /**
455         * <p>Returns the reflection of the source image. The appearance of the
456         * reflection is defined by the opacity, the length and the blur
457         * properties.</p>
458         * * <p>The width of the generated image will be augmented when
459         * {@link #isBlurEnabled()} is true. The generated image will have the width
460         * of the source image plus twice the effective blur radius (see
461         * {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the
462         * width will be augmented by 6. You might need to take this into account
463         * at drawing time.</p>
464         * <p>The returned image height depends on the value returned by
465         * {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance,
466         * if the length is 0.5 (or 50%) and the source image is 480 pixels high,
467         * then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.</p>
468         * <p>The returned image contains <strong>only</strong>
469         * the reflection. You will have to append it to the source image to produce
470         * the illusion of a reflective environement. The method
471         * {@link #appendReflection(java.awt.image.BufferedImage)} provides an easy
472         * way to create an image containing both the source and the reflection.</p>
473         *
474         * @param image the source image
475         * @return the reflection of the source image
476         * @see #appendReflection(java.awt.image.BufferedImage)
477         */
478        public BufferedImage createReflection(BufferedImage image) {
479            if (length == 0.0f) {
480                return GraphicsUtilities.createCompatibleTranslucentImage(1, 1);
481            }
482    
483            int blurOffset = isBlurEnabled() ?
484                             stackBlurFilter.getEffectiveRadius() : 0;
485            int height = (int) (image.getHeight() * length);
486    
487            BufferedImage buffer =
488                    GraphicsUtilities.createCompatibleTranslucentImage(
489                            image.getWidth() + blurOffset * 2,
490                            height + blurOffset * 2);
491            Graphics2D g2 = buffer.createGraphics();
492    
493            try {
494                g2.translate(0, image.getHeight());
495                g2.scale(1.0, -1.0);
496    
497                g2.drawImage(image, blurOffset, -blurOffset, null);
498    
499                g2.scale(1.0, -1.0);
500                g2.translate(0, -image.getHeight());
501    
502                g2.setComposite(AlphaComposite.DstIn);
503                g2.setPaint(new GradientPaint(0.0f, 0.0f, new Color(0.0f, 0.0f,
504                        0.0f, getOpacity()), 0.0f, buffer.getHeight(), new Color(
505                        0.0f, 0.0f, 0.0f, 0.0f), true));
506                g2.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
507            } finally {
508                g2.dispose();
509            }
510            
511            return isBlurEnabled() ? stackBlurFilter.filter(buffer, null) :
512                    buffer;
513        }
514    }