Reputation: 8678
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
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.
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:
The UI is
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
Reputation: 30883
Consider the following. I just went through the process of figuring this out, based on lots of examples.
My Goals:
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 )
#e9e9ff
Interactions
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