Skip to main content

Making Rainbow: Spotlight effect and soft clipping

Posted by alexfromsun on November 29, 2007 at 11:32 AM PST

Few weeks ago I submitted a session for the next JavaOne and it reminded me the previous session which we presented with Kirill

Before J1, he published a nice teaser with links to blogs about his famous ghost effects and transition layout, so it's hight time for me drop a few lines about implementing a spotLight effect. You can test the whole application on the main page of the Rainbow project, here is a flash demo which I made specially for this blog (press the green button on the bottom left side)



The shortest version of the SpotLightPainter was about 30 lines of code. I slightly modified the original painter from the Rainbow project and included the SpotLightPainter and the SpotLightDemo to the demo direcotry for JXLayer.

The painter's code is quite straightforward, there are just two things I'd like to comment:

ForegroundPainter

SpotlightPainter has a layered structure: the scrollPane's view component and the tranclucent foreground with the "holes" for matched components. I divide the painting code between two painters - SpotLightPainter extends DefaultPainter, so the wrapped component is automatically painted when you call super.paint(g2, l), the foregroundPainter is responsible for painting the tranclucent foreground:

 @Override
    public void paint(Graphics2D g2, JXLayer l) {
        super.paint(g2, l);
        foregroundPainter.paint(g2, l);
    }

The foregroundPainter extends AbstractBufferedPainter which means it is painted via the intermediate image, this is the best solution when it comes to translucency. I fill the whole area with the translucent color and then clip out the shapes over the matched
components:

this.foregroundPainter = new AbstractBufferedPainter() {
            @Override
            protected void paintToBuffer(Graphics2D g2, JXLayer l) {
               
                // cleaning up
                g2.setComposite(AlphaComposite.Clear);
                g2.fillRect(0, 0, l.getWidth(), l.getHeight());
               
                // filling with overlayColor
                g2.setComposite(AlphaComposite.SrcOver);
                g2.setColor(overlayColor);
                g2.fillRect(0, 0, l.getWidth(), l.getHeight());
               
                // clipping out the "holes" for matched components
                for (Shape shape : clipList) {
                    g2.setClip(shape);
                    g2.setComposite(AlphaComposite.Clear);
                    g2.fill(shape);
                    softClipping(g2, shape);
                }
            }
        };

The important thing that by default, AbstractBufferedPainter.isIncrementalUpdate() returns false, it means that this painter will be updated only if the whole JXLayer is repainted, e.g. when the frame is resized or when SpotLightPainter.fireLayerItemChanged() is called. This optimization allows reusing the foreground image for faster painting, it doesn't slow down the transition effect animation because when you move the mouse over a button, foregroundPainter paints the cached image.

Soft clipping

When we prepared our session, Kirill drew my attention to the fact that spotlights didn't look smooth, here the zoomed out screenshot which shows how it looked like:

Initial clipping

as the result it was possible to see the jaggies on the spotlight's borders and that made this effect look unprofessional. The remedy for this problem is called "soft clipping" and the pefect article by Chris Cambell is the first thing that came to my mind when I thought about that.

The trick from Chris produces the very good result but it works for fixed shapes only, I mean you should know the exact shape in advance to create a mask for it. I decided to find a way how to make soft clipping for arbitrary shapes and I actually found it, but...
a few minutes ago I came across the next Chris's blog Light and Shadow (I missed it for some reason) where he described exactly what I thought was invented by me! :-)

In the comments for that blog there are a few more reports from people who came to this solution even earlier.

The idea is to mix in the alpha value to the pixels on the shape's border by drawing the same shape with a set BasicStroke. For the Rainbow project I painted it once with the 50% alpha and doubled basicStroke width.

Initial clipping

For the JXLayer's demo I updated SpotLightPainter to be able to set the custom width for the soft clipping, and make it as smooth as you want:

 private void softClipping(Graphics2D g2, Shape shape) {
        g2.setComposite(AlphaComposite.Src);
        for (int i = 0; i < softClipWidth; i++) {
            int alpha = (i + 1) * overlayColor.getAlpha()
                    / (softClipWidth + 1);
            Color temp = new Color(
                    overlayColor.getRed(),
                    overlayColor.getGreen(),
                    overlayColor.getBlue(), alpha);
            g2.setColor(temp);
            g2.setStroke(new BasicStroke(softClipWidth - i));
            g2.draw(shape);
        }
    }

Spotlight demo

MouseEvents filtering

Components which are outside the spotlights shouldn't react on the mouse events and this was the simplest task for the SpotLightPainter

public boolean contains(int x, int y, JXLayer l) {
        for (Shape shape : clipList) {
            if (shape.contains(x, y)) {
                return true;
            }
        }
        return false;
    }

Conclusion

To add this functionality to your application you just need to wrap you component with JXLayer:

JXLayer layer = 
     new JXLayer(myComponent, spotLightPainter)

add(new JScrollPane(layer));

All source and binary files you can find on the SwingHelper project site

Thanks

alexp

Related Topics >>