I'm new to graphics programming. I'm trying to create a program that allows you to draw directed graphs. For a start I have managed to draw a set of rectangles (representing the nodes) and have made pan and zoom capabilities by overriding the paint method in Java.
This all seems to work reasonably well while there aren't too many nodes. My problem is when it comes to trying to draw a dot grid. I used a simple bit of test code at first that overlayed a dot grid using two nested for loops:
int iPanX = (int) panX;
int iPanY = (int) panY;
int a = this.figure.getWidth() - iPanX;
int b = this.figure.getHeight() - (int) iPanY;
for (int i = -iPanX; i < a; i += 10) {
for (int j = -iPanY; j < b; j += 10) {
g.drawLine(i, j, i, j);
}
}
This allows me to pan the grid but not zoom. However, the performance when panning is terrible! I've done a lot of searching but I feel that I must be missing something obvious because I can't find anything on the subject.
Any help or pointers would be greatly appreciated.
--Stephen
Use a BufferedImage for the dot grid. Initialize it once and later only paint the image instead of drawing the grid over and over.
private init(){
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = image.getGraphics();
// then draw your grid into g
}
public void paint(Graphics g) {
g.drawImage(image, 0, 0, null);
// then draw the graphs
}
And zooming is easily achieved using this:
g.drawImage(image, 0, 0, null); // so you paint the grid at a 1:1 resolution
Graphics2D g2 = (Graphics2D) g;
g2.scale(zoom, zoom);
// then draw the rest into g2 instead of g
Drawing into the zoomed Graphics will lead to proportionally larger line width, etc.
I think re-drawing all your dots every time the mouse moves is going to give you performance problems. Perhaps you should look into taking a snapshot of the view as a bitmap and panning that around, redrawing the view 'properly' when the user releases the mouse button?
Related
I am stuck (beyond the limits of fun) at trying to fix text quality with offscreen image double buffering.
Screen capture worth a thousand words.
The ugly String is drawn to an offscreen image, and then copied to the paintComponent's Graphics argument.
The good looking String is written directly to the paintComponent's Graphics argument, bypassing the offscreen image.
Both Graphics instances (onscreen and offscreen) are identically setup in terms of rendering quality, antialiasing, and so on...
Thank you very much in advance for your wisdom.
The very simple code follows:
public class AcceleratedPanel extends JPanel {
private Dimension osd; //offscreen dimension
private BufferedImage osi; //offscreen image
private Graphics osg; //offscreen graphic
public AcceleratedPanel() {
super();
}
#Override
public final void paintComponent(Graphics g) {
super.paintComponent(g);
// --------------------------------------
//OffScreen painting
Graphics2D osg2D = getOffscreenGraphics();
setupGraphics(osg2D);
osg2D.drawString("Offscreen painting", 10, 20);
//Dump offscreen buffer to screen
g.drawImage(osi, 0, 0, this);
// --------------------------------------
// OnScreen painting
Graphics2D gg = (Graphics2D)g;
setupGraphics(gg);
gg.drawString("Direct painting", 10, 35);
}
/*
To make sure same settings are used in different Graphics instances,
a unique setup procedure is used.
*/
private void setupGraphics(Graphics2D g) {
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
private Graphics2D getOffscreenGraphics() {
//Graphics Acceleration
Dimension currentDimension = getSize();
if (osi == null || !currentDimension.equals(osd)) {
osi = (BufferedImage)createImage(currentDimension.width, currentDimension.height);
osg = osi.createGraphics();
osd = currentDimension;
}
return (Graphics2D) osg;
}
} //End of mistery
You are not drawing your two strings with the same color. The default color for the offscreen Graphics is rgb(0, 0, 0) (that is, pure black), while Swing will set the color of a Graphics object to the look-and-feel’s default color—which, for me on Windows 7, using the default theme, is rgb(51, 51, 51), or dark gray.
Try placing g.setColor(Color.BLACK); in your setupGraphics method, to ensure both strings are drawn with the same color.
Thanks for the replies.
With mentioning DPI, MadProgrammer has lead me to a working fix which I offer here more as workaround than as a 'clean' solution to be proud of. It solves the issue, anyway.
I noticed that while my screen resolution is 2880x1800 (Retina Display), MouseEvent's getPoint() method reads x=1440, y=900 at the lower right corner of the screen. Then, the JPanel size is half the screen resolution, although it covers the full screen.
This seen, the solution is as follows:
first, create an offscreen image matching the screen resolution, not the JPanel.getSize() as suggested in dozens of double buffering articles.
then, draw in the offscreen image applying a magnifying transform, bigger than needed, in particular scaling by r = screen dimension / panel dimension ratio.
finally, copy a down scaled version of the offscreen image back into the screen, applying a shrinking factor of r (or scaling factor 1/r).
The solution implementation is split into two methods:
An ammended version of the initial paintComponent posted earlier,
a helper method getDPIFactor() explained afterwards.
The ammended paintComponent method follows:
public final void paintComponent(Graphics g) {
super.paintComponent(g);
double dpiFactor = getDPIFactor();
// --------------------------------------
//OffScreen painting
Graphics2D osg2D = getOffscreenGraphics();
setupGraphics(osg2D);
//Paint stuff bigger than needed
osg2D.setTransform(AffineTransform.getScaleInstance(dpiFactor, dpiFactor));
//Custom painting
performPainting(osg2D);
//Shrink offscreen buffer to screen.
((Graphics2D)g).drawImage(
osi,
AffineTransform.getScaleInstance(1.0/dpiFactor, 1.0/dpiFactor),
this);
// --------------------------------------
// OnScreen painting
Graphics2D gg = (Graphics2D)g;
setupGraphics(gg);
gg.drawString("Direct painting", 10, 35);
}
To complete the task, the screen resolution must be obtained.
A call to Toolkit.getDefaultToolkit().getScreenResolution() doesn't solve the problem, as it returns the size of a JPanel covering the whole screen. As seen above, this figure doesn't match the actual screen size in physical dots.
The way to get this datum is cleared by Sarge Bosch in this stackoverflow post.
I have adapted his code to implement the last part of the puzzle, getDPIFactor().
/*
* Adapted from Sarge Bosch post in StackOverflow.
* https://stackoverflow.com/questions/40399643/how-to-find-real-display-density-dpi-from-java-code
*/
private Double getDPIFactor() {
GraphicsDevice defaultScreenDevice =
GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice();
// on OS X, it would be CGraphicsDevice
if (defaultScreenDevice instanceof CGraphicsDevice) {
CGraphicsDevice device = (CGraphicsDevice) defaultScreenDevice;
// this is the missing correction factor.
// It's equal to 2 on HiDPI a.k.a. Retina displays
int scaleFactor = device.getScaleFactor();
// now we can compute the real DPI of the screen
return scaleFactor * (device.getXResolution() + device.getYResolution()) / 2
/ Toolkit.getDefaultToolkit().getScreenResolution();
} else
return 1.0;
}
This code solves the issue for Mac Retina displays, but I am affraid nowhere else, since CGraphicsDevice is an explicit mention to a proprietary implementation of GraphicsDevice.
I do not have other HDPI hardware with which to play around to have a chance to offer a wider solution.
I'm quite new to graphics in java and I'm trying to create a shape that clips to the bottom of another shape. Here is an example of what I'm trying to achieve:
Where the white line at the base of the shape is the sort of clipped within the round edges.
The current way I am doing this is like so:
g2.setColor(gray);
Shape shape = getShape(); //round rectangle
g2.fill(shape);
Rectangle rect = new Rectangle(shape.getBounds().x, shape.getBounds().y, width, height - 3);
Area area = new Area(shape);
area.subtract(new Area(rect));
g2.setColor(white);
g2.fill(area);
I'm still experimenting with the clip methods but I can't seem to get it right. Is this current method ok (performance wise, since the component repaints quite often) or is there a more efficient way?
I think your original idea about using the clip methods was the right way to do it. This works for me:
static void drawShapes(Graphics2D g, int width, int height,
Shape clipShape) {
g.setPaint(Color.BLACK);
g.fillRect(0, 0, width, height);
g.clip(clipShape);
int centerX = width / 2;
g.setPaint(new GradientPaint(
centerX, 0, Color.WHITE,
centerX, height, new Color(255, 204, 0)));
g.fillRect(0, 0, width, height);
g.setPaint(Color.WHITE);
int whiteRectHeight = height * 4 / 5;
g.fillRect(0, whiteRectHeight,
width, height - whiteRectHeight);
}
Is this current method ok (performance wise, since the component repaints quite often) ..
Subtracting shapes is how I'd go about it. The objects could be a few instances or (possibly) a single instance that is transformed as needed.
A text demo., using scaling & fading.
Here's one with simple lines (..and dots, ..and it is animated).
Of course, if the image is purely additive, use a BufferedImage as the canvas & display it in a JLabel/ImageIcon combo. As in both of those examples.
public void draw(Graphics2D g) {
// draw background
g.drawImage(bgImage, 0, 0, null);
g.drawImage(bgImage, 700, 0, null);
g.drawImage(bgImage, 0, 500, null);
g.drawImage(bgImage, 700, 500, null);
AffineTransform transform = new AffineTransform();
for (int i = 0; i < NUM_SPRITES; i++) {
Sprite sprite = sprites[i];
// translate the sprite
transform.setToTranslation(sprite.getX(),
sprite.getY());
// if the sprite is moving left, flip the image
if (sprite.getVelocityX() < 0) {
transform.scale(-1, 1);
transform.translate(-sprite.getWidth(), 0);
}
// draw it
g.drawImage(sprite.getImage(), transform, null);
}
}
}
I was looking at a draw method in this class. The method is called with an animation loop, it draws a set of animated sprites on the screen that pass the method the correct frame in their animation depending on a timer. The sprites are also moving around the screen at a randomly generated velocity bouncing off the edges of the frame. Depending on whether they are traveling left or right the draw method transforms the image so the sprite is facing in the correct direction. All that transform information is contained in the transform for the drawImage method which I'm used to seeing as g.drawImage(Image, x, y, null); though obviously you can't do that here as you would lose the image flipping capabilities. Is there a way to do it though? Is there a way to transform the image scale but set its location by coordinate?
I ask because transforming is a more processors intensive activity and if lots of sprites need to be active at once it might really slow everything down. BONUS QUESTION: Is this a legitimate concern or should I not be too worried considering the strength of modern systems.
I am basically trying to do something like classic "Paint" (Microsoft's program). But i want to work with layers when painting. I thought i can use JPanel component as layer.
I was testing the code below. The goal is drawing a rectangle with mouse. There is a temp layer (temp) to draw on it while dragging the mouse, and there is actual layer (area) to draw when mouse released. But every time i start drawing a new rectangle, old ones are disappear. Also if i execute setVisible(false) and true again, everything disappears.
MouseInputAdapter mia = new MouseInputAdapter() {
private int startx = 0, starty = 0, stopx = 0, stopy = 0;
public void mousePressed(MouseEvent evt) {
startx = evt.getX();
starty = evt.getY();
}
public void mouseDragged(MouseEvent evt) {
Graphics2D tempg = (Graphics2D) temp.getGraphics();
int width = Math.abs(startx - evt.getX());
int height = Math.abs(starty - evt.getY());
int x = evt.getX(), y = evt.getY();
if(x > startx)
x = startx;
if(y > starty)
y = starty;
Rectangle r = new Rectangle(x, y, width, height);
tempg.clearRect(0, 0, getWidth(), getHeight());
tempg.draw(r);
}
public void mouseReleased(MouseEvent evt) {
Graphics2D g = (Graphics2D) area.getGraphics();
stopx = evt.getX();
stopy = evt.getY();
int width = Math.abs(startx - stopx);
int height = Math.abs(starty - stopy);
int x = startx, y = starty;
if(x > stopx)
x = stopx;
if(y > stopy)
y = stopy;
Rectangle r = new Rectangle(x, y, width, height);
g.draw(r);
}
};
area.addMouseListener(mia);
area.addMouseMotionListener(mia);
temp.addMouseListener(mia);
temp.addMouseMotionListener(mia);
What is wrong with that code?
Every time there's a repaint there's no guarantee you'll get the same graphics in the state you left it.
Two a two-step instead:
Create a List of Rectangles in your class.
In your mouse listener instead of drawing to the graphics, add a rectangle to the list.
Override paintComponent and in there draw the list of rectangles to the graphics it is passed.
Using the list is nice as items at the start of the list will be painted below ones at the end.
Classic bitmap-based graphics painting software operates on a target bitmap. You can render multiple Layers in paintComponent(), as #Keily suggests for Rectangles.
Alternatively, you may want to to look at classic object-based drawing software, outlined here.
Here's a general idea: (I'm assuming you mean layers such as in photoshop)
Set up a single JPanel for drawing.
Make a data structure containing all drawable objects you need for drawing.
In this data structure, also make a field containing an integer expressing which layer that specific drawable object is tied to.
In your paintComponent() method, check which layer is currently active and only draw the the data in that layer or below it.
This is what i was looking for; http://www.leepoint.net/notes-java/examples/mouse/paintdemo.html
My mistake; using getGraphics() method out of paintComponent() and expecting keep changes.
Why #Keilly's answer not working for me; Because if i put shapes in a list or array, when a shape changed (for example; deleting a circle's 1/4) i can't update the element in the list. Because it doesn't be same shape anymore. So i have to keep shapes as drawings, and i don't have to (and dont want to) keep them separately.
I am developing an application using Java2d. The weird thing I noticed is, the origin is at the top left corner and positive x goes right and positive y increases down.
Is there a way to move the origin bottom left?
Thank you.
You are going to need to do a Scale and a translate.
in your paintComponent method you could do this:
public void paintComponent(Graphics g)
{
Graphics2D g2d = (Graphics2D) g;
g2d.translate(0, -height);
g2d.scale(1.0, -1.0);
//draw your component with the new coordinates
//you may want to reset the transforms at the end to prevent
//other controls from making incorrect assumptions
g2d.scale(1.0, -1.0);
g2d.translate(0, height);
}
my Swing is a little rusty but this should accomplish the task.
We can use the following way to resolve easily the problem,
public void paintComponent(Graphics g)
{
Graphics2D g2d = (Graphics2D) g;
// Flip the sign of the coordinate system
g2d.translate(0.0, getHeight());
g2d.scale(1.0, -1.0);
......
}
Have you tried Graphics2D.translate()?
You're going to want to just get used to it. Like luke mentioned, you technically CAN apply a transform to the graphics instance, but that will end up affecting performance negatively.
Just doing a translate could move the position of 0,0 to the bottom left, but movement along the positive axes will still be right in the x direction and down in the y direction, so the only thing you would accomplish is drawing everything offscreen. You'd need to do a rotate to accomplish what you're asking, which would add the overhead of radian calculations to the transform matrix of the graphics instance. That is not a good tradeoff.
Just for later reference, I had to swap the order of the calls to scale and translate in my code. Maybe this will help someone in the future:
#Test
public void bottomLeftOriginTest() throws IOException {
int width = 256;
int height = 512;
BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics2D ig = bi.createGraphics();
// save the "old" transform
AffineTransform old = ig.getTransform();
// origin is top left:
// update graphics object with the inverted y-transform
if (true) { /* order ok */
ig.scale(1.0, -1.0);
ig.translate(0, -bi.getHeight());
} else {
ig.translate(0, -bi.getHeight());
ig.scale(1.0, -1.0);
}
int xPoints[] = new int[] { 0, width, width };
int yPoints[] = new int[] { 0, height, 0 };
int nPoints = xPoints.length;
ig.setColor(Color.BLUE);
ig.fillRect(0, 0, bi.getWidth(), bi.getHeight());
ig.setColor(Color.RED);
ig.fillPolygon(xPoints, yPoints, nPoints);
// restore the old transform
ig.setTransform(old);
// Export the result to a file
ImageIO.write(bi, "PNG", new File("origin.png"));
}