Cloning 60FPS game into four screen - java

I have multiple games running on 60 frame per second rate. I need to capture each frame and clone into four different screen with specific filter.
Software will work on dual screen monitor where primary and secondary screen will be responsible for running game and displaying four filter view respectively.
Limitation on applying filter:
Filter will work on image only.
Filter is written in java which almost impossible to rewrite.
So I am applying logic something like below:
import java.awt.*;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import com.sun.jna.platform.win32.GDI32Util;
import com.sun.jna.platform.win32.WinDef.HWND;
import java.awt.image.BufferedImage;
public class MultiFrameApplet implements Runnable
{
public JFrame currentFrame = null;
public PanelPaint currentCanvas = null;
public BufferedImage screenshotImage = null;
com.sun.jna.platform.win32.User32 user32 = null;
HWND hwnd = null;
Thread th = null;
String CurrentFrameText = "";
private long lastTime;
private double fps; //could be int or long for integer values
public MultiFrameApplet(int filtertype)
{
main(null,filtertype);
}
public void main(String[] argv,int filtertype)
{
try
{
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] gd = ge.getScreenDevices();
//First Screen
GraphicsConfiguration gcFirst = gd[0].getDefaultConfiguration();
Toolkit toolkit = Toolkit.getDefaultToolkit();
if (toolkit == null)
{
return;
}
Rectangle screenRectFirst = gcFirst.getBounds();
Insets screenInsetsFirst = toolkit.getScreenInsets(gcFirst);
screenRectFirst.x = screenInsetsFirst.left;
screenRectFirst.y = screenInsetsFirst.top;
Robot robot = new Robot(gcFirst.getDevice());
//Second Screen
GraphicsConfiguration gcSecond = gd[1].getDefaultConfiguration();
Rectangle screenRectSecond = gcSecond.getBounds();
Insets screenInsetsSecond = Toolkit.getDefaultToolkit().getScreenInsets(gcSecond);
Rectangle effectiveScreenArea = new Rectangle();
/*Remove start bar area*/
effectiveScreenArea.x = screenRectSecond.x + screenInsetsSecond.left;
effectiveScreenArea.y = screenRectSecond.y + screenInsetsSecond.top;
effectiveScreenArea.height = screenRectSecond.height - screenInsetsSecond.top - screenInsetsSecond.bottom;
effectiveScreenArea.width = screenRectSecond.width - screenInsetsSecond.left - screenInsetsSecond.right;
//Scaling will decide capture image needs to shrink or not.!
double xscaling = 0;
double yscaling = 0;
screenshotImage = robot.createScreenCapture(screenRectFirst);
int differenceWidth = screenRectSecond.width / screenRectFirst.width;
int differenceheight = screenRectSecond.height / screenRectFirst.height;
xscaling = differenceWidth / 2.0;
yscaling = differenceheight / 2.0;
yscaling = yscaling - 0.018;
currentFrame = new JFrame();
currentFrame.setSize((int)effectiveScreenArea.width/2, (int)effectiveScreenArea.height/2);
if(filtertype == 0)
{
currentFrame.setLocation(effectiveScreenArea.x, effectiveScreenArea.y);
currentFrame.setTitle("First");
CurrentFrameText = "First";
}
else if(filtertype == 1)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2), effectiveScreenArea.y);
currentFrame.setTitle("Second");
CurrentFrameText = "Second";
}
else if(filtertype == 2)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2), effectiveScreenArea.y);
currentFrame.setTitle("Third");
CurrentFrameText = "Third";
}
else if(filtertype == 3)
{
currentFrame.setLocation(effectiveScreenArea.x + ((int)effectiveScreenArea.width/2),effectiveScreenArea.y + ((int)effectiveScreenArea.height/2));
currentFrame.setTitle("Forth");
CurrentFrameText = "Forth";
}
currentCanvas = new PanelPaint((int)effectiveScreenArea.width/2,(int)effectiveScreenArea.height/2,xscaling,yscaling,CurrentFrameText);
currentCanvas.xpos = (int)effectiveScreenArea.width/2;
currentCanvas.ypos = (int)effectiveScreenArea.height/2;
currentFrame.getContentPane().add(currentCanvas);
currentFrame.setUndecorated(true);
currentFrame.setVisible(true);
user32 = com.sun.jna.platform.win32.User32.INSTANCE;
hwnd = user32.GetDesktopWindow();
th = new Thread(this);
th.start();
}
catch (AWTException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (Exception e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void close()
{
currentFrame.dispose();
currentFrame = null;
currentCanvas.close();
currentCanvas = null;
screenshotImage = null;
user32 = null;
hwnd = null;
th = null;
}
#Override
public void run()
{
while(true)
{
lastTime = System.nanoTime();
screenshotImage = GDI32Util.getScreenshot(hwnd);
///screenshotImage = screenshotImage.Convert(); //Place where filter applied
currentCanvas.setImg(screenshotImage,fps);
java.awt.EventQueue.invokeLater(currentCanvas::repaint);
fps = 1000000000.0 / (System.nanoTime() - lastTime); //one second(nano) divided by amount of time it takes for one frame to finish
lastTime = System.nanoTime();
}
}
}
#SuppressWarnings("serial")
class PanelPaint extends javax.swing.JPanel
{
public int xpos = 0;
public int ypos = 0;
BufferedImage img = null;
java.awt.Graphics2D gc = null;
Font currentFont = null;
double fps = 0;
String CurrentFrameText;
PanelPaint(int xpos,int ypos,double sx,double sy,String argCurrentFrameText)
{
img = new BufferedImage(xpos, ypos, BufferedImage.TYPE_INT_ARGB);
gc = img.createGraphics();
gc.scale(sx, sy);
currentFont = new Font("default", Font.BOLD, 30);
CurrentFrameText = argCurrentFrameText;
gc.setFont(currentFont);
gc.setColor(Color.RED);
}
#Override
public Dimension getPreferredSize()
{
return new Dimension(xpos, ypos);
}
public void setImg(BufferedImage img,double argfps)
{
gc.drawImage(img, 0, 0, null);
gc.drawString(CurrentFrameText + ": " + (int)fps, 25, 25);
fps = argfps;
}
#Override
public void paint(java.awt.Graphics g)
{
g.drawImage(img, 0, 0, null);
}
public void close()
{
img = null;
gc = null;
currentFont = null;
}
}
Above is slower for high contrast images and taking time for "g.drawImage(img, 0, 0, null);" code.
Can performance for draw image can be improve?

You can use an OpenGL fragment shader to filter and then blit the source image as a texture to the 4 screens as screen quads (orthonormal projections onto a quad poly).
This is by far the optimal solution to your requirement. If your game is OpenGL based, it will be even faster since you wont have to use the Robot package as an intermediate - you can simply render the game texture directly (image conversion not required).
This will also allow full control over the filter in the fragment shader. See ShaderToy.com for many excellent filter shaders.

Related

JAVA Error with loading sprite using BufferedImage

im following a tutorial and im coming across this is the error, i cannot seem to work out what the problem is. All im trying to do is load a Sprite image. Here is the code:
Here is the error:
Exception in thread "Thread-0" java.lang.NullPointerException
at com.mainpkg.game.Handler.gg(Handler.java:27)
at com.mainpkg.game.Game.render(Game.java:107)
at com.mainpkg.game.Game.run(Game.java:83)
at java.base/java.lang.Thread.run(Thread.java:844)
Main Game Class:
public class Game extends Canvas implements Runnable {
public static final int WIDTH = 800, HEIGHT = WIDTH / 12 * 9;
private Thread thread;
private boolean running = false;
private Handler handler;
private BufferedImage grassTile;
public Game() {
new Window(WIDTH, HEIGHT, "MOON EXPOLATION", this);
handler = new Handler(getWidth(), getHeight());
testImage = loadImage("Resources/GrassTile.png");
}
private BufferedImage loadImage(String path) {
try {
BufferedImage loadedImage = ImageIO.read(new FileInputStream(path));
BufferedImage formattedImage = new BufferedImage(loadedImage.getWidth(), loadedImage.getHeight(),
BufferedImage.TYPE_INT_RGB);
formattedImage.getGraphics().drawImage(loadedImage, 0, 0, null);
return formattedImage;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
public synchronized void start() {
thread = new Thread(this);
thread.start();
running = true;
}
public synchronized void stop() {
try {
thread.join();
running = false;
} catch (Exception e) {
e.printStackTrace();
}
}
public void run() {
int FRAMES = 0;
int TICKS = 0;
long lastTime = System.nanoTime();
double unprocessed = 0;
double nsPerSecs = 1000000000 / 60.0;
long Timer = System.currentTimeMillis();
while (running) {
long now = System.nanoTime();
unprocessed += (now - lastTime) / nsPerSecs;
lastTime = now;
if (unprocessed >= 1) {
TICKS++;
ticks();
unprocessed -= 1;
}
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
FRAMES++;
render();
if (System.currentTimeMillis() - Timer > 1000) {
System.out.println("Ticks: " + TICKS + " FPS: " + FRAMES);
TICKS = 0;
FRAMES = 0;
Timer += 1000;
}
}
stop();
}
private void ticks() {
}
void render() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
super.paint(getGraphics());
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
handler.renderImage(testImage, 0, 0);
handler.render(g);
// g.setColor(Color.BLACK);
// g.fillRect(0,0,WIDTH,HEIGHT);
g.dispose();
bs.show();
}
public static void main(String[] args) {
new Game();
}
Handler Class:
public class Handler {
private BufferedImage view;
private int pixels[];
public Handler(int width, int height) {
view = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
pixels = ((DataBufferInt) view.getRaster().getDataBuffer()).getData();
}
public void render(Graphics g) {
g.drawImage(view, 0, 0, view.getWidth(), view.getHeight(), null);
}
public void renderImage(BufferedImage image, int xPosition,int yPosition) {
int[] imagepixels = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
for(int y = 0; y < image.getHeight(); y++) {
for(int x = 0; x < image.getWidth(); x++) {
pixels[(x + xPosition) + (y + yPosition ) * view.getWidth()] = imagepixels[x + y * image.getWidth()];
}
}
}
The problem is here:
BufferedImage loadedImage = ImageIO.read(Game.class.getResource(path));
getResource(path) is returning null and that is causing the exception.
Try changing the image path to "Assets/GrassTile.png"
One tip:
You should avoid using absolute paths to locate your resources. This is a really bad idea because if you change the location of your project it will stop working, try using relative paths.

Java: game slowing down while using affine transform

I want to create a game similar to SpaceInvaders, but instead of getting to the bottom of the screen, the aliens shoot projectiles. One type of aliens I wanted to create(in the code below), turns
to 45 degree and back. I tried it with affine transform, but everytime they turn the game slows down to half of the speed. The player and the projectiles are moving at half the speed then. The code below is the class that creates a JPanel
and draws everything.
import javax.swing.Timer;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.*;
import java.util.*;
import javax.swing.JPanel;
import javax.imageio.*;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.io.IOException;
import javax.swing.*;
public class WELTZEICHNER2 extends JPanel implements ActionListener ,
KeyListener
{
Player p;
Timer t = new Timer (5, this);
ArrayList<ANGRIFF> ziele = new ArrayList<ANGRIFF>();
ArrayList<ANGRIFF> ziele2 = new ArrayList<ANGRIFF>();
ArrayList<ALIEN1> aliens1 = new ArrayList<ALIEN1>();
private boolean left,right,space;
private int lastshot = 100;
private int score =0;
BufferedImage image;
BufferedImage image2;
BufferedImage image3;
BufferedImage image4;
int count = 0;
int count2 = 0;
int d = 0;
public WELTZEICHNER2()
{
setDoubleBuffered(true);
p = new Player(500,900,100000);
t.start();
addKeyListener(this);
setFocusable(true);
URL resource = getClass().getResource("alien2.png");
URL resource2 = getClass().getResource("background.png");
URL resource3 = getClass().getResource("raumschifftest.png");
URL resource4 = getClass().getResource("kreislertest.png");
try {
image = ImageIO.read(resource);
} catch (IOException e) {
e.printStackTrace();
}
try {
image2 = ImageIO.read(resource2);
} catch (IOException e) {
e.printStackTrace();
}
try {
image3 = ImageIO.read(resource3);
} catch (IOException e) {
e.printStackTrace();
}
try {
image4 = ImageIO.read(resource4);
} catch (IOException e) {
e.printStackTrace();
}
for (int i= 0;i < 20;i++)
{
for (int j =0;j <5;j++)
{
aliens1.add(new ALIEN1(70+i*90,80+j*70,1));
}
}
}
public void erzeugeANGRIFF()
{
ANGRIFF b = new ANGRIFF(p.getxN() + 11, p.getyN(),true);
ziele2.add(b);
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g.drawImage(image2,1,1,this); //background image
g.drawImage(image3,p.getxN(),p.getyN(),this); //player image
for (ANGRIFF b : ziele)
{
g2.setColor(Color.RED);
g2.fill( new Ellipse2D.Double(b.getxN(),b.getyN(),5,10)); //alien´s projectiles
}
for (ANGRIFF b : ziele2)
{
g2.setColor(Color.GREEN);
g2.fill( new Ellipse2D.Double(b.getxN(),b.getyN(),5,10)); // player´s projectiles
}
for (ALIEN1 i : aliens1) //draw alien images
{
if(count2 > 10000)
{
AffineTransform trans = new AffineTransform();
trans.rotate(Math.toRadians(45), image4.getWidth() / 2, image4.getHeight() / 2);
BufferedImage rotated = new BufferedImage(image4.getWidth(),
image4.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g3 = rotated.createGraphics();
g3.drawImage(image4, trans, null);
g2.drawImage(rotated,i.getxN(),i.getyN(),null);
}
else
{
g.drawImage(image4,i.getxN(),i.getyN(),this);
}
}
g2.setColor(Color.RED);
g2.drawString("Score:"+ score,5,15);
g2.drawString("Health:"+ p.health,5,30);
g2.drawString("Count:"+ count,5,45);
if(p.health == 0) //Game Over screen
{
g2.setColor(Color.BLACK);
g2.fill(new Rectangle2D.Double(1,1,1920,1080));
g2.setColor(Color.RED);
String text = "Game Over";
Font endtext = new Font("TimesNewRoman",Font.PLAIN, 200 );
g2.setFont(endtext);
g2.drawString(text,450,540);
}
}
public void actionPerformed(ActionEvent e)
{
if ( right == true)
{
p.right();
}
if (left == true)
{
p.left();
}
if(space == true && lastshot < 0)
{
erzeugeANGRIFF();
lastshot = 100;
}
lastshot -=1;
int bulletCount =ziele.size();
int bulletCount2 =ziele2.size();
int Alien1Count = aliens1.size();
ArrayList<Integer> remANGRIFF= new ArrayList<Integer>();
ArrayList<Integer> remANGRIFF2= new ArrayList<Integer>();
ArrayList<Integer>remAlien1=new ArrayList<Integer>();
for( int i = 0; i < bulletCount2;i++)
{
ANGRIFF b = ziele2.get(i);
b.bewegeANGRIFF();
if (b.getyN() >1000 )
{
remANGRIFF2.add(i);
}
for (int j =0;j< Alien1Count;j++ )
{
ALIEN1 n = aliens1.get(j);
if (b.checkCollision(n) && b.player == true)
{
n.health -=1;
score +=50;
if (n.health <= 0)
{
remAlien1.add(j);
score +=100;
}
remANGRIFF2.add(i);
}
}
}
for( int i = 0; i < bulletCount;i++)
{
ANGRIFF b = ziele.get(i);
b.bewegeANGRIFF();
if (b.getyN() < -100 )
{
remANGRIFF.add(i);
}
if (b.checkCollision(p) && b.player == false)
{
p.health -=50;
if (p.health <= 0)
{
p.health = 0;
}
remANGRIFF.add(i);
}
}
for (ALIEN1 i : aliens1)
{
// i.Bewegungsmuster();
count2++;
if(count2 > 20000)
{
count2 = 0;
}
if (i.newANGRIFF())
{
ziele.add(new ANGRIFF(i.getxN()+50,i.getyN()+50,false));
}
}
for (int i: remANGRIFF)
{
if(i < ziele.size())
{
ziele.remove(i);
}
}
for (int i: remANGRIFF2)
{
if(i < ziele2.size())
{
ziele2.remove(i);
}
}
for (int i: remAlien1)
{
if (i<aliens1.size())
{
aliens1.remove(i);
}
}
repaint();
}
public void keyPressed(KeyEvent e)
{
int code = e.getKeyCode();
if ( code == KeyEvent.VK_RIGHT)
{
right = true;
}
if ( code == KeyEvent.VK_LEFT)
{
left = true;
}
if ( code == KeyEvent.VK_SPACE)
{
space = true;
}
}
public void keyReleased(KeyEvent e)
{
int code = e.getKeyCode();
if ( code == KeyEvent.VK_RIGHT)
{
right = false;
}
if ( code == KeyEvent.VK_LEFT)
{
left = false;
}
if ( code == KeyEvent.VK_SPACE)
{
space = false;
lastshot =0;
}
}
public void keyTyped(KeyEvent e)
{
int code = e.getKeyCode();
if ( code == KeyEvent.VK_SPACE)
{
erzeugeANGRIFF();
}
}
}
This is the class that starts the game.
import javax.swing.*;
public class start
{
public static void main(String[] args)
{
//System.setProperty("sun.java2d.d3d", "true");
//System.setProperty("sun.java2d.noddraw", "false");
//-Dsun.java2d.noddraw=false;
JFrame f = new JFrame();
WELTZEICHNER2 d = new WELTZEICHNER2();
f.setSize(1920,1080);
f.setTitle("BlueJ Space Invader");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(d);
f.setVisible(true);
}
}
Any help is appreciated.
You're "core" problem is here...
if(count2 > 10000)
{
AffineTransform trans = new AffineTransform();
trans.rotate(Math.toRadians(45), image4.getWidth() / 2, image4.getHeight() / 2);
BufferedImage rotated = new BufferedImage(image4.getWidth(),
image4.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g3 = rotated.createGraphics();
g3.drawImage(image4, trans, null);
g2.drawImage(rotated,i.getxN(),i.getyN(),null);
}
This is creating a number of short lived objects on every paint cycle, which is putting extra strain one the GC, slowing down your program - not to mention the time it takes to create a BufferedImage
A better solution is to simply rotate the current Graphics context. The problem with this is, it can become very complex very quickly.
So, basically what I would do, is I would use the AffineTransform to translate the origin point/offset to the position of the object you are painting. The rotation then becomes as simple as rotating about the centre point of the image and then painting the image at 0x0.
The trick is reseting the transform when you're finished. This is where creating another copy of the Graphics context before hand, applying the transform to it, painting the image and then disposing of the copy comes in very, very handy.
if (count2 > 10000) {
AffineTransform trans = new AffineTransform();
trans.translate(i.getxN(), i.getyN());
trans.rotate(Math.toRadians(45), image4.getWidth() / 2, image4.getHeight() / 2);
//BufferedImage rotated = new BufferedImage(image4.getWidth(),
//image4.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g3 = (Graphics2D) g2.create();
g3.setTransform(trans);
//g3.drawImage(image4, trans, null);
g3.drawImage(image4, 0, 0, null);
g3.dispose();
}
When I was testing your code, I had witnessed an uneven frame rate. The ActionListener was been called at intervals in far greater values of 5 milliseconds. By the time it had performed 30, 000 cycles, it was already at an average of 75 milliseconds and slowly increasing, which suggest that you have some more issues to deal with.
Focus on the ArrayList and the creation/disposal of your objects and consider using "pools" of objects to further reduce the GC overhead where possible
You could have a look at Swing animation running extremely slow for an example.
PS # about 300, 000 cycles, the update cycle is up to an average of 200 milliseconds per update :P

Finished project, how to add a background in NetBeans

I am using NetBeans IDE 8.2 and I created a simple clone game from tutorials.
I am looking to add a background to the app and my research keeps pointing to using JFrame Forms and a JLabel.
None of the tutorials touched on backgrounds or JFrame Forms/JLabels. So I am uncertain how to take my completed project and add a background. I have attempted to reproduce JFrame Forms and JLabel code only to be unable to put my classes/interfaces "inside?" or "on top?" of the JFrame Form/JLabel.
I apologize if this really isn't an ideal first question, I just signed up and this is my first dip into the Java pool. Game class with JFrame (not Form) settings
EDIT: Adding full paste of my Game.java class.
package game;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import javax.swing.*;
public class Game {
public final static int WIDTH = 1920, HEIGHT = 1080;
private String gameName = "Tutorial Game";
private Canvas game = new Canvas();
private Input input;
private ArrayList<Updatable> updatables = new ArrayList<>();
private ArrayList<Renderable> renderables = new ArrayList<>();
// Helper methods for update/render Arrays
public void addUpdatable(Updatable u) {
updatables.add(u);
}
public void removeUpdatable(Updatable u) {
updatables.remove(u);
}
public void addRenderable(Renderable r) {
renderables.add(r);
}
public void removeRenderable(Renderable r) {
renderables.remove(r);
}
public void start() {
// Initialize windows
Dimension gameSize = new Dimension(Game.WIDTH, Game.HEIGHT);
JFrame gameWindow = new JFrame(gameName);
gameWindow.setDefaultCloseOperation(3);
gameWindow.setSize(gameSize);
gameWindow.setResizable(false);
gameWindow.setLocationRelativeTo(null);
gameWindow.add(game);
game.setSize(gameSize);
game.setMinimumSize(gameSize);
game.setMaximumSize(gameSize);
game.setPreferredSize(gameSize);
gameWindow.setVisible(true);
// Initialize Input
input = new Input();
game.addKeyListener(input);
// Game loop
final int TICKS_PER_SECOND = 60;
final int TIME_PER_TICK = 1000 / TICKS_PER_SECOND;
final int MAX_FRAMESKIPS = 5;
long nextGameTick = System.currentTimeMillis();
int loops;
float interpolation;
long timeAtLastFPSCheck = 0;
int ticks = 0;
boolean running = true;
while(running) {
// Updating
loops = 0;
while(System.currentTimeMillis() > nextGameTick && loops < MAX_FRAMESKIPS) {
update();
ticks++;
nextGameTick += TIME_PER_TICK;
loops++;
}
// Rendering
interpolation = (float) (System.currentTimeMillis() + TIME_PER_TICK - nextGameTick)
/ (float) TIME_PER_TICK;
render(interpolation);
// FPS Check
if(System.currentTimeMillis() - timeAtLastFPSCheck >= 1000) {
System.out.println("FPS: " + ticks);
gameWindow.setTitle(gameName + " - FPS: " + ticks);
ticks = 0;
timeAtLastFPSCheck = System.currentTimeMillis();
}
}
}
private void update() {
for(Updatable u : updatables) {
u.update(input);
}
}
private void render(float interpolation) {
BufferStrategy b = game.getBufferStrategy();
if(b == null) {
game.createBufferStrategy(2);
return;
}
Graphics2D g = (Graphics2D) b.getDrawGraphics();
g.clearRect(0, 0, game.getWidth(), game.getHeight());
for(Renderable r : renderables) {
r.render(g, interpolation);
}
g.dispose();
b.show();
}
}

How to serialize Java 2D Shape objects as XML?

The Shape interface is implemented by objects of Java 2D (Arc2D, Area, CubicCurve2D, Ellipse2D, GeneralPath etc..).
Some of the concrete objects are marked as Serializable and can be stored and restored using object serialization, but others like Area do not implement the interface and throw errors.
But since we are constantly warned that such naive serialization is not necessarily stable across Java implementations or versions, I'd prefer to use some form of serialization that is.
That leads us to storing/restoring from XML using XMLEncoder and XMLDecoder, but that is capable of handling even less of the Java 2D Shape objects.
Some results for both can be seen below. We start with 6 shapes, and attempt to store/restore them via object serialization and standard XML serialization.
How would we store all Shape objects correctly via XML?
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.border.TitledBorder;
public class Serialize2D {
private JPanel ui;
Serialize2D() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
ui = new JPanel(new GridLayout(0, 1));
int[] xpoints = {205, 295, 205, 295};
int[] ypoints = {5, 25, 25, 45};
Polygon polygon = new Polygon(xpoints, ypoints, xpoints.length);
ArrayList<Shape> shapes = new ArrayList<Shape>();
int w = 45;
shapes.add(new Rectangle2D.Double(5, 5, 90, 40));
shapes.add(new Ellipse2D.Double(105, 5, 90, 40));
shapes.add(polygon);
shapes.add(new GeneralPath(new Rectangle2D.Double(5, 55, 90, 40)));
shapes.add(new Path2D.Double(new Rectangle2D.Double(105, 55, 90, 40)));
shapes.add(new Area(new Rectangle2D.Double(205, 55, 90, 40)));
addTitledLabelToPanel(shapes, "Original Shapes");
addTitledLabelToPanel(
serializeToFromObject(shapes), "Serialize via Object");
addTitledLabelToPanel(
serializeToFromXML(shapes), "Serialize via XML");
}
public JComponent getUI() {
return ui;
}
public ArrayList<Shape> serializeToFromObject(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
for (Shape shape : shapes) {
try {
oos.writeObject(shape);
} catch (Exception ex) {
System.err.println(ex.toString());
}
}
oos.flush();
oos.close();
System.out.println("length Obj: " + baos.toByteArray().length);
ByteArrayInputStream bais = new ByteArrayInputStream(
baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = null;
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
while (o != null) {
shps.add((Shape) o);
try {
o = ois.readObject();
} catch (NotSerializableException ex) {
System.err.println(ex.getMessage());
} catch (ClassNotFoundException ex) {
ex.printStackTrace();
}
}
return shps;
} catch (IOException ex) {
ex.printStackTrace();
}
return shps;
}
public ArrayList<Shape> serializeToFromXML(ArrayList<Shape> shapes) {
ArrayList<Shape> shps = new ArrayList<Shape>();
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XMLEncoder xmle = new XMLEncoder(baos);
for (Shape shape : shapes) {
xmle.writeObject(shape);
}
xmle.flush();
xmle.close();
System.out.println("length XML: " + baos.toByteArray().length);
ByteArrayInputStream bais
= new ByteArrayInputStream(baos.toByteArray());
XMLDecoder xmld = new XMLDecoder(bais);
Shape shape = (Shape) xmld.readObject();
while (shape != null) {
shps.add(shape);
try {
shape = (Shape) xmld.readObject();
} catch (ArrayIndexOutOfBoundsException aioobe) {
// we've read last object
shape = null;
}
}
xmld.close();
} catch (Exception ex) {
ex.printStackTrace();
}
return shps;
}
private final static String getType(Object o) {
String s = o.getClass().getName();
String[] parts = s.split("\\.");
s = parts[parts.length - 1].split("\\$")[0];
return s;
}
public static void drawShapesToImage(
ArrayList<Shape> shapes, BufferedImage image) {
Graphics2D g = image.createGraphics();
g.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.WHITE);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
for (Shape shape : shapes) {
String s = getType(shape);
g.setColor(Color.GREEN);
g.fill(shape);
g.setColor(Color.BLACK);
g.draw(shape);
Rectangle r = shape.getBounds();
int x = r.x + 5;
int y = r.y + 16;
if (r.width * r.height != 0) {
g.drawString(s, x, y);
}
}
g.dispose();
}
private void addTitledLabelToPanel(ArrayList<Shape> shapes, String title) {
int w = 300;
int h = 100;
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
drawShapesToImage(shapes, bi);
JLabel l = new JLabel(new ImageIcon(bi));
l.setBorder(new TitledBorder(title));
ui.add(l);
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
Serialize2D ss = new Serialize2D();
JOptionPane.showMessageDialog(null, ss.getUI());
}
};
SwingUtilities.invokeLater(r);
}
}
Unfortunately, naive encoding/decoding of a Shape to XML using XMLEncoder/Decoder often destroys all the vital information of the Shape!
So to do this, still using the above mentioned classes, we serialize and restore properly constructed beans that represent the parts of the shape as obtained from a PathIterator. These beans are:
PathBean which stores the collection of PathSegment objects that form the shape of the Java-2D Shape.
PathSegment which stores the details of a particular part of the path (segment type, winding rule & coords).
SerializeShapes GUI
A GUI to demonstrate storing and restoring shapes.
Click the Ellipse (Ellipse2D), Rectangle (Rectangle2D) or Face (Area) buttons a couple of times.
Exit the GUI. The shapes will be serialized to disk.
Restart the GUI. The randomly drawn shapes from last time will be restored from disk & reappear in the GUI.
The selected shape will be filled in green, other shapes in red.
package serialize2d;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;
/** A GUI to make it easy to add/remove shapes from a canvas.
It should persist the shapes between runs. */
public class SerializeShapes {
JPanel ui;
JPanel shapePanel;
Random rand;
JPanel shapeCanvas;
DefaultListModel<Shape> allShapesModel;
ListSelectionModel shapeSelectionModel;
RenderingHints renderingHints;
SerializeShapes() {
initUI();
}
public void initUI() {
if (ui != null) {
return;
}
renderingHints = new RenderingHints(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
renderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
renderingHints.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_NORMALIZE);
ui = new JPanel(new BorderLayout(4, 4));
ui.setBorder(new EmptyBorder(4, 4, 4, 4));
JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
ui.add(controls, BorderLayout.PAGE_START);
shapeCanvas = new ShapeCanvas();
ui.add(shapeCanvas);
rand = new Random();
allShapesModel = new DefaultListModel<Shape>();
JList<Shape> allShapes = new JList<Shape>(allShapesModel);
allShapes.setCellRenderer(new ShapeListCellRenderer());
shapeSelectionModel = allShapes.getSelectionModel();
shapeSelectionModel.setSelectionMode(
ListSelectionModel.SINGLE_SELECTION);
ListSelectionListener shapesSelectionListener
= new ListSelectionListener() {
#Override
public void valueChanged(ListSelectionEvent e) {
shapeCanvas.repaint();
}
};
allShapes.addListSelectionListener(shapesSelectionListener);
JScrollPane shapesScroll = new JScrollPane(
allShapes,
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
);
// TODO fix this hack..
shapesScroll.getViewport().setPreferredSize(new Dimension(60, 200));
ui.add(shapesScroll, BorderLayout.LINE_START);
Action addEllipse = new AbstractAction("Ellipse") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Ellipse2D ellipse = new Ellipse2D.Double(x, y, w, h);
addShape(ellipse);
}
};
addEllipse.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_E);
Action addRectangle = new AbstractAction("Rectangle") {
#Override
public void actionPerformed(ActionEvent e) {
int w = rand.nextInt(100) + 10;
int h = rand.nextInt(100) + 10;
int x = rand.nextInt(shapeCanvas.getWidth() - w);
int y = rand.nextInt(shapeCanvas.getHeight() - h);
Rectangle2D rectangle = new Rectangle2D.Double(x, y, w, h);
addShape(rectangle);
}
};
addRectangle.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_R);
final int faceStart = 128513;
final int faceEnd = 128528;
final int diff = faceEnd - faceStart;
StringBuilder sb = new StringBuilder();
for (int count = faceStart; count <= faceEnd; count++) {
sb.append(Character.toChars(count));
}
final String s = sb.toString();
Vector<Font> compatibleFontList = new Vector<Font>();
GraphicsEnvironment ge
= GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] fonts = ge.getAllFonts();
for (Font font : fonts) {
if (font.canDisplayUpTo(s) < 0) {
compatibleFontList.add(font);
}
}
JComboBox fontChooser = new JComboBox(compatibleFontList);
ListCellRenderer fontRenderer = new DefaultListCellRenderer() {
#Override
public Component getListCellRendererComponent(
JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(
list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Font font = (Font) value;
l.setText(font.getName());
return l;
}
};
fontChooser.setRenderer(fontRenderer);
final ComboBoxModel<Font> fontModel = fontChooser.getModel();
BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bi.createGraphics();
final FontRenderContext fontRenderContext = g.getFontRenderContext();
Action addFace = new AbstractAction("Face") {
#Override
public void actionPerformed(ActionEvent e) {
int codepoint = faceStart + rand.nextInt(diff);
String text = new String(Character.toChars(codepoint));
Font font = (Font) fontModel.getSelectedItem();
Area area = new Area(
font.deriveFont(80f).
createGlyphVector(fontRenderContext, text).
getOutline());
Rectangle bounds = area.getBounds();
float x = rand.nextInt(
shapeCanvas.getWidth() - bounds.width) - bounds.x;
float y = rand.nextInt(
shapeCanvas.getHeight() - bounds.height) - bounds.y;
AffineTransform move = AffineTransform.
getTranslateInstance(x, y);
area.transform(move);
addShape(area);
}
};
addFace.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
Action delete = new AbstractAction("Delete") {
#Override
public void actionPerformed(ActionEvent e) {
int idx = shapeSelectionModel.getMinSelectionIndex();
if (idx < 0) {
JOptionPane.showMessageDialog(
ui,
"Select a shape to delete",
"Select a Shape",
JOptionPane.ERROR_MESSAGE);
} else {
allShapesModel.removeElementAt(idx);
shapeCanvas.repaint();
}
}
};
delete.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
controls.add(new JButton(addEllipse));
controls.add(new JButton(addRectangle));
controls.add(new JButton(addFace));
controls.add(fontChooser);
controls.add(new JButton(delete));
try {
ArrayList<Shape> shapes = deserializeShapes();
for (Shape shape : shapes) {
allShapesModel.addElement(shape);
}
} catch (Exception ex) {
System.err.println("If first launch, this is as expected!");
ex.printStackTrace();
}
}
private void addShape(Shape shape) {
allShapesModel.addElement(shape);
int size = allShapesModel.getSize() - 1;
shapeSelectionModel.addSelectionInterval(size, size);
}
class ShapeCanvas extends JPanel {
ShapeCanvas() {
setBackground(Color.WHITE);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Stroke stroke = new BasicStroke(1.5f);
g2.setStroke(stroke);
int idx = shapeSelectionModel.getMinSelectionIndex();
Shape selectedShape = null;
if (idx > -1) {
selectedShape = allShapesModel.get(idx);
}
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
if (shape.equals(selectedShape)) {
g2.setColor(new Color(0, 255, 0, 191));
} else {
g2.setColor(new Color(255, 0, 0, 191));
}
g2.fill(shape);
g2.setColor(new Color(0, 0, 0, 224));
g2.draw(shape);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(500, 300);
}
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
SerializeShapes se = new SerializeShapes();
JFrame f = new JFrame("Serialize Shapes");
f.addWindowListener(new SerializeWindowListener(se));
f.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
f.setContentPane(se.getUI());
f.setResizable(false);
f.pack();
f.setLocationByPlatform(true);
f.setVisible(true);
}
};
SwingUtilities.invokeLater(r);
}
public void serializeShapes() throws FileNotFoundException {
ArrayList<Shape> shapes
= new ArrayList<Shape>();
Enumeration en = allShapesModel.elements();
while (en.hasMoreElements()) {
Shape shape = (Shape) en.nextElement();
shapes.add(shape);
}
ShapeIO.serializeShapes(shapes, this.getClass());
try {
Desktop.getDesktop().open(
ShapeIO.getSerializeFile(this.getClass()));
} catch (Exception e) {
e.printStackTrace();
}
}
public ArrayList<Shape> deserializeShapes() throws FileNotFoundException {
return ShapeIO.deserializeShapes(this.getClass());
}
class ShapeListCellRenderer extends DefaultListCellRenderer {
#Override
public Component getListCellRendererComponent(
JList<? extends Object> list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
Component c = super.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
JLabel l = (JLabel) c;
Shape shape = (Shape) value;
ShapeIcon icon = new ShapeIcon(shape, 40);
l.setIcon(icon);
l.setText("");
return l;
}
}
class ShapeIcon implements Icon {
Shape shape;
int size;
ShapeIcon(Shape shape, int size) {
this.shape = shape;
this.size = size;
}
#Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHints(renderingHints);
Rectangle bounds = shape.getBounds();
int xOff = -bounds.x;
int yOff = -bounds.y;
double xRatio = (double) bounds.width / (double) size;
double yRatio = (double) bounds.height / (double) size;
double ratio = xRatio > yRatio ? xRatio : yRatio;
AffineTransform scale = AffineTransform.getScaleInstance(1 / ratio, 1 / ratio);
AffineTransform shift = AffineTransform.getTranslateInstance(xOff, yOff);
AffineTransform totalTransform = new AffineTransform();
totalTransform.concatenate(scale);
totalTransform.concatenate(shift);
Area b = new Area(shape).createTransformedArea(totalTransform);
bounds = b.getBounds();
g2.setColor(Color.BLACK);
g2.fill(b);
}
#Override
public int getIconWidth() {
return size;
}
#Override
public int getIconHeight() {
return size;
}
}
}
class SerializeWindowListener extends WindowAdapter {
SerializeShapes serializeShapes;
SerializeWindowListener(SerializeShapes serializeShapes) {
this.serializeShapes = serializeShapes;
}
#Override
public void windowClosing(WindowEvent e) {
try {
serializeShapes.serializeShapes();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
System.exit(1);
}
System.exit(0);
}
}
ShapeIO
Performs the I/O to/from XML.
package serialize2d;
import java.awt.Shape;
import java.beans.*;
import java.io.*;
import java.util.ArrayList;
public class ShapeIO {
/** Save the list of shapes to the file system. */
public static void serializeShapes(
ArrayList<Shape> shapes, Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLEncoder xmle = new XMLEncoder(new FileOutputStream(f));
ArrayList<PathBean> pathSegmentsCollection = new ArrayList<>();
for (Shape shape : shapes) {
ArrayList<PathSegment> pathSegments =
BeanConverter.getSegmentsFromShape(shape);
PathBean as = new PathBean(pathSegments);
pathSegmentsCollection.add(as);
}
xmle.writeObject(pathSegmentsCollection);
xmle.flush();
xmle.close();
}
/** Load the list of shapes from the file system. */
public static ArrayList<Shape> deserializeShapes(Class serializeClass)
throws FileNotFoundException {
File f = getSerializeFile(serializeClass);
XMLDecoder xmld = new XMLDecoder(new FileInputStream(f));
ArrayList<PathBean> pathSegmentsCollection
= (ArrayList<PathBean>) xmld.readObject();
ArrayList<Shape> shapes = new ArrayList<Shape>();
for (PathBean pathSegments : pathSegmentsCollection) {
shapes.add(BeanConverter.getShapeFromSegments(pathSegments));
}
return shapes;
}
/** Provide an unique, reproducible & readable/writable path for a class. */
public static File getSerializeFile(Class serializeClass) {
File f = new File(System.getProperty("user.home"));
String[] nameParts = serializeClass.getCanonicalName().split("\\.");
f = new File(f, "java");
for (String namePart : nameParts) {
f = new File(f, namePart);
}
f.mkdirs();
f = new File(f, nameParts[nameParts.length-1] + ".xml");
return f;
}
}
BeanConverter
Obtains a PathIterator from the Shape and converts it to a serializable bean. Converts the bean back into a GeneralPath.
package serialize2d;
import java.awt.Shape;
import java.awt.geom.*;
import java.util.ArrayList;
/** Utility class to convert bean to/from a Shape. */
public class BeanConverter {
/** Convert a shape to a serializable bean. */
public static ArrayList<PathSegment> getSegmentsFromShape(Shape shape) {
ArrayList<PathSegment> shapeSegments = new ArrayList<PathSegment>();
for (
PathIterator pi = shape.getPathIterator(null);
!pi.isDone();
pi.next()) {
double[] coords = new double[6];
int pathSegmentType = pi.currentSegment(coords);
int windingRule = pi.getWindingRule();
PathSegment as = new PathSegment(
pathSegmentType, windingRule, coords);
shapeSegments.add(as);
}
return shapeSegments;
}
/** Convert a serializable bean to a shape. */
public static Shape getShapeFromSegments(PathBean shapeSegments) {
GeneralPath gp = new GeneralPath();
for (PathSegment shapeSegment : shapeSegments.getPathSegments()) {
double[] coords = shapeSegment.getCoords();
int pathSegmentType = shapeSegment.getPathSegmentType();
int windingRule = shapeSegment.getWindingRule();
gp.setWindingRule(windingRule);
if (pathSegmentType == PathIterator.SEG_MOVETO) {
gp.moveTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_LINETO) {
gp.lineTo(coords[0], coords[1]);
} else if (pathSegmentType == PathIterator.SEG_QUADTO) {
gp.quadTo(coords[0], coords[1], coords[2], coords[3]);
} else if (pathSegmentType == PathIterator.SEG_CUBICTO) {
gp.curveTo(
coords[0], coords[1], coords[2],
coords[3], coords[4], coords[5]);
} else if (pathSegmentType == PathIterator.SEG_CLOSE) {
gp.closePath();
} else {
System.err.println("Unexpected value! " + pathSegmentType);
}
}
return gp;
}
}
PathBean
Stores a collection of path segments in a seriallizable bean.
package serialize2d;
import java.awt.geom.*;
import java.util.ArrayList;
/** PathBean stores the collection of PathSegment objects
that constitute the path of a Shape. */
public class PathBean {
public ArrayList<PathSegment> pathSegments;
public PathBean() {}
public PathBean(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
public ArrayList<PathSegment> getPathSegments() {
return pathSegments;
}
public void setPathSegments(ArrayList<PathSegment> pathSegments) {
this.pathSegments = pathSegments;
}
#Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
for (PathSegment pathSegment : pathSegments) {
sb.append(" \n\t");
sb.append(pathSegment.toString());
}
sb.append(" \n");
sb.append("}");
return "PathSegments: " + sb.toString();
}
}
PathSegment
Stores the path segment of one part of the entire path.
package serialize2d;
import java.util.Arrays;
/** PathSegment bean stores the detail on one segment of the path
that constitutes a Shape. */
public class PathSegment {
public int pathSegmentType;
public int windingRule;
public double[] coords;
public PathSegment() {}
public PathSegment(int pathSegmentType, int windingRule, double[] coords) {
this.pathSegmentType = pathSegmentType;
this.windingRule = windingRule;
this.coords = coords;
}
public int getPathSegmentType() {
return pathSegmentType;
}
public void setPathSegmentType(int pathSegmentType) {
this.pathSegmentType = pathSegmentType;
}
public int getWindingRule() {
return windingRule;
}
public void setWindingRule(int windingRule) {
this.windingRule = windingRule;
}
public double[] getCoords() {
return coords;
}
public void setCoords(double[] coords) {
this.coords = coords;
}
#Override
public String toString() {
String sC = (coords != null ? "" : Arrays.toString(coords));
String s = String.format(
"PathSegment: Path Segment Type:- %d \t"
+ "Winding Rule:- %d \tcoords:- %s",
getPathSegmentType(), getWindingRule(), sC);
return s;
}
}
Notes
This is intended as a proof of concept as opposed to a polished approach.
XML serialized data becomes big real fast, it would normally be zipped. Zip compression might shave 30-40% off the byte size of a serialized object or a class file, but 80-95% off XML. In any case, zip works well for the next point as well.
For the type of project where we wish to offer to serialize and restore shapes, we'll probably also want to include more details of the shapes (e.g. fill color or texture and draw color or stroke etc.) as well as other data like images or fonts. This is also where Zip comes in handy, since we can put them all in the same archive, each with best levels of compression (e.g. standard for the XML and none for images).
A zip archive of the source files in this answer can be downloaded from my cloud drive.
A custom PersistenceDelegate can be used with XMLEncoder to serialize a Path2D or GeneralPath to XML.
Consider the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
<void method="moveTo">
<float>1.0</float>
<float>1.0</float>
</void>
<void method="lineTo">
<float>2.0</float>
<float>0.0</float>
</void>
<void method="lineTo">
<float>0.0</float>
<float>3.0</float>
</void>
<void method="closePath"/>
</object>
</java>
When read by an XMLEncoder instance, the following commands will be executed ...
Path2D.Float object = new Path2D.Float();
object.setWindingRule(0); // Note: 0 => Path2D.WIND_EVEN_ODD
object.moveTo(1.0, 1.0);
object.lineTo(2.0, 0.0);
object.lineTo(0.0, 3.0);
object.closePath();
... and a closed triangle object will be returned by XMLDecoder.readObject().
Based on this, we can conclude that XMLDecoder can already deserialize a Path2D shape, if it is properly encoded. What does the XMLEncoder do for us now?
Path2D.Float path = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 10);
path.moveTo(1, 1);
path.lineTo(2, 0);
path.lineTo(0, 3);
path.closePath();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.writeObject(path);
}
This produces the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_60" class="java.beans.XMLDecoder">
<object class="java.awt.geom.Path2D$Float">
<void property="windingRule">
<int>0</int>
</void>
</object>
</java>
Not great, but not too bad. We're just missing the path data. So we just need to extend the DefaultPersistenceDelegate to add the required path commands to the output.
public class Path2DPersistenceDelegate extends DefaultPersistenceDelegate {
#Override
protected void initialize(Class<?> cls, Object oldInstance, Object newInstance, Encoder out) {
super.initialize(cls, oldInstance, newInstance, out);
Shape shape = (Shape) oldInstance;
float coords[] = new float[6];
Float pnt0[] = new Float[0];
Float pnt1[] = new Float[2];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6];
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = pnt0;
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = pnt1;
break;
case PathIterator.SEG_LINETO:
cmd = "lineTo";
pnts = pnt1;
break;
case PathIterator.SEG_QUADTO:
cmd = "quadTo";
pnts = pnt2;
break;
case PathIterator.SEG_CUBICTO:
cmd = "curveTo";
pnts = pnt3;
break;
default:
throw new IllegalStateException("Unexpected segment type: " + type);
}
for (int i = 0; i < pnts.length; i++) {
pnts[i] = coords[i];
}
out.writeStatement(new Statement(oldInstance, cmd, pnts));
iterator.next();
}
}
}
Then, we just register this persistence delegate with the XMLEncoder, and it will produce the XML shown at the top of this post.
Path2DPersistenceDelegate path2d_delegate = new Path2DPersistenceDelegate();
try (XMLEncoder xml = new XMLEncoder(System.out)) {
xml.setPersistenceDelegate(Path2D.Float.class, path2d_delegate);
xml.writeObject(path);
}
Since Path2D.Float is the parent class of GeneralPath, a GeneralPath will also be encoded properly. If you want properly encode Path2D.Double shapes, you will need to modify the delegate to use double values and Double objects.
Update:
To construct the Path2D.Float object with the proper windingRule property instead of setting the property afterwards, add the following constructor to the Path2DPersistenceDelegate:
public Path2DPersistenceDelegate() {
super(new String[] { "windingRule" });
}
The XML will then read:
...
<object class="java.awt.geom.Path2D$Float">
<int>0</int>
<void method="moveTo">
...
This loses some human-readable context information in the XML; a human would need to read the documentation to determine that with the Path2D.Float(int) constructor, the int parameter is the windingRule property.
Update 2:
The Polygon persistence delegate is fairly simple:
public class PolygonPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Polygon polygon = (Polygon) oldInstance;
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { polygon.xpoints, polygon.ypoints, polygon.npoints });
}
}
Since Area Constructive Area Geometry object is more complex, it cannot be created by moveTo and lineTo type methods, but rather only by adding, subtracting, or exclusive-or-ing Shape objects. But the constructor takes a Shape object, and a Path2D.Double can be constructed from an Area object, so the persistence delegate actually can be written quite simply as well:
public class AreaPersistenceDelegate extends PersistenceDelegate {
#Override
protected Expression instantiate(Object oldInstance, Encoder out) {
Area area = (Area) oldInstance;
Path2D.Double p2d = new Path2D.Double(area);
return new Expression(oldInstance, oldInstance.getClass(), "new",
new Object[] { p2d });
}
}
Since we are using Path2D.Double internally, we would need to add both persistent delegates to the XMLEncoder:
try (XMLEncoder encoder = new XMLEncoder(baos)) {
encoder.setPersistenceDelegate(Area.class, new AreaPersistenceDelegate());
encoder.setPersistenceDelegate(Path2D.Double.class, new Path2DPersistenceDelegate.Double());
encoder.writeObject(area);
}
Update 3:
A project with the PersistenceDelegate for Area, Path2D and GeneralPath has been created on GitHub.
Notes:
The persistence delegate for Polygon was removed, as it seems to be unnecessary for Java 1.7
Update 4:
For Java 1.7, the pnts array must be allocated for each new Statement(); it cannot be allocated once and reused. Thus, the Path2D delegates must be changed as follows:
float coords[] = new float[6];
/* Removed: Float pnt0[] = new Float[0];
Float pnt1[] = new Float[0];
Float pnt2[] = new Float[4];
Float pnt3[] = new Float[6]; */
Float pnts[];
PathIterator iterator = shape.getPathIterator(null);
while (!iterator.isDone()) {
int type = iterator.currentSegment(coords);
String cmd;
switch (type) {
case PathIterator.SEG_CLOSE:
cmd = "closePath";
pnts = new Float[0]; // Allocate for each segment
break;
case PathIterator.SEG_MOVETO:
cmd = "moveTo";
pnts = new Float[2]; // Allocate for each segment
break;
/* ... etc ...*/

java creating smooth multi image animation

I've been looking through/for guides all night trying to find how to smooth out my multiple image animation. I tried a few guides, found one i liked but it's quite choppy.
atm my animation works but flashes.
my animation clas:
package graphics;
import java.awt.Image;
import java.util.ArrayList;
public class Animation {
private ArrayList<OneScene> scenes;
private int sceneIndex;
private long movieTime;
private long totalTime;
//Constructor
public Animation() {
scenes = new ArrayList<OneScene>();
totalTime = 0;
start();
}
//Add scene to ArayList & set time for each scene
public synchronized void addScene(Image i, long t) {
totalTime += t;
scenes.add(new OneScene(i, totalTime));
}
//Start animation
public synchronized void start() {
movieTime = 0;
sceneIndex = 0;
}
//Change Scenes
public synchronized void update(long timePassed) {
if (scenes.size() > 1) {
movieTime += timePassed;
if (movieTime >= totalTime) {
movieTime = 0;
sceneIndex = 0;
}
while(movieTime > getScene(sceneIndex).endTime) {
sceneIndex++;
}
}
}
//Get animation (Image) current scene
public synchronized Image getImage() {
if (scenes.size() ==0) {
return null;
}else{
return getScene(sceneIndex).pic;
}
}
//Get Scene
private OneScene getScene(int x) {
return (OneScene)scenes.get(x);
}
//Private Class
public class OneScene {
Image pic;
long endTime;
public OneScene(Image pic, long endTime) {
this.pic = pic;
this.endTime = endTime;
}
}
& a few of my methods:
Image load1 = new ImageIcon(""+cachemainDir+"1.png").getImage();
Image load2 = new ImageIcon(""+cachemainDir+"2.png").getImage();
Image load3 = new ImageIcon(""+cachemainDir+"3.png").getImage();
Image load4 = new ImageIcon(""+cachemainDir+"4.png").getImage();
Image load5 = new ImageIcon(""+cachemainDir+"5.png").getImage();
Image load6 = new ImageIcon(""+cachemainDir+"6.png").getImage();
Image load7 = new ImageIcon(""+cachemainDir+"7.png").getImage();
Image load8 = new ImageIcon(""+cachemainDir+"8.png").getImage();
Image load9 = new ImageIcon(""+cachemainDir+"9").getImage();
Image load10 = new ImageIcon(""+cachemainDir+"10.png").getImage();
Image load11 = new ImageIcon(""+cachemainDir+"11.png").getImage();
Image load12 = new ImageIcon(""+cachemainDir+"12.png").getImage();
Anim = new Animation();
Anim.addScene(load1, 100);
Anim.addScene(load2, 100);
Anim.addScene(load3, 100);
Anim.addScene(load4, 100);
Anim.addScene(load5, 100);
Anim.addScene(load6, 100);
Anim.addScene(load7, 100);
Anim.addScene(load8, 100);
Anim.addScene(load9, 100);
Anim.addScene(load10, 100);
Anim.addScene(load11, 100);
Anim.addScene(load12, 100);
and...
while (AppLoading) {
long timePassed = System.currentTimeMillis() - cumTime;
cumTime += timePassed;
Anim.update(timePassed);
Graphics g = getGraphics();
draw(g);
g.dispose();
try{
Thread.sleep(0);
}catch(Exception ex){}
}
lastly....
public void draw(Graphics g) {
new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
g.drawImage(bg, 0, 0, getWidth(), getHeight(), null);
g.drawImage(Anim.getImage(), 300, 300, null);
}
What do you mean by flashing? Is there a blank frame between the actual frames? (You can test this by just making a few stills and animating them.)It could be that you are updating to little frames, as you only have 12. This is fine if the images are animated correctly out side of run time. Make the 'cachemainDir' images closer in animation. It may help if you Thread.sleep(5); //or a number besides 0

Categories

Resources