Previously I was using the Line class in FabricJS to allow users to draw lines on a canvas. I now need to implement curved lines, but as Fabric's Line class doesn't support quadratic curves I have re-written my code to use the Path class instead.
Previously when drawing a Line the x1, y1, x2, and y2 values would update automatically if the line was moved on the canvas which made it easy to update the position of the start and end anchor points that I had created (just rectangles on the canvas which can be manipulated in the same way you would manipulate anchor points in Adobe Illustrator).
The Path class doesn't use x1, y1, x2 or y2, instead you pass a string which is converted into an array e.g. M 100 100, Q 200 200 500 500
. These values are then available as part of a path
object (with 100 100 being the x and y of the start point of the path, 200 200 being the curve and 500 500 being the end) which means I can get the start and end points in a similar way that I did with the Line class.
The issue is that the path object doesn't update when the line itself is moved. The left
and top
values update but I'm finding it difficult to manually update the path values based on this. I feel like I may be missing something obvious (e.g. the ability to auto-update the path object or get a delta/difference that I can use to manually reposition the anchors on the object:moving function).
The snippet below is taken from the quadratic curve demo on Fabric's website. If you move drag the actual line (not the anchor points) you can see that the anchor points remain where they are.
(function() {
var canvas = this.__canvas = new fabric.Canvas('c', {
height: 563,
width: 1000,
});
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
canvas.on({
'object:selected': onObjectSelected,
'object:moving': onObjectMoving,
'before:selection:cleared': onBeforeSelectionCleared
});
(function drawQuadratic() {
var line = new fabric.Path('M 100 100, Q 200 200 200 200', { fill: '', stroke: 'black', objectCaching: false });
console.log(line);
console.log(line);
line.selectable = true;
canvas.add(line);
var p1 = makeCurvePoint(200, 200, null, line, null)
p1.name = "p1";
canvas.add(p1);
var p0 = makeCurveCircle(100, 100, line, p1, null);
p0.name = "p0";
canvas.add(p0);
var p2 = makeCurveCircle(300, 100, null, p1, line);
p2.name = "p2";
canvas.add(p2);
})();
function makeCurveCircle(left, top, line1, line2, line3) {
var c = new fabric.Circle({
left: left,
top: top,
strokeWidth: 5,
radius: 12,
fill: '#fff',
stroke: '#666'
});
c.hasBorders = c.hasControls = false;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
return c;
}
function makeCurvePoint(left, top, line1, line2, line3) {
var c = new fabric.Circle({
left: left,
top: top,
strokeWidth: 8,
radius: 14,
fill: '#fff',
stroke: '#666'
});
c.hasBorders = c.hasControls = false;
c.line1 = line1;
c.line2 = line2;
c.line3 = line3;
return c;
}
function onObjectSelected(e) {
var activeObject = e.target;
if (activeObject.name == "p0" || activeObject.name == "p2") {
activeObject.line2.animate('opacity', '1', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.line2.selectable = true;
}
}
function onBeforeSelectionCleared(e) {
var activeObject = e.target;
if (activeObject.name == "p0" || activeObject.name == "p2") {
activeObject.line2.animate('opacity', '0', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.line2.selectable = false;
}
else if (activeObject.name == "p1") {
activeObject.animate('opacity', '0', {
duration: 200,
onChange: canvas.renderAll.bind(canvas),
});
activeObject.selectable = false;
}
}
function onObjectMoving(e) {
if (e.target.name == "p0" || e.target.name == "p2") {
var p = e.target;
if (p.line1) {
p.line1.path[0][1] = p.left;
p.line1.path[0][2] = p.top;
}
else if (p.line3) {
p.line3.path[1][3] = p.left;
p.line3.path[1][4] = p.top;
}
}
else if (e.target.name == "p1") {
var p = e.target;
if (p.line2) {
p.line2.path[1][1] = p.left;
p.line2.path[1][2] = p.top;
}
}
else if (e.target.name == "p0" || e.target.name == "p2") {
var p = e.target;
p.line1 && p.line1.set({ 'x2': p.left, 'y2': p.top });
p.line2 && p.line2.set({ 'x1': p.left, 'y1': p.top });
p.line3 && p.line3.set({ 'x1': p.left, 'y1': p.top });
p.line4 && p.line4.set({ 'x1': p.left, 'y1': p.top });
}
}
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.21/fabric.min.js"></script>
<canvas id="c"></canvas>
This might not be a perfect solution. You need to calculate your path's end points from the path object. Then set it to all three circles. And there is no way to set path to a path object for now, so you need to create a new path object with the calculated path from points.
DEMO