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}