/*
* Copyright (c) 2007, Romain Guy
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of the TimingFramework project nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ import java.awt.Color; import java.awt.image.BufferedImage; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.awt.GraphicsConfiguration; import java.awt.Transparency; import java.awt.Graphics; import java.awt.GraphicsEnvironment; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.io.IOException; import java.net.URL; import javax.imageio.ImageIO; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.IOException; import javax.swing.Box; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener;
/**
* @author Romain Guy <[email protected]>
*/ public class DropShadowDemo extends JFrame { private BlurTestPanel blurTestPanel; private JSlider shadowSizeSlider; private JSlider shadowOpacitySlider; private JCheckBox fastRenderingCheck;
public DropShadowDemo() { super("Drop Shadow");
blurTestPanel = new BlurTestPanel();
add(blurTestPanel);
shadowSizeSlider = new JSlider(1, 20, 5);
shadowSizeSlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) {
blurTestPanel.setShadowSize(shadowSizeSlider.getValue());
}
});
shadowOpacitySlider = new JSlider(0, 100, 50);
shadowOpacitySlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) {
blurTestPanel.setShadowOpacity((float) shadowOpacitySlider.getValue() / 100.0f);
}
});
fastRenderingCheck = new JCheckBox("Fast rendering");
fastRenderingCheck.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) {
blurTestPanel.setFastRendering(fastRenderingCheck.isSelected());
}
});
JPanel metaControls = new JPanel(new GridLayout(3, 1));
public static ConvolveOp getGaussianBlurFilter(int radius, boolean horizontal) { if (radius < 1) { throw new IllegalArgumentException("Radius must be >= 1");
}
int size = radius * 2 + 1; float[] data = new float[size];
for (int i = -radius; i <= radius; i++) { float distance = i * i; int index = i + radius;
data[index] = (float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
total += data[index];
}
for (int i = 0; i < data.length; i++) {
data[i] /= total;
}
Kernel kernel = null; if (horizontal) {
kernel = new Kernel(size, 1, data);
} else {
kernel = new Kernel(1, size, data);
} return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
}
public static void main(String... args) {
SwingUtilities.invokeLater(new Runnable() { public void run() { new DropShadowDemo().setVisible(true);
}
});
}
}
/*
* Copyright (c) 2007, Romain Guy
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of the TimingFramework project nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* <p><code>GraphicsUtilities</code> contains a set of tools to perform
* common graphics operations easily. These operations are divided into
* several themes, listed below.</p>
* <h2>Compatible Images</h2>
* <p>Compatible images can, and should, be used to increase drawing
* performance. This class provides a number of methods to load compatible
* images directly from files or to convert existing images to compatibles
* images.</p>
* <h2>Creating Thumbnails</h2>
* <p>This class provides a number of methods to easily scale down images.
* Some of these methods offer a trade-off between speed and result quality and
* shouuld be used all the time. They also offer the advantage of producing
* compatible images, thus automatically resulting into better runtime
* performance.</p>
* <p>All these methodes are both faster than
* {@link java.awt.Image#getScaledInstance(int, int, int)} and produce
* better-looking results than the various <code>drawImage()</code> methods
* in {@link java.awt.Graphics}, which can be used for image scaling.</p>
* <h2>Image Manipulation</h2>
* <p>This class provides two methods to get and set pixels in a buffered image.
* These methods try to avoid unmanaging the image in order to keep good
* performance.</p>
*
* @author Romain Guy <[email protected]>
*/ class GraphicsUtilities { private GraphicsUtilities() {
}
// Returns the graphics configuration for the primary screen private static GraphicsConfiguration getGraphicsConfiguration() { return GraphicsEnvironment.getLocalGraphicsEnvironment().
getDefaultScreenDevice().getDefaultConfiguration();
}
/**
* <p>Returns a new <code>BufferedImage</code> using the same color model
* as the image passed as a parameter. The returned image is only compatible
* with the image passed as a parameter. This does not mean the returned
* image is compatible with the hardware.</p>
*
* @param image the reference image from which the color model of the new
* image is obtained
* @return a new <code>BufferedImage</code>, compatible with the color model
* of <code>image</code>
*/ public static BufferedImage createColorModelCompatibleImage(BufferedImage image) {
ColorModel cm = image.getColorModel(); return new BufferedImage(cm,
cm.createCompatibleWritableRaster(image.getWidth(),
image.getHeight()),
cm.isAlphaPremultiplied(), null);
}
/**
* <p>Returns a new compatible image with the same width, height and
* transparency as the image specified as a parameter.</p>
*
* @see java.awt.Transparency
* @see #createCompatibleImage(int, int)
* @see #createCompatibleImage(java.awt.image.BufferedImage, int, int)
* @see #createCompatibleTranslucentImage(int, int)
* @see #loadCompatibleImage(java.net.URL)
* @see #toCompatibleImage(java.awt.image.BufferedImage)
* @param image the reference image from which the dimension and the
* transparency of the new image are obtained
* @return a new compatible <code>BufferedImage</code> with the same
* dimension and transparency as <code>image</code>
*/ public static BufferedImage createCompatibleImage(BufferedImage image) { return createCompatibleImage(image, image.getWidth(), image.getHeight());
}
/**
* <p>Returns a new compatible image of the specified width and height, and
* the same transparency setting as the image specified as a parameter.</p>
*
* @see java.awt.Transparency
* @see #createCompatibleImage(java.awt.image.BufferedImage)
* @see #createCompatibleImage(int, int)
* @see #createCompatibleTranslucentImage(int, int)
* @see #loadCompatibleImage(java.net.URL)
* @see #toCompatibleImage(java.awt.image.BufferedImage)
* @param width the width of the new image
* @param height the height of the new image
* @param image the reference image from which the transparency of the new
* image is obtained
* @return a new compatible <code>BufferedImage</code> with the same
* transparency as <code>image</code> and the specified dimension
*/ public static BufferedImage createCompatibleImage(BufferedImage image, int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height,
image.getTransparency());
}
/**
* <p>Returns a new opaque compatible image of the specified width and
* height.</p>
*
* @see #createCompatibleImage(java.awt.image.BufferedImage)
* @see #createCompatibleImage(java.awt.image.BufferedImage, int, int)
* @see #createCompatibleTranslucentImage(int, int)
* @see #loadCompatibleImage(java.net.URL)
* @see #toCompatibleImage(java.awt.image.BufferedImage)
* @param width the width of the new image
* @param height the height of the new image
* @return a new opaque compatible <code>BufferedImage</code> of the
* specified width and height
*/ public static BufferedImage createCompatibleImage(int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height);
}
/**
* <p>Returns a new translucent compatible image of the specified width
* and height.</p>
*
* @see #createCompatibleImage(java.awt.image.BufferedImage)
* @see #createCompatibleImage(java.awt.image.BufferedImage, int, int)
* @see #createCompatibleImage(int, int)
* @see #loadCompatibleImage(java.net.URL)
* @see #toCompatibleImage(java.awt.image.BufferedImage)
* @param width the width of the new image
* @param height the height of the new image
* @return a new translucent compatible <code>BufferedImage</code> of the
* specified width and height
*/ public static BufferedImage createCompatibleTranslucentImage(int width, int height) { return getGraphicsConfiguration().createCompatibleImage(width, height,
Transparency.TRANSLUCENT);
}
/**
* <p>Returns a new compatible image from a URL. The image is loaded from the
* specified location and then turned, if necessary into a compatible
* image.</p>
*
* @see #createCompatibleImage(java.awt.image.BufferedImage)
* @see #createCompatibleImage(java.awt.image.BufferedImage, int, int)
* @see #createCompatibleImage(int, int)
* @see #createCompatibleTranslucentImage(int, int)
* @see #toCompatibleImage(java.awt.image.BufferedImage)
* @param resource the URL of the picture to load as a compatible image
* @return a new translucent compatible <code>BufferedImage</code> of the
* specified width and height
* @throws java.io.IOException if the image cannot be read or loaded
*/ public static BufferedImage loadCompatibleImage(URL resource) throws IOException {
BufferedImage image = ImageIO.read(resource); return toCompatibleImage(image);
}
/**
* <p>Return a new compatible image that contains a copy of the specified
* image. This method ensures an image is compatible with the hardware,
* and therefore optimized for fast blitting operations.</p>
*
* @see #createCompatibleImage(java.awt.image.BufferedImage)
* @see #createCompatibleImage(java.awt.image.BufferedImage, int, int)
* @see #createCompatibleImage(int, int)
* @see #createCompatibleTranslucentImage(int, int)
* @see #loadCompatibleImage(java.net.URL)
* @param image the image to copy into a new compatible image
* @return a new compatible copy, with the
* same width and height and transparency and content, of <code>image</code>
*/ public static BufferedImage toCompatibleImage(BufferedImage image) { if (image.getColorModel().equals(
getGraphicsConfiguration().getColorModel())) { return image;
}
/**
* <p>Returns a thumbnail of a source image. <code>newSize</code> defines
* the length of the longest dimension of the thumbnail. The other
* dimension is then computed according to the dimensions ratio of the
* original picture.</p>
* <p>This method favors speed over quality. When the new size is less than
* half the longest dimension of the source image,
* {@link #createThumbnail(BufferedImage, int)} or
* {@link #createThumbnail(BufferedImage, int, int)} should be used instead
* to ensure the quality of the result without sacrificing too much
* performance.</p>
*
* @see #createThumbnailFast(java.awt.image.BufferedImage, int, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int, int)
* @param image the source image
* @param newSize the length of the largest dimension of the thumbnail
* @return a new compatible <code>BufferedImage</code> containing a
* thumbnail of <code>image</code>
* @throws IllegalArgumentException if <code>newSize</code> is larger than
* the largest dimension of <code>image</code> or <= 0
*/ public static BufferedImage createThumbnailFast(BufferedImage image, int newSize) { float ratio; int width = image.getWidth(); int height = image.getHeight();
if (width > height) { if (newSize >= width) { throw new IllegalArgumentException("newSize must be lower than" +
" the image width");
} else if (newSize <= 0) { throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
ratio = (float) width / (float) height;
width = newSize;
height = (int) (newSize / ratio);
} else { if (newSize >= height) { throw new IllegalArgumentException("newSize must be lower than" +
" the image height");
} else if (newSize <= 0) { throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
/**
* <p>Returns a thumbnail of a source image.</p>
* <p>This method favors speed over quality. When the new size is less than
* half the longest dimension of the source image,
* {@link #createThumbnail(BufferedImage, int)} or
* {@link #createThumbnail(BufferedImage, int, int)} should be used instead
* to ensure the quality of the result without sacrificing too much
* performance.</p>
*
* @see #createThumbnailFast(java.awt.image.BufferedImage, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int, int)
* @param image the source image
* @param newWidth the width of the thumbnail
* @param newHeight the height of the thumbnail
* @return a new compatible <code>BufferedImage</code> containing a
* thumbnail of <code>image</code>
* @throws IllegalArgumentException if <code>newWidth</code> is larger than
* the width of <code>image</code> or if code>newHeight</code> is larger
* than the height of <code>image</code> or if one of the dimensions
* is <= 0
*/ public static BufferedImage createThumbnailFast(BufferedImage image, int newWidth, int newHeight) { if (newWidth >= image.getWidth() ||
newHeight >= image.getHeight()) { throw new IllegalArgumentException("newWidth and newHeight cannot" +
" be greater than the image" +
" dimensions");
} else if (newWidth <= 0 || newHeight <= 0) { throw new IllegalArgumentException("newWidth and newHeight must" +
" be greater than 0");
}
/**
* <p>Returns a thumbnail of a source image. <code>newSize</code> defines
* the length of the longest dimension of the thumbnail. The other
* dimension is then computed according to the dimensions ratio of the
* original picture.</p>
* <p>This method offers a good trade-off between speed and quality.
* The result looks better than
* {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when
* the new size is less than half the longest dimension of the source
* image, yet the rendering speed is almost similar.</p>
*
* @see #createThumbnailFast(java.awt.image.BufferedImage, int, int)
* @see #createThumbnailFast(java.awt.image.BufferedImage, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int, int)
* @param image the source image
* @param newSize the length of the largest dimension of the thumbnail
* @return a new compatible <code>BufferedImage</code> containing a
* thumbnail of <code>image</code>
* @throws IllegalArgumentException if <code>newSize</code> is larger than
* the largest dimension of <code>image</code> or <= 0
*/ public static BufferedImage createThumbnail(BufferedImage image, int newSize) { int width = image.getWidth(); int height = image.getHeight();
boolean isWidthGreater = width > height;
if (isWidthGreater) { if (newSize >= width) { throw new IllegalArgumentException("newSize must be lower than" +
" the image width");
}
} else if (newSize >= height) { throw new IllegalArgumentException("newSize must be lower than" +
" the image height");
}
if (newSize <= 0) { throw new IllegalArgumentException("newSize must" +
" be greater than 0");
}
thumb = temp;
} while (newSize != (isWidthGreater ? width : height));
return thumb;
}
/**
* <p>Returns a thumbnail of a source image.</p>
* <p>This method offers a good trade-off between speed and quality.
* The result looks better than
* {@link #createThumbnailFast(java.awt.image.BufferedImage, int)} when
* the new size is less than half the longest dimension of the source
* image, yet the rendering speed is almost similar.</p>
*
* @see #createThumbnailFast(java.awt.image.BufferedImage, int)
* @see #createThumbnailFast(java.awt.image.BufferedImage, int, int)
* @see #createThumbnail(java.awt.image.BufferedImage, int)
* @param image the source image
* @param newWidth the width of the thumbnail
* @param newHeight the height of the thumbnail
* @return a new compatible <code>BufferedImage</code> containing a
* thumbnail of <code>image</code>
* @throws IllegalArgumentException if <code>newWidth</code> is larger than
* the width of <code>image</code> or if code>newHeight</code> is larger
* than the height of <code>image or if one the dimensions is not > 0</code>
*/ public static BufferedImage createThumbnail(BufferedImage image, int newWidth, int newHeight) { int width = image.getWidth(); int height = image.getHeight();
if (newWidth >= width || newHeight >= height) { throw new IllegalArgumentException("newWidth and newHeight cannot" +
" be greater than the image" +
" dimensions");
} else if (newWidth <= 0 || newHeight <= 0) { throw new IllegalArgumentException("newWidth and newHeight must" +
" be greater than 0");
}
BufferedImage thumb = image;
do { if (width > newWidth) {
width /= 2; if (width < newWidth) {
width = newWidth;
}
}
if (height > newHeight) {
height /= 2; if (height < newHeight) {
height = newHeight;
}
}
thumb = temp;
} while (width != newWidth || height != newHeight);
return thumb;
}
/**
* <p>Returns an array of pixels, stored as integers, from a
* <code>BufferedImage</code>. The pixels are grabbed from a rectangular
* area defined by a location and two dimensions. Calling this method on
* an image of type different from <code>BufferedImage.TYPE_INT_ARGB</code>
* and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p>
*
* @param img the source image
* @param x the x location at which to start grabbing pixels
* @param y the y location at which to start grabbing pixels
* @param w the width of the rectangle of pixels to grab
* @param h the height of the rectangle of pixels to grab
* @param pixels a pre-allocated array of pixels of size w*h; can be null
* @return <code>pixels</code> if non-null, a new array of integers
* otherwise
* @throws IllegalArgumentException is <code>pixels</code> is non-null and
* of length < w*h
*/ public static int[] getPixels(BufferedImage img, int x, int y, int w, int h, int[] pixels) { if (w == 0 || h == 0) { return new int[0];
}
if (pixels == null) {
pixels = new int[w * h];
} else if (pixels.length < w * h) { throw new IllegalArgumentException("pixels array must have a length" +
" >= w*h");
}
int imageType = img.getType(); if (imageType == BufferedImage.TYPE_INT_ARGB ||
imageType == BufferedImage.TYPE_INT_RGB) {
Raster raster = img.getRaster(); return (int[]) raster.getDataElements(x, y, w, h, pixels);
}
// Unmanages the image return img.getRGB(x, y, w, h, pixels, 0, w);
}
/**
* <p>Writes a rectangular area of pixels in the destination
* <code>BufferedImage</code>. Calling this method on
* an image of type different from <code>BufferedImage.TYPE_INT_ARGB</code>
* and <code>BufferedImage.TYPE_INT_RGB</code> will unmanage the image.</p>
*
* @param img the destination image
* @param x the x location at which to start storing pixels
* @param y the y location at which to start storing pixels
* @param w the width of the rectangle of pixels to store
* @param h the height of the rectangle of pixels to store
* @param pixels an array of pixels, stored as integers
* @throws IllegalArgumentException is <code>pixels</code> is non-null and
* of length < w*h
*/ public static void setPixels(BufferedImage img, int x, int y, int w, int h, int[] pixels) { if (pixels == null || w == 0 || h == 0) { return;
} else if (pixels.length < w * h) { throw new IllegalArgumentException("pixels array must have a length" +
" >= w*h");
}
int imageType = img.getType(); if (imageType == BufferedImage.TYPE_INT_ARGB ||
imageType == BufferedImage.TYPE_INT_RGB) {
WritableRaster raster = img.getRaster();
raster.setDataElements(x, y, w, h, pixels);
} else {
// Unmanages the image
img.setRGB(x, y, w, h, pixels, 0, w);
}
}
}
/*
* $Id: ShadowRenderer.java,v 1.1 2007/01/15 23:39:23 gfx Exp $
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* Licensed under LGPL.
*/
/**
* <p>A shadow renderer generates a drop shadow for any given picture, respecting
* the transparency channel if present. The resulting picture contains the
* shadow only and to create a drop shadow effect you will need to stack the
* original picture and the shadow generated by the renderer.</p>
* <h2>Shadow Properties</h2>
* <p>A shadow is defined by three properties:
* <ul>
* <li><i>size</i>: The size, in pixels, of the shadow. This property also
* defines the fuzzyness.</li>
* <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li>
* <li><i>color</i>: The color of the shadow. Shadows are not meant to be
* black only.</li>
* </ul>
* You can set these properties using the provided mutaters or the appropriate
* constructor. Here are two ways of creating a green shadow of size 10 and
* with an opacity of 50%:
* <pre>
* ShadowRenderer renderer = new ShadowRenderer(10, 0.5f, Color.GREEN);
* // ..
* renderer = new ShadowRenderer();
* renderer.setSize(10);
* renderer.setOpacity(0.5f);
* renderer.setColor(Color.GREEN);
* </pre>
* The default constructor provides the following default values:
* <ul>
* <li><i>size</i>: 5 pixels</li>
* <li><i>opacity</i>: 50%</li>
* <li><i>color</i>: Black</li>
* </ul></p>
* <h2>Generating a Shadow</h2>
* <p>A shadow is generated as a <code>BufferedImage</code> from another
* <code>BufferedImage</code>. Once the renderer is set up, you must call
* {@link #createShadow} to actually generate the shadow:
* <pre>
* ShadowRenderer renderer = new ShadowRenderer();
* // renderer setup
* BufferedImage shadow = renderer.createShadow(bufferedImage);
* </pre></p>
* <p>The generated image dimensions are computed as following:</p>
* <pre>
* width = imageWidth + 2 * shadowSize
* height = imageHeight + 2 * shadowSize
* </pre>
* <h2>Properties Changes</h2>
* <p>This renderer allows to register property change listeners with
* {@link #addPropertyChangeListener}. Listening to properties changes is very
* useful when you emebed the renderer in a graphical component and give the API
* user the ability to access the renderer. By listening to properties changes,
* you can easily repaint the component when needed.</p>
* <h2>Threading Issues</h2>
* <p><code>ShadowRenderer</code> is not guaranteed to be thread-safe.</p>
*
* @author Romain Guy <[email protected]>
* @author Sebastien Petrucci
*/ class ShadowRenderer {
/**
* <p>Identifies a change to the size used to render the shadow.</p>
* <p>When the property change event is fired, the old value and the new
* value are provided as <code>Integer</code> instances.</p>
*/ public static final String SIZE_CHANGED_PROPERTY = "shadow_size";
/**
* <p>Identifies a change to the opacity used to render the shadow.</p>
* <p>When the property change event is fired, the old value and the new
* value are provided as <code>Float</code> instances.</p>
*/ public static final String OPACITY_CHANGED_PROPERTY = "shadow_opacity";
/**
* <p>Identifies a change to the color used to render the shadow.</p>
*/ public static final String COLOR_CHANGED_PROPERTY = "shadow_color";
// size of the shadow in pixels (defines the fuzziness) private int size = 5;
// opacity of the shadow private float opacity = 0.5f;
// color of the shadow private Color color = Color.BLACK;
// notifies listeners of properties changes private PropertyChangeSupport changeSupport;
/**
* <p>Creates a default good looking shadow generator.
* The default shadow renderer provides the following default values:
* <ul>
* <li><i>size</i>: 5 pixels</li>
* <li><i>opacity</i>: 50%</li>
* <li><i>color</i>: Black</li>
* </ul></p>
* <p>These properties provide a regular, good looking shadow.</p>
*/ public ShadowRenderer() { this(5, 0.5f, Color.BLACK);
}
/**
* <p>A shadow renderer needs three properties to generate shadows.
* These properties are:</p>
* <ul>
* <li><i>size</i>: The size, in pixels, of the shadow. This property also
* defines the fuzzyness.</li>
* <li><i>opacity</i>: The opacity, between 0.0 and 1.0, of the shadow.</li>
* <li><i>color</i>: The color of the shadow. Shadows are not meant to be
* black only.</li>
* </ul>
* @param size the size of the shadow in pixels. Defines the fuzziness.
* @param opacity the opacity of the shadow.
* @param color the color of the shadow.
*/ public ShadowRenderer(final int size, final float opacity, final Color color) {
//noinspection ThisEscapedInObjectConstruction
changeSupport = new PropertyChangeSupport(this);
/**
* <p>Add a PropertyChangeListener to the listener list. The listener is
* registered for all properties. The same listener object may be added
* more than once, and will be called as many times as it is added. If
* <code>listener</code> is null, no exception is thrown and no action
* is taken.</p>
* @param listener the PropertyChangeListener to be added
*/ public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* <p>Remove a PropertyChangeListener from the listener list. This removes
* a PropertyChangeListener that was registered for all properties. If
* <code>listener</code> was added more than once to the same event source,
* it will be notified one less time after being removed. If
* <code>listener</code> is null, or was never added, no exception is thrown
* and no action is taken.</p>
* @param listener the PropertyChangeListener to be removed
*/ public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* <p>Gets the color used by the renderer to generate shadows.</p>
* @return this renderer's shadow color
*/ public Color getColor() { return color;
}
/**
* <p>Sets the color used by the renderer to generate shadows.</p>
* <p>Consecutive calls to {@link #createShadow} will all use this color
* until it is set again.</p>
* <p>If the color provided is null, the previous color will be retained.</p>
* @param shadowColor the generated shadows color
*/ public void setColor(final Color shadowColor) { if (shadowColor != null) {
Color oldColor = this.color; this.color = shadowColor;
changeSupport.firePropertyChange(COLOR_CHANGED_PROPERTY,
oldColor, this.color);
}
}
/**
* <p>Gets the opacity used by the renderer to generate shadows.</p>
* <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
* transparent and 1.0f fully opaque.</p>
* @return this renderer's shadow opacity
*/ public float getOpacity() { return opacity;
}
/**
* <p>Sets the opacity used by the renderer to generate shadows.</p>
* <p>Consecutive calls to {@link #createShadow} will all use this opacity
* until it is set again.</p>
* <p>The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
* transparent and 1.0f fully opaque. If you provide a value out of these
* boundaries, it will be restrained to the closest boundary.</p>
* @param shadowOpacity the generated shadows opacity
*/ public void setOpacity(final float shadowOpacity) { float oldOpacity = this.opacity;
/**
* <p>Gets the size in pixel used by the renderer to generate shadows.</p>
* @return this renderer's shadow size
*/ public int getSize() { return size;
}
/**
* <p>Sets the size, in pixels, used by the renderer to generate shadows.</p>
* <p>The size defines the blur radius applied to the shadow to create the
* fuzziness.</p>
* <p>There is virtually no limit to the size. The size cannot be negative.
* If you provide a negative value, the size will be 0 instead.</p>
* @param shadowSize the generated shadows size in pixels (fuzziness)
*/ public void setSize(final int shadowSize) { int oldSize = this.size;
changeSupport.firePropertyChange(SIZE_CHANGED_PROPERTY, new Integer(oldSize), new Integer(this.size));
}
/**
* <p>Generates the shadow for a given picture and the current properties
* of the renderer.</p>
* <p>The generated image dimensions are computed as following:</p>
* <pre>
* width = imageWidth + 2 * shadowSize
* height = imageHeight + 2 * shadowSize
* </pre>
* @param image the picture from which the shadow must be cast
* @return the picture containing the shadow of <code>image</code>
*/ public BufferedImage createShadow(final BufferedImage image) {
// Written by Sesbastien Petrucci int shadowSize = size * 2;
int srcWidth = image.getWidth(); int srcHeight = image.getHeight();
int dstWidth = srcWidth + shadowSize; int dstHeight = srcHeight + shadowSize;
int left = size; int right = shadowSize - left;
int yStop = dstHeight - right;
int shadowRgb = color.getRGB() & 0x00FFFFFF; int[] aHistory = new int[shadowSize]; int historyIdx;
int aSum;
BufferedImage dst = new BufferedImage(dstWidth, dstHeight,
BufferedImage.TYPE_INT_ARGB);
int[] dstBuffer = new int[dstWidth * dstHeight]; int[] srcBuffer = new int[srcWidth * srcHeight];
int lastPixelOffset = right * dstWidth; float hSumDivider = 1.0f / shadowSize; float vSumDivider = opacity / shadowSize;
int[] hSumLookup = new int[256 * shadowSize]; for (int i = 0; i < hSumLookup.length; i++) {
hSumLookup[i] = (int) (i * hSumDivider);
}
int[] vSumLookup = new int[256 * shadowSize]; for (int i = 0; i < vSumLookup.length; i++) {
vSumLookup[i] = (int) (i * vSumDivider);
}
int srcOffset;
// horizontal pass : extract the alpha mask from the source picture and
// blur it into the destination picture for (int srcY = 0, dstOffset = left * dstWidth; srcY < srcHeight; srcY++) {
// first pixels are empty for (historyIdx = 0; historyIdx < shadowSize; ) {
aHistory[historyIdx++] = 0;
}
// compute the blur average with pixels from the source image for (int srcX = 0; srcX < srcWidth; srcX++) {
int a = hSumLookup[aSum];
dstBuffer[dstOffset++] = a << 24; // store the alpha value only
// the shadow color will be added in the next pass
aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum
// extract the new pixel ...
a = srcBuffer[srcOffset + srcX] >>> 24;
aHistory[historyIdx] = a; // ... and store its value into history
aSum += a; // ... and add its value to the sum
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
// blur the end of the row - no new pixels to grab for (int i = 0; i < shadowSize; i++) {
int a = hSumLookup[aSum];
dstBuffer[dstOffset++] = a << 24;
// substract the oldest pixel from the sum ... and nothing new to add !
aSum -= aHistory[historyIdx];
// vertical pass for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
aSum = 0;
// first pixels are empty for (historyIdx = 0; historyIdx < left;) {
aHistory[historyIdx++] = 0;
}
// and then they come from the dstBuffer for (int y = 0; y < right; y++, bufferOffset += dstWidth) { int a = dstBuffer[bufferOffset] >>> 24; // extract alpha
aHistory[historyIdx++] = a; // store into history
aSum += a; // and add to sum
}
bufferOffset = x;
historyIdx = 0;
// compute the blur avera`ge with pixels from the previous pass for (int y = 0; y < yStop; y++, bufferOffset += dstWidth) {
int a = vSumLookup[aSum];
dstBuffer[bufferOffset] = a << 24 | shadowRgb; // store alpha value + shadow color
aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum
a = dstBuffer[bufferOffset + lastPixelOffset] >>> 24; // extract the new pixel ...
aHistory[historyIdx] = a; // ... and store its value into history
aSum += a; // ... and add its value to the sum
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
// blur the end of the column - no pixels to grab anymore for (int y = yStop; y < dstHeight; y++, bufferOffset += dstWidth) {
int a = vSumLookup[aSum];
dstBuffer[bufferOffset] = a << 24 | shadowRgb;
aSum -= aHistory[historyIdx]; // substract the oldest pixel from the sum