I am wanting to make a game that has each level loaded from an image.
I want to draw up the whole level in Photoshop, and then set it as the background and allow the player to walk over it.
I want another invisible image to go over top which will be black in all places that I want to collide with.
The reason I don't want to use tiles, which are much easier with rectangle collision and such, is because there will be complex corners and not everything will be rectangle.
Is this a good idea, and is it possible to do easily?
Would this be a big CPU hog or is there a better way to do this?
Level image
Obstacles shown in red
..there will be complex corners and not everything will be rectangle.
This could be achieved by drawing and dealing with Shape and Area instances. E.G.
Yellow is a little animated 'player'.
The bounds of the image represent walls that contain the path of the player (it bounces off them).
Obstacles are painted green when not in collision, red otherwise.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
class ShapeCollision {
private BufferedImage img;
private Area[] obstacles = new Area[4];
private Area walls;
int x;
int y;
int xDelta = 3;
int yDelta = 2;
/** A method to determine if two instances of Area intersect */
public boolean doAreasCollide(Area area1, Area area2) {
boolean collide = false;
Area collide1 = new Area(area1);
collide1.subtract(area2);
if (!collide1.equals(area1)) {
collide = true;
}
Area collide2 = new Area(area2);
collide2.subtract(area1);
if (!collide2.equals(area2)) {
collide = true;
}
return collide;
}
ShapeCollision() {
int w = 400;
int h = 200;
img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
final JLabel imageLabel = new JLabel(new ImageIcon(img));
x = w/2;
y = h/2;
//circle
obstacles[0] = new Area(new Ellipse2D.Double(40, 40, 30, 30));
int[] xTriangle = {330,360,345};
int[] yTriangle = {60,60,40};
//triangle
obstacles[1] = new Area(new Polygon(xTriangle, yTriangle, 3));
int[] xDiamond = {60,80,60,40};
int[] yDiamond = {120,140,160,140};
//diamond
obstacles[2] = new Area(new Polygon(xDiamond, yDiamond, 4));
int[] xOther = {360,340,360,340};
int[] yOther = {130,110,170,150};
// other
obstacles[3] = new Area(new Polygon(xOther, yOther, 4));
walls = new Area(new Rectangle(0,0,w,h));
ActionListener animate = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
animate();
imageLabel.repaint();
}
};
Timer timer = new Timer(50, animate);
timer.start();
JOptionPane.showMessageDialog(null, imageLabel);
timer.stop();
}
public void animate() {
Graphics2D g = img.createGraphics();
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.BLUE);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
x+=xDelta;
y+=yDelta;
int s = 15;
Area player = new Area(new Ellipse2D.Double(x, y, s, s));
// Acid test of edge collision;
if (doAreasCollide(player,walls)) {
if ( x+s>img.getWidth() || x<0 ) {
xDelta *= -1;
}
if(y+s>img.getHeight() || y<0 ) {
yDelta *= -1;
}
}
g.setColor(Color.ORANGE);
for (Area obstacle : obstacles) {
if (doAreasCollide(obstacle, player)) {
g.setColor(Color.RED);
} else {
g.setColor(Color.GREEN);
}
g.fill(obstacle);
}
g.setColor(Color.YELLOW);
g.fill(player);
g.dispose();
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
new ShapeCollision();
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
SwingUtilities.invokeLater(r);
}
}
Edit
make it detect all the red color and set that as the collision bounds
At start-up, use the source seen in the Smoothing a jagged path question to get an outline of the red pixels (see the getOutline(Color target, BufferedImage bi) method). Store that Area as the single obstacle on start-up.
Related
I am making a game (like Civilization) that has different tile types that I want to render as images. I have 9 different 16x16 png images to load in (called Con1, Con2, etc.), and here is my image loading code: (img[] is my BufferedImage array)
public void loadImages(){
for(int i = 0; i < 9; i++){
try {
img[i] = ImageIO.read(this.getClass().getResource("Con"+i+".png"));
}catch (Exception ex) {
System.out.println("Missing Image");
ex.printStackTrace();
}
}
}
I then paint these images with this code: (t[][] is my tile type array)
public void paint(Graphics g){
if(loop){
BufferedImage B = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics r = B.getGraphics();
for (int x = 0; x < WIDTH; x++){
for (int y = 0; y < HEIGHT; y++){
if(i[x][y] == 0){
if (t[x][y] == 0){
g.drawImage(img[0], x, y, this);
}
else if(t[x][y] == 1){
g.drawImage(img[1], x, y, this);
}
else if(t[x][y] == 3){
g.drawImage(img[3], x, y, this);
}
else if(t[x][y] == 4){
g.drawImage(img[4], x, y, this);
}
else if(t[x][y] == 5){
g.drawImage(img[5], x, y, this);
}
}
r.fillRect(x*SCALE, y*SCALE, SCALE, SCALE);
}
}
g.drawImage(B, 0, 22, this);
}
}
My problem is that it doesn't show up correctly when I run it. I get this image:
that flashes on and off in the top-left corner of the window. What I am supposed to see is an image similar to the top-left portion of the above one (landmasses surrounded by ocean) except large enough to fill the window. Here is some runnable code: (I don't think the code will run without the required images but I would appreciate some help with getting the images to you all.)
//imports
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
public class MCVCon extends JFrame implements KeyListener, MouseListener{
//setting up variables
public BufferedImage[] img = new BufferedImage[9];
private final int WIDTH = 50, HEIGHT = 50;
private boolean loop = false;
private int SCALE = 16;
int t[][] = new int[WIDTH][HEIGHT]; //terrain type (since I took out the terrain generation it is set to 0 or ocean)
public MCVCon(){
//creating the window
super("Conqueror");
setSize(SCALE*WIDTH, SCALE*HEIGHT+22);
setVisible(true);
requestFocusInWindow();
setDefaultCloseOperation(EXIT_ON_CLOSE);
loadImages();
loop = true;
while(true){
this.repaint();
//delay for repaint
try{
Thread.sleep(50);
}
catch(Exception ex){
ex.printStackTrace();
}
}
}
//load images
public void loadImages(){
for(int i = 0; i < 9; i++){
try {
img[i] = ImageIO.read(this.getClass().getResource("Con"+i+".png"));
}catch (Exception ex) {
System.out.println("Missing Image");
ex.printStackTrace();
}
}
}
//paint the images
public void paint(Graphics g){
if(loop){
BufferedImage B = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics r = B.getGraphics();
for (int x = 0; x < WIDTH; x++){
for (int y = 0; y < HEIGHT; y++){
if (t[x][y] == 0){
g.drawImage(img[0], x, y, this);
}
else if(t[x][y] == 1){
g.drawImage(img[1], x, y, this);
}
r.fillRect(x*SCALE, y*SCALE, SCALE, SCALE);
}
}
g.drawImage(B, 0, 22, this);
}
}
//run the program
public static void main(String[] args) {
new MCVCon();
}
//necessary overrides
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
}
#Override
public void keyReleased(KeyEvent e) {
}
#Override
public void mouseClicked(MouseEvent e) {
}
#Override
public void mousePressed(MouseEvent e) {
}
#Override
public void mouseReleased(MouseEvent e) {
}
#Override
public void mouseEntered(MouseEvent e) {
}
#Override
public void mouseExited(MouseEvent e) {
}
}
I was wondering what the problem might be. Thanks in advance.
So, I had a look at your code, there's no easy way to say, but it's a mess, with compounding issues which would make it very difficult to isolate the origin of any one problem, other than to say, they all feed into each other.
Let's start with the painting...
You're painting directly to the frame. This is generally discouraged for a number of reasons.
Let's start with the fact that JFrame isn't a single component, it's made up of a number compounding components
This makes it inherently dangerous to paint directly to, as any one of the child components could be painted without the frame's paint method been called.
Painting directly to a frame also means you're painting without consideration to the frame's borders/decorations, which are added into the visible area of the window, but wait...
setSize(SCALE*WIDTH, SCALE*HEIGHT+22);
suggests that you've tried to compensate for this, but this is "guess" work, as the decorations could take up more or less space depending on the configuration of the system
And, finally, top level containers aren't actually double buffered.
"But I'm painting to my own buffer" you say - but you're not
You create a BufferdImage and assign it's Graphics context t r
BufferedImage B = new BufferedImage(this.getWidth(), this.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics r = B.getGraphics();
But when you paint anything, you're using g, which is the Graphics context passed to the paint method
g.drawImage(img[0], x, y, this);
This is also woefully inefficient, as you're creating a new BufferedImage each time paint is called, which takes time to create, takes up memory and puts extra strain on the garbage collection process as the local object becomes eligible for disposal almost immediately
Don't even get me started on the "main paint loop"
The next problem you have, is you have no concept of virtual and real world space.
You make a virtual map of your world using t, which maintains information about which tile should be used for a given x/y coordinate, but you never map this to the real world, instead, you paint each tile exactly at the same pixel x/y position, which means they now overlap, tiles have width and height, which means they need to be offset when painted onto the real world.
And finally, which I think is your actually question, is about scaling. There are a number of ways you could apply scaling, you could pre-scale the tiles when you load them, this gives you a lot of control over "how" they are scaled, but locks you into a single scale.
You could instead maintain a list of the scaled tiles, generated from a master list, which can be updated if the scale changes
Or you could simply scale the Graphics context directly.
Now, I've create a simple example, based on your code, correcting for most of the above. It creates a series of randomly colored rectangles at 10x10 pixels, instead of tiles, but the concept is the same.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class MCVCon extends JFrame {
//setting up variables
public MCVCon() {
//creating the window
super("Conqueror");
add(new GamePane());
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
MCVCon frame = new MCVCon();
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
//necessary overrides
public class GamePane extends JPanel {
public BufferedImage[] img = new BufferedImage[9];
private final int width = 5, height = 5;
private int scale = 16;
int t[][] = new int[width][height]; //terrain type (since I took out the terrain generation it is set to 0 or ocean)
Color[] colors = new Color[]{
Color.RED,
Color.BLUE,
Color.CYAN,
Color.DARK_GRAY,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.ORANGE,
Color.PINK,
Color.YELLOW
};
int tileHeight = 10;
int tileWidth = 10;
public GamePane() {
loadImages();
Random rnd = new Random();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int value = rnd.nextInt(9);
System.out.println(value + "- " + colors[value]);
t[x][y] = value;
}
}
Timer timer = new Timer(50, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(width * tileWidth * scale, height * tileHeight * scale);
}
public void loadImages() {
for (int i = 0; i < 9; i++) {
try {
img[i] = new BufferedImage(tileWidth, tileHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = img[i].createGraphics();
g2d.setColor(colors[i]);
g2d.fill(new Rectangle(0, 0, tileWidth, tileHeight));
g2d.dispose();
} catch (Exception ex) {
System.out.println("Missing Image");
ex.printStackTrace();
}
}
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setTransform(AffineTransform.getScaleInstance(scale, scale));
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
g2d.drawImage(img[t[x][y]], x * tileWidth, y * tileHeight, this);
}
}
g2d.dispose();
}
}
}
I am wanting to make a game that has each level loaded from an image.
I want to draw up the whole level in Photoshop, and then set it as the background and allow the player to walk over it.
I want another invisible image to go over top which will be black in all places that I want to collide with.
The reason I don't want to use tiles, which are much easier with rectangle collision and such, is because there will be complex corners and not everything will be rectangle.
Is this a good idea, and is it possible to do easily?
Would this be a big CPU hog or is there a better way to do this?
Level image
Obstacles shown in red
..there will be complex corners and not everything will be rectangle.
This could be achieved by drawing and dealing with Shape and Area instances. E.G.
Yellow is a little animated 'player'.
The bounds of the image represent walls that contain the path of the player (it bounces off them).
Obstacles are painted green when not in collision, red otherwise.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
class ShapeCollision {
private BufferedImage img;
private Area[] obstacles = new Area[4];
private Area walls;
int x;
int y;
int xDelta = 3;
int yDelta = 2;
/** A method to determine if two instances of Area intersect */
public boolean doAreasCollide(Area area1, Area area2) {
boolean collide = false;
Area collide1 = new Area(area1);
collide1.subtract(area2);
if (!collide1.equals(area1)) {
collide = true;
}
Area collide2 = new Area(area2);
collide2.subtract(area1);
if (!collide2.equals(area2)) {
collide = true;
}
return collide;
}
ShapeCollision() {
int w = 400;
int h = 200;
img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
final JLabel imageLabel = new JLabel(new ImageIcon(img));
x = w/2;
y = h/2;
//circle
obstacles[0] = new Area(new Ellipse2D.Double(40, 40, 30, 30));
int[] xTriangle = {330,360,345};
int[] yTriangle = {60,60,40};
//triangle
obstacles[1] = new Area(new Polygon(xTriangle, yTriangle, 3));
int[] xDiamond = {60,80,60,40};
int[] yDiamond = {120,140,160,140};
//diamond
obstacles[2] = new Area(new Polygon(xDiamond, yDiamond, 4));
int[] xOther = {360,340,360,340};
int[] yOther = {130,110,170,150};
// other
obstacles[3] = new Area(new Polygon(xOther, yOther, 4));
walls = new Area(new Rectangle(0,0,w,h));
ActionListener animate = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
animate();
imageLabel.repaint();
}
};
Timer timer = new Timer(50, animate);
timer.start();
JOptionPane.showMessageDialog(null, imageLabel);
timer.stop();
}
public void animate() {
Graphics2D g = img.createGraphics();
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.BLUE);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
x+=xDelta;
y+=yDelta;
int s = 15;
Area player = new Area(new Ellipse2D.Double(x, y, s, s));
// Acid test of edge collision;
if (doAreasCollide(player,walls)) {
if ( x+s>img.getWidth() || x<0 ) {
xDelta *= -1;
}
if(y+s>img.getHeight() || y<0 ) {
yDelta *= -1;
}
}
g.setColor(Color.ORANGE);
for (Area obstacle : obstacles) {
if (doAreasCollide(obstacle, player)) {
g.setColor(Color.RED);
} else {
g.setColor(Color.GREEN);
}
g.fill(obstacle);
}
g.setColor(Color.YELLOW);
g.fill(player);
g.dispose();
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
new ShapeCollision();
}
};
// Swing GUIs should be created and updated on the EDT
// http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html
SwingUtilities.invokeLater(r);
}
}
Edit
make it detect all the red color and set that as the collision bounds
At start-up, use the source seen in the Smoothing a jagged path question to get an outline of the red pixels (see the getOutline(Color target, BufferedImage bi) method). Store that Area as the single obstacle on start-up.
Does java Shape interface contract and library routines allow combining multiple shapes into one object extending Shape interface?
For example, may I define class Flower which will consist of several ovals for petals and core?
Or the Shape supposes only one continuous outline? If so then is there any class in Java for holding multiple shapes, may be some class for vectorized graphics?
To manipulate shapes in Java like you're describing, you want to use the Area class, which has these operations. Just convert a Shape to an Area with new Area(Shape).
Here is my attempt - using a rotate transform anchored on the center of the flower shape.
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
public class DaisyDisplay {
DaisyDisplay() {
BufferedImage daisy = new BufferedImage(
200,200,BufferedImage.TYPE_INT_RGB);
Graphics2D g = daisy.createGraphics();
g.setColor(Color.GREEN.darker());
g.fillRect(0, 0, 200, 200);
Daisy daisyPainter = new Daisy();
daisyPainter.paint(g);
g.dispose();
JOptionPane.showMessageDialog(null, new JLabel(new ImageIcon(daisy)));
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new DaisyDisplay();
}
});
}
}
class Daisy {
public void paint(Graphics2D g) {
Area daisyArea = getDaisyShape();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
Math.PI*1/8,
100,100));
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
Math.PI*3/8,
100,100);
paintDaisyPart(g,daisyArea);
g.setTransform(AffineTransform.getRotateInstance(
Math.PI*2/8,
100,100));
paintDaisyPart(g,daisyArea);
}
public void paintDaisyPart(Graphics2D g, Area daisyArea) {
g.setClip(daisyArea);
g.setColor(Color.YELLOW);
g.fillRect(0, 0, 200, 200);
g.setColor(Color.YELLOW.darker());
g.setClip(null);
g.setStroke(new BasicStroke(3));
g.draw(daisyArea);
}
public Area getDaisyShape() {
Ellipse2D.Double core = new Ellipse2D.Double(70,70,60,60);
Area area = new Area(core);
int size = 200;
int pad = 10;
int petalWidth = 50;
int petalLength = 75;
// left petal
area.add(new Area(new Ellipse2D.Double(
pad,(size-petalWidth)/2,petalLength,petalWidth)));
// right petal
area.add(new Area(new Ellipse2D.Double(
(size-petalLength-pad),(size-petalWidth)/2,petalLength,petalWidth)));
// top petal
area.add(new Area(new Ellipse2D.Double(
(size-petalWidth)/2,pad,petalWidth,petalLength)));
// bottom petal
area.add(new Area(new Ellipse2D.Double(
(size-petalWidth)/2,(size-petalLength-pad),petalWidth,petalLength)));
return area;
}
}
I was participating in the thread Image/Graphic into a Shape the other day and made a hackish attempt to get the outline of an image by adding a Rectangle iteratively to an Area. That was very slow.
This example instead builds a GeneralPath and creates the Area from the GP. Much faster.
The image on the upper left is the 'source image'. The two on the right are various stages of processing the outline. Both of them have jagged edges around the circle and along the slanted sides of the triangle.
I'd like to gain a shape that has that jaggedness removed or reduced.
In ASCII art.
Case 1:
1234
1 **
2 **
3 ***
4 ***
5 ****
6 ****
Corners are at:
(2,3) inner corner
(3,3)
(3,5) inner corner
(4,5)
Case 2:
1234
1 ****
2 ****
3 **
4 **
5 ****
6 ****
Corners are at:
(4,2)
(2,2) inner corner
(2,5) inner corner
(4,5)
Assuming our path had the shapes shown, and the points as listed, I'd like to drop the 'inner corner' points of the first set, while retaining the 'pair' of inner corners (a bite out of the image) for the 2nd.
Can anybody suggest some clever inbuilt method to do the heavy lifting of this job?
Failing that, what would be a good approach to identifying the location & nature (pair/single) of the inner corners? (I'm guessing I could get a PathIterator and build a new GeneralPath dropping the singular inner corners - if only I could figure how to identify them!).
Here's the code to play with:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
/* Gain the outline of an image for further processing. */
class ImageOutline {
private BufferedImage image;
private TwoToneImageFilter twoToneFilter;
private BufferedImage imageTwoTone;
private JLabel labelTwoTone;
private BufferedImage imageOutline;
private Area areaOutline = null;
private JLabel labelOutline;
private JLabel targetColor;
private JSlider tolerance;
private JProgressBar progress;
private SwingWorker sw;
public ImageOutline(BufferedImage image) {
this.image = image;
imageTwoTone = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB);
}
public void drawOutline() {
if (areaOutline!=null) {
Graphics2D g = imageOutline.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());
g.setColor(Color.RED);
g.setClip(areaOutline);
g.fillRect(0,0,imageOutline.getWidth(),imageOutline.getHeight());
g.setColor(Color.BLACK);
g.setClip(null);
g.draw(areaOutline);
g.dispose();
}
}
public Area getOutline(Color target, BufferedImage bi) {
// construct the GeneralPath
GeneralPath gp = new GeneralPath();
boolean cont = false;
int targetRGB = target.getRGB();
for (int xx=0; xx<bi.getWidth(); xx++) {
for (int yy=0; yy<bi.getHeight(); yy++) {
if (bi.getRGB(xx,yy)==targetRGB) {
if (cont) {
gp.lineTo(xx,yy);
gp.lineTo(xx,yy+1);
gp.lineTo(xx+1,yy+1);
gp.lineTo(xx+1,yy);
gp.lineTo(xx,yy);
} else {
gp.moveTo(xx,yy);
}
cont = true;
} else {
cont = false;
}
}
cont = false;
}
gp.closePath();
// construct the Area from the GP & return it
return new Area(gp);
}
public JPanel getGui() {
JPanel images = new JPanel(new GridLayout(2,2,2,2));
JPanel gui = new JPanel(new BorderLayout(3,3));
JPanel originalImage = new JPanel(new BorderLayout(2,2));
final JLabel originalLabel = new JLabel(new ImageIcon(image));
targetColor = new JLabel("Target Color");
targetColor.setForeground(Color.RED);
targetColor.setBackground(Color.WHITE);
targetColor.setBorder(new LineBorder(Color.BLACK));
targetColor.setOpaque(true);
JPanel controls = new JPanel(new BorderLayout());
controls.add(targetColor, BorderLayout.WEST);
originalLabel.addMouseListener( new MouseAdapter() {
#Override
public void mouseEntered(MouseEvent me) {
originalLabel.setCursor(
Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
#Override
public void mouseExited(MouseEvent me) {
originalLabel.setCursor(Cursor.getDefaultCursor());
}
#Override
public void mouseClicked(MouseEvent me) {
int x = me.getX();
int y = me.getY();
Color c = new Color( image.getRGB(x,y) );
targetColor.setBackground( c );
updateImages();
}
});
originalImage.add(originalLabel);
tolerance = new JSlider(
JSlider.HORIZONTAL,
0,
255,
104
);
tolerance.addChangeListener( new ChangeListener() {
public void stateChanged(ChangeEvent ce) {
updateImages();
}
});
controls.add(tolerance, BorderLayout.CENTER);
gui.add(controls,BorderLayout.NORTH);
images.add(originalImage);
labelTwoTone = new JLabel(new ImageIcon(imageTwoTone));
images.add(labelTwoTone);
images.add(new JLabel("Smoothed Outline"));
imageOutline = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);
labelOutline = new JLabel(new ImageIcon(imageOutline));
images.add(labelOutline);
updateImages();
progress = new JProgressBar();
gui.add(images, BorderLayout.CENTER);
gui.add(progress, BorderLayout.SOUTH);
return gui;
}
private void updateImages() {
if (sw!=null) {
sw.cancel(true);
}
sw = new SwingWorker() {
#Override
public String doInBackground() {
progress.setIndeterminate(true);
adjustTwoToneImage();
labelTwoTone.repaint();
areaOutline = getOutline(Color.BLACK, imageTwoTone);
drawOutline();
return "";
}
#Override
protected void done() {
labelOutline.repaint();
progress.setIndeterminate(false);
}
};
sw.execute();
}
public void adjustTwoToneImage() {
twoToneFilter = new TwoToneImageFilter(
targetColor.getBackground(),
tolerance.getValue());
Graphics2D g = imageTwoTone.createGraphics();
g.drawImage(image, twoToneFilter, 0, 0);
g.dispose();
}
public static void main(String[] args) throws Exception {
int size = 150;
final BufferedImage outline =
new BufferedImage(size,size,BufferedImage.TYPE_INT_RGB);
Graphics2D g = outline.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,size,size);
g.setRenderingHint(
RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Polygon p = new Polygon();
p.addPoint(size/2, size/10);
p.addPoint(size-10, size-10);
p.addPoint(10, size-10);
Area a = new Area(p);
Rectangle r = new Rectangle(size/4, 8*size/10, size/2, 2*size/10);
a.subtract(new Area(r));
int radius = size/10;
Ellipse2D.Double c = new Ellipse2D.Double(
(size/2)-radius,
(size/2)-radius,
2*radius,
2*radius
);
a.subtract(new Area(c));
g.setColor(Color.BLACK);
g.fill(a);
ImageOutline io = new ImageOutline(outline);
JFrame f = new JFrame("Image Outline");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(io.getGui());
f.pack();
f.setResizable(false);
f.setLocationByPlatform(true);
f.setVisible(true);
}
}
class TwoToneImageFilter implements BufferedImageOp {
Color target;
int tolerance;
TwoToneImageFilter(Color target, int tolerance) {
this.target = target;
this.tolerance = tolerance;
}
private boolean isIncluded(Color pixel) {
int rT = target.getRed();
int gT = target.getGreen();
int bT = target.getBlue();
int rP = pixel.getRed();
int gP = pixel.getGreen();
int bP = pixel.getBlue();
return(
(rP-tolerance<=rT) && (rT<=rP+tolerance) &&
(gP-tolerance<=gT) && (gT<=gP+tolerance) &&
(bP-tolerance<=bT) && (bT<=bP+tolerance) );
}
public BufferedImage createCompatibleDestImage(
BufferedImage src,
ColorModel destCM) {
BufferedImage bi = new BufferedImage(
src.getWidth(),
src.getHeight(),
BufferedImage.TYPE_INT_RGB);
return bi;
}
public BufferedImage filter(
BufferedImage src,
BufferedImage dest) {
if (dest==null) {
dest = createCompatibleDestImage(src, null);
}
for (int x=0; x<src.getWidth(); x++) {
for (int y=0; y<src.getHeight(); y++) {
Color pixel = new Color(src.getRGB(x,y));
Color write = Color.BLACK;
if (isIncluded(pixel)) {
write = Color.WHITE;
}
dest.setRGB(x,y,write.getRGB());
}
}
return dest;
}
public Rectangle2D getBounds2D(BufferedImage src) {
return new Rectangle2D.Double(0, 0, src.getWidth(), src.getHeight());
}
public Point2D getPoint2D(
Point2D srcPt,
Point2D dstPt) {
// no co-ord translation
return srcPt;
}
public RenderingHints getRenderingHints() {
return null;
}
}
This is a big subject. You might find Depixelizing Pixel Art1 by Johannes Kopf & Dani Lischinski useful: it's readable, recent, includes a summary of previous work, and explains their approach in detail.
See also slides covering similar background and video(!).
Here are some screenshots from the document of 'nearest neighbor' vs. 'their technique'.
The most general version of this problem is one of the initial stages in most computer vision pipelines. It's called Image Segementation. It splits an image into regions of pixels considered to be visually identical. These regions are separated by "contours" (see for example this article), which amount to paths through the image running along pixel boundaries.
There is a simple recursive algorithm for representing contours as a polyline defined such that no point in it deviates more than some fixed amount (say max_dev) you get to pick. Normally it's 1/2 to 2 pixels.
function getPolyline(points [p0, p1, p2... pn] in a contour, max_dev) {
if n <= 1 (there are only one or two pixels), return the whole contour
Let pi, 0 <= i <= n, be the point farthest from the line segment p0<->pn
if distance(pi, p0<->pn) < max_dev
return [ p0 -> pn ]
else
return concat(getPolyline [ p0, ..., pi ], getPolyline [ pi, ..., pn] )
The thought behind this is that you seem to have cartoon-like images that are already segmented. So if you code a simple search that assembles the edge pixels into chains, you can use the algorithm above to convert them into line segment chains that will will be smooth. They can even be drawn with anti-aliasing.
If you already know the segment or edge try blurring with Gaussian or average or one of your own kernel and move to the edge you want to smooth.
This is a quick solution and may not suit best in complex images but for self drawn, its good.
For homework, I'm trying to create a "CustomButton" that has a frame and in that frame, I draw two triangles, and a square over it. It's supposed to give the user the effect of a button press once it is depressed. So for starters, I am trying to set up the beginning graphics, drawing two triangles, and a square. The problem I have is although I set my frame to 200, 200, and the triangles I have drawn I think to the correct ends of my frame size, when I run the program, I have to extend my window to make the whole artwork, my "CustomButton," viewable. Is that normal? Thanks.
Code:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CustomButton
{
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
CustomButtonFrame frame = new CustomButtonFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
class CustomButtonFrame extends JFrame
{
// constructor for CustomButtonFrame
public CustomButtonFrame()
{
setTitle("Custom Button");
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
CustomButtonSetup buttonSetup = new CustomButtonSetup();
this.add(buttonSetup);
}
private static final int DEFAULT_WIDTH = 200;
private static final int DEFAULT_HEIGHT = 200;
}
class CustomButtonSetup extends JComponent
{
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D) g;
// first triangle coords
int x[] = new int[TRIANGLE_SIDES];
int y[] = new int[TRIANGLE_SIDES];
x[0] = 0; y[0] = 0;
x[1] = 200; y[1] = 0;
x[2] = 0; y[2] = 200;
Polygon firstTriangle = new Polygon(x, y, TRIANGLE_SIDES);
// second triangle coords
x[0] = 0; y[0] = 200;
x[1] = 200; y[1] = 200;
x[2] = 200; y[2] = 0;
Polygon secondTriangle = new Polygon(x, y, TRIANGLE_SIDES);
g2.drawPolygon(firstTriangle);
g2.setColor(Color.WHITE);
g2.fillPolygon(firstTriangle);
g2.drawPolygon(secondTriangle);
g2.setColor(Color.GRAY);
g2.fillPolygon(secondTriangle);
// draw rectangle 10 pixels off border
g2.drawRect(10, 10, 180, 180);
}
public static final int TRIANGLE_SIDES = 3;
}
Try adding
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
to your CustomButtonSetup class.
And then do
setTitle("Custom Button");
//setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
CustomButtonSetup buttonSetup = new CustomButtonSetup();
this.add(buttonSetup);
pack();
(From the api-docs on pack():)
Causes this Window to be sized to fit the preferred size and layouts of its subcomponents.
You should get something like:
The DEFAULT_WIDTH and DEFAULT_HEIGHT that you set is for the entire frame, including borders, window titles, icons, etc. It's not the size of the drawing canvas itself. Thus, it is expected that if you draw something in a 200x200 canvas, it would not necessarily fit in a 200x200 window containing that canvas.