how to rotate line in fabricjs by clicking any point to select/drag?












2














I have a line with one end at a fixed point that can be rotated or stretched by the user.



jsfiddle here



It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.



I want to simplify this to: click anywhere on the line and drag to rotate.



ideas?



thx.



code snippet:



var canvas = new fabric.Canvas("c", {stateful: true});


var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});









share|improve this question






















  • What's the reason behind setting evented: false?
    – shkaper
    Nov 20 '18 at 18:08










  • just copy/pasted from an example. didn't think it important....
    – danda
    Nov 21 '18 at 19:47
















2














I have a line with one end at a fixed point that can be rotated or stretched by the user.



jsfiddle here



It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.



I want to simplify this to: click anywhere on the line and drag to rotate.



ideas?



thx.



code snippet:



var canvas = new fabric.Canvas("c", {stateful: true});


var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});









share|improve this question






















  • What's the reason behind setting evented: false?
    – shkaper
    Nov 20 '18 at 18:08










  • just copy/pasted from an example. didn't think it important....
    – danda
    Nov 21 '18 at 19:47














2












2








2







I have a line with one end at a fixed point that can be rotated or stretched by the user.



jsfiddle here



It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.



I want to simplify this to: click anywhere on the line and drag to rotate.



ideas?



thx.



code snippet:



var canvas = new fabric.Canvas("c", {stateful: true});


var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});









share|improve this question













I have a line with one end at a fixed point that can be rotated or stretched by the user.



jsfiddle here



It seems that in fabricJS the only way to select a line/object for rotation is the little selection box in the middle. Also, a line is narrow so it is hard to select. Typically one must drag a rectangular selection box across the line to select it, then grab the unlabeled rotation box.



I want to simplify this to: click anywhere on the line and drag to rotate.



ideas?



thx.



code snippet:



var canvas = new fabric.Canvas("c", {stateful: true});


var line1 = new fabric.Line([ 100, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
selectable: true,
evented: false,
minScaleLimit: 0.25,
lockRotation: false,
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true
});






javascript fabricjs






share|improve this question













share|improve this question











share|improve this question




share|improve this question










asked Nov 19 '18 at 17:31









danda

133




133












  • What's the reason behind setting evented: false?
    – shkaper
    Nov 20 '18 at 18:08










  • just copy/pasted from an example. didn't think it important....
    – danda
    Nov 21 '18 at 19:47


















  • What's the reason behind setting evented: false?
    – shkaper
    Nov 20 '18 at 18:08










  • just copy/pasted from an example. didn't think it important....
    – danda
    Nov 21 '18 at 19:47
















What's the reason behind setting evented: false?
– shkaper
Nov 20 '18 at 18:08




What's the reason behind setting evented: false?
– shkaper
Nov 20 '18 at 18:08












just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47




just copy/pasted from an example. didn't think it important....
– danda
Nov 21 '18 at 19:47












1 Answer
1






active

oldest

votes


















0














Here's one way to do what you need.



The idea is that on each scale event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject(), supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.



This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100] as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.



Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options), where x1, y1 - fixed point, x2, y2 - draggable tip.



Lastly, we're changing some of the properties. E.g. evented: false was the reason you couldn't select the line on click.



Below is the snippet with more comments, just in case.






const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>








share|improve this answer





















  • Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
    – danda
    Dec 3 '18 at 14:05












  • @danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
    – shkaper
    Dec 3 '18 at 14:34











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53379861%2fhow-to-rotate-line-in-fabricjs-by-clicking-any-point-to-select-drag%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes









0














Here's one way to do what you need.



The idea is that on each scale event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject(), supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.



This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100] as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.



Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options), where x1, y1 - fixed point, x2, y2 - draggable tip.



Lastly, we're changing some of the properties. E.g. evented: false was the reason you couldn't select the line on click.



Below is the snippet with more comments, just in case.






const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>








share|improve this answer





















  • Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
    – danda
    Dec 3 '18 at 14:05












  • @danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
    – shkaper
    Dec 3 '18 at 14:34
















0














Here's one way to do what you need.



The idea is that on each scale event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject(), supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.



This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100] as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.



Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options), where x1, y1 - fixed point, x2, y2 - draggable tip.



Lastly, we're changing some of the properties. E.g. evented: false was the reason you couldn't select the line on click.



Below is the snippet with more comments, just in case.






const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>








share|improve this answer





















  • Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
    – danda
    Dec 3 '18 at 14:05












  • @danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
    – shkaper
    Dec 3 '18 at 14:34














0












0








0






Here's one way to do what you need.



The idea is that on each scale event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject(), supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.



This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100] as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.



Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options), where x1, y1 - fixed point, x2, y2 - draggable tip.



Lastly, we're changing some of the properties. E.g. evented: false was the reason you couldn't select the line on click.



Below is the snippet with more comments, just in case.






const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>








share|improve this answer












Here's one way to do what you need.



The idea is that on each scale event, we're going to rotate the line using fabric's internal fabric.canvas._rotateObject(), supplying it with current pointer's position. Then, immediately adjust the length of line to match the scale and reset the line's scale to 1.



This would be it, but while your example is relatively easy to do (the line is horizontal), it gets much trickier if you want to initialize a diagonal line. Imagine a line with [0, 0, 100, 100] as coordinates. This would render a rectangular 100x100 bounding box. You'd be able to rotate the line but the huge bounding box is obviously not something you want.



Because of that, we need to initialize the line as if it was rotated back to a horizontal position, then set an angle that it's supposed to have. To do that, we extend the built-in fabric.Line class and modify the constructor to make the calculations. And, since we already have new class, we're going to add the scale handler and default options into it as well. The constructor signature stays the same - new fabric.RotatingLine([x1, y1, x2, y2], options), where x1, y1 - fixed point, x2, y2 - draggable tip.



Lastly, we're changing some of the properties. E.g. evented: false was the reason you couldn't select the line on click.



Below is the snippet with more comments, just in case.






const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>








const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>





const canvas = new fabric.Canvas("c", {stateful: true})

fabric.RotatingLine = fabric.util.createClass(fabric.Line, {
minLength: 50, // we need to set this thing in px now

initialize: function (points, options) {
const a = new fabric.Point(points[0], points[1])
const b = new fabric.Point(points[2], points[3])
// find this line's vector
const vectorB = b.subtract(a)
// find angle between line's vector and x axis
let angleRad = Math.atan2(vectorB.y, vectorB.x)
if (angleRad < 0) {
angleRad = 2 * Math.PI + angleRad
}
const angleDeg = fabric.util.radiansToDegrees(angleRad)
// find initial horizontal position by rotating the tip back
const c = fabric.util.rotatePoint(b.clone(), a, -angleRad)
options = options || {}
// finally, initialize using transform points to make a horizontal line
this.callSuper('initialize', [a.x, a.y, c.x, c.y], {
noScaleCache: false, // false to force cache update while scaling (doesn't redraw parts of line otherwise)
selectable: true,
evented: true, // true because you want to select line on click
//minScaleLimit: 0.25, // has no effect now because we're resetting scale on each scale event
lockRotation: false,
hasRotatingPoint: false, // to disable rotation control
centeredRotation: false,
centeredScaling: false,

originX: "left", // origin of rotation/transformation.
originY: "bottom", // origin of rotation/transformation.

lockMovementX: true,
lockMovementY: true,
lockScalingFlip: true,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
lockUniScaling: true,
...options,
angle: angleDeg // note that we use the calculated angle no matter what
})

this.setControlsVisibility({
tr: false,
tl: false,
bl: false,
mt: false, // middle top disable
mb: false, // midle bottom
ml: false, // middle left
mr: false, // I think you get it
})

this.on('scaling', function (e) {
// rotate to the pointer's x,y
this.canvas._rotateObject(e.pointer.x, e.pointer.y)
// while _rotateObject() tries to keep left/top at initial value,
// it sometimes fails because of rounding errors (?)
// so we need to do it manually again
this.set({left: this.x1, top: this.y1})
// calculate new length before resetting scale
const xOffset = (this.x2 - this.x1) * this.scaleX
const newLength = Math.max(this.minLength, xOffset)
// reset scaleX/scaleY and set new x coord for the tip point
this.set({
scaleX: 1,
scaleY: 1,
x2: this.x1 + newLength
})
})
}
})


const line1 = new fabric.RotatingLine([ 200, 200, 330, 200 ], {
fill: 'red',
stroke: 'red',
strokeWidth: 3,
});

const line2 = new fabric.RotatingLine([ 200, 200, 100, 100 ], {
fill: 'blue',
stroke: 'blue',
strokeWidth: 3,
});

canvas.add(line1, line2)

// Disables group selection.
canvas.on('selection:created', (e) => {
if(e.target.type === 'activeSelection') {
canvas.discardActiveObject();
} else {
//do nothing
}
})

// Keeps objects inside canvas. undos move/rotate/scale out of canvas.
canvas.on('object:modified', function (options) {
let obj = options.target;
let boundingRect = obj.getBoundingRect(true);
if (boundingRect.left < 0
|| boundingRect.top < 0
|| boundingRect.left + boundingRect.width > canvas.getWidth()
|| boundingRect.top + boundingRect.height > canvas.getHeight()) {
obj.top = obj._stateProperties.top;
obj.left = obj._stateProperties.left;
obj.angle = obj._stateProperties.angle;
obj.scaleX = obj._stateProperties.scaleX;
obj.scaleY = obj._stateProperties.scaleY;
obj.setCoords();
obj.saveState();
}
});

<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
<canvas id='c' width="500" height="400"></canvas>






share|improve this answer












share|improve this answer



share|improve this answer










answered Nov 22 '18 at 5:59









shkaper

1,042514




1,042514












  • Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
    – danda
    Dec 3 '18 at 14:05












  • @danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
    – shkaper
    Dec 3 '18 at 14:34


















  • Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
    – danda
    Dec 3 '18 at 14:05












  • @danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
    – shkaper
    Dec 3 '18 at 14:34
















Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05






Sorry I only just saw this now. I already came up with another simple solution based on the stickman demo where line is redrawn whenever circle at endpoint is moved. It works well for me. The code snippet above gives a JS error when I try to run it, so I can't easily test. Still, I appreciate the thought and effort that went into this answer, so I am selecting it as the answer. thanks!!
– danda
Dec 3 '18 at 14:05














@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34




@danda this is weird, I don't get any errors if I run the embedded snippet here. What's the error you're seeing?
– shkaper
Dec 3 '18 at 14:34


















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.





Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


Please pay close attention to the following guidance:


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53379861%2fhow-to-rotate-line-in-fabricjs-by-clicking-any-point-to-select-drag%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







Popular posts from this blog

Can a sorcerer learn a 5th-level spell early by creating spell slots using the Font of Magic feature?

ts Property 'filter' does not exist on type '{}'

Notepad++ export/extract a list of installed plugins