Mark
Mark

Reputation: 8678

how to drag/resize and rotate rectangle in paperjs?

I want to drag and resize a rectangle in paperjs, I also want to rotate the rectangle and resize it while maintaining its relative dimensions.

Ideally I'd like to do so with my mouse by dragging one of its corners (anchors). What mathematics or feature is helpful in doing this in paperjs?

I have tried this by using scaling and modifying the corners but it doesn't work as I want it to. Could someone point me to a solution?

Thanks in advance.

Upvotes: 3

Views: 5199

Answers (2)

bmacnaughton
bmacnaughton

Reputation: 5308

Here's a simple solution that should get you started. It doesn't handle rotation because I'm not sure how you envision the UI working, but by modifying the bounding box to resize the rectangle you should be able to rotate it without problems.

paperjs sketch

I decided to make up my own UI and go ahead and make the example more complicated to address as much of you question as I can without more information. Here's the new sketch:

new sketch

The UI is

  1. click in rectangle to move it by dragging
  2. click on a corner and drag to resize it
  3. control-click on a corner to rotate it

It's a bit tricky to click the corners, but that's an exercise left to the reader. They are colored circles just to emphasize where each segment point of the Path is located.

Key points of the code:

  • Use the rectangle's bounds to scale. Path.Rectangle is not a rectangle as far as paper is concerned. It is four curves (which happen to be straight) connecting four segment points. When you need to work with a rectangle to get its center, top left, etc., you need a Rectangle. Scale the visible rectangle by using the rectangle's bounds (Path.Rectangle.bounds). The code illustrates the bounds with an additional aqua rectangle so it's visible (it's easiest to see when rotating).

  • onMouseDown() sets the state for onMouseDrag() and sets up data needed for each state, e.g., saving the scale base for resizing.

  • onMouseDrag() implements moving, resizing, and rotating.

    tool.onMouseDrag = function(e) {
        if (rect.data.state === 'moving') {
            rect.position = rect.position + e.point - e.lastPoint;
            adjustRect(rect);
        } else if (rect.data.state === 'resizing') {
            // scale by distance from down point
            var bounds = rect.data.bounds;
            var scale = e.point.subtract(bounds.center).length /
                            rect.data.scaleBase.length;
            var tlVec = bounds.topLeft.subtract(bounds.center).multiply(scale);
            var brVec = bounds.bottomRight.subtract(bounds.center).multiply(scale);
            var newBounds = new Rectangle(tlVec + bounds.center, brVec + bounds.center);        
            rect.bounds = newBounds;        
            adjustRect(rect);
        } else if (rect.data.state === 'rotating') {
            // rotate by difference of angles, relative to center, of
            // the last two points.
            var center = rect.bounds.center;
            var baseVec = center - e.lastPoint;
            var nowVec = center - e.point;
            var angle = nowVec.angle - baseVec.angle;
            rect.rotate(angle);
            adjustRect(rect);
        }
    }
    
  • Moving is pretty easy - just calculate the difference between the current and last points from the event and change the position of the rectangle by that much.

  • Resizing is not as obvious. The strategy is to adjust the x and y bounds based on the original distance (scaleBase.length) between the mousedown point and the center of the rectangle. Note that while paper-full.js allows using operators ("+", "-", "*", "/") with points, I used the raw subtract() and multiply() methods a few times - I find it natural to chain the calculations that way.

  • Rotating uses the very nice paper concept that a point also defines a vector and a vector has an angle. It just notes the difference in the angles between the event lastPoint and point relative to the rectangle's center and rotates the rectangle by that difference.

  • moveCircles() and adjustRect() are just bookkeeping functions to update the corner circles and aqua rectangle.

Upvotes: 9

Twisty
Twisty

Reputation: 30883

Consider the following. I just went through the process of figuring this out, based on lots of examples.

My Goals:

  • use my own bounding box when selecting an item
  • Move, Resize, and Rotate (with snap to rotation [45 degrees]) the selected item
  • Show a title / name of the item

Example Sketch

Paper.js Code

var hitOptions = {
    segments: true,
    stroke: true,
    fill: true,
    tolerance: 5
};

function drawHex(w, c, n){
    var h = new Path.RegularPolygon(new Point(100, 100), 6, w / 2);
    h.selectedColor = 'transparent';
    
    c = c != undefined ? c : "#e9e9ff";
    n = n != undefined ? n : "Hexayurt";
    h.name = n;
    h.fillColor = c;
    h.data.highlight = new Group({
       children: [makeBounds(h), makeCorners(h), makeTitle(h)],
       strokeColor: '#a2a2ff',
       visible: false
    });
    
    return h;
}

function makeCorners(o, s){
    s = s != undefined ? s : 5;
    var g = new Group();
    var corners = [
        o.bounds.topLeft,
        o.bounds.topRight,
        o.bounds.bottomLeft,
        o.bounds.bottomRight
    ];
    corners.forEach(function(corner, i) {
        var h = new Path.Rectangle({
            center: corner,
            size: s
        });
        g.addChild(h);
    });
    return g;
}

function makeBounds(o){
    return new Path.Rectangle({
        rectangle: o.bounds
    });
}

function makeTitle(o, n, c){
    c = c != undefined ? c : 'black';
    var t = new PointText({
        fillColor: c,
        content: n != undefined ? n : o.name,
        strokeWidth: 0
    });
    t.bounds.center = o.bounds.center;
    return t;
}

function selectItem(o){
    console.log("Select Item", o.name);
    o.selected = true;
    o.data.highlight.visible = true;
    o.data.highlight.bringToFront();
}

function clearSelected(){
    project.selectedItems.forEach(function(o, i){
        console.log("Unselect Item", o.name);
        o.data.highlight.visible = false;
    });
    project.activeLayer.selected = false;
}

function moveBoxes(o){
    var boxes = o.data.highlight.children[1].children;
    boxes[0].position = o.bounds.topLeft;
    boxes[1].position = o.bounds.topRight;
    boxes[2].position = o.bounds.bottomLeft;
    boxes[3].position = o.bounds.bottomRight;
}

function moveTitle(o){
    var t = o.data.highlight.children[2];
    t.bounds.center = o.bounds.center;
}

function adjustBounds(o){
    if(o.data.state == "moving"){
        o.data.highlight.position = o.position;
    } else {
        o.data.highlight.children[0].bounds = o.bounds;
        moveBoxes(o);
    }
}

var hex1 = drawHex(200);
console.log(hex1.data, hex1.data.highlight);

var segment, path;
var movePath = false;
var tool = new Tool();

tool.minDistance = 10;

tool.onMouseDown = function(event) {
    segment = path = null;
    var hitResult = project.hitTest(event.point, hitOptions);
    if (!hitResult){
        clearSelected();
        return;
    }
    if(hitResult && hitResult.type == "fill"){
        path = hitResult.item;
    }
    if (hitResult && hitResult.type == "segment") {
        path = project.selectedItems[0];
        segment = hitResult.segment;
        if(event.modifiers.control){
            path.data.state = "rotating";
        } else {
            path.data.state = "resizing";
            path.data.bounds = path.bounds.clone();
            path.data.scaleBase = event.point - path.bounds.center;
        }
        console.log(path.data);
    }
    movePath = hitResult.type == 'fill';
    if (movePath){
        project.activeLayer.addChild(hitResult.item);
        path.data.state = "moving";
        selectItem(path);
        console.log("Init Event", path.data.state);
    }
};

tool.onMouseDrag = function(event) {
    console.log(path, segment, path.data.state);
    if (segment && path.data.state == "resizing") {
        var bounds = path.data.bounds;
        var scale = event.point.subtract(bounds.center).length / path.data.scaleBase.length;
        var tlVec = bounds.topLeft.subtract(bounds.center).multiply(scale);
        var brVec = bounds.bottomRight.subtract(bounds.center).multiply(scale);
        var newBounds = new Rectangle(tlVec + bounds.center, brVec + bounds.center);        
        path.bounds = newBounds;
        adjustBounds(path);
    } else if(segment && path.data.state == "rotating") {
        var center = path.bounds.center;
        var baseVec = center - event.lastPoint;
        var nowVec = center - event.point;
        var angle = nowVec.angle - baseVec.angle;
        if(angle < 0){
            path.rotate(-45);
        } else {
            path.rotate(45);
        }
        adjustBounds(path);
    } else if (path && path.data.state == "moving") {
        path.position += event.delta;
        adjustBounds(path);
    }
};

This makes use of .data to store references of the bounding box, handles, and title as a Group. This way, they are always there, they can just visible true or false. This makes it easy to show and hide them as needed.

drawHex( width , color, name )

  • Width - Required, number of pixels wide
  • Color - Optional, string that defines the Fill Color. Default: #e9e9ff
  • Name - Optional, string to be used as the Name and Title. Default: "Hexayurt"

Interactions

  • click - Select item (show bounding box & Handles)
  • click + drag - Move item
  • click + drag handle - Resize item
  • Ctrl + click + drag handle - Rotate item

This is my first pass at it and I may cleanup a lot of the code. For example, I could bind events to the handles specifically instead of looking at more global events.

Upvotes: 1

Related Questions