001 /* 002 * $Id: ShadowRenderer.java 3100 2008-10-14 22:33:10Z rah003 $ 003 * 004 * Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle, 005 * Santa Clara, California 95054, U.S.A. All rights reserved. 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * You should have received a copy of the GNU Lesser General Public 018 * License along with this library; if not, write to the Free Software 019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 020 */ 021 package org.jdesktop.swingx.graphics; 022 023 import java.awt.Color; 024 import java.awt.image.BufferedImage; 025 import java.beans.PropertyChangeListener; 026 import java.beans.PropertyChangeSupport; 027 028 /** 029 * <p>A shadow renderer generates a drop shadow for any given picture, respecting 030 * the transparency channel if present. The resulting picture contains the 031 * shadow only and to create a drop shadow effect you will need to stack the 032 * original picture and the shadow generated by the renderer.</p> 033 * <h2>Shadow Properties</h2> 034 * <p>A shadow is defined by three properties: 035 * <ul> 036 * <li><i>size</i>: The size, in pixels, of the shadow. This property also 037 * defines the fuzzyness.</li> 038 * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li> 039 * <li><i>color</i>: The color of the shadow. Shadows are not meant to be 040 * black only.</li> 041 * </ul> 042 * You can set these properties using the provided mutaters or the appropriate 043 * constructor. Here are two ways of creating a green shadow of size 10 and 044 * with an opacity of 50%: 045 * <pre> 046 * ShadowRenderer renderer = new ShadowRenderer(10, 0.5f, Color.GREEN); 047 * // .. 048 * renderer = new ShadowRenderer(); 049 * renderer.setSize(10); 050 * renderer.setOpacity(0.5f); 051 * renderer.setColor(Color.GREEN); 052 * </pre> 053 * The default constructor provides the following default values: 054 * <ul> 055 * <li><i>size</i>: 5 pixels</li> 056 * <li><i>opacity</i>: 50%</li> 057 * <li><i>color</i>: Black</li> 058 * </ul></p> 059 * <h2>Generating a Shadow</h2> 060 * <p>A shadow is generated as a <code>BufferedImage</code> from another 061 * <code>BufferedImage</code>. Once the renderer is set up, you must call 062 * {@link #createShadow} to actually generate the shadow: 063 * <pre> 064 * ShadowRenderer renderer = new ShadowRenderer(); 065 * // renderer setup 066 * BufferedImage shadow = renderer.createShadow(bufferedImage); 067 * </pre></p> 068 * <p>The generated image dimensions are computed as following:</p> 069 * <pre> 070 * width = imageWidth + 2 * shadowSize 071 * height = imageHeight + 2 * shadowSize 072 * </pre> 073 * <h2>Properties Changes</h2> 074 * <p>This renderer allows to register property change listeners with 075 * {@link #addPropertyChangeListener}. Listening to properties changes is very 076 * useful when you emebed the renderer in a graphical component and give the API 077 * user the ability to access the renderer. By listening to properties changes, 078 * you can easily repaint the component when needed.</p> 079 * <h2>Threading Issues</h2> 080 * <p><code>ShadowRenderer</code> is not guaranteed to be thread-safe.</p> 081 * 082 * @author Romain Guy <romain.guy@mac.com> 083 * @author Sebastien Petrucci 084 */ 085 public class ShadowRenderer { 086 /** 087 * <p>Identifies a change to the size used to render the shadow.</p> 088 * <p>When the property change event is fired, the old value and the new 089 * value are provided as <code>Integer</code> instances.</p> 090 */ 091 public static final String SIZE_CHANGED_PROPERTY = "shadow_size"; 092 093 /** 094 * <p>Identifies a change to the opacity used to render the shadow.</p> 095 * <p>When the property change event is fired, the old value and the new 096 * value are provided as <code>Float</code> instances.</p> 097 */ 098 public static final String OPACITY_CHANGED_PROPERTY = "shadow_opacity"; 099 100 /** 101 * <p>Identifies a change to the color used to render the shadow.</p> 102 */ 103 public static final String COLOR_CHANGED_PROPERTY = "shadow_color"; 104 105 // size of the shadow in pixels (defines the fuzziness) 106 private int size = 5; 107 108 // opacity of the shadow 109 private float opacity = 0.5f; 110 111 // color of the shadow 112 private Color color = Color.BLACK; 113 114 // notifies listeners of properties changes 115 private PropertyChangeSupport changeSupport; 116 117 /** 118 * <p>Creates a default good looking shadow generator. 119 * The default shadow renderer provides the following default values: 120 * <ul> 121 * <li><i>size</i>: 5 pixels</li> 122 * <li><i>opacity</i>: 50%</li> 123 * <li><i>color</i>: Black</li> 124 * </ul></p> 125 * <p>These properties provide a regular, good looking shadow.</p> 126 */ 127 public ShadowRenderer() { 128 this(5, 0.5f, Color.BLACK); 129 } 130 131 /** 132 * <p>A shadow renderer needs three properties to generate shadows. 133 * These properties are:</p> 134 * <ul> 135 * <li><i>size</i>: The size, in pixels, of the shadow. This property also 136 * defines the fuzzyness.</li> 137 * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li> 138 * <li><i>color</i>: The color of the shadow. Shadows are not meant to be 139 * black only.</li> 140 * </ul> 141 * @param size the size of the shadow in pixels. Defines the fuzziness. 142 * @param opacity the opacity of the shadow. 143 * @param color the color of the shadow. 144 */ 145 public ShadowRenderer(final int size, final float opacity, final Color color) { 146 //noinspection ThisEscapedInObjectConstruction 147 changeSupport = new PropertyChangeSupport(this); 148 149 setSize(size); 150 setOpacity(opacity); 151 setColor(color); 152 } 153 154 /** 155 * <p>Add a PropertyChangeListener to the listener list. The listener is 156 * registered for all properties. The same listener object may be added 157 * more than once, and will be called as many times as it is added. If 158 * <code>listener</code> is null, no exception is thrown and no action 159 * is taken.</p> 160 * @param listener the PropertyChangeListener to be added 161 */ 162 public void addPropertyChangeListener(PropertyChangeListener listener) { 163 changeSupport.addPropertyChangeListener(listener); 164 } 165 166 /** 167 * <p>Remove a PropertyChangeListener from the listener list. This removes 168 * a PropertyChangeListener that was registered for all properties. If 169 * <code>listener</code> was added more than once to the same event source, 170 * it will be notified one less time after being removed. If 171 * <code>listener</code> is null, or was never added, no exception is thrown 172 * and no action is taken.</p> 173 * @param listener the PropertyChangeListener to be removed 174 */ 175 public void removePropertyChangeListener(PropertyChangeListener listener) { 176 changeSupport.removePropertyChangeListener(listener); 177 } 178 179 /** 180 * <p>Gets the color used by the renderer to generate shadows.</p> 181 * @return this renderer's shadow color 182 */ 183 public Color getColor() { 184 return color; 185 } 186 187 /** 188 * <p>Sets the color used by the renderer to generate shadows.</p> 189 * <p>Consecutive calls to {@link #createShadow} will all use this color 190 * until it is set again.</p> 191 * <p>If the color provided is null, the previous color will be retained.</p> 192 * @param shadowColor the generated shadows color 193 */ 194 public void setColor(final Color shadowColor) { 195 if (shadowColor != null) { 196 Color oldColor = this.color; 197 this.color = shadowColor; 198 changeSupport.firePropertyChange(COLOR_CHANGED_PROPERTY, 199 oldColor, 200 this.color); 201 } 202 } 203 204 /** 205 * <p>Gets the opacity used by the renderer to generate shadows.</p> 206 * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully 207 * transparent and 1.0f fully opaque.</p> 208 * @return this renderer's shadow opacity 209 */ 210 public float getOpacity() { 211 return opacity; 212 } 213 214 /** 215 * <p>Sets the opacity used by the renderer to generate shadows.</p> 216 * <p>Consecutive calls to {@link #createShadow} will all use this opacity 217 * until it is set again.</p> 218 * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully 219 * transparent and 1.0f fully opaque. If you provide a value out of these 220 * boundaries, it will be restrained to the closest boundary.</p> 221 * @param shadowOpacity the generated shadows opacity 222 */ 223 public void setOpacity(final float shadowOpacity) { 224 float oldOpacity = this.opacity; 225 226 if (shadowOpacity < 0.0) { 227 this.opacity = 0.0f; 228 } else if (shadowOpacity > 1.0f) { 229 this.opacity = 1.0f; 230 } else { 231 this.opacity = shadowOpacity; 232 } 233 234 changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY, 235 oldOpacity, 236 this.opacity); 237 } 238 239 /** 240 * <p>Gets the size in pixel used by the renderer to generate shadows.</p> 241 * @return this renderer's shadow size 242 */ 243 public int getSize() { 244 return size; 245 } 246 247 /** 248 * <p>Sets the size, in pixels, used by the renderer to generate shadows.</p> 249 * <p>The size defines the blur radius applied to the shadow to create the 250 * fuzziness.</p> 251 * <p>There is virtually no limit to the size. The size cannot be negative. 252 * If you provide a negative value, the size will be 0 instead.</p> 253 * @param shadowSize the generated shadows size in pixels (fuzziness) 254 */ 255 public void setSize(final int shadowSize) { 256 int oldSize = this.size; 257 258 if (shadowSize < 0) { 259 this.size = 0; 260 } else { 261 this.size = shadowSize; 262 } 263 264 changeSupport.firePropertyChange(SIZE_CHANGED_PROPERTY, 265 new Integer(oldSize), 266 new Integer(this.size)); 267 } 268 269 /** 270 * <p>Generates the shadow for a given picture and the current properties 271 * of the renderer.</p> 272 * <p>The generated image dimensions are computed as following:</p> 273 * <pre> 274 * width = imageWidth + 2 * shadowSize 275 * height = imageHeight + 2 * shadowSize 276 * </pre> 277 * @param image the picture from which the shadow must be cast 278 * @return the picture containing the shadow of <code>image</code> 279 */ 280 public BufferedImage createShadow(final BufferedImage image) { 281 // Written by Sesbastien Petrucci 282 int shadowSize = size * 2; 283 284 int srcWidth = image.getWidth(); 285 int srcHeight = image.getHeight(); 286 287 int dstWidth = srcWidth + shadowSize; 288 int dstHeight = srcHeight + shadowSize; 289 290 int left = size; 291 int right = shadowSize - left; 292 293 int yStop = dstHeight - right; 294 295 int shadowRgb = color.getRGB() & 0x00FFFFFF; 296 int[] aHistory = new int[shadowSize]; 297 int historyIdx; 298 299 int aSum; 300 301 BufferedImage dst = new BufferedImage(dstWidth, dstHeight, 302 BufferedImage.TYPE_INT_ARGB); 303 304 int[] dstBuffer = new int[dstWidth * dstHeight]; 305 int[] srcBuffer = new int[srcWidth * srcHeight]; 306 307 GraphicsUtilities.getPixels(image, 0, 0, srcWidth, srcHeight, srcBuffer); 308 309 int lastPixelOffset = right * dstWidth; 310 float hSumDivider = 1.0f / shadowSize; 311 float vSumDivider = opacity / shadowSize; 312 313 int[] hSumLookup = new int[256 * shadowSize]; 314 for (int i = 0; i < hSumLookup.length; i++) { 315 hSumLookup[i] = (int) (i * hSumDivider); 316 } 317 318 int[] vSumLookup = new int[256 * shadowSize]; 319 for (int i = 0; i < vSumLookup.length; i++) { 320 vSumLookup[i] = (int) (i * vSumDivider); 321 } 322 323 int srcOffset; 324 325 // horizontal pass : extract the alpha mask from the source picture and 326 // blur it into the destination picture 327 for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) { 328 329 // first pixels are empty 330 for (historyIdx = 0; historyIdx < shadowSize; ) { 331 aHistory[historyIdx++] = 0; 332 } 333 334 aSum = 0; 335 historyIdx = 0; 336 srcOffset = srcY * srcWidth; 337 338 // compute the blur average with pixels from the source image 339 for (int srcX = 0; srcX < srcWidth; srcX++) { 340 341 int a = hSumLookup[aSum]; 342 dstBuffer[dstOffset++] = a << 24; // store the alpha value only 343 // the shadow color will be added in the next pass 344 345 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 346 347 // extract the new pixel ... 348 a = srcBuffer[srcOffset + srcX] >>> 24; 349 aHistory[historyIdx] = a; // ... and store its value into history 350 aSum += a; // ... and add its value to the sum 351 352 if (++historyIdx >= shadowSize) { 353 historyIdx -= shadowSize; 354 } 355 } 356 357 // blur the end of the row - no new pixels to grab 358 for (int i = 0; i < shadowSize; i++) { 359 360 int a = hSumLookup[aSum]; 361 dstBuffer[dstOffset++] = a << 24; 362 363 // substract the oldest pixel from the sum ... and nothing new to add ! 364 aSum -= aHistory[historyIdx]; 365 366 if (++historyIdx >= shadowSize) { 367 historyIdx -= shadowSize; 368 } 369 } 370 } 371 372 // vertical pass 373 for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { 374 375 aSum = 0; 376 377 // first pixels are empty 378 for (historyIdx = 0; historyIdx < left;) { 379 aHistory[historyIdx++] = 0; 380 } 381 382 // and then they come from the dstBuffer 383 for (int y = 0; y < right; y++, bufferOffset += dstWidth) { 384 int a = dstBuffer[bufferOffset] >>> 24; // extract alpha 385 aHistory[historyIdx++] = a; // store into history 386 aSum += a; // and add to sum 387 } 388 389 bufferOffset = x; 390 historyIdx = 0; 391 392 // compute the blur avera`ge with pixels from the previous pass 393 for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) { 394 395 int a = vSumLookup[aSum]; 396 dstBuffer[bufferOffset] = a << 24 | shadowRgb; // store alpha value + shadow color 397 398 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 399 400 a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; // extract the new pixel ... 401 aHistory[historyIdx] = a; // ... and store its value into history 402 aSum += a; // ... and add its value to the sum 403 404 if (++historyIdx >= shadowSize) { 405 historyIdx -= shadowSize; 406 } 407 } 408 409 // blur the end of the column - no pixels to grab anymore 410 for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) { 411 412 int a = vSumLookup[aSum]; 413 dstBuffer[bufferOffset] = a << 24 | shadowRgb; 414 415 aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum 416 417 if (++historyIdx >= shadowSize) { 418 historyIdx -= shadowSize; 419 } 420 } 421 } 422 423 GraphicsUtilities.setPixels(dst, 0, 0, dstWidth, dstHeight, dstBuffer); 424 return dst; 425 } 426 }