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 }