Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag mouse to rotate and scale in Java

I'm trying to create an Illustrator style selection box for geometric objects in java.

Select box

When the object is selected a border is drawn and it's possible to drag the little rectangles to re-size the object. I'd also like to be able to rotate the box by dragging.

So far I can scale the box and I can rotate the box but I can't do the two together. Imagine the box is at an angle of 45 degrees. When you drag the corner to enlarge the box in the x direction this will increase both the width and height of the box because of the angle.

I can get it to work by using:

    dx = dx*cos(theta) - dy*sin(theta);
    dy = dy*cos(theta) + dx*sin(theta);

But this only works when the pivot point is in the top left corner. I want to be able to move the pivot around and then scale and rotate. This problem must have been solved lots of times before. Is there a way I can use an affine transform to convert my mouse draw to the coordinate space of the rotated object? I'd prefer not to have to dig through the trigonometry! Thanks in advance.

like image 327
James Andrews Avatar asked Sep 28 '12 16:09

James Andrews


2 Answers

You are in luck - Java2D provides an AffineTransform class that should do everything you are looking for.

It can handle rotations, scaling, shears, flips, translations etc.

There is a concatenate function that should enable you to combine multiple transforms (as moonwave99 points out you need to do them in the right order as the combination of affine transformations is not commutative)

like image 64
mikera Avatar answered Oct 21 '22 21:10

mikera


I pretty much worked the answer out myself although it's still not possible to move the pivot point around. In case it's helpful here's the full code for a working example using JavaFX 2.2. You can scale and rotate the box by dragging the corners around:

public class SelectionBoxDemo extends Application {

public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage arg0) throws Exception {
    Stage stage = new Stage ();

    // Root is the base pane in which we put everything
    Pane root = new Pane ();

    SelectionBox sb = new SelectionBox ();

    sb.setSize(100, 100);

    root.getChildren().add(sb);

    // Create a new scene
    Scene scene = new Scene (root);

    stage.setScene(scene);

    stage.setMinHeight(500);
    stage.setMinWidth(500);

    stage.show();
}

public static class SelectionBox extends Region {

    private enum Position {
        TopLeft, Top, TopRight, Right, BottomRight, Bottom, BottomLeft, Left; 
    }

    // Create the corners
    private Rectangle tr, tl, br, bl;

    // Create selection lines
    final private Line top, right, bottom, left;

    // Size of corner boxes
    private double cornerSize = 10;

    // Create a new rotate transform
    private final Rotate rotate = new Rotate();
    {
        getTransforms().add(rotate);
        rotate.setPivotX(cornerSize);
        rotate.setPivotY(cornerSize);
    }

    // Circle which is dragged to rotate the box
    private final Circle rotateCircle;

    // Variables to store mouse x and y
    private double x, y;

    public SelectionBox () {

        // Create the circle which can be dragged to rotate the box
        rotateCircle = new Circle(5);
        rotateCircle.setFill(Color.PINK);
        rotateCircle.setStroke(Color.rgb(0,0,0, 0.75));

        // Make it draggable
        rotateCircle.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent event) {
                setMouse(event.getSceneX(), event.getSceneY());
            }
        });

        // When it's dragged rotate the box
        rotateCircle.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent event) {

                // Used to get the scene position of the corner of the box
                Transform localToScene = getLocalToSceneTransform();

                double x1 = getMouseX();
                double y1 = getMouseY();

                double x2 = event.getSceneX();
                double y2 = event.getSceneY();

                double px = rotate.getPivotX() + localToScene.getTx();
                double py = rotate.getPivotY() + localToScene.getTy();

                // Work out the angle rotated
                double th1 = clockAngle(x1, y1, px, py);
                double th2 = clockAngle(x2, y2, px, py);

                double angle = rotate.getAngle();

                angle += th2 - th1;

                // Rotate the rectangle
                rotate.setAngle(angle);

                setMouse(event.getSceneX(), event.getSceneY());
            }
        });

        // Build the corners
        tr = buildCorner (0,0, Position.TopRight);
        tl = buildCorner (0,0, Position.TopLeft);
        br = buildCorner (0,0, Position.BottomRight);
        bl = buildCorner (0,0, Position.BottomLeft);

        // Build the lines
        top = buildLine(0, 100, -100, 0);
        bottom = buildLine(0, 0, 0, 0);
        left = buildLine(0, 0, 0, 0);
        right = buildLine(0, 0, 0, 0);

        getChildren().addAll(top, bottom, left, right, tr, tl, br, bl, rotateCircle);

    }

    // Return the angle from 0 - 360 degrees
    public double clockAngle (double x, double y, double px, double py) {
        double dx = x - px;
        double dy = y - py;

        double angle = Math.abs(Math.toDegrees(Math.atan2(dy, dx)));

        if(dy < 0) {
            angle = 360 - angle;
        }

        return angle;
    }

    // Set the size of the selection box
    public void setSize (double width, double height) {
        tl.setX(0);
        tl.setY(0);

        tr.setX(width + cornerSize);
        tr.setY(0);

        bl.setX(0);
        bl.setY(height + cornerSize);

        br.setX(width + cornerSize);
        br.setY(height + cornerSize);

        setLine(top, cornerSize, cornerSize, width + cornerSize, cornerSize);
        setLine(bottom, cornerSize, height + cornerSize, width + cornerSize, height + cornerSize);
        setLine(right, width + cornerSize, cornerSize, width + cornerSize, height + cornerSize);
        setLine(left, cornerSize, cornerSize, cornerSize, height + cornerSize);

        top.setCursor(Cursor.V_RESIZE);
        bottom.setCursor(Cursor.V_RESIZE);
        left.setCursor(Cursor.H_RESIZE);
        right.setCursor(Cursor.H_RESIZE);

        tr.setCursor(Cursor.CROSSHAIR);
        tl.setCursor(Cursor.CROSSHAIR);
        br.setCursor(Cursor.CROSSHAIR);
        bl.setCursor(Cursor.CROSSHAIR);

        rotateCircle.setTranslateX(width + 2 * cornerSize + rotateCircle.getRadius());
        rotateCircle.setTranslateY(height + 2 * cornerSize + rotateCircle.getRadius());

    }

    // Set the start and end points of a line
    private void setLine (Line l, double x1, double y1, double x2, double y2) {
        l.setStartX(x1);
        l.setStartY(y1);
        l.setEndX(x2);
        l.setEndY(y2);
    }

    // Save mouse coordinates
    private void setMouse(double x, double y) {
        this.x = x;
        this.y = y;
    }

    private double getMouseX () {
        return x;
    }

    private double getMouseY () {
        return y;
    }

    // Selection box width
    public double w () {
        return Math.abs(bottom.getEndX() - bottom.getStartX());
    }

    // Selection box height
    public double h () {
        return Math.abs(right.getEndY() - right.getStartY());
    }

    // Build a corner of the rectangle
    private Rectangle buildCorner (double x, double y, final Position pos) {

        // Create the rectangle
        Rectangle r = new Rectangle();
        r.setX(x);
        r.setY(y);
        r.setWidth(cornerSize);
        r.setHeight(cornerSize);
        r.setStroke(Color.rgb(0,0,0,0.75));
        r.setFill(Color.rgb(0, 0, 0, 0.25));
        r.setStrokeWidth(1);

        r.setStrokeType(StrokeType.INSIDE);


        // Make it draggable
        r.addEventHandler(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent event) {
                setMouse(event.getSceneX(), event.getSceneY());
            }
        });

        r.addEventHandler(MouseEvent.MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent event) {

                // Get the mouse deltas
                double dx = event.getSceneX() - getMouseX();
                double dy = event.getSceneY() - getMouseY();

                // Set save the current mouse value
                setMouse(event.getSceneX(), event.getSceneY());

                // Get the rotation angle in radians
                double tau = - Math.toRadians(rotate.getAngle());

                // Create variables for the sin and cosine
                double sinTau = Math.sin(tau);
                double cosTau = Math.cos(tau);

                // Perform a rotation on dx and dy to the object co-ordinate frame
                double dx_ = dx * cosTau - dy * sinTau;
                double dy_ = dy * cosTau + dx * sinTau;

                // Create a variable for the change in height of the box
                double dh = h();

                // Work out the new positions for the resize corners
                if(pos == Position.TopLeft) {
                    // Set the size based on the transformed dx and dy values
                    setSize(w() - dx_, h() - dy_);

                    // Move the shape 
                    setTranslateX(getTranslateX() + dx); 
                    setTranslateY(getTranslateY() + dy);
                }
                else if (pos == Position.TopRight) {

                    // This comes down to geometry - you need to know the 
                    // amount the height of the shape has increased
                    setSize(w() + dx_ , h() - dy_);

                    // Work out the delta height - that is then used to work out 
                    // the correct translations
                    dh = h() - dh;

                    setTranslateX (getTranslateX() - dh * sinTau);
                    setTranslateY (getTranslateY() - dh * cosTau);
                }
                else if (pos == Position.BottomRight) {
                    setSize(w() + dx_ , h() + dy_ );
                }
                else if (pos == Position.BottomLeft) {

                    setSize(w() - dx_, h() + dy_);

                    dh = h() - dh;

                    setTranslateX(getTranslateX() + dx - dh * sinTau );
                    setTranslateY(getTranslateY() + dy - dh * cosTau);
                }
            }
        });


        return r;
    }



    private Line buildLine (double x1, double y1, double x2, double y2) {
        Line l = new Line (x1, y1, x2, y2);

        l.setStroke(Color.rgb(0, 0, 0, 0.75));
        l.setStrokeWidth (0.5);

        return l;
    }


}

}

like image 27
James Andrews Avatar answered Oct 21 '22 21:10

James Andrews