AffineTransform без преобразования штриха?

При использовании функции 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-масштабных факторов.

Я знаю, что производит этот эффект (объект Stroke создает поглаженную форму прямоугольника, который будет нарисован в пользовательских координатах, которые затем переводятся на экранные координаты), но я не уверен, как это решить.

  • Должен ли я создать новую реализацию Stroke, которая поглаживает Shapes по-разному в X- и Y-направлении (тем самым отменив здесь искажение)? (Или кто-нибудь уже знает такую ​​реализацию?)
  • Должен ли я преобразовать свои фигуры в координаты экрана и погладить там?
  • Любые другие (лучшие) идеи?

Ответ 1

Оказывается, мой вопрос не был настолько ужасным и что мои две идеи, заданные в вопросе, на самом деле суть одна и та же идея. Вот класс TransformedStroke, который реализует искаженный Stroke путем преобразования 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;
    }

}

Мой метод рисования, используя его, выглядит следующим образом:

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(). Итак, если кто-то сделал какое-то масштабирование перед тем, как передать графику моему компоненту, это все равно будет рисоваться с использованием штриха ширины 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, переводится (поэтому (0,0) - это верхний левый угол вашего компонента), не масштабируется, поэтому нет разницы.

Ответ 2

Существует более простое и менее "хакерское" решение, чем исходный ответ TransformedStroke.

У меня появилась идея, когда я прочитал, как работает конвейер рендеринга:

(из http://docs.oracle.com/javase/7/docs/technotes/guides/2d/spec/j2d-awt.html)

  • Если Shape следует погладить, атрибут Stroke в контексте Graphics2D используется для создания нового Shape, который охватывает положенный путь.
  • Координаты пути Shape s преобразуются из пространства пользователя в пространство устройства в соответствии с атрибутом преобразования в контексте Graphics2D.
  • Путь Shape s обрезается с использованием атрибута клипа в контексте Graphics2D.
  • Остальные Shape, если они есть, заполняются с использованием атрибутов Paint и Composite в контексте Graphics2D.

То, что вы и я, в идеале, ищете, - это способ замены первых двух шагов.

Если вы внимательно посмотрите на второй шаг, 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 означают, что интерфейс будет использоваться. Это также требует дополнительного класса.

Это решение поддерживает отдельный Transform и изменяет Shape вместо преобразования объекта Graphics. Это, однако, ни в коем случае не хак, потому что я не злоупотребляю существующей функциональностью, но использую функциональность API точно так, как это должно было быть использовано. Я просто использую более явные части API, а не методы "быстрого доступа" / "удобства" API (g.scale() и т.д.).

Эффективно, это решение может быть только более эффективным. Эффективно один шаг теперь пропущен. В исходном решении TransformedStroke дважды преобразует фигуру и один раз ударяет фигуру. Это решение преобразует форму в явном виде, а такт * current * обводят форму один раз.

Ответ 3

Вы просто попытались сделать int x и int y в приложении больше, чем int x = 500 int y = 900??? Также мое предложение состоит в том, что без перезаписи весь код предназначен для реализации, когда реплики становятся более толстыми, когда приложение ближе друг к другу, более похожее на удвоение прямоугольника сверху и снизу, но когда приложение расширяется, реквизиты сверху и снизу идут вернуться к нормальной жизни...