org.jdesktop.swingx.graphics.ReflectionRenderer Maven / Gradle / Ivy
Show all versions of swingx-all Show documentation
/*
* $Id$
*
* Dual-licensed under LGPL (Sun and Romain Guy) and BSD (Romain Guy).
*
* Copyright 2006 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* Copyright (c) 2006 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:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. 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.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
*/
package org.jdesktop.swingx.graphics;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import org.jdesktop.swingx.image.StackBlurFilter;
import org.jdesktop.swingx.util.GraphicsUtilities;
/**
* A reflection renderer generates the reflection of a given picture. The
* result can be either the reflection itself, or an image containing both the
* source image and its reflection.
* Reflection Properties
* A reflection is defined by three properties:
*
* - opacity: the opacity of the reflection. You will usually
* change this valued according to the background color.
* - length: the length of the reflection. The length is a fraction
* of the height of the source image.
* - blur enabled: perfect reflections are hardly natural. You can
* blur the reflection to make it look a bit more natural.
*
* You can set these properties using the provided mutators or the appropriate
* constructor. Here are two ways of creating a blurred reflection, with an
* opacity of 50% and a length of 30% the height of the original image:
*
* ReflectionRenderer renderer = new ReflectionRenderer(0.5f, 0.3f, true);
* // ..
* renderer = new ReflectionRenderer();
* renderer.setOpacity(0.5f);
* renderer.setLength(0.3f);
* renderer.setBlurEnabled(true);
*
* The default constructor provides the following default values:
*
* - opacity: 35%
* - length: 40%
* - blur enabled: false
*
* Generating Reflections
* A reflection is generated as a BufferedImage
from another
* BufferedImage
. Once the renderer is set up, you must call
* {@link #createReflection(java.awt.image.BufferedImage)} to actually generate
* the reflection:
*
* ReflectionRenderer renderer = new ReflectionRenderer();
* // renderer setup
* BufferedImage reflection = renderer.createReflection(bufferedImage);
*
* The returned image contains only the reflection. You will have to append
* it to the source image at painting time to get a realistic results. You can
* also asks the rendered to return a picture composed of both the source image
* and its reflection:
*
* ReflectionRenderer renderer = new ReflectionRenderer();
* // renderer setup
* BufferedImage reflection = renderer.appendReflection(bufferedImage);
*
* Properties Changes
* This renderer allows to register property change listeners with
* {@link #addPropertyChangeListener}. Listening to properties changes is very
* useful when you embed 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.
* Threading Issues
* ReflectionRenderer
is not guaranteed to be thread-safe.
*
* @author Romain Guy
*/
public class ReflectionRenderer {
/**
* Identifies a change to the opacity used to render the reflection.
*/
public static final String OPACITY_CHANGED_PROPERTY = "reflection_opacity";
/**
* Identifies a change to the length of the rendered reflection.
*/
public static final String LENGTH_CHANGED_PROPERTY = "reflection_length";
/**
* Identifies a change to the blurring of the rendered reflection.
*/
public static final String BLUR_ENABLED_CHANGED_PROPERTY = "reflection_blur";
// opacity of the reflection
private float opacity;
// length of the reflection
private float length;
// should the reflection be blurred?
private boolean blurEnabled;
// notifies listeners of properties changes
private PropertyChangeSupport changeSupport;
private StackBlurFilter stackBlurFilter;
/**
* Creates a default good looking reflections generator.
* The default reflection renderer provides the following default values:
*
* - opacity: 35%
* - length: 40%
* - blurring: disabled with a radius of 1 pixel
*
* These properties provide a regular, good looking reflection.
*
* @see #getOpacity()
* @see #setOpacity(float)
* @see #getLength()
* @see #setLength(float)
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #getBlurRadius()
* @see #setBlurRadius(int)
*/
public ReflectionRenderer() {
this(0.35f, 0.4f, false);
}
/**
* Creates a default good looking reflections generator with the
* specified opacity. The default reflection renderer provides the following
* default values:
*
* - length: 40%
* - blurring: disabled with a radius of 1 pixel
*
*
* @param opacity the opacity of the reflection, between 0.0 and 1.0
* @see #getOpacity()
* @see #setOpacity(float)
* @see #getLength()
* @see #setLength(float)
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #getBlurRadius()
* @see #setBlurRadius(int)
*/
public ReflectionRenderer(float opacity) {
this(opacity, 0.4f, false);
}
/**
* Creates a reflections generator with the specified properties. Both
* opacity and length are numbers between 0.0 (0%) and 1.0 (100%). If the
* provided numbers are outside this range, they are clamped.
* Enabling the blur generates a different kind of reflections that might
* look more natural. The default blur radius is 1 pixel
*
* @param opacity the opacity of the reflection
* @param length the length of the reflection
* @param blurEnabled if true, the reflection is blurred
* @see #getOpacity()
* @see #setOpacity(float)
* @see #getLength()
* @see #setLength(float)
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #getBlurRadius()
* @see #setBlurRadius(int)
*/
public ReflectionRenderer(float opacity, float length, boolean blurEnabled) {
//noinspection ThisEscapedInObjectConstruction
this.changeSupport = new PropertyChangeSupport(this);
this.stackBlurFilter = new StackBlurFilter(1);
setOpacity(opacity);
setLength(length);
setBlurEnabled(blurEnabled);
}
/**
* 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
* listener
is null, no exception is thrown and no action
* is taken.
*
* @param listener the PropertyChangeListener to be added
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* Remove a PropertyChangeListener from the listener list. This removes
* a PropertyChangeListener that was registered for all properties. If
* listener
was added more than once to the same event source,
* it will be notified one less time after being removed. If
* listener
is null, or was never added, no exception is thrown
* and no action is taken.
*
* @param listener the PropertyChangeListener to be removed
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* Gets the opacity used by the factory to generate reflections.
* The opacity is comprised between 0.0f and 1.0f; 0.0f being fully
* transparent and 1.0f fully opaque.
*
* @return this factory's shadow opacity
* @see #getOpacity()
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public float getOpacity() {
return opacity;
}
/**
* Sets the opacity used by the factory to generate reflections.
* Consecutive calls to {@link #createReflection} will all use this
* opacity until it is set again.
* 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.
*
* @param opacity the generated reflection opacity
* @see #setOpacity(float)
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public void setOpacity(float opacity) {
float oldOpacity = this.opacity;
if (opacity < 0.0f) {
opacity = 0.0f;
} else if (opacity > 1.0f) {
opacity = 1.0f;
}
if (oldOpacity != opacity) {
this.opacity = opacity;
changeSupport.firePropertyChange(OPACITY_CHANGED_PROPERTY,
oldOpacity,
this.opacity);
}
}
/**
* Returns the length of the reflection. The result is a number between
* 0.0 and 1.0. This number is the fraction of the height of the source
* image that is used to compute the size of the reflection.
*
* @return the length of the reflection, as a fraction of the source image
* height
* @see #setLength(float)
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public float getLength() {
return length;
}
/**
* Sets the length of the reflection, as a fraction of the height of the
* source image.
* Consecutive calls to {@link #createReflection} will all use this
* opacity until it is set again.
* 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.
*
* @param length the length of the reflection, as a fraction of the source
* image height
* @see #getLength()
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public void setLength(float length) {
float oldLength = this.length;
if (length < 0.0f) {
length = 0.0f;
} else if (length > 1.0f) {
length = 1.0f;
}
if (oldLength != length) {
this.length = length;
changeSupport.firePropertyChange(LENGTH_CHANGED_PROPERTY,
oldLength,
this.length);
}
}
/**
* Returns true if the blurring of the reflection is enabled, false
* otherwise. When blurring is enabled, the reflection is blurred to look
* more natural.
*
* @return true if blur is enabled, false otherwise
* @see #setBlurEnabled(boolean)
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public boolean isBlurEnabled() {
return blurEnabled;
}
/**
* Setting the blur to true will enable the blurring of the reflection
* when {@link #createReflection} is invoked.
* Enabling the blurring of the reflection can yield to more natural
* results which may or may not be better looking, depending on the source
* picture.
* Consecutive calls to {@link #createReflection} will all use this
* opacity until it is set again.
*
* @param blurEnabled true to enable the blur, false otherwise
* @see #isBlurEnabled()
* @see #createReflection(java.awt.image.BufferedImage)
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public void setBlurEnabled(boolean blurEnabled) {
if (blurEnabled != this.blurEnabled) {
boolean oldBlur = this.blurEnabled;
this.blurEnabled= blurEnabled;
changeSupport.firePropertyChange(BLUR_ENABLED_CHANGED_PROPERTY,
oldBlur,
this.blurEnabled);
}
}
/**
* Returns the effective radius, in pixels, of the blur used by this
* renderer when {@link #isBlurEnabled()} is true.
*
* @return the effective radius of the blur used when
* isBlurEnabled
is true
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #setBlurRadius(int)
* @see #getBlurRadius()
*/
public int getEffectiveBlurRadius() {
return stackBlurFilter.getEffectiveRadius();
}
/**
* Returns the radius, in pixels, of the blur used by this renderer when
* {@link #isBlurEnabled()} is true.
*
* @return the radius of the blur used when isBlurEnabled
* is true
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #setBlurRadius(int)
* @see #getEffectiveBlurRadius()
*/
public int getBlurRadius() {
return stackBlurFilter.getRadius();
}
/**
* Sets the radius, in pixels, of the blur used by this renderer when
* {@link #isBlurEnabled()} is true. This radius changes the size of the
* generated image when blurring is applied.
*
* @param radius the radius, in pixels, of the blur
* @see #isBlurEnabled()
* @see #setBlurEnabled(boolean)
* @see #getBlurRadius()
*/
public void setBlurRadius(int radius) {
this.stackBlurFilter = new StackBlurFilter(radius);
}
/**
* Returns the source image and its reflection. The appearance of the
* reflection is defined by the opacity, the length and the blur
* properties.
* * The width of the generated image will be augmented when
* {@link #isBlurEnabled()} is true. The generated image will have the width
* of the source image plus twice the effective blur radius (see
* {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the
* width will be augmented by 6. You might need to take this into account
* at drawing time.
* The returned image height depends on the value returned by
* {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance,
* if the length is 0.5 (or 50%) and the source image is 480 pixels high,
* then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.
* You can create only the reflection by calling
* {@link #createReflection(java.awt.image.BufferedImage)}.
*
* @param image the source image
* @return the source image with its reflection below
* @see #createReflection(java.awt.image.BufferedImage)
*/
public BufferedImage appendReflection(BufferedImage image) {
BufferedImage reflection = createReflection(image);
BufferedImage buffer = GraphicsUtilities.createCompatibleTranslucentImage(
reflection.getWidth(), image.getHeight() + reflection.getHeight());
Graphics2D g2 = buffer.createGraphics();
try {
int effectiveRadius = isBlurEnabled() ? stackBlurFilter
.getEffectiveRadius() : 0;
g2.drawImage(image, effectiveRadius, 0, null);
g2.drawImage(reflection, 0, image.getHeight() - effectiveRadius,
null);
} finally {
g2.dispose();
}
reflection.flush();
return buffer;
}
/**
* Returns the reflection of the source image. The appearance of the
* reflection is defined by the opacity, the length and the blur
* properties.
* * The width of the generated image will be augmented when
* {@link #isBlurEnabled()} is true. The generated image will have the width
* of the source image plus twice the effective blur radius (see
* {@link #getEffectiveBlurRadius()}). The default blur radius is 1 so the
* width will be augmented by 6. You might need to take this into account
* at drawing time.
* The returned image height depends on the value returned by
* {@link #getLength()} and {@link #getEffectiveBlurRadius()}. For instance,
* if the length is 0.5 (or 50%) and the source image is 480 pixels high,
* then the reflection will be 246 (480 * 0.5 + 3 * 2) pixels high.
* The returned image contains only
* the reflection. You will have to append it to the source image to produce
* the illusion of a reflective environment. The method
* {@link #appendReflection(java.awt.image.BufferedImage)} provides an easy
* way to create an image containing both the source and the reflection.
*
* @param image the source image
* @return the reflection of the source image
* @see #appendReflection(java.awt.image.BufferedImage)
*/
public BufferedImage createReflection(BufferedImage image) {
if (length == 0.0f) {
return GraphicsUtilities.createCompatibleTranslucentImage(1, 1);
}
int blurOffset = isBlurEnabled() ?
stackBlurFilter.getEffectiveRadius() : 0;
int height = (int) (image.getHeight() * length);
BufferedImage buffer =
GraphicsUtilities.createCompatibleTranslucentImage(
image.getWidth() + blurOffset * 2,
height + blurOffset * 2);
Graphics2D g2 = buffer.createGraphics();
try {
g2.translate(0, image.getHeight());
g2.scale(1.0, -1.0);
g2.drawImage(image, blurOffset, -blurOffset, null);
g2.scale(1.0, -1.0);
g2.translate(0, -image.getHeight());
g2.setComposite(AlphaComposite.DstIn);
g2.setPaint(new GradientPaint(0.0f, 0.0f, new Color(0.0f, 0.0f,
0.0f, getOpacity()), 0.0f, buffer.getHeight(), new Color(
0.0f, 0.0f, 0.0f, 0.0f), true));
g2.fillRect(0, 0, buffer.getWidth(), buffer.getHeight());
} finally {
g2.dispose();
}
return isBlurEnabled() ? stackBlurFilter.filter(buffer, null) :
buffer;
}
}