/*
 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.javafx.sg.prism;

import com.sun.prism.Graphics;
import com.sun.prism.Image;
import com.sun.prism.Texture;
import com.sun.prism.paint.Color;
import com.sun.scenario.effect.Color4f;
import com.sun.scenario.effect.DropShadow;
import com.sun.scenario.effect.Effect;
import com.sun.scenario.effect.InnerShadow;

/**
 */
class EffectUtil {
    // We must use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms
    private static final int TEX_SIZE = 256;

    private static Texture itex;
    private static Texture dtex;

    
If possible, uses an optimized codepath to render the an effect (InnerShadow or DropShadow) on the given rectangular node (NGRectangle, NGImageView, etc). If successful, returns true; otherwise returns false to indicate that the caller should fall back on existing methods to render the effect.
/** * If possible, uses an optimized codepath to render the an * effect (InnerShadow or DropShadow) on the given rectangular node * (NGRectangle, NGImageView, etc). If successful, returns true; * otherwise returns false to indicate that the caller should fall * back on existing methods to render the effect. */
static boolean renderEffectForRectangularNode(NGNode node, Graphics g, Effect effect, float alpha, boolean aa, float rx, float ry, float rw, float rh) { if (!g.getTransformNoClone().is2D() && g.isDepthBuffer() && g.isDepthTest()) { // TODO: Both of our optimizations below rely on layering of // 2 primitives that are dispatched with x and y coordinates // that are calculated displacements of the rx,ry,rw,rh. // Given that we are doing the calculations with IEEE floating // point (even if we used double precision) we are going to // end up with cases where interpolating between the calculated // coordinates generates Z values that are 1 ulp above or below // the Z value that is calculated for the primitive and it will // be essentially random whether we see the primitive or the // shadow on a per-pixel basis. In practice, there tends to be // a sharp cutover from one to the other along some horizontal // or vertical threshold rather than random noise, but we cannot // force the latter rendered objects to overwrite the previously // rendered objects if we have depth buffers on. // Note that a Z offset could be used to force layering, but // that has 2 issues: // - What Z offset do we use? It is dependent on the precision // of the depth buffer and also on the 3D transform and probably // the camera and near/far clipping planes. We can try to // install a pre-translation (translate by N device units), // but we still may choose a value that does not quite "bump" // the Z coordinates far enough since floating point has a // scaled mantissa. // - If the primitive turns around it will lose its inner shadow // or be covered completly by the drop shadow. Both results // are inconsistent with the image-based results delivered by // Decora. This could also be adjusted by examining the facing // of the primitive and then changing the direction we bump the // shadow, but the simplest and most compatible solution is to // just turn off the optimizations when we have a 3D depth buffer. // (RT-26982) return false; } if (effect instanceof InnerShadow && !aa) { // TODO: Handle AA, or at least case when rectangle is pixel aligned... // (RT-26982) InnerShadow shadow = (InnerShadow)effect; float radius = shadow.getRadius(); if (radius > 0f && radius < rw/2 && radius < rh/2 && shadow.getChoke() == 0f && shadow.getShadowSourceInput() == null && shadow.getContentInput() == null) { node.renderContent(g); EffectUtil.renderRectInnerShadow(g, shadow, alpha, rx, ry, rw, rh); return true; } } else if (effect instanceof DropShadow) { DropShadow shadow = (DropShadow)effect; float radius = shadow.getRadius(); if (radius > 0f && radius < rw/2 && radius < rh/2 && shadow.getSpread() == 0f && shadow.getShadowSourceInput() == null && shadow.getContentInput() == null) { EffectUtil.renderRectDropShadow(g, shadow, alpha, rx, ry, rw, rh); node.renderContent(g); return true; } } return false; } static void renderRectInnerShadow(Graphics g, InnerShadow shadow, float alpha, float rx, float ry, float rw, float rh) { if (itex == null) { byte[] sdata = new byte[TEX_SIZE * TEX_SIZE]; fillGaussian(sdata, TEX_SIZE, TEX_SIZE/2, shadow.getChoke(), true); Image img = Image.fromByteAlphaData(sdata, TEX_SIZE, TEX_SIZE); itex = g.getResourceFactory().createTexture(img, Texture.Usage.STATIC, Texture.WrapMode.CLAMP_TO_EDGE); // We use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms assert itex.getWrapMode() == Texture.WrapMode.CLAMP_TO_EDGE; itex.contentsUseful(); itex.makePermanent(); } float r = shadow.getRadius(); int texsize = itex.getPhysicalWidth(); int tcx1 = itex.getContentX(); int tcx2 = tcx1 + itex.getContentWidth(); float t1 = (tcx1 + 0.5f) / texsize; float t2 = (tcx2 - 0.5f) / texsize; // end of image float cx1 = rx; float cy1 = ry; float cx2 = rx + rw; float cy2 = ry + rh; float ox1 = cx1 + shadow.getOffsetX(); float oy1 = cy1 + shadow.getOffsetY(); float ox2 = ox1 + rw; float oy2 = oy1 + rh; g.setPaint(toPrismColor(shadow.getColor(), alpha)); // TODO: The "outer edge" slices below overlap at the corners... (RT-26982) drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, cx1, cy1, cx2, oy1-r, t1, t1, t1, t1); // outside above drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox1-r, oy1-r, ox1+r, oy1+r, t1, t1, t2, t2); // top-left corner drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox1+r, oy1-r, ox2-r, oy1+r, t2, t1, t2, t2); // top edge drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox2-r, oy1-r, ox2+r, oy1+r, t2, t1, t1, t2); // top-right corner drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, cx1, oy1-r, ox1-r, oy2+r, t1, t1, t1, t1); // outside left drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox1-r, oy1+r, ox1+r, oy2-r, t1, t2, t2, t2); // left edge drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox2-r, oy1+r, ox2+r, oy2-r, t2, t2, t1, t2); // right edge drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox2+r, oy1-r, cx2, oy2+r, t1, t1, t1, t1); // outside right drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox1-r, oy2-r, ox1+r, oy2+r, t1, t2, t2, t1); // bot-left corner drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox1+r, oy2-r, ox2-r, oy2+r, t2, t2, t2, t1); // bot edge drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, ox2-r, oy2-r, ox2+r, oy2+r, t2, t2, t1, t1); // bot-right corner drawClippedTexture(g, itex, cx1, cy1, cx2, cy2, cx1, oy2+r, cx2, cy2, t1, t1, t1, t1); // outside below } static void drawClippedTexture(Graphics g, Texture tex, float cx1, float cy1, float cx2, float cy2, float ox1, float oy1, float ox2, float oy2, float tx1, float ty1, float tx2, float ty2) { if (ox1 >= ox2 || oy1 >= oy2 || cx1 >= cx2 || cy1 >= cy2) return; if (ox2 > cx1 && ox1 < cx2) { if (ox1 < cx1) { tx1 += (tx2 - tx1) * (cx1 - ox1) / (ox2 - ox1); ox1 = cx1; } if (ox2 > cx2) { tx2 -= (tx2 - tx1) * (ox2 - cx2) / (ox2 - ox1); ox2 = cx2; } } else { return; } if (oy2 > cy1 && oy1 < cy2) { if (oy1 < cy1) { ty1 += (ty2 - ty1) * (cy1 - oy1) / (oy2 - oy1); oy1 = cy1; } if (oy2 > cy2) { ty2 -= (ty2 - ty1) * (oy2 - cy2) / (oy2 - oy1); oy2 = cy2; } } else { return; } g.drawTextureRaw(tex, ox1, oy1, ox2, oy2, tx1, ty1, tx2, ty2); } static void renderRectDropShadow(Graphics g, DropShadow shadow, float alpha, float rx, float ry, float rw, float rh) { if (dtex == null) { byte[] sdata = new byte[TEX_SIZE * TEX_SIZE]; fillGaussian(sdata, TEX_SIZE, TEX_SIZE / 2, shadow.getSpread(), false); //fillTestPattern(sdata, imgsize); Image img = Image.fromByteAlphaData(sdata, TEX_SIZE, TEX_SIZE); dtex = g.getResourceFactory().createTexture(img, Texture.Usage.STATIC, Texture.WrapMode.CLAMP_TO_EDGE); // We use a power-of-2 size so that we can get true CLAMP_TO_EDGE on all platforms assert dtex.getWrapMode() == Texture.WrapMode.CLAMP_TO_EDGE; dtex.contentsUseful(); dtex.makePermanent(); } float r = shadow.getRadius(); int texsize = dtex.getPhysicalWidth(); int cx1 = dtex.getContentX(); int cx2 = cx1 + dtex.getContentWidth(); float t1 = (cx1 + 0.5f) / texsize; float t2 = (cx2 - 0.5f) / texsize; // end of image float x1 = rx + shadow.getOffsetX(); float y1 = ry + shadow.getOffsetY(); float x2 = x1 + rw; float y2 = y1 + rh; g.setPaint(toPrismColor(shadow.getColor(), alpha)); g.drawTextureRaw(dtex, x1-r, y1-r, x1+r, y1+r, t1, t1, t2, t2); // top-left corner g.drawTextureRaw(dtex, x2-r, y1-r, x2+r, y1+r, t2, t1, t1, t2); // top-right corner g.drawTextureRaw(dtex, x2-r, y2-r, x2+r, y2+r, t2, t2, t1, t1); // bot-right corner g.drawTextureRaw(dtex, x1-r, y2-r, x1+r, y2+r, t1, t2, t2, t1); // bot-left corner g.drawTextureRaw(dtex, x1+r, y1+r, x2-r, y2-r, t2, t2, t2, t2); // center section g.drawTextureRaw(dtex, x1-r, y1+r, x1+r, y2-r, t1, t2, t2, t2); // left edge g.drawTextureRaw(dtex, x2-r, y1+r, x2+r, y2-r, t2, t2, t1, t2); // right edge g.drawTextureRaw(dtex, x1+r, y1-r, x2-r, y1+r, t2, t1, t2, t2); // top edge g.drawTextureRaw(dtex, x1+r, y2-r, x2-r, y2+r, t2, t2, t2, t1); // bottom edge } private static void fillGaussian(byte[] pixels, int dim, float r, float spread, boolean inner) { // Only works right if w==r and h==r float sigma = r / 3; float sigma22 = 2 * sigma * sigma; if (sigma22 < Float.MIN_VALUE) { // Avoid divide by 0 below (can generate NaN values) sigma22 = Float.MIN_VALUE; } // Really should just be new float[r] float kvals[] = new float[dim]; int center = (dim+1)/2; float total = 0.0f; for (int i = 0; i < kvals.length; i++) { int d = center - i; total += (float) Math.exp(-(d * d) / sigma22); kvals[i] = total; } // total += (kvals[kvals.length-1] - total) * spread; for (int i = 0; i < kvals.length; i++) { kvals[i] /= total; } for (int y = 0; y < dim; y++) { for (int x = 0; x < dim; x++) { float v = kvals[y] * kvals[x]; if (inner) { // For inner shadow, invert v = 1.0f - v; } int a = (int) (v * 255); //System.err.println(x + " " + y + " " + v + " " + kvals[x] + " " + kvals[y]); if (a < 0) a = 0; else if (a > 255) a = 255; pixels[y*dim+x] = (byte)a; } } } private static Color toPrismColor(Color4f decoraColor, float alpha) { float r = decoraColor.getRed(); float g = decoraColor.getGreen(); float b = decoraColor.getBlue(); float a = decoraColor.getAlpha() * alpha; return new Color(r, g, b, a); } private EffectUtil() { } }