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