仿射变形而不变换笔画?

2022-09-04 21:00:34

当将 Graphics2D 函数与两个不同的参数(在 x 和 y 方向上按不同比率缩放)结合使用时,稍后在此 Graphics2D 对象上绘制的所有内容也会被缩放。这有一个奇怪的效果,即在一个方向上绘制的线比在另一个方向上绘制的线更粗。以下程序产生此效果,它显示此窗口:scale()

example screenshot

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);
        }});

    }

}

我使用此坐标转换来避免手动转换我的应用程序模型坐标(在本示例中为 (2,1, 2,4))转换为屏幕(或组件)像素坐标,但我不希望出现这种笔划失真。换句话说,我希望所有线的宽度都相同,与当前的 x 和 y 尺度因子无关。

我知道是什么产生了这种效果(Stroke对象创建了要在用户坐标中绘制的矩形的描边形状,然后将其转换为屏幕坐标),但我不确定如何解决这个问题。

  • 我是否应该创建一个新的 Stroke 实现,在 X 和 Y 方向上以不同的方式描边形状(从而撤消此处的失真)?(或者有人已经知道这样的实现吗?
  • 我应该将形状转换为屏幕坐标并在那里描边吗?
  • 还有其他(更好)的想法吗?

答案 1

事实证明,我的问题并没有那么可怕,我在问题中给出的两个想法实际上是同一个想法。下面是一个类,它通过转换 .TransformedStrokeStrokeShape

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;
    }

}

我使用它的绘画方法看起来像这样:

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));
}

然后我的窗口看起来像这样:

screenshot of undistorted stroke

我对此很满意,但如果有人有更多的想法,请随时回答。


注意力:这是返回 g 相对于设备空间的完整变换,而不仅仅是在 之后应用的变换。因此,如果有人在将图形提供给我的组件之前进行了一些缩放,这仍然会以2个设备像素宽度的笔划绘制,而不是为我的方法提供的grapic的2个像素。如果这是一个问题,请像这样使用它:g.getTransform().create()

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));
}

在 Swing 中,通常您提供给 的图形仅被平移(因此 (0,0) 是组件的左上角),未缩放,因此没有区别。paintComponent


答案 2

有一个比原始答案更简单,更少“黑客”的解决方案。TransformedStroke

当我阅读渲染管线的工作原理时,我得到了这个想法:

由 http://docs.oracle.com/javase/7/docs/technotes/guides/2d/spec/j2d-awt.html 日起))

  • 如果要描边,则上下文中的属性用于生成包含描边路径的新路径。ShapeStrokeGraphics2DShape
  • 路径的坐标根据上下文中的变换属性从用户空间转换为设备空间。ShapeGraphics2D
  • 的路径在上下文中使用 clip 属性进行剪切。ShapeGraphics2D
  • 其余 (如果有)使用上下文中的 和 属性填充。ShapePaintCompositeGraphics2D

理想情况下,你和我寻求的是一种交换前两步的方法。

如果仔细查看第二步,则 已包含部分解决方案。TransformedStroke

Shape sTrans = transform.createTransformedShape(s);

溶液

代替:

g.scale(..., ..., 等等,)g.transform()
g.draw(new Rectangle( 1, 2, 2, 4));

或者,使用 :TransformedStroke

g.setStroke(new TransformedStroke(new BasicStroke(2f), g.getTransform());
g.draw(new Rectangle( 1, 2, 2, 4));

我建议你做:

transform =无论什么
g.draw(transform.createTransformedShape(new Rectangle( 1, 2, 2, 4));

不要再转型了。曾。请改用自己进行和修改的变换来变换形状。g

讨论

TransformedStroke感觉更像是一个“黑客”,而不是作者想要使用的界面。它还需要一个额外的类。Stroke

此解决方案保留了一个单独的周围,并修改而不是转换对象。然而,这绝不是黑客攻击,因为我没有滥用现有功能,而是完全按照其使用方式使用API功能。我只是使用API的更明确的部分,而不是API的“快捷方式”/“便利性”方法(等)。TransformShapeGraphicsg.scale()

在性能方面,此解决方案只能提高效率。实际上,现在跳过了一个步骤。在原始解决方案中,转换形状两次,并描边形状一次。此解决方案显式转换形状,*当前* 描边对形状进行一次描边。TransformedStroke


推荐