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 }