blob: 40b777d4574c53a92296f9143989afafc396369d [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.rendering;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
public class ShadowPainter {
/**
* Adds a drop shadow to a semi-transparent image (of an arbitrary shape) and returns it as a new image.
* This method attempts to mimic the same visual characteristics as the rectangular shadow painting
* methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)} and
* {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}.
*
* @param source the source image
* @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link #SMALL_SHADOW_SIZE}}
* @return a new image with the shadow painted in
*/
@NotNull
public static BufferedImage createDropShadow(BufferedImage source, int shadowSize) {
shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class
return createDropShadow(source, shadowSize, 0.7f, 0);
}
/**
* Creates a drop shadow of a given image and returns a new image which shows the
* input image on top of its drop shadow.
* <p>
* <b>NOTE: If the shape is rectangular and opaque, consider using
* {@link #drawRectangleShadow(java.awt.Graphics, int, int, int, int)} instead.</b>
*
* @param source the source image to be shadowed
* @param shadowSize the size of the shadow in pixels
* @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque
* @param shadowRgb the RGB int to use for the shadow color
* @return a new image with the source image on top of its shadow
*/
@SuppressWarnings({"AssignmentToForLoopParameter", "UnnecessaryLocalVariable", // Imported code
"SuspiciousNameCombination", "UnusedAssignment"})
public static BufferedImage createDropShadow(BufferedImage source, int shadowSize,
float shadowOpacity, int shadowRgb) {
// This code is based on
// http://www.jroller.com/gfx/entry/non_rectangular_shadow
BufferedImage image;
int width = source.getWidth();
int height = source.getHeight();
boolean isRetina = ImageUtils.isRetinaImage(source);
if (isRetina && UIUtil.isAppleRetina()) {
// This shadow painting code doesn't work right with the Apple JDK 6 Retina support.
// Since this code isn't used very frequently, just skip the drop shadows for now
return source;
}
if (isRetina) {
image = ImageUtils.createDipImage(width + SHADOW_SIZE, height + SHADOW_SIZE, BufferedImage.TYPE_INT_ARGB);
} else {
//noinspection UndesirableClassUsage
image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, BufferedImage.TYPE_INT_ARGB);
}
Graphics2D g2 = image.createGraphics();
//noinspection ConstantConditions
UIUtil.drawImage(g2, source, shadowSize, shadowSize, null);
int dstWidth = image.getWidth();
int dstHeight = image.getHeight();
int left = (shadowSize - 1) >> 1;
int right = shadowSize - left;
int xStart = left;
int xStop = dstWidth - right;
int yStart = left;
int yStop = dstHeight - right;
shadowRgb &= 0x00FFFFFF;
int[] aHistory = new int[shadowSize];
int historyIdx = 0;
int aSum;
int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int lastPixelOffset = right * dstWidth;
float sumDivider = shadowOpacity / shadowSize;
// horizontal pass
for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
aSum = 0;
historyIdx = 0;
for (int x = 0; x < shadowSize; x++, bufferOffset++) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[x] = a;
aSum += a;
}
bufferOffset -= right;
for (int x = xStart; x < xStop; x++, bufferOffset++) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + right] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
// vertical pass
for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
aSum = 0;
historyIdx = 0;
for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[y] = a;
aSum += a;
}
bufferOffset -= lastPixelOffset;
for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
//noinspection ConstantConditions
UIUtil.drawImage(g2, source, null, 0, 0);
g2.dispose();
return image;
}
/**
* Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by
* {@link #SHADOW_SIZE} around the given source and returns a new image with
* both combined
*
* @param source the source image
* @return the source image with a drop shadow on the bottom and right
*/
public static BufferedImage createRectangularDropShadow(BufferedImage source) {
int type = source.getType();
if (type == BufferedImage.TYPE_CUSTOM) {
type = BufferedImage.TYPE_INT_ARGB;
}
int width = source.getWidth();
int height = source.getHeight();
BufferedImage image;
if (ImageUtils.isRetinaImage(source)) {
image = ImageUtils.createDipImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
} else {
//noinspection UndesirableClassUsage
image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type);
}
Graphics g = image.getGraphics();
//noinspection ConstantConditions
UIUtil.drawImage(g, source, 0, 0, null);
drawRectangleShadow(image, 0, 0, width, height);
g.dispose();
return image;
}
/**
* Draws a small rectangular drop shadow (of size {@link #SMALL_SHADOW_SIZE} by
* {@link #SMALL_SHADOW_SIZE} around the given source and returns a new image with
* both combined
*
* @param source the source image
* @return the source image with a drop shadow on the bottom and right
*/
public static BufferedImage createSmallRectangularDropShadow(BufferedImage source) {
int type = source.getType();
if (type == BufferedImage.TYPE_CUSTOM) {
type = BufferedImage.TYPE_INT_ARGB;
}
int width = source.getWidth();
int height = source.getHeight();
BufferedImage image;
if (ImageUtils.isRetinaImage(source)) {
image = ImageUtils.createDipImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type);
} else {
//noinspection UndesirableClassUsage
image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type);
}
Graphics g = image.getGraphics();
//noinspection ConstantConditions
UIUtil.drawImage(g, source, 0, 0, null);
drawSmallRectangleShadow(image, 0, 0, width, height);
g.dispose();
return image;
}
/**
* Draws a drop shadow for the given rectangle into the given context. It
* will not draw anything if the rectangle is smaller than a minimum
* determined by the assets used to draw the shadow graphics.
* The size of the shadow is {@link #SHADOW_SIZE}.
*
* @param image the image to draw the shadow into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawRectangleShadow(BufferedImage image,
int x, int y, int width, int height) {
Graphics gc = image.getGraphics();
try {
drawRectangleShadow(gc, x, y, width, height);
} finally {
gc.dispose();
}
}
/**
* Draws a small drop shadow for the given rectangle into the given context. It
* will not draw anything if the rectangle is smaller than a minimum
* determined by the assets used to draw the shadow graphics.
* The size of the shadow is {@link #SMALL_SHADOW_SIZE}.
*
* @param image the image to draw the shadow into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
public static void drawSmallRectangleShadow(BufferedImage image,
int x, int y, int width, int height) {
Graphics gc = image.getGraphics();
try {
drawSmallRectangleShadow(gc, x, y, width, height);
} finally {
gc.dispose();
}
}
/**
* The width and height of the drop shadow painted by
* {@link #drawRectangleShadow(Graphics, int, int, int, int)}
*/
public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics
/**
* The width and height of the drop shadow painted by
* {@link #drawSmallRectangleShadow(Graphics, int, int, int, int)}
*/
public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics
/**
* Draws a drop shadow for the given rectangle into the given context. It
* will not draw anything if the rectangle is smaller than a minimum
* determined by the assets used to draw the shadow graphics.
*
* @param gc the graphics context to draw into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
@SuppressWarnings("ConstantConditions")
public static void drawRectangleShadow(Graphics gc, int x, int y, int width, int height) {
assert ShadowBottomLeft != null;
assert ShadowBottomRight.getWidth(null) == SHADOW_SIZE;
assert ShadowBottomRight.getHeight(null) == SHADOW_SIZE;
int blWidth = ShadowBottomLeft.getWidth(null);
int trHeight = ShadowTopRight.getHeight(null);
if (width < blWidth) {
return;
}
if (height < trHeight) {
return;
}
UIUtil.drawImage(gc, ShadowBottomLeft, x, y + height, null);
UIUtil.drawImage(gc, ShadowBottomRight, x + width, y + height, null);
UIUtil.drawImage(gc, ShadowTopRight, x + width, y, null);
ImageUtils.drawDipImage(gc, ShadowBottom, x + ShadowBottomLeft.getWidth(null), y + height, x + width,
y + height + ShadowBottom.getHeight(null), 0, 0, ShadowBottom.getWidth(null), ShadowBottom.getHeight(null),
null);
ImageUtils.drawDipImage(gc, ShadowRight, x + width, y + ShadowTopRight.getHeight(null), x + width + ShadowRight.getWidth(null),
y + height, 0, 0, ShadowRight.getWidth(null), ShadowRight.getHeight(null), null);
}
/**
* Draws a small drop shadow for the given rectangle into the given context. It
* will not draw anything if the rectangle is smaller than a minimum
* determined by the assets used to draw the shadow graphics.
* <p>
*
* @param gc the graphics context to draw into
* @param x the left coordinate of the left hand side of the rectangle
* @param y the top coordinate of the top of the rectangle
* @param width the width of the rectangle
* @param height the height of the rectangle
*/
@SuppressWarnings("ConstantConditions")
public static void drawSmallRectangleShadow(Graphics gc, int x, int y, int width, int height) {
assert Shadow2BottomLeft != null;
assert Shadow2TopRight != null;
assert Shadow2BottomRight.getWidth(null) == SMALL_SHADOW_SIZE;
assert Shadow2BottomRight.getHeight(null) == SMALL_SHADOW_SIZE;
int blWidth = Shadow2BottomLeft.getWidth(null);
int trHeight = Shadow2TopRight.getHeight(null);
if (width < blWidth) {
return;
}
if (height < trHeight) {
return;
}
UIUtil.drawImage(gc, Shadow2BottomLeft, x, y + height, null);
UIUtil.drawImage(gc, Shadow2BottomRight, x + width, y + height, null);
UIUtil.drawImage(gc, Shadow2TopRight, x + width, y, null);
ImageUtils.drawDipImage(gc, Shadow2Bottom, x + Shadow2BottomLeft.getWidth(null), y + height, x + width,
y + height + Shadow2Bottom.getHeight(null), 0, 0, Shadow2Bottom.getWidth(null), Shadow2Bottom.getHeight(null),
null);
ImageUtils.drawDipImage(gc, Shadow2Right, x + width, y + Shadow2TopRight.getHeight(null), x + width + Shadow2Right.getWidth(null),
y + height, 0, 0, Shadow2Right.getWidth(null), Shadow2Right.getHeight(null), null);
}
// Shadow graphics. This was generated by creating a drop shadow in
// Gimp, using the parameters x offset=10, y offset=10, blur radius=10,
// (for the small drop shadows x offset=10, y offset=10, blur radius=10)
// color=black, and opacity=51. These values attempt to make a shadow
// that is legible both for dark and light themes, on top of the
// canvas background (rgb(150,150,150). Darker shadows would tend to
// blend into the foreground for a dark holo screen, and lighter shadows
// would be hard to spot on the canvas background. If you make adjustments,
// make sure to check the shadow with both dark and light themes.
//
// After making the graphics, I cut out the top right, bottom left
// and bottom right corners as 20x20 images, and these are reproduced by
// painting them in the corresponding places in the target graphics context.
// I then grabbed a single horizontal gradient line from the middle of the
// right edge,and a single vertical gradient line from the bottom. These
// are then painted scaled/stretched in the target to fill the gaps between
// the three corner images.
//
// Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right
// Normal Drop Shadow
private static final Image ShadowBottom = ImageUtils.loadIcon("/icons/shadow-b.png");
private static final Image ShadowBottomLeft = ImageUtils.loadIcon("/icons/shadow-bl.png");
private static final Image ShadowBottomRight = ImageUtils.loadIcon("/icons/shadow-br.png");
private static final Image ShadowRight = ImageUtils.loadIcon("/icons/shadow-r.png");
private static final Image ShadowTopRight = ImageUtils.loadIcon("/icons/shadow-tr.png");
// Small Drop Shadow
private static final Image Shadow2Bottom = ImageUtils.loadIcon("/icons/shadow2-b.png");
private static final Image Shadow2BottomLeft = ImageUtils.loadIcon("/icons/shadow2-bl.png");
private static final Image Shadow2BottomRight = ImageUtils.loadIcon("/icons/shadow2-br.png");
private static final Image Shadow2Right = ImageUtils.loadIcon("/icons/shadow2-r.png");
private static final Image Shadow2TopRight = ImageUtils.loadIcon("/icons/shadow2-tr.png");
}