Tuesday, November 9, 2010

BufferedImage transparency

BufferedImage class is a very useful tool for certain animations. Not only you could double buffer with these images, but you could organise a complicated animated scene of many layered BufferedImages on top of each other. In my Sudoku program, when mouse is over a cell it gets highlighted in yellow like so:
I could think of 2 ways to achieve this effect: 
1. clear the whole image -> paint yellow square in its position -> paint table and digits
2. paint yellow square over table -> paint digit over it -> clear cell when mouse is relocated and paint digit again
However, the third easier and more efficient, in my opinion, way that I used is use BufferedImage. The first Buffered image contains the table and digits; the second image contains the highlight of the cell with the mouse over it. In doing this, all I need to do is print the table and digits once, then paint a yellow square on the second BufferedImage, under the current cell. If mouse is moved I can easily clear the whole second image with the yellow square on it without affecting the sudoku table. Here is how to do it:

/* This is still in my ...extends JPanel class from the last tutorial*/
private BufferedImage img, backImg; //these are the two images
Graphics2D g, gBack;           //and these graphics objects will be used to paint on the two images
/...
/*in Constructor among other initializations*/
        //initialize all buffered images used for the drawing of sudoku and the graphics obj connected to them
        img = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.BITMASK);
        backImg = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.BITMASK);
        g = (Graphics2D) img.getGraphics();  //now g can be used to paint on the img BufferedImage
        gBack = (Graphics2D) backImg.getGraphics();

So up to this point we have 2 transparent BufferedImages with the size of the current container. 
ATTENTION! The container's size has to be already set and I even said setVisible(true); before initializing the images. If the container's size is still 0 then the images won't initialize properly.
BufferedImage.BITMASK type is essential for this problem, since it makes the images transparent.
Now in a method that I do not want to bother you with, I paint the grid on the img object. With Graphics2D you can do things like:

g.setStroke(new BasicStroke(thickStroke)); /*to set the width of painted lines where thickStroke is an integer indicating stroke in pixels*/
g.drawLine(x, inset, x, y); // to draw a line with the stroke specified
g.setColor(Color.BLACK); //sets the colour to use in painting 
, etc.

Going to my highlight method I have:

    /**
     * Highlights a cell on the graphics component provided.
     * @param row
     * @param column
     * @param g
     * @param highlight If set to null, this method simply makes everything transparent.
     */
    private void highlightCell(int row, int column, Graphics2D g, Color highlight) {

        Composite c = g.getComposite(); //save original composite
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR)); //set to draw transparent
        g.fillRect(0, 0, getWidth(), getHeight()); //clear everything with transparent rectangle
        //determine x and y coordinates of upper-right corner of inticated cell
        int highlightX = inset + (column * squareWidth);
        int highlightY = inset + (row * squareWidth);

        //draw a square at the specific cell
        g.setComposite(c); //reset the composite to normal drawing
        if (highlight != null) {
            g.setColor(highlight);
            g.fillRect(highlightX, highlightY, squareWidth, squareWidth);
        }
        repaint();
    }
}

Since I am not very familiar with Composite class, I decided to backup the normal Composite used before I change it into transparent drawing mode by the second line of code in the method. When I call the method, I pass gBack as the second parameter in order to paint the highlight on the backImg object. The method is easy to understand and in the end you can see the repaint(); command. This command calls the paintComponent(Graphics g) method of JPanel class. Here is how I've overridden it:
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        //paint from bottom to top backImg and the sudoku itself on top
        if (backImg!= null) {
            g.drawImage(backImg, 0, 0, this);
        }
        if (img != null) {
            g.drawImage(img, 0, 0, this);
        }
    }

As you should already know, everything that gets painted on the JPanel has to be painted here in this method. super.paintComponent(g); is there to clean the last things painted on the JPanel. After that I paint the new things I would like to see on my JPanel which is the highlight on backImg and on top of it the sudoku table on img. 
Notice that in the case of my sudoku application, in the highlight method, I could simply clear the BufferedImage with the background colour (white) instead of doing all the Composite stuff to clear it with transparency. I did it because in my real application I actually have a third BufferedImage under the other 2 so I need to paint with transparency in order to see it. Besides you would need to know this in order to use this technique efficiently. It could be used for any animations where you have something in front that needs to be independent from something on the back that is still visible. 

Good luck and comment if you have any recommendations, ideas or questions.

No comments:

Post a Comment