AffineTransform without transforming Stroke?

2019-04-19 16:28发布

问题:

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.

  • Should I create a new Stroke implementation which strokes Shapes differently in X- and Y-direction (thereby undoing the distortion here)? (Or does anyone already knows such an implementation?)
  • Should I transform my shapes to screen coordinates and stroke there?
  • Any other (better) ideas?

回答1:

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.



回答2:

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, the Stroke attribute in the Graphics2D context is used to generate a new Shape 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 the Graphics2D context.
  • The Shape’s path is clipped using the clip attribute in the Graphics2D context.
  • The remaining Shape, if any, is filled using the Paint and Composite attributes in the Graphics2D 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);

solution

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.

discussion

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.



回答3:

Have you just tried to make the int x and int y on the application bigger like int x = 500 int y = 900??? Also my suggestion is that with out rewritten the whole code is to implement where the recs are thicker when the app is closer together more like doubling the rectangle on the top and the bottom but when the app is extended the recs on the top and bottom go back to normal...