package org.newdawn.slick.font.effects;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.Glyph;
/**
* An effect to generate soft shadows beneath text
*
* @author Nathan Sweet <misc@n4te.com>
*/
public class ShadowEffect implements ConfigurableEffect {
/** The number of kernels to apply */
public static final int NUM_KERNELS = 16;
/** The blur kernels applied across the effect */
public static final float[][] GAUSSIAN_BLUR_KERNELS = generateGaussianBlurKernels(NUM_KERNELS);
/** The colour of the shadow to render */
private Color color = Color.black;
/** The transparency factor of the shadow */
private float opacity = 0.6f;
/** The distance on the x axis of the shadow from the text */
private float xDistance = 2;
/** The distance on the y axis of the shadow from the text */
private float yDistance = 2;
/** The size of the kernel used to blur the shadow */
private int blurKernelSize = 0;
/** The number of passes applied to create the blur */
private int blurPasses = 1;
/**
* Default constructor for injection
*/
public ShadowEffect() {
}
/**
* Create a new effect to apply a drop shadow to text
*
* @param color The colour of the shadow to generate
* @param xDistance The distance from the text on the x axis the shadow should be rendered
* @param yDistance The distance from the text on the y axis the shadow should be rendered
* @param opacity The transparency factor of the shadow
*/
public ShadowEffect (Color color, int xDistance, int yDistance, float opacity) {
this.color = color;
this.xDistance = xDistance;
this.yDistance = yDistance;
this.opacity = opacity;
}
/**
* @see org.newdawn.slick.font.effects.Effect#draw(java.awt.image.BufferedImage, java.awt.Graphics2D, org.newdawn.slick.UnicodeFont, org.newdawn.slick.font.Glyph)
*/
public void draw(BufferedImage image, Graphics2D g, UnicodeFont unicodeFont, Glyph glyph) {
g = (Graphics2D)g.create();
g.translate(xDistance, yDistance);
g.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.round(opacity * 255)));
g.fill(glyph.getShape());
// Also shadow the outline, if one exists.
for (Iterator iter = unicodeFont.getEffects().iterator(); iter.hasNext();) {
Effect effect = (Effect)iter.next();
if (effect instanceof OutlineEffect) {
Composite composite = g.getComposite();
g.setComposite(AlphaComposite.Src); // Prevent shadow and outline shadow alpha from combining.
g.setStroke(((OutlineEffect)effect).getStroke());
g.draw(glyph.getShape());
g.setComposite(composite);
break;
}
}
g.dispose();
if (blurKernelSize > 1 && blurKernelSize < NUM_KERNELS && blurPasses > 0) blur(image);
}
/**
* Apply blurring to the generate image
*
* @param image The image to be blurred
*/
private void blur(BufferedImage image) {
float[] matrix = GAUSSIAN_BLUR_KERNELS[blurKernelSize - 1];
Kernel gaussianBlur1 = new Kernel(matrix.length, 1, matrix);
Kernel gaussianBlur2 = new Kernel(1, matrix.length, matrix);
RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
ConvolveOp gaussianOp1 = new ConvolveOp(gaussianBlur1, ConvolveOp.EDGE_NO_OP, hints);
ConvolveOp gaussianOp2 = new ConvolveOp(gaussianBlur2, ConvolveOp.EDGE_NO_OP, hints);
BufferedImage scratchImage = EffectUtil.getScratchImage();
for (int i = 0; i < blurPasses; i++) {
gaussianOp1.filter(image, scratchImage);
gaussianOp2.filter(scratchImage, image);
}
}
/**
* Get the colour of the shadow generated
*
* @return The colour of the shadow generated
*/
public Color getColor() {
return color;
}
/**
* Set the colour of the shadow to be generated
*
* @param color The colour ofthe shadow to be generated
*/
public void setColor(Color color) {
this.color = color;
}
/**
* Get the distance on the X axis from the text the shadow should
* be generated at
*
* @return The distance on the X axis the shadow will be from the text
*/
public float getXDistance() {
return xDistance;
}
/**
* Sets the pixels to offset the shadow on the x axis. The glyphs will need padding so the
* shadow doesn't get clipped.
*
* @param distance The offset on the x axis
*/
public void setXDistance(float distance) {
xDistance = distance;
}
/**
* Get the distance on the Y axis from the text the shadow should
* be generated at
*
* @return The distance on the Y axis the shadow will be from the text
*/
public float getYDistance() {
return yDistance;
}
/**
* Sets the pixels to offset the shadow on the y axis. The glyphs will need
* padding so the shadow doesn't get clipped.
*
* @param distance The offset on the y axis
*/
public void setYDistance (float distance) {
yDistance = distance;
}
/**
* Get the size of the kernel used to apply the blur
*
* @return The blur kernel size
*/
public int getBlurKernelSize() {
return blurKernelSize;
}
/**
* Sets how many neighboring pixels are used to blur the shadow. Set to 0 for no blur.
*
* @param blurKernelSize The size of the kernel to apply the blur with
*/
public void setBlurKernelSize (int blurKernelSize) {
this.blurKernelSize = blurKernelSize;
}
/**
* Get the number of passes to apply the kernel for blurring
*
* @return The number of passes
*/
public int getBlurPasses() {
return blurPasses;
}
/**
* Sets the number of times to apply a blur to the shadow. Set to 0 for no blur.
*
* @param blurPasses The number of passes to apply when blurring
*/
public void setBlurPasses (int blurPasses) {
this.blurPasses = blurPasses;
}
/**
* Get the opacity of the shadow, i.e. how transparent it is
*
* @return The opacity of the shadow
*/
public float getOpacity() {
return opacity;
}
/**
* Set the opacity of the shadow, i.e. how transparent it is
*
* @param opacity The opacity of the shadow
*/
public void setOpacity(float opacity) {
this.opacity = opacity;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return "Shadow";
}
/**
* @see org.newdawn.slick.font.effects.ConfigurableEffect#getValues()
*/
public List getValues() {
List values = new ArrayList();
values.add(EffectUtil.colorValue("Color", color));
values.add(EffectUtil.floatValue("Opacity", opacity, 0, 1, "This setting sets the translucency of the shadow."));
values.add(EffectUtil.floatValue("X distance", xDistance, Float.MIN_VALUE, Float.MAX_VALUE, "This setting is the amount of pixels to offset the shadow on the"
+ " x axis. The glyphs will need padding so the shadow doesn't get clipped."));
values.add(EffectUtil.floatValue("Y distance", yDistance, Float.MIN_VALUE, Float.MAX_VALUE, "This setting is the amount of pixels to offset the shadow on the"
+ " y axis. The glyphs will need padding so the shadow doesn't get clipped."));
List options = new ArrayList();
options.add(new String[] {"None", "0"});
for (int i = 2; i < NUM_KERNELS; i++)
options.add(new String[] {String.valueOf(i)});
String[][] optionsArray = (String[][])options.toArray(new String[options.size()][]);
values.add(EffectUtil.optionValue("Blur kernel size", String.valueOf(blurKernelSize), optionsArray,
"This setting controls how many neighboring pixels are used to blur the shadow. Set to \"None\" for no blur."));
values.add(EffectUtil.intValue("Blur passes", blurPasses,
"The setting is the number of times to apply a blur to the shadow. Set to \"0\" for no blur."));
return values;
}
/**
* @see org.newdawn.slick.font.effects.ConfigurableEffect#setValues(java.util.List)
*/
public void setValues(List values) {
for (Iterator iter = values.iterator(); iter.hasNext();) {
Value value = (Value)iter.next();
if (value.getName().equals("Color")) {
color = (Color)value.getObject();
} else if (value.getName().equals("Opacity")) {
opacity = ((Float)value.getObject()).floatValue();
} else if (value.getName().equals("X distance")) {
xDistance = ((Integer)value.getObject()).floatValue();
} else if (value.getName().equals("Y distance")) {
yDistance = ((Integer)value.getObject()).floatValue();
} else if (value.getName().equals("Blur kernel size")) {
blurKernelSize = Integer.parseInt((String)value.getObject());
} else if (value.getName().equals("Blur passes")) {
blurPasses = ((Integer)value.getObject()).intValue();
}
}
}
/**
* Generate the blur kernels which will be repeatedly applied when blurring images
*
* @param level The number of kernels to generate
* @return The kernels generated
*/
private static float[][] generateGaussianBlurKernels(int level) {
float[][] pascalsTriangle = generatePascalsTriangle(level);
float[][] gaussianTriangle = new float[pascalsTriangle.length][];
for (int i = 0; i < gaussianTriangle.length; i++) {
float total = 0.0f;
gaussianTriangle[i] = new float[pascalsTriangle[i].length];
for (int j = 0; j < pascalsTriangle[i].length; j++)
total += pascalsTriangle[i][j];
float coefficient = 1 / total;
for (int j = 0; j < pascalsTriangle[i].length; j++)
gaussianTriangle[i][j] = coefficient * pascalsTriangle[i][j];
}
return gaussianTriangle;
}
/**
* Generate Pascal's triangle
*
* @param level The level of the triangle to generate
* @return The Pascal's triangle kernel
*/
private static float[][] generatePascalsTriangle(int level) {
if (level < 2) level = 2;
float[][] triangle = new float[level][];
triangle[0] = new float[1];
triangle[1] = new float[2];
triangle[0][0] = 1.0f;
triangle[1][0] = 1.0f;
triangle[1][1] = 1.0f;
for (int i = 2; i < level; i++) {
triangle[i] = new float[i + 1];
triangle[i][0] = 1.0f;
triangle[i][i] = 1.0f;
for (int j = 1; j < triangle[i].length - 1; j++)
triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
}
return triangle;
}
}