2

My question is pretty specific. I'm developing a game in which there is a potion jar that can drop from enemies. The image for the potion jar is a transparent jar with a black border, and an opaque white background. The reason why there is an opaque white background is because I also need to draw how much potion is left in the jar. My current method for doing this is:

  1. Draw a rectangle of height H that signifies how much potion is left
  2. Draw the potion jar image on top of the rectangle.

Thus, this will overlap the ugly red rectangle drawn to signify the amount of potion remaining with the opaque white space around the potion's transparent area. Here is an image of the result:

https://i.stack.imgur.com/rHcpr.png

The problem arises because the potion jar really does look pretty bad in game. The white background just looks horrible.

My question is: Is there any way to remove that opaque white background, while still being able to "fill" the potion jar exactly in the space that the image defines it as?

Ashok
  • 134
  • 13
  • Well from looking through Java2D API, I read that there exists a couple of methods to inspect the pixels (one by one) of the BufferedImage. So maybe you can inspect the pixels of your jar picture, and where they turn out to be of white color, make them transparent. Tomorrow I'll try to make the implementation of this, but maybe there's some better solution. – croraf Dec 12 '13 at 23:58

2 Answers2

4

Basically, you can create a "mask" of the original image, which can be generated from the non-opaque portions of the image.

So I created two potion images (sure you could use a sprite sheet). One which has an opaque center and one that is transparent (just the outline).

BaseOutline

From this, I'm able to generate a mask of first image (in the color I want), use subimage to reduce the amount of the image I want to use then render it and the outline

enter image description here

import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestOverlay {

    public static void main(String[] args) {
        new TestOverlay();
    }

    public TestOverlay() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private BufferedImage potionBase;
        private BufferedImage potionOutline;
        private float value = 1f;

        public TestPane() {
            try {
                potionBase = ImageIO.read(new File("Potion.png"));
                potionOutline = ImageIO.read(new File("PotionOutline.png"));
            } catch (IOException ex) {
                ex.printStackTrace();
            }

            Timer timer = new Timer(40, new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    value = value - 0.01f;
                    if (value < 0) {
                        value = 1f;
                    }
                    repaint();
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return potionBase == null ? super.getPreferredSize() : new Dimension(potionBase.getWidth(), potionBase.getHeight());
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            BufferedImage mask = generateMask(potionBase, Color.RED, 1f);

            int y = (int) (mask.getHeight() * (1f - value));
            if (y < mask.getHeight()) {

                mask = mask.getSubimage(0, y, mask.getWidth(), mask.getHeight() - y);

            }

            int x = (getWidth() - mask.getWidth()) / 2;
            y = y + ((getHeight() - potionOutline.getHeight()) / 2);

            g2d.drawImage(mask, x, y, this);
            y = ((getHeight() - potionOutline.getHeight()) / 2);
            g2d.drawImage(potionOutline, x, y, this);
            g2d.dispose();
        }

    }

    public static BufferedImage generateMask(BufferedImage imgSource, Color color, float alpha) {

        int imgWidth = imgSource.getWidth();
        int imgHeight = imgSource.getHeight();

        BufferedImage imgMask = createCompatibleImage(imgWidth, imgHeight, Transparency.TRANSLUCENT);
        Graphics2D g2 = imgMask.createGraphics();

        g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        g2.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
        g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        g2.drawImage(imgSource, 0, 0, null);
        g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, alpha));
        g2.setColor(color);

        g2.fillRect(0, 0, imgSource.getWidth(), imgSource.getHeight());
        g2.dispose();

        return imgMask;

    }

    public static BufferedImage createCompatibleImage(int width, int height, int transparency) {

        BufferedImage image = getGraphicsConfiguration().createCompatibleImage(width, height, transparency);
        image.coerceData(true);
        return image;

    }

    public static GraphicsConfiguration getGraphicsConfiguration() {

        return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();

    }

}

Now, having gone through the problem, what I should have done was simply create a "filled" potion and a "empty" potion (whose inner container is empty) and simply used the same process of generating a sub image (of the filled jar) instead of generating the mask...but it was fun working up to it ;)

Basically, what this means, is the "empty" jar can be completely transparent (except for the outline) and you can paint "within" it...

This is based on the concept of tinting an image as demonstrated here

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • I don't quite understand what a couple of those things do: the setRenderingHint in Graphics2D? Also, the generateMask with the parameter 1f? – Ashok Dec 13 '13 at 05:40
  • Rendering hints change the state of the `Graphics` context allowing to customise the context to your needs. Because the generation of the mask can produce rough edges, I push the `Graphics` context to a high level of quality (personal preference). The `1f` effects the alpha level of the mask. Making it `1f` makes it completely opaque. I use this method to do things like tint images and generate drop shadow effects, so I like to turn down the alpha value – MadProgrammer Dec 13 '13 at 06:11
  • Alright, so the 1f is like saying 255 in ints for the alpha? I forgot about hexcode, is that what that is? – Ashok Dec 13 '13 at 06:54
  • Yes and no. Because I'm using an `AlphaComposite`, it takes it's alpha values in the range of `0-1` (where `0.5f` would be 50%) – MadProgrammer Dec 13 '13 at 06:58
  • Hey, I have another question. You said that you should have "created a filled potion and an empty potion" instead of what you did. What if there is a way to find the parts where the background is WHITE in the potion image, and erase those areas? Is that possible? It seems simpler and will only require that one image. For example, in the image I have, I would still do the same thing I'm doing now (draw the red rectangle and the image on top), then erase the parts where the image is white (which is the outer parts of the potion)? – Ashok Feb 15 '14 at 16:48
  • Essentially your asking to do what over already done. The masking process does just that. – MadProgrammer Feb 15 '14 at 20:52
1

Yes. You need to use a clipping algorithm to perform the clipping outside of your Sprite's drawing region.

Elliott Frisch
  • 198,278
  • 20
  • 158
  • 249
  • I thought of that, but to clip you need the Shape of clipping region. But how to obtain the shape of the jar? – croraf Dec 12 '13 at 23:59
  • Add another copy of your jar image but remove the white background, the black border could easily be used to derive the Shape of the clipping region. – Elliott Frisch Dec 13 '13 at 00:01
  • What does it mean to remove the white background? It seams to me that you reasked the original question, but maybe it's just late:) – croraf Dec 13 '13 at 00:04
  • With a standard image editor, like gimp or photoshop. – Elliott Frisch Dec 13 '13 at 00:04
  • Sure, but what does it mean to remove it, to make it of grey color, or what? You must pass the rectangle image to Java. – croraf Dec 13 '13 at 00:07
  • I see. Yes, I'd probably just solid fill the outside region black, then detect the black region and use it as a clipping mask. – Elliott Frisch Dec 13 '13 at 00:11