Search code examples
javaswinggraphicslook-and-feelnimbus

swing nimbus ShadowEffect


I use Nimbus look and feel in my swing applications and it's pretty cool.
I noticed that it is a "pure" look and feel: it "skins" components, but does not add new graphical elements.
I wonder if some part of its rendering technology is reusable, for example the class:

javax.swing.plaf.nimbus.ShadowEffect

is available to add shadows to other elements like JLabel, that has no shadow by default?


Solution

  • I got IllegalAccess, so I copied the class if I needed this in a little Application.
    I know this is not nice, but ShadowEffect is not reusable for any other class. :(

    Example:

    public class Example extends JFrame {
        private JSlider slider = new JSlider(JSlider.VERTICAL,0,20,2);
        private JTextField txt = new JTextField();
        private BufferedImage raw = null;
        private Image src = null;
        private Rectangle rect = new Rectangle(400,200);
    
        public Example() {
            slider.addChangeListener( new ChangeListener() {
                @Override
                public void stateChanged( ChangeEvent e ) {
                    repaint();
                }
            });
    
            JButton btn = new JButton(new AbstractAction("load") {
                @Override
                public void actionPerformed( ActionEvent ev ) {
                    try {
                        src = new ImageIcon( new URL( txt.getText() ) ).getImage();
                        rect = new Rectangle( src.getWidth( null ), src.getHeight( null ) );
                        raw = new BufferedImage( rect.width+20, rect.height+20, BufferedImage.TYPE_INT_ARGB );
                        raw.getGraphics().drawImage( src, 0, 0, null );
                        pack();
                    } catch( Exception e ) {
                        e.printStackTrace();
                    }
                }
            });
            JPanel ori = new JPanel(new BorderLayout());
            ori.add( txt, BorderLayout.CENTER );
            txt.setText( "http://upload.wikimedia.org/wikipedia/en/d/d5/Transparent_google_logo.png" );
            ori.add( btn, BorderLayout.EAST);
            JPanel pnl = new JPanel() {
                @Override
                protected void paintComponent( Graphics g ) {
                    if(raw != null && src != null){
                        BufferedImage dst = new BufferedImage( rect.width+20, rect.height+20, BufferedImage.TYPE_INT_ARGB );
                        new MyEffect().applyEffect( raw, dst, rect.width+20, rect.height+20 );
                        g.drawImage( dst, slider.getValue(), slider.getValue(), null );
                        g.drawImage( src, 0, 0, null );
                    }
                }
                @Override
                public Dimension getPreferredSize() {
                    return new Dimension(rect.width+5,rect.height+5);
                }
            };
    
            setDefaultCloseOperation( EXIT_ON_CLOSE );
            getContentPane().setLayout( new BorderLayout() );
            getContentPane().setBackground( Color.WHITE );
            getContentPane().add( ori, BorderLayout.NORTH );
            getContentPane().add( pnl, BorderLayout.CENTER );
            getContentPane().add( slider, BorderLayout.EAST );
            pack();
        }
    
        public static void main( String[] args ) {
            try {
                for( UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels() ) {
                    if( "Nimbus".equals( info.getName() ) ) {
                        UIManager.setLookAndFeel( info.getClassName() );
                        break;
                    }
                }
            } catch( Exception system ) {
                system.printStackTrace();
            }
            EventQueue.invokeLater( new Runnable() {
                @Override
                public void run() {
                    new Example().setVisible( true );
                }
            } );
        }
    
        enum EffectType {
            UNDER, BLENDED, OVER
        }
    
        public class MyEffect {
    
            protected Color color    = Color.BLACK;
            /** Opacity a float 0-1 for percentage */
            protected float opacity  = 0.75f;
            /** Angle in degrees between 0-360 */
            protected int   angle    = 135;
            /** Distance in pixels */
            protected int   distance = 5;
            /** The shadow spread between 0-100 % */
            protected int   spread   = 0;
            /** Size in pixels */
            protected int   size     = 5;
    
            protected ArrayCache getArrayCache() {
                ArrayCache cache = (ArrayCache)AppContext.getAppContext().get( ArrayCache.class );
                if( cache == null ) {
                    cache = new ArrayCache();
                    AppContext.getAppContext().put( ArrayCache.class, cache );
                }
                return cache;
            }
    
            protected class ArrayCache {
                private SoftReference<int[]>  tmpIntArray   = null;
                private SoftReference<byte[]> tmpByteArray1 = null;
                private SoftReference<byte[]> tmpByteArray2 = null;
                private SoftReference<byte[]> tmpByteArray3 = null;
    
                protected int[] getTmpIntArray( int size ) {
                    int[] tmp;
                    if( tmpIntArray == null || (tmp = tmpIntArray.get()) == null || tmp.length < size ) {
                        // create new array
                        tmp = new int[size];
                        tmpIntArray = new SoftReference<int[]>( tmp );
                    }
                    return tmp;
                }
    
                protected byte[] getTmpByteArray1( int size ) {
                    byte[] tmp;
                    if( tmpByteArray1 == null || (tmp = tmpByteArray1.get()) == null || tmp.length < size ) {
                        // create new array
                        tmp = new byte[size];
                        tmpByteArray1 = new SoftReference<byte[]>( tmp );
                    }
                    return tmp;
                }
    
                protected byte[] getTmpByteArray2( int size ) {
                    byte[] tmp;
                    if( tmpByteArray2 == null || (tmp = tmpByteArray2.get()) == null || tmp.length < size ) {
                        // create new array
                        tmp = new byte[size];
                        tmpByteArray2 = new SoftReference<byte[]>( tmp );
                    }
                    return tmp;
                }
    
                protected byte[] getTmpByteArray3( int size ) {
                    byte[] tmp;
                    if( tmpByteArray3 == null || (tmp = tmpByteArray3.get()) == null || tmp.length < size ) {
                        // create new array
                        tmp = new byte[size];
                        tmpByteArray3 = new SoftReference<byte[]>( tmp );
                    }
                    return tmp;
                }
            }
    
            Color getColor() {
                return color;
            }
    
            void setColor( Color color ) {
                Color old = getColor();
                this.color = color;
            }
    
            float getOpacity() {
                return opacity;
            }
    
            void setOpacity( float opacity ) {
                float old = getOpacity();
                this.opacity = opacity;
            }
    
            int getAngle() {
                return angle;
            }
    
            void setAngle( int angle ) {
                int old = getAngle();
                this.angle = angle;
            }
    
            int getDistance() {
                return distance;
            }
    
            void setDistance( int distance ) {
                int old = getDistance();
                this.distance = distance;
            }
    
            int getSpread() {
                return spread;
            }
    
            void setSpread( int spread ) {
                int old = getSpread();
                this.spread = spread;
            }
    
            int getSize() {
                return size;
            }
    
            void setSize( int size ) {
                int old = getSize();
                this.size = size;
            }
    
            EffectType getEffectType() {
                return EffectType.UNDER;
            }
    
            BufferedImage applyEffect( BufferedImage src, BufferedImage dst, int w, int h ) {
                if( src == null || src.getType() != BufferedImage.TYPE_INT_ARGB ) {
                    throw new IllegalArgumentException( "Effect only works with "
                        + "source images of type BufferedImage.TYPE_INT_ARGB." );
                }
                if( dst != null && dst.getType() != BufferedImage.TYPE_INT_ARGB ) {
                    throw new IllegalArgumentException( "Effect only works with "
                        + "destination images of type BufferedImage.TYPE_INT_ARGB." );
                }
                // calculate offset
                double trangleAngle = Math.toRadians( angle - 90 );
                int offsetX = (int)(Math.sin( trangleAngle ) * distance);
                int offsetY = (int)(Math.cos( trangleAngle ) * distance);
                // clac expanded size
                int tmpOffX = offsetX + size;
                int tmpOffY = offsetX + size;
                int tmpW = w + offsetX + size + size;
                int tmpH = h + offsetX + size;
                // create tmp buffers
                int[] lineBuf = getArrayCache().getTmpIntArray( w );
                byte[] tmpBuf1 = getArrayCache().getTmpByteArray1( tmpW * tmpH );
                Arrays.fill( tmpBuf1, (byte)0x00 );
                byte[] tmpBuf2 = getArrayCache().getTmpByteArray2( tmpW * tmpH );
                // extract src image alpha channel and inverse and offset
                Raster srcRaster = src.getRaster();
                for( int y = 0; y < h; y++ ) {
                    int dy = (y + tmpOffY);
                    int offset = dy * tmpW;
                    srcRaster.getDataElements( 0, y, w, 1, lineBuf );
                    for( int x = 0; x < w; x++ ) {
                        int dx = x + tmpOffX;
                        tmpBuf1[offset + dx] = (byte)((lineBuf[x] & 0xFF000000) >>> 24);
                    }
                }
                // blur
                float[] kernel = createGaussianKernel( size );
                blur( tmpBuf1, tmpBuf2, tmpW, tmpH, kernel, size ); // horizontal pass
                blur( tmpBuf2, tmpBuf1, tmpH, tmpW, kernel, size );// vertical pass
                //rescale
                float spread = Math.min( 1 / (1 - (0.01f * this.spread)), 255 );
                for( int i = 0; i < tmpBuf1.length; i++ ) {
                    int val = (int)((tmpBuf1[i] & 0xFF) * spread);
                    tmpBuf1[i] = (val > 255) ? (byte)0xFF : (byte)val;
                }
                // create color image with shadow color and greyscale image as alpha
                if( dst == null ) {
                    dst = new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
                }
                WritableRaster shadowRaster = dst.getRaster();
                int red = color.getRed(), green = color.getGreen(), blue = color.getBlue();
                for( int y = 0; y < h; y++ ) {
                    int srcY = y + tmpOffY;
                    int shadowOffset = (srcY - offsetY) * tmpW;
                    for( int x = 0; x < w; x++ ) {
                        int srcX = x + tmpOffX;
                        lineBuf[x] = tmpBuf1[shadowOffset + (srcX - offsetX)] << 24 | red << 16 | green << 8 | blue;
                    }
                    shadowRaster.setDataElements( 0, y, w, 1, lineBuf );
                }
                return dst;
            }
        }
    
        static float[] createGaussianKernel( int radius ) {
            if( radius < 1 ) {
                throw new IllegalArgumentException( "Radius must be >= 1" );
            }
    
            float[] data = new float[radius * 2 + 1];
    
            float sigma = radius / 3.0f;
            float twoSigmaSquare = 2.0f * sigma * sigma;
            float sigmaRoot = (float)Math.sqrt( twoSigmaSquare * Math.PI );
            float total = 0.0f;
    
            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;
            }
    
            return data;
        }
    
        static void blur( byte[] srcPixels, byte[] dstPixels, int width, int height, float[] kernel, int radius ) {
            float p;
            int cp;
            for( int y = 0; y < height; y++ ) {
                int index = y;
                int offset = y * width;
                for( int x = 0; x < width; x++ ) {
                    p = 0.0f;
                    for( int i = -radius; i <= radius; i++ ) {
                        int subOffset = x + i;
                        //                 if (subOffset < 0) subOffset = 0;
                        //                 if (subOffset >= width) subOffset = width-1;
                        if( subOffset < 0 || subOffset >= width ) {
                            subOffset = (x + width) % width;
                        }
                        int pixel = srcPixels[offset + subOffset] & 0xFF;
                        float blurFactor = kernel[radius + i];
                        p += blurFactor * pixel;
                    }
                    cp = (int)(p + 0.5f);
                    dstPixels[index] = (byte)(cp > 255 ? 255 : cp);
                    index += height;
                }
            }
        }
    }