001: /*
002: * ShadowFactory.java
003: *
004: * Copyright 2005 Sun Microsystems, Inc., 4150 Network Circle,
005: * Santa Clara, California 95054, U.S.A. All rights reserved.
006: */
007:
008: package com.jidesoft.swing;
009:
010: import java.awt.*;
011: import java.awt.image.*;
012: import java.beans.PropertyChangeListener;
013: import java.beans.PropertyChangeSupport;
014: import java.util.HashMap;
015:
016: /**
017: * <p>A shadow factory generates a drop shadow for any given picture, respecting
018: * the transparency channel if present. The resulting picture contains the
019: * shadow only and to create a drop shadow effect you will need to stack the
020: * original picture and the shadow generated by the factory.</p>
021: * <h2>Shadow Properties</h2>
022: * <p>A shadow is defined by three properties:
023: * <ul>
024: * <li><i>size</i>: The size, in pixels, of the shadow. This property also
025: * defines the fuzzyness.</li>
026: * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li>
027: * <li><i>color</i>: The color of the shadow. Shadows are not meant to be
028: * black only.</li>
029: * </ul>
030: * You can set these properties using the provided mutaters or the appropriate
031: * constructor. Here are two ways of creating a green shadow of size 10 and
032: * with an opacity of 50%:
033: * <pre>
034: * ShadowFactory factory = new ShadowFactory(10, 0.5f, Color.GREEN);
035: * // ..
036: * factory = new ShadowFactory();
037: * factory.setSize(10);
038: * factory.setOpacity(0.5f);
039: * factory.setColor(Color.GREEN);
040: * </pre>
041: * The default constructor provides the following default values:
042: * <ul>
043: * <li><i>size</i>: 5 pixels</li>
044: * <li><i>opacity</i>: 50%</li>
045: * <li><i>color</i>: Black</li>
046: * </ul></p>
047: * <h2>Shadow Quality</h2>
048: * <p>The factory provides two shadow generation algorithms: <i>fast quality blur</i>
049: * and <i>high quality blur</i>. You can select your preferred algorithm by
050: * setting the appropriate rendering hint:
051: * <pre>
052: * ShadowFactory factory = new ShadowFactory();
053: * factory.setRenderingHint(ShadowFactory.KEY_BLUR_QUALITY,
054: * ShadowFactory.VALUE_BLUR_QUALITY_HIGH);
055: * </pre>
056: * The default rendering algorihtm is <code>VALUE_BLUR_QUALITY_FAST</code>.</p>
057: * <p>The current implementation should provide the same quality with both
058: * algorithms but performances are guaranteed to be better (about 30 times
059: * faster) with the <i>fast quality blur</i>.</p>
060: * <h2>Generating a Shadow</h2>
061: * <p>A shadow is generated as a <code>BufferedImage</code> from another
062: * <code>BufferedImage</code>. Once the factory is set up, you must call
063: * {@link #createShadow} to actually generate the shadow:
064: * <pre>
065: * ShadowFactory factory = new ShadowFactory();
066: * // factory setup
067: * BufferedImage shadow = factory.createShadow(bufferedImage);
068: * </pre>
069: * The resulting image is of type <code>BufferedImage.TYPE_INT_ARGB</code>.
070: * Both dimensions of this image are larger than original image's:
071: * <ul>
072: * <li>new width = original width + 2 * shadow size</li>
073: * <li>new height = original height + 2 * shadow size</li>
074: * </ul>
075: * This must be taken into account when you need to create a drop shadow effect.</p>
076: * <h2>Properties Changes</h2>
077: * <p>This factory allows to register property change listeners with
078: * {@link #addPropertyChangeListener}. Listening to properties changes is very
079: * useful when you emebed the factory in a graphical component and give the API
080: * user the ability to access the factory. By listening to properties changes,
081: * you can easily repaint the component when needed.</p>
082: * <h2>Threading Issues</h2>
083: * <p><code>ShadowFactory</code> is not guaranteed to be thread-safe.</p>
084: *
085: * @author Romain Guy <romain.guy@mac.com>
086: * @author Sebastien Petrucci <sebastien_petrucci@yahoo.fr>
087: */
088:
089: public class ShadowFactory {
090: /**
091: * <p>Key for the blur quality rendering hint.</p>
092: */
093: public static final String KEY_BLUR_QUALITY = "blur_quality";
094:
095: /**
096: * <p>Selects the fast rendering algorithm. This is the default rendering
097: * hint for <code>KEY_BLUR_QUALITY</code>.</p>
098: */
099: public static final String VALUE_BLUR_QUALITY_FAST = "fast";
100:
101: /**
102: * <p>Selects the high quality rendering algorithm. With current implementation,
103: * This algorithm does not guarantee a better rendering quality and should
104: * not be used.</p>
105: */
106: public static final String VALUE_BLUR_QUALITY_HIGH = "high";
107:
108: /**
109: * <p>Identifies a change to the size used to render the shadow.</p>
110: * <p>When the property change event is fired, the old value and the new
111: * value are provided as <code>Integer</code> instances.</p>
112: */
113: public static final String SIZE_CHANGED_PROPERTY = "shadow_size";
114:
115: /**
116: * <p>Identifies a change to the opacity used to render the shadow.</p>
117: * <p>When the property change event is fired, the old value and the new
118: * value are provided as <code>Float</code> instances.</p>
119: */
120: public static final String OPACITY_CHANGED_PROPERTY = "shadow_opacity";
121:
122: /**
123: * <p>Identifies a change to the color used to render the shadow.</p>
124: */
125: public static final String COLOR_CHANGED_PROPERTY = "shadow_color";
126:
127: // size of the shadow in pixels (defines the fuzziness)
128: private int size = 5;
129:
130: // opacity of the shadow
131: private float opacity = 0.5f;
132:
133: // color of the shadow
134: private Color color = Color.BLACK;
135:
136: // rendering hints map
137: private HashMap hints;
138:
139: // notifies listeners of properties changes
140: private PropertyChangeSupport changeSupport;
141:
142: /**
143: * <p>Creates a default good looking shadow generator.
144: * The default shadow factory provides the following default values:
145: * <ul>
146: * <li><i>size</i>: 5 pixels</li>
147: * <li><i>opacity</i>: 50%</li>
148: * <li><i>color</i>: Black</li>
149: * <li><i>rendering quality</i>: VALUE_BLUR_QUALITY_FAST</li>
150: * </ul></p>
151: * <p>These properties provide a regular, good looking shadow.</p>
152: */
153: public ShadowFactory() {
154: this (5, 0.5f, Color.BLACK);
155: }
156:
157: /**
158: * <p>A shadow factory needs three properties to generate shadows.
159: * These properties are:</p>
160: * <ul>
161: * <li><i>size</i>: The size, in pixels, of the shadow. This property also
162: * defines the fuzzyness.</li>
163: * <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li>
164: * <li><i>color</i>: The color of the shadow. Shadows are not meant to be
165: * black only.</li>
166: * </ul></p>
167: * <p>Besides these properties you can set rendering hints to control the
168: * rendering process. The default rendering hints let the factory use the
169: * fastest shadow generation algorithm.</p>
170: *
171: * @param size The size of the shadow in pixels. Defines the fuzziness.
172: * @param opacity The opacity of the shadow.
173: * @param color The color of the shadow.
174: * @see #setRenderingHint(Object,Object)
175: */
176: public ShadowFactory(final int size, final float opacity,
177: final Color color) {
178: hints = new HashMap();
179: hints.put(KEY_BLUR_QUALITY, VALUE_BLUR_QUALITY_FAST);
180:
181: changeSupport = new PropertyChangeSupport(this );
182:
183: setSize(size);
184: setOpacity(opacity);
185: setColor(color);
186: }
187:
188: /**
189: * <p>Add a PropertyChangeListener to the listener list. The listener is
190: * registered for all properties. The same listener object may be added
191: * more than once, and will be called as many times as it is added. If
192: * <code>listener</code> is null, no exception is thrown and no action
193: * is taken.</p>
194: *
195: * @param listener the PropertyChangeListener to be added
196: */
197: public void addPropertyChangeListener(
198: PropertyChangeListener listener) {
199: changeSupport.addPropertyChangeListener(listener);
200: }
201:
202: /**
203: * <p>Remove a PropertyChangeListener from the listener list. This removes
204: * a PropertyChangeListener that was registered for all properties. If
205: * <code>listener</code> was added more than once to the same event source,
206: * it will be notified one less time after being removed. If
207: * <code>listener</code> is null, or was never added, no exception is thrown
208: * and no action is taken.</p>
209: *
210: * @param listener
211: */
212: public void removePropertyChangeListener(
213: PropertyChangeListener listener) {
214: changeSupport.removePropertyChangeListener(listener);
215: }
216:
217: /**
218: * <p>Maps the specified rendering hint <code>key</code> to the specified
219: * <code>value</code> in this <code>SahdowFactory</code> object.</p>
220: *
221: * @param key The rendering hint key
222: * @param value The rendering hint value
223: */
224: public void setRenderingHint(final Object key, final Object value) {
225: hints.put(key, value);
226: }
227:
228: /**
229: * <p>Gets the color used by the factory to generate shadows.</p>
230: *
231: * @return this factory's shadow color
232: */
233: public Color getColor() {
234: return color;
235: }
236:
237: /**
238: * <p>Sets the color used by the factory to generate shadows.</p>
239: * <p>Consecutive calls to {@link #createShadow} will all use this color
240: * until it is set again.</p>
241: * <p>If the color provided is null, the previous color will be retained.</p>
242: *
243: * @param shadowColor the generated shadows color
244: */
245: public void setColor(final Color shadowColor) {
246: if (shadowColor != null) {
247: Color oldColor = this .color;
248: this .color = shadowColor;
249: changeSupport.firePropertyChange(COLOR_CHANGED_PROPERTY,
250: oldColor, this .color);
251: }
252: }
253:
254: /**
255: * <p>Gets the opacity used by the factory to generate shadows.</p>
256: * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
257: * transparent and 1.0f fully opaque.</p>
258: *
259: * @return this factory's shadow opacity
260: */
261: public float getOpacity() {
262: return opacity;
263: }
264:
265: /**
266: * <p>Sets the opacity used by the factory to generate shadows.</p>
267: * <p>Consecutive calls to {@link #createShadow} will all use this color
268: * until it is set again.</p>
269: * <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
270: * transparent and 1.0f fully opaque. If you provide a value out of these
271: * boundaries, it will be restrained to the closest boundary.</p>
272: *
273: * @param shadowOpacity the generated shadows opacity
274: */
275: public void setOpacity(final float shadowOpacity) {
276: float oldOpacity = this .opacity;
277:
278: if (shadowOpacity < 0.0) {
279: this .opacity = 0.0f;
280: } else if (shadowOpacity > 1.0f) {
281: this .opacity = 1.0f;
282: } else {
283: this .opacity = shadowOpacity;
284: }
285:
286: changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY,
287: oldOpacity, this .opacity);
288: }
289:
290: /**
291: * <p>Gets the size in pixel used by the factory to generate shadows.</p>
292: *
293: * @return this factory's shadow size
294: */
295: public int getSize() {
296: return size;
297: }
298:
299: /**
300: * <p>Sets the size, in pixels, used by the factory to generate shadows.</p>
301: * <p>The size defines the blur radius applied to the shadow to create the
302: * fuzziness.</p>
303: * <p>There is virtually no limit to the size but it has an impact on shadow
304: * generation performances. The greater this value, the longer it will take
305: * to generate the shadow. Remember the generated shadow image dimensions
306: * are computed as follow:
307: * <ul>
308: * <li>new width = original width + 2 * shadow size</li>
309: * <li>new height = original height + 2 * shadow size</li>
310: * </ul>
311: * The size cannot be negative. If you provide a negative value, the size
312: * will be 0 instead.</p>
313: *
314: * @param shadowSize the generated shadows size in pixels (fuzziness)
315: */
316: public void setSize(final int shadowSize) {
317: int oldSize = this .size;
318:
319: if (shadowSize < 0) {
320: this .size = 0;
321: } else {
322: this .size = shadowSize;
323: }
324:
325: changeSupport.firePropertyChange(SIZE_CHANGED_PROPERTY,
326: new Integer(oldSize), new Integer(this .size));
327: }
328:
329: /**
330: * <p>Generates the shadow for a given picture and the current properties
331: * of the factory.</p>
332: * <p>The generated shadow image dimensions are computed as follow:
333: * <ul>
334: * <li>new width = original width + 2 * shadow size</li>
335: * <li>new height = original height + 2 * shadow size</li>
336: * </ul></p>
337: * <p>The time taken by a call to this method depends on the size of the
338: * shadow, the larger the longer it takes, and on the selected rendering
339: * algorithm.</p>
340: *
341: * @param image the picture from which the shadow must be cast
342: * @return the picture containing the shadow of <code>image</code>
343: */
344: public BufferedImage createShadow(final BufferedImage image) {
345: if (hints.get(KEY_BLUR_QUALITY) == VALUE_BLUR_QUALITY_HIGH) {
346: // the high quality algorithm is a 3-pass algorithm
347: // it goes through all the pixels of the original picture at least
348: // three times to generate the shadow
349: // it is easy to understand but very slow
350: BufferedImage subject = prepareImage(image);
351: BufferedImage shadow = new BufferedImage(
352: subject.getWidth(), subject.getHeight(),
353: BufferedImage.TYPE_INT_ARGB);
354: BufferedImage shadowMask = createShadowMask(subject);
355: getLinearBlurOp(size).filter(shadowMask, shadow);
356: return shadow;
357: }
358:
359: // call the fast rendering algorithm
360: return createShadowFast(image);
361: }
362:
363: // prepares the picture for the high quality rendering algorithm
364: private BufferedImage prepareImage(final BufferedImage image) {
365: BufferedImage subject = new BufferedImage(image.getWidth()
366: + size * 2, image.getHeight() + size * 2,
367: BufferedImage.TYPE_INT_ARGB);
368:
369: Graphics2D g2 = subject.createGraphics();
370: g2.drawImage(image, null, size, size);
371: g2.dispose();
372:
373: return subject;
374: }
375:
376: // fast rendering algorithm
377: // basically applies duplicates the picture and applies a size*size kernel
378: // in only one pass.
379: // the kernel is simulated by an horizontal and a vertical pass
380: // implemented by Sebastien Petrucci
381: private BufferedImage createShadowFast(final BufferedImage src) {
382: int shadowSize = this .size;
383:
384: int srcWidth = src.getWidth();
385: int srcHeight = src.getHeight();
386:
387: int dstWidth = srcWidth + size;
388: int dstHeight = srcHeight + size;
389:
390: int left = (shadowSize - 1) >> 1;
391: int right = shadowSize - left;
392:
393: int yStop = dstHeight - right;
394:
395: BufferedImage dst = new BufferedImage(dstWidth, dstHeight,
396: BufferedImage.TYPE_INT_ARGB);
397:
398: int shadowRgb = color.getRGB() & 0x00FFFFFF;
399:
400: int[] aHistory = new int[shadowSize];
401: int historyIdx;
402:
403: int aSum;
404:
405: ColorModel srcColorModel = src.getColorModel();
406: WritableRaster srcRaster = src.getRaster();
407: int[] dstBuffer = ((DataBufferInt) dst.getRaster()
408: .getDataBuffer()).getData();
409:
410: int lastPixelOffset = right * dstWidth;
411: float hSumDivider = 1.0f / size;
412: float vSumDivider = opacity / size;
413:
414: // horizontal pass : extract the alpha mask from the source picture and
415: // blur it into the destination picture
416: for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) {
417:
418: // first pixels are empty
419: for (historyIdx = 0; historyIdx < shadowSize;) {
420: aHistory[historyIdx++] = 0;
421: }
422:
423: aSum = 0;
424: historyIdx = 0;
425:
426: // compute the blur average with pixels from the source image
427: for (int srcX = 0; srcX < srcWidth; srcX++) {
428:
429: int a = (int) (aSum * hSumDivider); // calculate alpha value
430: dstBuffer[dstOffset++] = a << 24; // store the alpha value only
431: // the shadow color will be added in the next pass
432:
433: aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum
434:
435: // extract the new pixel ...
436: a = srcColorModel.getAlpha(srcRaster.getDataElements(
437: srcX, srcY, null));
438: aHistory[historyIdx] = a; // ... and store its value into history
439: aSum += a; // ... and add its value to the sum
440:
441: if (++historyIdx >= shadowSize) {
442: historyIdx -= shadowSize;
443: }
444: }
445:
446: // blur the end of the row - no new pixels to grab
447: for (int i = 0; i < shadowSize; i++) {
448:
449: int a = (int) (aSum * hSumDivider);
450: dstBuffer[dstOffset++] = a << 24;
451:
452: // substract the oldest pixel from the sum ... and nothing new to add !
453: aSum -= aHistory[historyIdx];
454:
455: if (++historyIdx >= shadowSize) {
456: historyIdx -= shadowSize;
457: }
458: }
459: }
460:
461: // vertical pass
462: for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
463:
464: aSum = 0;
465:
466: // first pixels are empty
467: for (historyIdx = 0; historyIdx < left;) {
468: aHistory[historyIdx++] = 0;
469: }
470:
471: // and then they come from the dstBuffer
472: for (int y = 0; y < right; y++, bufferOffset += dstWidth) {
473: int a = dstBuffer[bufferOffset] >>> 24; // extract alpha
474: aHistory[historyIdx++] = a; // store into history
475: aSum += a; // and add to sum
476: }
477:
478: bufferOffset = x;
479: historyIdx = 0;
480:
481: // compute the blur average with pixels from the previous pass
482: for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) {
483:
484: int a = (int) (aSum * vSumDivider); // calculate alpha value
485: dstBuffer[bufferOffset] = a << 24 | shadowRgb; // store alpha value + shadow color
486:
487: aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum
488:
489: a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; // extract the new pixel ...
490: aHistory[historyIdx] = a; // ... and store its value into history
491: aSum += a; // ... and add its value to the sum
492:
493: if (++historyIdx >= shadowSize) {
494: historyIdx -= shadowSize;
495: }
496: }
497:
498: // blur the end of the column - no pixels to grab anymore
499: for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) {
500:
501: int a = (int) (aSum * vSumDivider);
502: dstBuffer[bufferOffset] = a << 24 | shadowRgb;
503:
504: aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum
505:
506: if (++historyIdx >= shadowSize) {
507: historyIdx -= shadowSize;
508: }
509: }
510: }
511:
512: return dst;
513: }
514:
515: // creates the shadow mask for the original picture
516: // it colorize all the pixels with the shadow color according to their
517: // original transparency
518: private BufferedImage createShadowMask(final BufferedImage image) {
519: BufferedImage mask = new BufferedImage(image.getWidth(), image
520: .getHeight(), BufferedImage.TYPE_INT_ARGB);
521:
522: Graphics2D g2d = mask.createGraphics();
523: g2d.drawImage(image, 0, 0, null);
524: g2d.setComposite(AlphaComposite.getInstance(
525: AlphaComposite.SRC_IN, opacity));
526: g2d.setColor(color);
527: g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
528: g2d.dispose();
529:
530: return mask;
531: }
532:
533: // creates a blur convolve operation by generating a kernel of
534: // dimensions (size, size).
535: private ConvolveOp getLinearBlurOp(final int size) {
536: float[] data = new float[size * size];
537: float value = 1.0f / (float) (size * size);
538: for (int i = 0; i < data.length; i++) {
539: data[i] = value;
540: }
541: return new ConvolveOp(new Kernel(size, size, data));
542: }
543: }
|