西北角具有固定纵横比的拖动调整大小的矩形

Drag-resizing rectangle with fixed aspect ratio northwest corner

我有一个 Java 应用程序,用户可以在其中裁剪原始图像的子图像。通过在原始图像上绘制一个矩形来选择裁剪区域。然后可以沿对角线调整矩形的大小。到目前为止,一切正常!

用户还可以选择将矩形的纵横比锁定为 4:3。我可以通过将宽度设置为 w = h / 4 * 3;

来实现这一点

但是,当涉及到使用锁定比例调整大小时,从 西北角 拖动时,矩形表现异常并且不再静止(参见下面的 gif)。西南角有同样的问题,但可以通过将高度设置为 h = w / 3 * 4; 来解决,但我不知道如何在数学上为 西北角。我提供了一个可复制粘贴的演示用于实验:

public class CropDemo {
    public static void main(String[] args) {
        CropPanel cropPanel = new CropPanel();
        cropPanel.setPreferredSize(new Dimension(640, 480));

        JFrame jFrame = new JFrame("Crop Panel");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.getContentPane().add(cropPanel);
        jFrame.setResizable(false);
        jFrame.pack();
        jFrame.setLocationRelativeTo(null);
        jFrame.setVisible(true);
    }
}

class CropPanel extends JPanel {
    private static final long serialVersionUID = 1L;

    private boolean fixedRatio = true;

    private Rectangle rectangle;
    private Point clickPoint;

    private static final int HOVERING = 0;
    private static final int MOVING = 1;
    private static final int RESIZING = 2;

    public CropPanel() {
        setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));

        MouseAdapter mouseHandler = new MouseAdapter() {
            private Point startPoint = null;

            @Override
            public void mouseClicked(MouseEvent e) {
                if (rectangle != null && getCursorState() == HOVERING) {
                    rectangle = null;

                    repaint();
                }
            }

            @Override
            public void mousePressed(MouseEvent e) {
                clickPoint = e.getPoint();
                startPoint = e.getPoint();
            }

            @Override
            public void mouseMoved(MouseEvent e) {
                if (rectangle != null) {
                    Point mouse = e.getPoint();

                    int width = rectangle.x + rectangle.width;
                    int height = rectangle.y + rectangle.height;

                    final int off = 5;

                    if (mouse.x > rectangle.x - off && mouse.x < width + off && mouse.y > rectangle.y - off
                            && mouse.y < height + off) {
                        if (mouse.x <= rectangle.x + off && mouse.y >= height - off) {
                            setCursor(Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR));
                        } else if (mouse.x >= width - off && mouse.y >= height - off) {
                            setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
                        } else if (mouse.x <= rectangle.x + off && mouse.y <= rectangle.y + off) {
                            setCursor(Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR));
                        } else if (mouse.x >= width - off && mouse.y <= rectangle.y + off) {
                            setCursor(Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR));
                        } else {
                            setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                        }
                    } else {
                        setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
                    }
                }
            }

            @Override
            public void mouseDragged(MouseEvent e) {

                if (clickPoint != null) {
                    Point mouse = e.getPoint();

                    if (getCursorState() == MOVING) {
                        int dx = rectangle.x + mouse.x - clickPoint.x;
                        int dy = rectangle.y + mouse.y - clickPoint.y;

                        rectangle.setLocation(dx, dy);
                        clickPoint = e.getPoint();

                    } else if (getCursorState() == RESIZING) {
                        int dx = mouse.x - startPoint.x;
                        int dy = mouse.y - startPoint.y;

                        int height = rectangle.height;
                        int width = rectangle.width;

                        int x = 0;
                        int y = 0;
                        int w = 0;
                        int h = 0;

                        switch (getCursor().getType()) {
                        case Cursor.SW_RESIZE_CURSOR:
                            x = mouse.x + dx;
                            y = rectangle.y;
                            w = width - dx;
                            h = height + dy;

                            if (fixedRatio) {
                                h = w / 3 * 4;
                            }
                            break;
                        case Cursor.SE_RESIZE_CURSOR:
                            x = rectangle.x;
                            y = rectangle.y;
                            w = width + dx;
                            h = height + dy;

                            if (fixedRatio) {
                                w = h / 4 * 3;
                            }
                            break;
                        case Cursor.NW_RESIZE_CURSOR:
                            x = mouse.x + dx;
                            y = mouse.y + dy;
                            w = width - dx;
                            h = height - dy;

                            // This is where I'm lost
                            // something else needs to be done
                            if (fixedRatio) {
                                w = h / 4 * 3;
                            }
                            break;
                        case Cursor.NE_RESIZE_CURSOR:
                            x = rectangle.x;
                            y = mouse.y + dy;
                            w = width + dx;
                            h = height - dy;

                            if (fixedRatio) {
                                w = h / 4 * 3;
                            }
                            break;
                        }

                        rectangle.setBounds(x, y, w, h);
                        startPoint = mouse;
                    } else {
                        int x = Math.min(clickPoint.x, mouse.x);
                        int y = Math.min(clickPoint.y, mouse.y);
                        int w = Math.max(clickPoint.x - mouse.x, mouse.x - clickPoint.x);
                        int h = Math.max(clickPoint.y - mouse.y, mouse.y - clickPoint.y);

                        if (rectangle == null) {
                            rectangle = new Rectangle(x, y, w, h);
                        } else {
                            rectangle.setBounds(x, y, w, h);
                        }

                    }
                    repaint();
                }
            }
        };

        addMouseListener(mouseHandler);
        addMouseMotionListener(mouseHandler);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        g.setColor(Color.DARK_GRAY);
        g.fillRect(0, 0, getWidth(), getHeight());

        Graphics2D graphics2D = (Graphics2D) g.create();

        if (rectangle != null) {
            Area fill = new Area(new Rectangle(new Point(0, 0), getSize()));
            fill.subtract(new Area(rectangle));

            if (clickPoint != null) {
                graphics2D.setColor(new Color(0, 0, 0, 0));
            } else {
                graphics2D.setColor(new Color(0, 0, 0, 200));
            }

            int x = rectangle.x;
            int y = rectangle.y;
            int w = rectangle.width;
            int h = rectangle.height;

            graphics2D.fill(fill);
            graphics2D.setColor(Color.WHITE);
            graphics2D.setStroke(
                    new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 6 }, 0));
            graphics2D.drawRect(x, y, w, h);

            if (w >= 30 && h >= 30) {
                graphics2D.setStroke(new BasicStroke(3));

                graphics2D.drawLine(x + 1, y + 1, x + 8, y + 1);
                graphics2D.drawLine(x + 1, y + 1, x + 1, y + 8);
                graphics2D.drawLine(x + w - 1, y + 1, x + w - 8, y + 1);
                graphics2D.drawLine(x + w - 1, y + 1, x + w - 1, y + 8);
                graphics2D.drawLine(x + 1, y + h - 1, x + 8, y + h - 1);
                graphics2D.drawLine(x + 1, y + h - 1, x + 1, y + h - 8);
                graphics2D.drawLine(x + w - 1, y + h - 1, x + w - 8, y + h - 1);
                graphics2D.drawLine(x + w - 1, y + h - 1, x + w - 1, y + h - 8);
            }
        }

        graphics2D.dispose();
        g.dispose();
    }

    private int getCursorState() {
        switch (getCursor().getType()) {
        case Cursor.CROSSHAIR_CURSOR:
            return HOVERING;
        case Cursor.MOVE_CURSOR:
            return MOVING;
        case Cursor.SW_RESIZE_CURSOR:
        case Cursor.SE_RESIZE_CURSOR:
        case Cursor.NW_RESIZE_CURSOR:
        case Cursor.NE_RESIZE_CURSOR:
        case Cursor.N_RESIZE_CURSOR:
        case Cursor.S_RESIZE_CURSOR:
        case Cursor.W_RESIZE_CURSOR:
        case Cursor.E_RESIZE_CURSOR:
            return RESIZING;
        default:
            return -1;
        }
    }
}

首先请注意,您使用的宽高比是 3:4 而不是 4:3:

3:4表示每3个单位的宽度对应4个单位的高度。

4:3表示每4个单位宽度对应3个单位高度

w = h / 4 * 3 正在计算 3:4,而不是 4:3

w = h / 3 * 4h = w / 4 * 3 计算 4:3

继续讨论调整大小中断的原因,当您创建 Rectangle 时,您提供了它左上角的 x、y 坐标,以及它的宽度和高度:

Rectangle rectangle = new Rectangle(x, y, width, height)

矩形将从 x, y 绘制到 x + width, y + height

您的代码的调整大小部分工作正常,拖动鼠标时您可以正确更新 xywidthheight

之所以应用纵横比会破坏它,是因为您正在更新 widthheight,但没有更新 xy

假设用户执行了 Northwest 调整大小,您现在有一个如下所示的矩形:

x => 10
y => 10
width => 5
height => 10

然后应用宽高比 w = h / 4 * 3:

x => 10
y => 10
width => 8
height => 10

因为您是从左上角绘制的,所以矩形现在是从左向右增长的,但是您希望它从右向左增长。当您在 Northwest 方向调整大小时,您总是希望矩形的右下角保持在同一位置。您的代码不会发生这种情况的原因是,当您将纵横比应用于矩形的宽度时,您不会更新矩形的起始 x、y 点。

使用上面的例子,x 和 y 应该更新如下:

x => 7
y => 10
width => 8
height => 10

这是我想出的解决方案:

else if (getCursorState() == RESIZING) {
    Point startPoint = null;
    Point endPoint = null;

    switch(getCursor().getType()) {
        case Cursor.SW_RESIZE_CURSOR:
            startPoint = new Point((int) mouse.getX(), (int) rectangle.getMinY());
            endPoint = new Point((int) rectangle.getMaxX(), (int) mouse.getY());
            break;
        case Cursor.NW_RESIZE_CURSOR:
            startPoint = new Point((int) mouse.getX(), (int) mouse.getY());
            endPoint = new Point((int) rectangle.getMaxX(), (int) rectangle.getMaxY());
            break;
        case Cursor.NE_RESIZE_CURSOR:
            startPoint = new Point((int) rectangle.getMinX(), (int) mouse.getY());
            endPoint = new Point((int) mouse.getX(), (int) rectangle.getMaxY());
            break;
        case Cursor.SE_RESIZE_CURSOR:
            startPoint = new Point((int) rectangle.getMinX(), (int) rectangle.getMinY());
            endPoint = new Point((int) mouse.getX(), (int) mouse.getY());
            break;
    }

    rectangle.setFrameFromDiagonal(startPoint, endPoint);    

    if (fixedRatio) {
        // Calculate 3:4 aspect ratio
        rectangle.height = rectangle.width / 3 * 4;

        // If this is a NW or NE resize, we need to adjust the start y coordinate to account for the new height
        // This keeps the bottom right corner in the same place for a NW resize
        // and the bottom left corner in the same place for a NE resize
        if (getCursor().getType() == Cursor.NW_RESIZE_CURSOR || getCursor().getType() == Cursor.NE_RESIZE_CURSOR) {
            rectangle.y = endPoint.y - rectangle.height;
        }
    }
} 

因此,当矩形在西北或东北方向调整大小时,并应用纵横比,我也会更新矩形的起始 y 坐标以说明高度的变化。