不进行Stroke转换的AffineTransform?

13

使用Graphics2D的scale()函数时,如果使用两个不同的参数(在x和y方向上按不同比例缩放),之后绘制到此Graphics2D对象上的所有内容都会被缩放。这会产生一种奇怪的效果,沿一个方向绘制的线条比沿另一个方向绘制的线条更粗。下面的程序演示了这种效果,并显示了该窗口:

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缩放因子无关。

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

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

这是我在这里的第一个问题...并且让我获得了我的ninth badge,耶! - Paŭlo Ebermann
3个回答

10
原来我的问题并不是那么难,而且我在问题中提出的两个想法实际上是同一个想法。下面是一个“TransformedStroke”类,它通过变换“Shape”来实现扭曲的“Stroke”。
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.getTransform()返回的是相对于设备空间的g的完整变换,而不仅仅是在.create()之后应用的变换。因此,如果有人在将Graphics传递给我的组件之前进行了一些缩放,这仍然会以2个设备像素宽度的描边绘制,而不是绘制到我的方法中给出的图形的2个像素。如果这是一个问题,请按如下方式使用:

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中,传递给paintComponent方法的Graphics对象只是被平移了(因此(0,0)点位于组件的左上角),而没有被缩放,因此不会有任何区别。

我添加了另一个关于 getTransform 的注释,当我在另一个地方测试我的组件时,我刚好遇到了一个类似工作的 TransformedStringDrawer - 在那里翻译是相关的。 - Paŭlo Ebermann
我将这些类(TransformedStrokeStrokeExample)添加到了我的 github 代码库 中。 - Paŭlo Ebermann

5
有一种比原来的TransformedStroke解决方案更简单和不那么“hacky”的方法。
当我阅读渲染管道如何工作时,我得到了这个想法:
(来自http://docs.oracle.com/javase/7/docs/technotes/guides/2d/spec/j2d-awt.html
1.如果要描边Shape,则使用Graphics2D上下文中的Stroke属性生成一个新的Shape,该形状包围描边路径。
2.Shape的路径的坐标根据Graphics2D上下文中的变换属性从用户空间转换为设备空间。
3.Shape的路径使用Graphics2D上下文中的裁剪属性进行裁剪。
4.使用PaintComposite属性在Graphics2D上下文中填充其余的Shape
你和我都希望找到一种交换前两个步骤的方法。
如果仔细看第二步,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感觉更像是一个“hack”,而不是Stroke接口作者打算使用的方式。它还需要一个额外的类。
此解决方案将一个单独的Transform保留并修改了Shape,而不是转换Graphics对象。但这绝不是一种黑客行为,因为我没有滥用现有功能,而是使用API功能的确切方式。我只是使用API的更明确部分,而不是API的“快捷”/“方便”方法(如g.scale()等)。
从性能上看,此解决方案只能更有效率。实际上跳过了一步。在原始解决方案中,TransformedStroke会两次转换形状并描边一次。而这个解决方案明确地转换了形状,当前*描边仅描边一次形状。

0

你尝试过把应用程序中的int x和int y变得更大,例如int x = 500 int y = 900吗?我的建议是,在不重写整个代码的情况下,实现当应用程序更加接近时矩形变得更厚,就像顶部和底部的矩形加倍,但当应用程序扩展时,顶部和底部的矩形恢复正常...


当我改变JFrame的比例时(因此也改变了我的组件),描边宽度的比例也会改变。但是我提问的重点是要独立于缩放因子。(对不起,我真的不理解第二句话。) - Paŭlo Ebermann
我的错,但建议是你可以尝试在顶部和底部做一个双重矩形,使其看起来更厚,而不必重新编写整个代码,这只是一个想法,即使没有意义,但可能会指引你朝着正确的方向前进 :) 但我运行了你的代码,我有一个解决方案,但另一个问题是当你再次运行它时,我将7.0<-高度和4.0<-宽度交换,它会把矩形切成一半,不确定如何解决它 :( - user635673

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接