When using the Graphics2D scale()
function with two different parameters (scaling by different ratios in x- and y-direction), everything drawn later on this Graphics2D object is scaled too. This has the strange effect that lines drawn in one direction are thicker than those in another direction. The following program produces this effect, it shows this window:
public class StrokeExample extends JPanel {
public void paintComponent(Graphics context) {
super.paintComponent(context);
Graphics2D g = (Graphics2D)context.create();
g.setStroke(new BasicStroke(0.2f));
int height = getHeight();
int width = getWidth();
g.scale(width/7.0, height/4.0);
g.setColor(Color.BLACK);
g.draw(new Rectangle( 2, 1, 4, 2));
}
public static void main(String[] params) {
EventQueue.invokeLater(new Runnable(){public void run() {
StrokeExample example = new StrokeExample();
JFrame f = new JFrame("StrokeExample");
f.setSize(100, 300);
f.getContentPane().setLayout(new BorderLayout());
f.getContentPane().add(example);
f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
f.setVisible(true);
}});
}
}
I'm using this coordinate transform to avoid having to manually transform my application model coordinates (the (2,1, 2,4) in this example) to screen (or component) pixel coordinates, but I don't want this stroke distortion. In other words, I want to have all lines the same width, independent of current x- and y-scale-factors.
I know what produces this effect (the Stroke object creates a stroked shape of the rectangle to be painted in user coordinates, which then are translated to screen coordinates), but I'm not sure on how to solve this.
Turns out my question was not so horrible difficult, and that my two ideas given in the question are actually the same idea. Here is a TransformedStroke
class which implements a distorted Stroke
by transforming the Shape
.
import java.awt.*;
import java.awt.geom.*;
/**
* A implementation of {@link Stroke} which transforms another Stroke
* with an {@link AffineTransform} before stroking with it.
*
* This class is immutable as long as the underlying stroke is
* immutable.
*/
public class TransformedStroke
implements Stroke
{
/**
* To make this serializable without problems.
*/
private static final long serialVersionUID = 1;
/**
* the AffineTransform used to transform the shape before stroking.
*/
private AffineTransform transform;
/**
* The inverse of {@link #transform}, used to transform
* back after stroking.
*/
private AffineTransform inverse;
/**
* Our base stroke.
*/
private Stroke stroke;
/**
* Creates a TransformedStroke based on another Stroke
* and an AffineTransform.
*/
public TransformedStroke(Stroke base, AffineTransform at)
throws NoninvertibleTransformException
{
this.transform = new AffineTransform(at);
this.inverse = transform.createInverse();
this.stroke = base;
}
/**
* Strokes the given Shape with this stroke, creating an outline.
*
* This outline is distorted by our AffineTransform relative to the
* outline which would be given by the base stroke, but only in terms
* of scaling (i.e. thickness of the lines), as translation and rotation
* are undone after the stroking.
*/
public Shape createStrokedShape(Shape s) {
Shape sTrans = transform.createTransformedShape(s);
Shape sTransStroked = stroke.createStrokedShape(sTrans);
Shape sStroked = inverse.createTransformedShape(sTransStroked);
return sStroked;
}
}
My paint-method using it then looks like this:
public void paintComponent(Graphics context) {
super.paintComponent(context);
Graphics2D g = (Graphics2D)context.create();
int height = getHeight();
int width = getWidth();
g.scale(width/4.0, height/7.0);
try {
g.setStroke(new TransformedStroke(new BasicStroke(2f),
g.getTransform()));
}
catch(NoninvertibleTransformException ex) {
// should not occur if width and height > 0
ex.printStackTrace();
}
g.setColor(Color.BLACK);
g.draw(new Rectangle( 1, 2, 2, 4));
}
Then my window looks like this:
I'm quite content with this, but if someone has more ideas, feel free to answer nevertheless.
Attention: This g.getTransform()
is returning the complete transformation of g relative to the device space, not only the transformation applied after the .create()
. So, if someone did some scaling before giving the Graphics to my component, this would still draw with a 2-device-pixel width stroke, not 2 pixels of the grapics given to my method. If this would be a problem, use it like this:
public void paintComponent(Graphics context) {
super.paintComponent(context);
Graphics2D g = (Graphics2D)context.create();
AffineTransform trans = new AffineTransform();
int height = getHeight();
int width = getWidth();
trans.scale(width/4.0, height/7.0);
g.transform(trans);
try {
g.setStroke(new TransformedStroke(new BasicStroke(2f),
trans));
}
catch(NoninvertibleTransformException ex) {
// should not occur if width and height > 0
ex.printStackTrace();
}
g.setColor(Color.BLACK);
g.draw(new Rectangle( 1, 2, 2, 4));
}
In Swing normally your Graphics given to the paintComponent
is only translated (so (0,0) is the upper left corner of your component), not scaled, so there is no difference.
There is a simpler and less 'hacky' solution than the original TransformedStroke
answer.
I got the idea when I read how the rendering pipeline works:
(from http://docs.oracle.com/javase/7/docs/technotes/guides/2d/spec/j2d-awt.html)
- If the
Shape
is to be stroked, theStroke
attribute in theGraphics2D
context is used to generate a newShape
that encompasses the stroked path.- The coordinates of the
Shape
’s path are transformed from user space into device space according to the transform attribute in theGraphics2D
context.- The
Shape
’s path is clipped using the clip attribute in theGraphics2D
context.- The remaining
Shape
, if any, is filled using thePaint
andComposite
attributes in theGraphics2D
context.
What you, and I, ideally seek is a way to swap the first two steps.
If you look closely at the second step, TransformedStroke
already contains part of the solution.
Shape sTrans = transform.createTransformedShape(s);
In stead of:
g.scale(
...)
, g.transform(
...)
, whatever,g.draw(new Rectangle( 1, 2, 2, 4));
Or, using TransformedStroke
:
g.setStroke(new TransformedStroke(new BasicStroke(2f), g.getTransform());
g.draw(new Rectangle( 1, 2, 2, 4));
I propose you do:
transform =
whatever,g.draw(transform.createTransformedShape(new Rectangle( 1, 2, 2, 4));
Don't transform g
anymore. Ever. Transform the shapes instead, using a transform that you make and modify yourself.
TransformedStroke
feels more like a 'hack' than a way the authors of Stroke
meant the interface to be used. It also requires an extra class.
This solution keeps a separate Transform
around and modifies the Shape
instead of transforming the Graphics
object. This is however in no way a hack, because I'm not abusing existing functionality but using API functionality exactly how it's meant to be used. I'm just using the more explicit parts of the API instead of the 'shortcut'/'convenience' methods of the API (g.scale()
etc.).
Performance-wise, this solution can only be more efficient. Effectively one step is now skipped. In the original solution, TransformedStroke
transforms the shape twice and strokes the shape once. This solution transforms the shape explicitly and the *current* stroke strokes the shape once.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With