Java read and load images - java

i just finish making a Minesweeper game, everything functions perfectly except one thing, the speed of loading the images into he game. I noticed if i have a large number of cells in the game images loads really slow after the mouse click on the cell and it gets faster if i have a smaller number of cells. is there any other way that would make loading images much faster than the one i used? here is the method i used in order to load the images into the game :
private void draw(Graphics g) {
BufferedImage gRec =null, flag=null, mine=null, aCount0=null,
aCount1=null,aCount2 =null,aCount3 =null,aCount4 =null,aCount5 =null,
aCount6 =null,aCount7 =null,aCount8 = null;
try {
gRec = ImageIO.read(new File("/Users/msa_666/Desktop/blank.gif"));
flag = ImageIO.read(new File("/Users/msa_666/Desktop/bombflagged.gif"));
mine = ImageIO.read(new File("/Users/msa_666/Desktop/bombdeath.gif"));
flag = ImageIO.read(new File("/Users/msa_666/Desktop/bombflagged.gif"));
aCount0 = ImageIO.read(new File("/Users/msa_666/Desktop/open0.gif"));
aCount1 = ImageIO.read(new File("/Users/msa_666/Desktop/open1.gif"));
aCount2 = ImageIO.read(new File("/Users/msa_666/Desktop/open2.gif"));
aCount3 = ImageIO.read(new File("/Users/msa_666/Desktop/open3.gif"));
aCount4 = ImageIO.read(new File("/Users/msa_666/Desktop/open4.gif"));
aCount5 = ImageIO.read(new File("/Users/msa_666/Desktop/open5.gif"));
aCount6 = ImageIO.read(new File("/Users/msa_666/Desktop/open6.gif"));
aCount7 = ImageIO.read(new File("/Users/msa_666/Desktop/open7.gif"));
aCount8 = ImageIO.read(new File("/Users/msa_666/Desktop/open8.gif"));
}
catch (IOException e) {
e.printStackTrace();
}
if (getCovered() == true && getMarked () == false) { // gray rectangle
g.drawImage(gRec,getX(),getY(),w,h,null);
}
else if (getCovered()==true && getMarked() == true) { //flag
g.drawImage(flag,getX(),getY(),w,h,null);
}
else if (getCovered()== false && getMined()== true){ //mine
g.drawImage(mine,getX(),getY(),w,h,null);
}
else if ( getCovered() == false && getMined() == false) { // adjacency count image
switch (getAdjCount()){
case 0:
g.drawImage(aCount0,getX(),getY(),w,h,null);
break;
case 1:
g.drawImage(aCount1,getX(),getY(),w,h,null);
break;
case 2:
g.drawImage(aCount2,getX(),getY(),w,h,null);
break;
case 3:
g.drawImage(aCount3,getX(),getY(),w,h,null);
break;
case 4:
g.drawImage(aCount4,getX(),getY(),w,h,null);
break;
case 5:
g.drawImage(aCount5,getX(),getY(),w,h,null);
break;
case 6:
g.drawImage(aCount6,getX(),getY(),w,h,null);
break;
case 7:
g.drawImage(aCount7,getX(),getY(),w,h,null);
break;
case 8:
g.drawImage(aCount8,getX(),getY(),w,h,null);
break;
}
}
}
here is the mouse listener to repaint each cell after clicking on it :
public void mouseClicked(MouseEvent e) {
int sRow, sCol;
sRow= e.getX() / cellHeight;
sCol = e.getY()/ cellWidth;
System.out.println(e.getX() +"," +sCol);
System.out.println(e.getY()+","+sRow);
if (e.getButton() == MouseEvent.BUTTON1) {
if( cells[sRow][sCol].getMarked() == false)
uncoverCell(cells[sRow][sCol]);
// cells[sRow][sCol].setCovered(false);
System.out.println(cells[sRow][sCol].getMined());
repaint();
}
else if (e.getButton() == MouseEvent.BUTTON2) {
}
else if (e.getButton() == MouseEvent.BUTTON3) {
if (cells[sRow][sCol].getMarked() == false){
cells[sRow][sCol].setMarked(true);
repaint();
}
else {
cells[sRow][sCol].setMarked(false);
repaint();
}
}
if (allMinesMarked() && allNonMinesUncovered()){
System.out.println("You Win");
}
}
public void paintComponent(Graphics g) {
for ( int i=0 ; i <rowCount; i++ ) {
for (int j=0; j<columnCount; j++) {
cells[i][j].draw(g);
}
}
}

You need to tell us:
Just where is draw(...) called?
How do you obtain the Graphics object, g, that is passed into the draw method's parameter?
I'm guessing here since we don't have all of the relevant code, but it looks as if you're re-reading in your images each time you want to display one. If so, don't do this. Read the images in only once at the start of the program, and then use the Images or perhaps better, ImageIcons, obtained when you need them.
Edit
Thanks for posting more code, and this in fact confirms my suspicion: you're re-reading in the image files with every repaint of your GUI. This is highly inefficient and will slow your program down to a crawl. Again, you should read the images into your program once and then use them multiple times.
Myself I'd create ImageIcons from the images, and then display them in a JLabel. When there is need to swap images, simply call setIcon(...) on the JLabel. This way there's no need to even mess with paintComponent(...).
Edit 2
For example (compile and run this):
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.*;
public class SwapIcons {
private static final int CELL_SIDE_COUNT = 3;
private ImageCell[] imageCells = new ImageCell[CELL_SIDE_COUNT * CELL_SIDE_COUNT];
private JPanel mainPanel = new JPanel();
public SwapIcons(final GetImages getImages) {
mainPanel.setLayout(new GridLayout(CELL_SIDE_COUNT, CELL_SIDE_COUNT));
mainPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
for (int i = 0; i < imageCells.length; i++) {
imageCells[i] = new ImageCell(getImages);
mainPanel.add(imageCells[i].getImgLabel());
}
}
public JComponent getMainComponent() {
return mainPanel;
}
private static void createAndShowGui(GetImages getImages) {
SwapIcons swapIcons = new SwapIcons(getImages);
JFrame frame = new JFrame("Click on Icons to Change");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(swapIcons.getMainComponent());
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
try {
final GetImages getImages = new GetImages();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui(getImages);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ImageCell {
private JLabel imgLabel = new JLabel();
private GetImages getImages;
private int iconIndex = 0;
public ImageCell(final GetImages getImages) {
this.getImages = getImages;
imgLabel.setIcon(getImages.getIcon(0));
imgLabel.addMouseListener(new MyMouseListener());
}
public JLabel getImgLabel() {
return imgLabel;
}
private class MyMouseListener extends MouseAdapter {
#Override
public void mousePressed(MouseEvent e) {
iconIndex++;
iconIndex %= getImages.getIconListSize();
imgLabel.setIcon(getImages.getIcon(iconIndex));
}
}
}
// Simply gets a SpriteSheet and subdivides it into a List of ImageIcons
class GetImages {
private static final String SPRITE_PATH = "http://th02.deviantart.net/"
+ "fs70/PRE/i/2011/169/0/8/blue_player_sprite_sheet_by_resetado-d3j7zba.png";
public static final int SPRITE_ROWS = 6;
public static final int SPRITE_COLS = 6;
public static final int SPRITE_CELLS = 35;
private List<ImageIcon> iconList = new ArrayList<ImageIcon>();
public GetImages() throws IOException {
URL imgUrl = new URL(SPRITE_PATH);
BufferedImage mainImage = ImageIO.read(imgUrl);
for (int i = 0; i < SPRITE_CELLS; i++) {
int row = i / SPRITE_COLS;
int col = i % SPRITE_COLS;
int x = (int) (((double) mainImage.getWidth() * col) / SPRITE_COLS);
int y = (int) ((double) (mainImage.getHeight() * row) / SPRITE_ROWS);
int w = (int) ((double) mainImage.getWidth() / SPRITE_COLS);
int h = (int) ((double) mainImage.getHeight() / SPRITE_ROWS);
BufferedImage img = mainImage.getSubimage(x, y, w, h);
ImageIcon icon = new ImageIcon(img);
iconList.add(icon);
}
}
// get the Icon from the List at index position
public ImageIcon getIcon(int index) {
if (index < 0 || index >= iconList.size()) {
throw new ArrayIndexOutOfBoundsException(index);
}
return iconList.get(index);
}
public int getIconListSize() {
return iconList.size();
}
}

Hovercraft Full Of Eels answer is good and will work.
And is fine for a standalone app, but for an applet or web start app can further optimize by having one large image and then copying parts of it to the graphics object that is visible, think grids and use function in java.awt.Graphics object (from javadoc):
public abstract boolean drawImage(Image img,
int dx1,
int dy1,
int dx2,
int dy2,
int sx1,
int sy1,
int sx2,
int sy2,
ImageObserver observer)
Draws as much of the specified area of the specified image as is currently available, scaling it on the fly to fit inside the specified area of the destination drawable surface. Transparent pixels do not affect whatever pixels are already there.
This method returns immediately in all cases, even if the image area to be drawn has not yet been scaled, dithered, and converted for the current output device. If the current output representation is not yet complete then drawImage returns false. As more of the image becomes available, the process that loads the image notifies the specified image observer.
This method always uses the unscaled version of the image to render the scaled rectangle and performs the required scaling on the fly. It does not use a cached, scaled version of the image for this operation. Scaling of the image from source to destination is performed such that the first coordinate of the source rectangle is mapped to the first coordinate of the destination rectangle, and the second source coordinate is mapped to the second destination coordinate. The subimage is scaled and flipped as needed to preserve those mappings.
Parameters:
img - the specified image to be drawn. This method does nothing if img is null.
dx1 - the x coordinate of the first corner of the destination rectangle.
dy1 - the y coordinate of the first corner of the destination rectangle.
dx2 - the x coordinate of the second corner of the destination rectangle.
dy2 - the y coordinate of the second corner of the destination rectangle.
sx1 - the x coordinate of the first corner of the source rectangle.
sy1 - the y coordinate of the first corner of the source rectangle.
sx2 - the x coordinate of the second corner of the source rectangle.
sy2 - the y coordinate of the second corner of the source rectangle.
observer - object to be notified as more of the image is scaled and converted.
Returns:
false if the image pixels are still changing; true otherwise.
This is better as it takes a few seconds to make a new connection and download image over internet, so if you have one main image that has all the sub images in a big table then total time to download, load and render will be less. extra logic to copy from an area is trivial maybe .1KB of jar file space :)

Related

How can I edit JPanel Graphics in one class?

So I'm trying to program snake on a JFrame and doing all graphical stuff (moving the 'snake', random food generation, etc.) on a JPanel. I'm in the beginning stages so all I'm trying to do right now is move a black square around on my frame using arrow keys. My while loop in the Panel class won't get interrupted by a key press in the Snake class, so is there a way to edit JPanel graphics from the same class with all my other code?
Here's all the code. My Panel class at the bottom follows the template I found here.
public class Snake {
// panel width and height
static int pW;
static int pH;
static int x = 10;
static int y = 10;
static int k;
static JFrame frame = new JFrame("SNAKE");
// getters for panel class
public int getPW() { return pW; }
public int getPH() { return pH; }
public int getX() { return x; }
public int getY() { return y; }
public static void main(String[] args) {
// get screen dimensions
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int sH = (int) screenSize.getHeight();
int sW = (int) screenSize.getWidth();
pW = (int) sW/2;
pH = (int) sH/2;
// initialize frame
frame.setSize (pW/1,pH/1);
frame.setLocation(pW/2,pH/2);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.addKeyListener( new KeyAdapter() {
public void keyPressed(KeyEvent e) {
k = e.getKeyCode();
switch(k) {
case 38: /* y -= square size */ break; // up
case 40: /* y += square size */ break; // down
case 37: /* x -= square size */ break; // left
case 39: /* x += square size */ break; // right
case 27: System.exit(0);
}
}
});
Panel panel = new Panel();
frame.add(panel);
frame.setVisible(true);
}
}
class Panel extends JPanel {
Snake snake = new Snake();
//square size and separation between squares
int sep = 0;
int size = 50;
// initial location of square on the panel/frame
int x = sep + size;
int y = sep + size;
// holding values to check if x or y have changed
int xH = x;
int yH = x;
public void paint(Graphics g) {
int pW = snake.getPW();
int pH = snake.getPH();
int i; int o;
Color on = Color.BLACK;
Color off = Color.GRAY;
// gray background
g.setColor(Color.GRAY);
g.fillRect(0,0,pW,pH);
// black square initialization
g.setColor(Color.BLACK);
g.fillRect(x, y, size, size);
/* this loop is supposed to check if the black
* rectangle has moved by repeatedly grabbing x & y
* values from the Snake class. When a key is pressed
* and the values change, a gray rectangle is placed at the old location
* and a black one is placed at the new location.
*
* When I run the program, I get stuck in this while loop.
* If I had the while loop in the same class I check for keys,
* I don't think I would have this problem
*/
while(true) {
x = snake.getX();
y = snake.getY();
if(x != xH || y != yH) {
g.setColor(off);
g.fillRect(xH, yH, size, size);
g.setColor(on);
g.fillRect(snake.getX(), snake.getY(), size, size);
xH = x;
yH = y;
}}
}
}
You should never have a while(true) loop in a painting method. This will just cause an infinite loop and your GUI will not be able to respond to events.
Instead you need to add methods to your snake class to move the snake. So when one of the arrow keys is pressed you update the starting position of the snake. Then the method will invoke repaint() and the snake will repaint itself when the paintComponent() method is invoked by Swing.
So your painting code should override paintComponent() not paint() and you should invoke super.paintComponent(g) as the first statement in the method.
Don't call your custom class "Panel", there is an AWT class with that name. Make your class name more descriptive.

Error displaying bufferedImage repainting with mouse Listening

Im having difficulty drawing a Sub Image of a Buffered Image everytime the Mouse Pointer Location equals that of the each border of the JPanel. The problem is that the BufferedImage that is equals the SubImage wont display
Here is the JPanel the initialization might not be correct Im still learning the components of Java and 2D graphics.
public class Map extends JPanel implements MouseListener, MouseMotionListener {
private final int SCR_W = 800;
private final int SCR_H = 600;
private int x;
private int y;
private int dx;
private int dy;
String dir = "C:\\imgs\\war\\";
private BufferedImage map_buffer;
public BufferedImage scr_buffer;
public void initScreen(int x, int y, int stage){
if(stage == 0){
try{ map_buffer = ImageIO.read(new File(dir + "map" + stage + ".jpg" ));
}catch(Exception error) { System.out.println("Error: cannot read tileset image.");
}
}
scr_buffer = map_buffer.getSubimage(x, y, SCR_W, SCR_H);
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
if(scr_buffer == null)
initScreen(x, y, 0);
g.drawImage(scr_buffer, 0, 0, this);
}
boolean isLeftBorder = false;
boolean isRightBorder = false;
boolean isTopBorder = false;
boolean isBottomBorder = false;
public Map(){
addMouseListener(new MouseAdapter() {
public void mouseMoved(MouseEvent e) {
/**
* Check location of mouse pointer if(specified_edge)move(scr_buffer)
*
*/
System.out.println("MouseMove: " + e.getPoint().getX() + " , " + e.getPoint().getY());
if(e.getPoint().getX() == SCR_W)isRightBorder = true;
if(e.getPoint().getY() == SCR_H)isBottomBorder = true;
if(e.getPoint().getX() == 0 && e.getPoint().getY() == SCR_H)isLeftBorder = true;
if(e.getPoint().getY() == 0 && e.getPoint().getX() == SCR_W)isTopBorder = true;
if(e.getPoint().getX() != 0 && e.getPoint().getX() != SCR_W
&& e.getPoint().getY() != 0 && e.getPoint().getY() != SCR_H){
isLeftBorder = false;
isRightBorder = false;
isTopBorder = false;
isBottomBorder = false;
}
if(isRightBorder){ x += 2; repaint(); }
if(isBottomBorder){ y -= 2; repaint(); }
if(isLeftBorder){ x -= 2; repaint();}
if(isTopBorder){ y += 2; repaint(); }
}
});
}
}
In the main I init a JFrame to contain the Panel all im getting is a error
public static void main(String[] args) {
JFrame f = new JFrame("War");
f.setSize(800, 600);
f.setLayout(null);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Map m = new Map();
f.getContentPane().add(f);
f.setVisible(true);
}
In order to detect mouse movement you should use a MosuseMotionListener, while technically MouseAdapter implements this, you need to register it with the JPanel correctly
Instead of using addMouseListener, you'll want to use addMouseMotionListener instead
I'd also be worried about the use of SRC_W and SRC_H, as you can't guarantee the size of the panel. Instead, you should be using getWidth and getHeight, which will tell you the actual size of the component
You can improve the chances of obtaining the size you want by overriding the getPreferredSize and return the size you would like. You'd then use pack on the frame to wrap the frame about it
f.getContentPane().add(f); is adding the frame to itself, it should probably be more like f.getContentPane().add(m);
f.setLayout(null); will prevent any of the child components from been sized and positioned and is best avoid, just get rid of it.
Avoid using null layouts, pixel perfect layouts are an illusion within modern ui design. There are too many factors which affect the individual size of components, none of which you can control. Swing was designed to work with layout managers at the core, discarding these will lead to no end of issues and problems that you will spend more and more time trying to rectify
This scr_buffer = map_buffer.getSubimage(x, y, SCR_W, SCR_H); is also a little dangerous, as it could be asking for more of the image then is available, you should be testing to see if x + SCR_W < image width (and same goes for the height)
I don't know if this deliberate or not, but you never reset the "border" flags, so once set, they will always be true...
addMouseMotionListener(new MouseAdapter() {
public void mouseMoved(MouseEvent e) {
/**
* Check location of mouse pointer if(specified_edge)move(scr_buffer)
*
*/
isRightBorder = false;
isBottomBorder = false;
isTopBorder = false;
isLeftBorder = false;
You may also want to have a "space" around the edge, which when the mouse enters it, it will set the border flags, for example...
if (e.getPoint().getX() >= getWidth() - 4) {
isRightBorder = true;
}
if (e.getPoint().getY() >= getHeight() - 4) {
isBottomBorder = true;
}
if (e.getPoint().getX() <= 4) {
isLeftBorder = true;
}
if (e.getPoint().getY() <= 4) {
isTopBorder = true;
}
Your logic for the vertical movement is wrong, when the mouse is within the bottom border, it should add to the y position and subtract when it's within the top border...
if (isBottomBorder) {
y += 2;
}
if (isTopBorder) {
y -= 2;
}
You need to perform some range checking after you've modified the x/y positions to make sure you're not request for a portion of the image which is not available...
if (x < 0) {
x = 0;
} else if (x + getWidth() > map_buffer.getWidth()) {
x = map_buffer.getWidth() - getWidth();
}
if (y < 0) {
y = 0;
} else if (y + getHeight() > map_buffer.getHeight()) {
y = map_buffer.getHeight() - getHeight();
}
There is a logic error within the initScreen method, src_buffer is never set to null, meaning that once it has a "sub image", it never tries to obtain a new one (also, you shouldn't be loading the map_buffer in there either).
scr_buffer = null;
repaint();
Thank you for you time and understanding.
Inside mouse moved
if (e.getPoint().getX() >= getWidth() - 4) {
isRightBorder = true; // unnecessary
scr_buffer = null;
x = x + 2;
repaint();
}

Hit detection in tilemaps

I'm working on a Mario game and am in need of assistance and suggestions on how to go about creating hit detection for a tilemap.
Currently, the player has the ability to walk/jump through the blocks.
I added in a fixed detection to the ground for now which I am hoping to replace with regular hit detection.
I understand that there are four sides to each block and the player. Only some blocks need hit detection and some things you might need to know is that the player stays at 300px(middle of screen) 98% of the time.
The only thing that moves is the map
The map is rendered from a .txt file and is rendered like so:
for(int y=0;y<map.length;y++) {
for(int x=0;x<map[y].length;x++) {
int index = map[y][x];
int yOffset = 0;
if(index>(tileSheet.getWidth() / Engine.TILE_WIDTH) -1) {
yOffset++;
index = index - (tileSheet.getWidth() / Engine.TILE_WIDTH);
}
g.drawImage(tileSheet,
((x * Engine.TILE_WIDTH)*scale)+position,
((y * Engine.TILE_HEIGHT)*scale),
(((x * Engine.TILE_WIDTH) + Engine.TILE_WIDTH )*scale)+position,
(((y * Engine.TILE_HEIGHT) + Engine.TILE_HEIGHT)*scale),
index * Engine.TILE_WIDTH,
yOffset * Engine.TILE_HEIGHT,
(index * Engine.TILE_WIDTH) + Engine.TILE_WIDTH,
(yOffset * Engine.TILE_HEIGHT) + Engine.TILE_HEIGHT,
null
);
}
}
//This code is actually longer(included file later on)
Colour hit detection is too slow and inconsistent for multi coloured tiles
Since the map is moving I suppose I need to move the hit detection boxes with it. As for selecting the boxes that it should detect might be difficult. Maybe it would be a better idea to make the code NOT hit detect certain tiles.
My attempts have ended in obfuscation of code. Can anyone suggest the easiest way to implement the hit detection? (keep in mind I have jumping).
The important codes are listed below:
Board.java(The panel where everything is drawn)
package EvilMario; //Include this class in the EvilMario game package
import java.awt.*; //Imported to allow use of Image
import java.awt.event.*; //Imported to allow use of ActionListener
import javax.swing.*; //Import swing
public class Board extends JPanel implements ActionListener { //Class Board
private TileLayer l; //Instance of TileLayer class
private Menu m; //Instance of menu class
private Player p; //Instance of player class
Timer time; //A timer
public static enum STATE {MENU,GAME}; //The game states
public static STATE State = STATE.MENU; //Set the first state to menu
//END
//GLOBAL
//DECLARATIONS
public Board() {
l = TileLayer.FromFile("D:/ICS3U1/EvilMario/map.txt"); //Tile map data from .txt file
this.addMouseListener(new MouseInput()); //Listen for mouse input
this.addKeyListener(new AL()); //Listen for key input
p = new Player(); //Start running Player class
m = new Menu(); //Start running Menu class
setFocusable(true); //Allows movement
time = new Timer(20,this); //Timer set to update "this" class every 20 milliseconds(Approximately 50fps)
time.start(); //Actually start the timer
}
public void actionPerformed(ActionEvent e) {
p.move(); //Call the move method from the player class
repaint(); //Repaint
}
public void paintComponent(Graphics g) { //Graphics method
super.paintComponent(g); //Super hero?
Graphics2D g2d = (Graphics2D) g; //Cast 2D graphics
if(State==STATE.GAME) {
if(p.distanceTraveled<300)l.DrawLayer(g,0);else l.DrawLayer(g, -(p.distanceTraveled-300)); //Draw the tile map
g2d.drawImage(p.getImage(), p.getX(), p.getY(), 48, 48, null); //Draw the player
if(p.distanceTraveled==3488) System.out.println("You have won the game!"); //Draw the end game screen
} else {
m.render(g); //Render the menu
}
}
private class AL extends KeyAdapter { //Action Listener extends key adapter
public void keyPressed(KeyEvent e) { //On key press
p.keyPressed(e); //Send whatever key was pressed TO the keyPressed method in the player class
}
public void keyReleased(KeyEvent e) { //On key release
p.keyReleased(e); //Send whatever key was released TO the keyReleased method in the player class
}
}
}
Player.java(player logic)
package EvilMario; //Include this class in the EvilMario game package
import java.awt.Image;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
public class Player {
int x, dx, y, distanceTraveled; //x coordinate,change in x coordinate,y coordinate,1st rep bg,2nd rep bg,dist traveled
Image player; //The player variable
ImageIcon walk_L_anim = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/walk_L_anim.gif");
ImageIcon walk_L_idle = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/walk_L_idle.png");
ImageIcon walk_R_anim = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/walk_R_anim.gif");
ImageIcon walk_R_idle = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/walk_R_idle.png");
ImageIcon jump_L_anim = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/jump_L_anim.gif");
ImageIcon jump_L_idle = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/jump_L_idle.png");
ImageIcon jump_R_anim = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/jump_R_anim.gif");
ImageIcon jump_R_idle = new ImageIcon("D:/ICS3U1/EvilMario/images/animatedMario/jump_R_idle.png");
boolean holdingLeft = false;
boolean holdingRight = false;
static boolean jumping = false;
static boolean falling = false;
static int jumpingTime = 350;
public Player() {
player = walk_R_idle.getImage(); //Give the player the image
x = 75; //The original x position of the player
y = 277; //The original y position of the player
distanceTraveled = 75; //Original distance traveled
}
public void move() {
if(x>=0 && x<=300) { //If the player is within the moving area
x = x+dx; //The x position is updated to become itself+the amount you moved
}
if(x<0) //If the player has reached he very left side of the screen(0px)
x=0; //Move him up a pixel so he can move again
if(x>300) //If the player has reached the center of the screen(300px)
x=300; //Move him down a pixel so he can move again
distanceTraveled=distanceTraveled+dx; //Calculate distanceTraveled
if(distanceTraveled<0) //Make sure distanceTraveled isn't a negative
distanceTraveled=0; //Make sure distanceTraveled isn't a negative
if(distanceTraveled>=300) //Keep player at center position once past 300 mario meters
x=300; //Keep player at center position once past 300 mario meters
if(holdingLeft && !holdingRight) {
if(distanceTraveled<300)dx=-5; else dx=-4;
if(jumping && !falling) {
player = jump_L_anim.getImage();
y-=8;
} else {
player = walk_L_anim.getImage();
if(y<277)
y+=8;
}
} else if(holdingRight && !holdingLeft) {
if(distanceTraveled<300)dx=5; else dx=4;
if(jumping && !falling) {
player = jump_R_anim.getImage();
y-=8;
} else {
player = walk_R_anim.getImage();
if(y<277)
y+=8;
}
} else if(!holdingRight && !holdingLeft) {
dx = 0;
if(jumping && !falling) {
player = jump_R_anim.getImage();
y-=8;
} else {
if(y<277)
y+=8;
}
}
if(y==277) {
falling = false;
}
System.out.println("LEFT: "+holdingLeft+" JUMP: "+jumping+" RIGHT: "+holdingRight+" FALLING: "+falling+" Y: "+y);
}
public int getX() { return x; } //This method will return the x. Is used by other classes
public int getY() { return y; } //This method will return the y. Is used by other classes
public Image getImage() { return player; } //This method will return the player. Is used by other classes
public void keyPressed(KeyEvent e) { //Called from the board class, the argument is whatever key was pressed
int key = e.getKeyCode(); //The key originally sent from the board class
if(key == KeyEvent.VK_LEFT && !holdingLeft)
holdingLeft = true;
if(key == KeyEvent.VK_RIGHT && !holdingRight)
holdingRight = true;
if(key == KeyEvent.VK_UP && !jumping && !falling)
new Thread(new JumpThread(this)).start();
}
public void keyReleased(KeyEvent e) { //Called from the board class, the argument is whatever key was released
int key = e.getKeyCode(); //The key originally sent from the board class
if(key == KeyEvent.VK_LEFT) { //If the left or right key was released
dx = 0; //Stop moving
holdingLeft = false;
player = walk_L_idle.getImage();
}
if(key == KeyEvent.VK_RIGHT) {
dx = 0;
holdingRight = false;
player = walk_R_idle.getImage();
}
}
}
TileLayer.java (Rendering of the tile layer)(Probably most important part relating to the question)
package EvilMario; //Include this class in the EvilMario game package
import java.awt.Graphics; //
public class TileLayer {
private int[][] map; //2D array
private BufferedImage tileSheet; //The tile sheet
public TileLayer(int[][] existingMap) { //
map = new int[existingMap.length][existingMap[0].length]; //map initialized
for(int y=0;y<map.length;y++) { //Loop through all boxes
for(int x=0;x<map[y].length;y++) { //Loop through all boxes
map[y][x] = existingMap[y][x]; //Update the map
}
}
tileSheet = LoadTileSheet("D:/ICS3U1/EvilMario/images/tilemap.gif"); //Load the tilesheet
}
public TileLayer(int width, int height) {
map = new int[height][width];
}
public static TileLayer FromFile(String fileName) {
TileLayer layer = null;
ArrayList<ArrayList<Integer>> tempLayout = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
String currentLine;
while((currentLine = br.readLine()) !=null) {
if(currentLine.isEmpty())
continue;
ArrayList<Integer> row = new ArrayList<>();
String[] values = currentLine.trim().split(" ");
for(String string: values) {
if(!string.isEmpty()) {
int id = Integer.parseInt(string);
row.add(id);
}
}
tempLayout.add(row);
}
} catch(IOException e) {
System.out.println("ERROR");
}
int width = tempLayout.get(0).size();
int height = tempLayout.size();
layer = new TileLayer(width,height);
for(int y=0;y<height;y++) {
for(int x=0;x<width;x++) {
layer.map[y][x] = tempLayout.get(y).get(x);
}
}
layer.tileSheet = layer.LoadTileSheet("D:/ICS3U1/EvilMario/images/tilemap.gif");
return layer;
}
public BufferedImage LoadTileSheet(String fileName) {
BufferedImage img = null;
try {
img = ImageIO.read(new File(fileName));
} catch(Exception e) {
System.out.println("Could not load image");
}
return img;
}
int scale = 2;
public void DrawLayer(Graphics g, int position) {
for(int y=0;y<map.length;y++) {
for(int x=0;x<map[y].length;x++) {
int index = map[y][x];
int yOffset = 0;
if(index>(tileSheet.getWidth() / Engine.TILE_WIDTH) -1) {
yOffset++;
index = index - (tileSheet.getWidth() / Engine.TILE_WIDTH);
}
g.drawImage(tileSheet,
((x * Engine.TILE_WIDTH)*scale)+position,
((y * Engine.TILE_HEIGHT)*scale),
(((x * Engine.TILE_WIDTH) + Engine.TILE_WIDTH )*scale)+position,
(((y * Engine.TILE_HEIGHT) + Engine.TILE_HEIGHT)*scale),
index * Engine.TILE_WIDTH,
yOffset * Engine.TILE_HEIGHT,
(index * Engine.TILE_WIDTH) + Engine.TILE_WIDTH,
(yOffset * Engine.TILE_HEIGHT) + Engine.TILE_HEIGHT,
null
);
}
}
}
}
Engine.java (Not as important)(Simple variables for tile sizes)
package EvilMario;
public class Engine {
public static final int TILE_WIDTH = 16;
public static final int TILE_HEIGHT = 16;
}
If you need other pieces of code, just ask for them. I am not asking you to give me a specific answer to the question but simply a method that would work with my following code.
A specific answer would be nice though :)
I also believe the answer to this question will be useful to others because this method was explained in a popular java 2d game tutorial video(They never showed hit detection).
Methods I tried:
Creating a new java file called HitDetectionLayer with the exact code in TileLayer.java that stored positions in arrays. It failed :(
Ok, I'm not entirely sure what you are doing, if you throw up some images it would be more clear.
At any rate, 'hit detection' aka collision detection is a very complex topic, but it depends on what you want to do. If you want everything to be boxes or circles, then it is quite easy. If however you want things to rotate or you want collision for complex shapes it becomes extreme difficult.
Most games use circles or spheres for collision. You put the majority of your graphics (it may not fit perfectly either leaving part of your images in or out of the circle but that's life). Now lets say you have your mario sprite and one of those turtles. Well, you have circles around them both and once the circles touch you trigger your event.
The math for this is very easy because circles are by definition a perimeter around a constant length. Look at this:
You probably already know this, and it may seem obvious, but if you think about it this is what a circle really is: a consistent length in every fathomable direction. The directions are measured in degrees and from there you move on to trigonometry but you don't need that. What you need is coordinance aka vectors. So look at this:
All you need to determine circle collision is the distance between the circles. No matter what angle the circles collide from it does not matter because the distances from the circle's centre are consistent all the way around. Even if the circles are different sizes, it doesn't matter, just account for the radii difference.
Too compute all of this, you would write a method like this:
public boolean testDistanceBetween( float radius1, float radius2,
float x1, float x2, float y1, float y2 ){
double distanceBetween = Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
if(distanceBetween < (radius1+radius2) ){
return true;
}
return false;
}
The moral of the story is that circles are just good that way. If you want to do rectangle collision you take the bottom-left and top right point and you test if other rectangles are in between those points. This should be pretty straight forward, each point is a vector, each rectangle has 4 points. If any of the 4 points of one rectangle are between points on the other rectangle, there is collision.
You can use this system to handle ground and walls also. For example, if ground is at Y=300, then if your sprite's y coordinance are == 300, you suspend your gravity.
The main thing I wanted to explain is that if you intend to have rotating rectangles or polygons and you want to detect collision on them... good luck. It can be done yes, but you should understand you are implementing complex physics, especially when/if you implement gravity.
So my answer is cautionary: there is NO easy way to detect collision of rotating rectangles or polygons. Circles and static rectangles are the limits. If you really want to do rotating rectangles/polygons get a physics engine. Box2d is pretty good and has a Java version Jbox2d.

Zoomable JScrollPane - setViewPosition fails to update

I'm trying to code a zoom-able image in a JScrollPane.
When the image is fully zoomed out it should be centered horizontally and vertically. When both scroll bars have appeared the zooming should always happen relative to the mouse coordinate, i.e. the same point of the image should be under the mouse before and after the zoom event.
I have almost achieves my goal. Unfortunately the "scrollPane.getViewport().setViewPosition()" method sometimes fails to update the view position correctly. Calling the method twice (hack!) overcomes the issue in most cases, but the view still flickers.
I have no explanation as to why this is happening. However I'm confident that it's not a math problem.
Below is a MWE. To see what my problem is in particular you can do the following:
Zoom in until you have some scroll bars (200% zoom or so)
Scroll into the bottom right corner by clicking the scroll bars
Place the mouse in the corner and zoom in twice. The second time you'll see how the scroll position jumps towards the center.
I would really appreciate if someone could tell me where the problem lies. Thank you!
package com.vitco;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel test case
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
// IMPORTANT: This call doesn't always update the viewport position. If the call is made twice
// it works correctly. However the screen still "flickers".
scrollPane.getViewport().setViewPosition(newViewportPosition);
// debug
if (!newViewportPosition.equals(scrollPane.getViewport().getViewPosition())) {
System.out.println("Error: " + newViewportPosition + " != " + scrollPane.getViewport().getViewPosition());
}
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Note: I have also looked at the question here JScrollPane setViewPosition After "Zoom" but unfortunately the problem and solution are slightly different and do not apply.
Edit
I have solved the issue by using a hack, however I'm still no closer to understanding as to what the underlying problem is. What is happening is that when the setViewPosition is called some internal state changes trigger additional calls to setViewPosition. These additional calls only happen occasionally. When I'm blocking them everything works perfectly.
To fix the problem I simply introduced a new boolean variable "blocked = false;" and replaced the lines
scrollPane = new JScrollPane(centerPanel);
and
scrollPane.getViewport().setViewPosition(newViewportPosition);
with
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
and
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
I would still really appreciate if someone could make sense of this!
Why does this hack work? Is there a cleaner way to achieve the same functionality?
Here is the completed, fully functional Code. I still don't understand why the hack is necessary, but at least it now works as expected:
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
public class ZoomScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomScrollPanel();
}
}
Some time ago I was facing the same issue. I had some scalable/zoomable content (SWT widgets) stored in Viewport in JScrollPane and some features implemented to enable panning and zooming the content. I didn't look into your code if it's basically the same, but the issue that I was observing was completely the same. When zooming outside from the right/bottom side, sometimes, the view position jumped a little bit into the center (from my point-of-view that definitely points to a scale factor). Using doubled "setViewPosition" somehow enhanced the behavior but still not usable.
After some investigation, I've found out that the issue on my side was between the moment when I changed the scale factor of the content inside the scroll panel and the moment when view position was set in scroll panel. The thing is that scroll panel doesn't know about the content size updates until layout is done. So basically, it's updating the position based on old content size, extent size and view position.
So, at my side, this helped a lot.
// updating scroll panel content scale goes here
viewport.doLayout();
// setting view position in viewport goes here
Checking method BasicScrollPaneUI#syncScrollPaneWithViewport() was very useful on my side.
very useful example, excellent zoom at mouse pointer, here is the same code slightly modified to include mouse panning:
original code added taken from --> Scroll JScrollPane by dragging mouse (Java swing)
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* Zoom-able scroll panel
*/
// https://stackoverflow.com/questions/22649636/zoomable-jscrollpane-setviewposition-fails-to-update
public class ZoomPanScrollPanel {
// the size of our image
private final static int IMAGE_SIZE = 1600;
// create an image to display
private BufferedImage getImage() {
BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// draw the small pixel first
Random rand = new Random();
for (int x = 0; x < IMAGE_SIZE; x += 10) {
for (int y = 0; y < IMAGE_SIZE; y += 10) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), rand.nextInt(255)));
g.fillRect(x, y, 10, 10);
}
}
// draw the larger transparent pixel second
for (int x = 0; x < IMAGE_SIZE; x += 100) {
for (int y = 0; y < IMAGE_SIZE; y += 100) {
g.setColor(new Color(rand.nextInt(255),rand.nextInt(255),rand.nextInt(255), 180));
g.fillRect(x, y, 100, 100);
}
}
return image;
}
// the image panel that resizes according to zoom level
private class ImagePanel extends JPanel {
private final BufferedImage image = getImage();
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g.create();
g2.scale(scale, scale);
g2.drawImage(image, 0, 0, null);
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension((int)Math.round(IMAGE_SIZE * scale), (int)Math.round(IMAGE_SIZE * scale));
}
}
// the current zoom level (100 means the image is shown in original size)
private double zoom = 100;
// the current scale (scale = zoom/100)
private double scale = 1;
// the last seen scale
private double lastScale = 1;
// true if currently executing setViewPosition
private boolean blocked = false;
public void alignViewPort(Point mousePosition) {
// if the scale didn't change there is nothing we should do
if (scale != lastScale) {
// compute the factor by that the image zoom has changed
double scaleChange = scale / lastScale;
// compute the scaled mouse position
Point scaledMousePosition = new Point(
(int)Math.round(mousePosition.x * scaleChange),
(int)Math.round(mousePosition.y * scaleChange)
);
// retrieve the current viewport position
Point viewportPosition = scrollPane.getViewport().getViewPosition();
// compute the new viewport position
Point newViewportPosition = new Point(
viewportPosition.x + scaledMousePosition.x - mousePosition.x,
viewportPosition.y + scaledMousePosition.y - mousePosition.y
);
// update the viewport position
blocked = true;
scrollPane.getViewport().setViewPosition(newViewportPosition);
blocked = false;
// remember the last scale
lastScale = scale;
}
}
// reference to the scroll pane container
private final JScrollPane scrollPane;
// constructor
public ZoomPanScrollPanel() {
// initialize the frame
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 600);
// initialize the components
final ImagePanel imagePanel = new ImagePanel();
final JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridBagLayout());
centerPanel.add(imagePanel);
scrollPane = new JScrollPane();
scrollPane.setViewport(new JViewport() {
private boolean inCall = false;
#Override
public void setViewPosition(Point pos) {
if (!inCall || !blocked) {
inCall = true;
super.setViewPosition(pos);
inCall = false;
}
}
});
scrollPane.getViewport().add(centerPanel);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
frame.add(scrollPane);
// add mouse wheel listener
imagePanel.addMouseWheelListener(new MouseAdapter() {
#Override
public void mouseWheelMoved(MouseWheelEvent e) {
super.mouseWheelMoved(e);
// check the rotation of the mousewheel
int rotation = e.getWheelRotation();
boolean zoomed = false;
if (rotation > 0) {
// only zoom out until no scrollbars are visible
if (scrollPane.getHeight() < imagePanel.getPreferredSize().getHeight() ||
scrollPane.getWidth() < imagePanel.getPreferredSize().getWidth()) {
zoom = zoom / 1.3;
zoomed = true;
}
} else {
// zoom in until maximum zoom size is reached
double newCurrentZoom = zoom * 1.3;
if (newCurrentZoom < 1000) { // 1000 ~ 10 times zoom
zoom = newCurrentZoom;
zoomed = true;
}
}
// check if a zoom happened
if (zoomed) {
// compute the scale
scale = (float) (zoom / 100f);
// align our viewport
alignViewPort(e.getPoint());
// invalidate and repaint to update components
imagePanel.revalidate();
scrollPane.repaint();
}
}
});
//mouse panning
//original code: https://stackoverflow.com/questions/31171502/scroll-jscrollpane-by-dragging-mouse-java-swing
MouseAdapter ma = new MouseAdapter() {
private Point origin;
#Override
public void mousePressed(MouseEvent e) {
origin = new Point(e.getPoint());
}
#Override
public void mouseReleased(MouseEvent e) {
}
#Override
public void mouseDragged(MouseEvent e) {
if (origin != null) {
JViewport viewPort = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, imagePanel);
if (viewPort != null) {
int deltaX = origin.x - e.getX();
int deltaY = origin.y - e.getY();
System.out.println("X pan = "+ deltaX);
System.out.println("Y pan = "+ deltaY);
Rectangle view = viewPort.getViewRect();
view.x += deltaX;
view.y += deltaY;
imagePanel.scrollRectToVisible(view);
}
}
}
};
imagePanel.addMouseListener(ma);
imagePanel.addMouseMotionListener(ma);
imagePanel.setAutoscrolls(true);
// display our frame
frame.setVisible(true);
}
// the main method
public static void main(String[] args) {
new ZoomPanScrollPanel();
}
}

Java bitmap font: blitting 1-bit image with different colors

I'd like to implement a simple bitmap font drawing in Java AWT-based application. Application draws on a Graphics object, where I'd like to implement a simple algorithm:
1) Load a file (probably using ImageIO.read(new File(fileName))), which is 1-bit PNG that looks something like that:
I.e. it's 16*16 (or 16*many, if I'd like to support Unicode) matrix of 8*8 characters. Black corresponds to background color, white corresponds to foreground.
2) Draw strings character-by-character, blitting relevant parts of this bitmap to target Graphics. So far I've only succeeded with something like that:
int posX = ch % 16;
int posY = ch / 16;
int fontX = posX * CHAR_WIDTH;
int fontY = posY * CHAR_HEIGHT;
g.drawImage(
font,
dx, dy, dx + CHAR_WIDTH, dy + CHAR_HEIGHT,
fontX, fontY, fontX + CHAR_WIDTH, fontY + CHAR_HEIGHT,
null
);
It works, but, alas, it blits the text as is, i.e. I can't substitute black and white with desired foreground and background colors, and I can't even make background transparent.
So, the question is: is there a simple (and fast!) way in Java to blit part of one 1-bit bitmap to another, colorizing it in process of blitting (i.e. replacing all 0 pixels with one given color and all 1 pixels with another)?
I've researched into a couple of solutions, all of them look suboptimal to me:
Using a custom colorizing BufferedImageOp, as outlined in this solution - it should work, but it seems that it would be very inefficient to recolorize a bitmap before every blit operation.
Using multiple 32-bit RGBA PNG, with alpha channel set to 0 for black pixels and to maximum for foreground. Every desired foreground color should get its own pre-rendered bitmap. This way I can make background transparent and draw it as a rectangle separately before blitting and then select one bitmap with my font, pre-colorized with desired color and draw a portion of it over that rectangle. Seems like a huge overkill to me - and what makes this option even worse - it limits number of foreground colors to a relatively small amount (i.e. I can realistically load up and hold like hundreds or thousands of bitmaps, not millions)
Bundling and loading a custom font, as outlined in this solution could work, but as far as I see in Font#createFont documentation, AWT's Font seems to work only with vector-based fonts, not with bitmap-based.
May be there's already any libraries that implement such functionality? Or it's time for me to switch to some sort of more advanced graphics library, something like lwjgl?
Benchmarking results
I've tested a couple of algorithms in a simple test: I have 2 strings, 71 characters each, and draw them continuously one after another, right on the same place:
for (int i = 0; i < N; i++) {
cv.putString(5, 5, STR, Color.RED, Color.BLUE);
cv.putString(5, 5, STR2, Color.RED, Color.BLUE);
}
Then I measure time taken and calculate speed: string per second and characters per second. So far, various implementation I've tested yield the following results:
bitmap font, 16*16 characters bitmap: 10991 strings / sec, 780391 chars / sec
bitmap font, pre-split images: 11048 strings / sec, 784443 chars / sec
g.drawString(): 8952 strings / sec, 635631 chars / sec
colored bitmap font, colorized using LookupOp and ByteLookupTable: 404 strings / sec, 28741 chars / sec
You might turn each bitmap into a Shape (or many of them) and draw the Shape. See Smoothing a jagged path for the process of gaining the Shape.
E.G.
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.*;
import java.util.Random;
/* Gain the outline of an image for further processing. */
class ImageShape {
private BufferedImage image;
private BufferedImage ImageShape;
private Area areaOutline = null;
private JLabel labelOutline;
private JLabel output;
private BufferedImage anim;
private Random random = new Random();
private int count = 0;
private long time = System.currentTimeMillis();
private String rate = "";
public ImageShape(BufferedImage image) {
this.image = image;
}
public void drawOutline() {
if (areaOutline!=null) {
Graphics2D g = ImageShape.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.getHeight());
g.setColor(Color.RED);
g.setClip(areaOutline);
g.fillRect(0,0,ImageShape.getWidth(),ImageShape.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(1,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));
originalImage.add(originalLabel);
images.add(originalImage);
ImageShape = new BufferedImage(
image.getWidth(),
image.getHeight(),
BufferedImage.TYPE_INT_RGB
);
labelOutline = new JLabel(new ImageIcon(ImageShape));
images.add(labelOutline);
anim = new BufferedImage(
image.getWidth()*2,
image.getHeight()*2,
BufferedImage.TYPE_INT_RGB);
output = new JLabel(new ImageIcon(anim));
gui.add(output, BorderLayout.CENTER);
updateImages();
gui.add(images, BorderLayout.NORTH);
animate();
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent ae) {
animate();
}
};
Timer timer = new Timer(1,al);
timer.start();
return gui;
}
private void updateImages() {
areaOutline = getOutline(Color.BLACK, image);
drawOutline();
}
private void animate() {
Graphics2D gr = anim.createGraphics();
gr.setColor(Color.BLUE);
gr.fillRect(0,0,anim.getWidth(),anim.getHeight());
count++;
if (count%100==0) {
long now = System.currentTimeMillis();
long duration = now-time;
double fraction = (double)duration/1000;
rate = "" + (double)100/fraction;
time = now;
}
gr.setColor(Color.WHITE);
gr.translate(0,0);
gr.drawString(rate, 20, 20);
int x = random.nextInt(image.getWidth());
int y = random.nextInt(image.getHeight());
gr.translate(x,y);
int r = 128+random.nextInt(127);
int g = 128+random.nextInt(127);
int b = 128+random.nextInt(127);
gr.setColor(new Color(r,g,b));
gr.draw(areaOutline);
gr.dispose();
output.repaint();
}
public static void main(String[] args) throws Exception {
int size = 150;
final BufferedImage outline = javax.imageio.ImageIO.read(new java.io.File("img.gif"));
ImageShape io = new ImageShape(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);
}
}
I have to figure there is a factor of ten error in the FPS count on the top left of the blue image though. 50 FPS I could believe, but 500 FPS seems ..wrong.
Okay, looks like I've found the best solution. The key to success was accessing raw pixel arrays in underlying AWT structures. Initialization goes something like that:
public class ConsoleCanvas extends Canvas {
protected BufferedImage buffer;
protected int w;
protected int h;
protected int[] data;
public ConsoleCanvas(int w, int h) {
super();
this.w = w;
this.h = h;
}
public void initialize() {
data = new int[h * w];
// Fill data array with pure solid black
Arrays.fill(data, 0xff000000);
// Java's endless black magic to get it working
DataBufferInt db = new DataBufferInt(data, h * w);
ColorModel cm = ColorModel.getRGBdefault();
SampleModel sm = cm.createCompatibleSampleModel(w, h);
WritableRaster wr = Raster.createWritableRaster(sm, db, null);
buffer = new BufferedImage(cm, wr, false, null);
}
#Override
public void paint(Graphics g) {
update(g);
}
#Override
public void update(Graphics g) {
g.drawImage(buffer, 0, 0, null);
}
}
After this one, you've got both a buffer that you can blit on canvas updates and underlying array of ARGB 4-byte ints - data.
Single character can be drawn like that:
private void putChar(int dx, int dy, char ch, int fore, int back) {
int charIdx = 0;
int canvasIdx = dy * canvas.w + dx;
for (int i = 0; i < CHAR_HEIGHT; i++) {
for (int j = 0; j < CHAR_WIDTH; j++) {
canvas.data[canvasIdx] = font[ch][charIdx] ? fore : back;
charIdx++;
canvasIdx++;
}
canvasIdx += canvas.w - CHAR_WIDTH;
}
}
This one uses a simple boolean[][] array, where first index chooses character and second index iterates over raw 1-bit character pixel data (true => foreground, false => background).
I'll try to publish a complete solution as a part of my Java terminal emulation class set soon.
This solution benchmarks for impressive 26007 strings / sec or 1846553 chars / sec - that's 2.3x times faster than previous best non-colorized drawImage().

Categories