I am using Fabric js for my project.
I have a use case where I want an object to animate along the boundary of other fabric object. Similar to motion paths in power point. To implement this, I am creating a fabric.Path object and using this path, I am getting all the boundary points of the object and animating the object along these points. The code is as shown below.
<script src="./js/fabric.js"></script>
<canvas
id="c"
width="500"
height="500"
style="border: 1px solid #ccc"
></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<script id="main">
var canvas = new fabric.Canvas("c");
var circle = new fabric.Circle({
radius: 30,
fill: "#f55"
});
canvas.add(circle);
var line = new fabric.Path(
"M 0 0 L 200 100 L 170 200 z",
{
fill: "",
stroke: "black",
objectCaching : true
}
);
line.set({ name: "dummy" });
canvas.add(line);
var points = getPathValues("M 0 0 L 200 100 L 170 200 z", 1000);
function getPathValues(path_val, samples) {
var path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.setAttribute("d", path_val);
var points = [];
var len = path.getTotalLength();
var step = (step = len / samples);
for (var i = 0; i <= len; i += step) {
var p = path.getPointAtLength(i);
points.push(p.x);
points.push(p.y);
}
return points;
}
var i = 0;
var interval = setInterval(function animate() {
i = i + 2;
if (i > points.length) {
// clearInterval(interval);
i = 0;
}
circle.left = line.left + points[i] - circle.radius;
circle.top = line.top + points[i + 1] - circle.radius;
canvas.renderAll();
}, 10);
With all this working well, Now when I scale or change position of the path object, I want to take the changed path, get the updated points and animate the object along those points. Now the problem is that when scale or change the position if the path object, The object.path for it is not getting updated automatically. I am not able to get the change path values which is needed for me to generate boundary points.
Is there any way to get the update path of the Fabric.Path object?
Is there any way to get the path of a normal fabric object?
Indeed the path data is transformed such that it becomes relative to the object's plane.
You should familiarize with relative planes and basics of matrix multiplication, at least the concepts.
This is why scale etc. don't affect it.
You need to apply the object's transformation matrix (via preTransform) to the points.
I can imagine this is too much.
That is why I have exposed fabric.util.sendPointToPlane. Check that out and it will save you a lot of headache.
fabric.util.sendPointToPlane(point, from, to), in your case fabric.util.sendPointToPlane(point, object.calcTransformMatrix(), null) will send the point to the canvas plane.
I wrote a post regarding relative planes but I can't find it, somewhere in fabric discussions
I am developing a radar which consists of concentric circular sectors using Raphael JS library. I have been able to create these sectors, however, I am having difficulty thinking up a suitable solution of how points (which are basically simple Raphael shapes- circles, triangles, etc) can be placed within each sector.
I am not sure but does a possible solution lie in using the getBBox() for each path? Keeping in mind that the bounding box for circular shapes have points that are not within the shape itself.
Draw a random invisible path inside the region
get a random point inside that path
and draw a random object using that point as center
var radius1 = 80;
var radius2 = 50;
var center = 250;
function circleToPath(c, r, d) {
if(d == 1) {
return "M "+(c-r)+","+c+" q 0,-"+r+" "+r+",-"+r+" "+r+",0 "+r+","+r+" 0,"+r+" -"+r+","+r+" -"+r+",0 "+"-"+r+",-"+r;
} else {
return "M "+(c-r)+","+c+" q 0,"+r+" "+r+","+r+" "+r+",0 "+r+",-"+r+" 0,-"+r+" -"+r+",-"+r+" -"+r+",0 "+"-"+r+","+r;
}
}
region = paper.path(circleToPath(center, radius1, 1) + circleToPath(center, radius2, 0) + "z").attr({fill: "red", "fill-opacity": 0.5,stroke: "none"});
for(i=0;i<5;i++){
randomRadius = Math.floor((Math.random() * (radius1 - radius2)) + radius2);
vir = paper.path(circleToPath(center, randomRadius, 1)).attr({fill: "none", stroke: "none"});
len = vir.getTotalLength();
pointCenter = vir.getPointAtLength(Math.floor(Math.random() * len));
paper.circle(pointCenter.x,pointCenter.y,(Math.floor(Math.random() * 15)) + 5);
}
http://jsfiddle.net/5L9g0xh4/
UPDATE:
A little bit cheating as path intersection exists but is not working well in Raphael
constrain the random point and then rotate each:
http://jsfiddle.net/crockz/opqhas0w/
When I can create a line as follows:
var lineData = [{ "x": 50, "y": 50 }, {"x": 100,"y": 100}, {"x": 150,"y": 150}, {"x": 200, "y": 200}];
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("basis");
var myLine = lineEnter.append("path")
.attr("d", lineFunction(lineData))
Now I want to add a text to the second point of this lineArray:
lineEnter.append("text").text("Yaprak").attr("y", function(d){
console.log(d); // This is null
console.log("MyLine");
console.log(myLine.attr("d")) // This is the string given below, unfortunately as a String
// return lineData[1].x
return 10;
} );
Output of the line console.log(myLine.attr("d")):
M50,50L58.33333333333332,58.33333333333332C66.66666666666666,66.66666666666666,83.33333333333331,83.33333333333331,99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,133.33333333333331,133.33333333333331,150,150C166.66666666666666,166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,191.66666666666663L200,200
I can get the path data in string format. Can I convert this data back to lineData array? Or, is there any other and simple way to regenerate or get the lineData when appending a text?
Please refer to this JSFiddle.
The SVGPathElement API has built-in methods for getting this info. You do not need to parse the data-string yourself.
Since you stored a selection for your line as a variable, you can easily access the path element's api using myLine.node() to refer to the path element itself.
For example:
var pathElement = myLine.node();
Then you can access the list of commands used to construct the path by accessing the pathSegList property:
var pathSegList = pathElement.pathSegList;
Using the length property of this object, you can easily loop through it to get the coordinates associated with each path segment:
for (var i = 0; i < pathSegList.length; i++) {
console.log(pathSegList[i]);
}
Inspecting the console output, you will find that each path segment has properties for x and y representing the endpoint of that segment. For bezier curves, arcs, and the like, the control points are also given as x1, y1, x2, and y2 as necessary.
In your case, regardless of whether you use this method or choose to parse the string yourself, you will run into difficulties because you used interpolate('basis') for your line interpolation. Therefore, the line generator outputs 6 commands (in your specific case) rather than 4, and their endpoints do not always correspond to the original points in the data. If you use interpolate('linear') you will be able to reconstruct the original dataset, since the linear interpolation has a one-to-one correspondence with the path data output.
Assuming you used linear interpolation, reconstructing the original dataset could be done as follows:
var pathSegList = myLine.node().pathSegList;
var restoredDataset = [];
// loop through segments, adding each endpoint to the restored dataset
for (var i = 0; i < pathSegList.length; i++) {
restoredDataset.push({
"x": pathSegList[i].x,
"y": pathSegList[i].y
})
}
EDIT:
As far as using the original data when appending text... I'm assuming you are looking to append labels to the points, there's no need to go through all the trouble of reconstructing the data. In fact the real issue is that you never used data-binding in the first place to make your line graph. Try binding the data using the .datum() method for your path, and using the .data() method for the labels. Also you might want to rename lineEnter since you're not using an enter selection and it simply represents a group. For example:
// THIS USED TO BE CALLED `lineEnter`
var lineGroup = svgContainer.append("g");
var myLine = lineGroup.append("path")
// HERE IS WHERE YOU BIND THE DATA FOR THE PATH
.datum(lineData)
// NOW YOU SIMPLY CALL `lineFunction` AND THE BOUND DATA IS USED AUTOMATICALLY
.attr("d", lineFunction)
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// FOR THE LABELS, CREATE AN EMPTY SELECTION
var myLabels = lineGroup.selectAll('.label')
// FILTER THE LINE DATA SINCE YOU ONLY WANT THE SECOND POINT
.data(lineData.filter(function(d,i) {return i === 1;})
// APPEND A TEXT ELEMENT FOR EACH ELEMENT IN THE ENTER SELECTION
.enter().append('text')
// NOW YOU CAN USE THE DATA TO SET THE POSITION OF THE TEXT
.attr('x', function(d) {return d.x;})
.attr('y', function(d) {return d.y;})
// FINALLY, ADD THE TEXT ITSELF
.text('Yaprak')
You can break the line into individual commands by splitting the string on the L, M, and C characters:
var str = "M50,50L58.33333333333332,58.33333333333332C66.66666666666666,
66.66666666666666,83.33333333333331,83.33333333333331,
99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,
133.33333333333331,133.33333333333331,150,150C166.66666666666666,
166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,
191.66666666666663L200,200"
var commands = str.split(/(?=[LMC])/);
This gives the sequence of commands that are used to render the path. Each will be a string comprised of a character (L, M, or C) followed by a bunch of numbers separated by commas. They will look something like this:
"C66.66666666666666,66.66666666666666,83.33333333333331,
83.33333333333331,99.99999999999999,99.99999999999999"
That describes a curve through three points, [66,66], [83,83], and [99,99]. You can process these into arrays of pairs points with another split command and a loop, contained in a map:
var pointArrays = commands.map(function(d){
var pointsArray = d.slice(1, d.length).split(',');
var pairsArray = [];
for(var i = 0; i < pointsArray.length; i += 2){
pairsArray.push([+pointsArray[i], +pointsArray[i+1]]);
}
return pairsArray;
});
This will return an array containing each command as an array of length-2 arrays, each of which is an (x,y) coordinate pair for a point in the corresponding part of the path.
You could also modify the function in map to return object that contain both the command type and the points in the array.
EDIT:
If you want to be able to access lineData, you can add it as data to a group, and then append the path to the group, and the text to the group.
var group = d3.selectAll('g').data([lineData])
.append('g');
var myLine = group.append('path')
.attr('d', function(d){ return lineFunction(d); });
var myText = group.append('text')
.attr('text', function(d){ return 'x = ' + d[1][0]; });
This would be a more d3-esque way of accessing the data than reverse-engineering the path. Also probably more understandable.
More info on SVG path elements
A little hacky, but you can use animateMotion to animate an object (e.g. a rect or a circle) along the path and then sample the x/y position of the object. You will have to make a bunch of choices (e.g. how fast do you animated the object, how fast do you sample the x/y position, etc.). You could also run this process multiple times and take some kind of average or median.
Full code (see it in action: http://jsfiddle.net/mqmkc7xz/)
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<path id="mypath"
style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 70,67 15,0 c 0,0 -7.659111,-14.20627 -10.920116,-27.28889 -3.261005,-13.08262 9.431756,-13.85172 6.297362,-15.57166 -3.134394,-1.71994 -7.526366,-1.75636 -2.404447,-3.77842 3.016991,-1.19107 9.623655,-5.44678 0.801482,-9.67404 C 76.821958,10 70,10 70,10"
/>
</svg>
<div id="points"></div>
<script>
/**
* Converts a path into an array of points.
*
* Uses animateMotion and setInterval to "steal" the points from the path.
* It's very hacky and I have no idea how well it works.
*
* #param SVGPathElement path to convert
* #param int approximate number of points to read
* #param callback gets called once the data is ready
*/
function PathToPoints(path, resolution, onDone) {
var ctx = {};
ctx.resolution = resolution;
ctx.onDone = onDone;
ctx.points = [];
ctx.interval = null;
// Walk up nodes until we find the root svg node
var svg = path;
while (!(svg instanceof SVGSVGElement)) {
svg = svg.parentElement;
}
// Create a rect, which will be used to trace the path
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
ctx.rect = rect;
svg.appendChild(rect);
var motion = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion");
motion.setAttribute("path", path.getAttribute("d"));
motion.setAttribute("begin", "0");
motion.setAttribute("dur", "3"); // TODO: set this to some larger value, e.g. 10 seconds?
motion.setAttribute("repeatCount", "1");
motion.onbegin = PathToPoints.beginRecording.bind(this, ctx);
motion.onend = PathToPoints.stopRecording.bind(this, ctx);
// Add rect
rect.appendChild(motion);
}
PathToPoints.beginRecording = function(ctx) {
var m = ctx.rect.getScreenCTM();
ctx.points.push({x: m.e, y: m.f});
ctx.interval = setInterval(PathToPoints.recordPosition.bind(this, ctx), 1000*3/ctx.resolution);
}
PathToPoints.stopRecording = function(ctx) {
clearInterval(ctx.interval);
// Remove the rect
ctx.rect.remove();
ctx.onDone(ctx.points);
}
PathToPoints.recordPosition = function(ctx) {
var m = ctx.rect.getScreenCTM();
ctx.points.push({x: m.e, y: m.f});
}
PathToPoints(mypath, 100, function(p){points.textContent = JSON.stringify(p)});
</script>
</body>
</html>
pathSegList is supported in old Chrome and is removed since Chrome 48.
But Chrome has not implemented the new API.
Use path seg polyfill to work with old API.
Use path data polyfill to work with new API. It's recommended.
var path = myLine.node();
//Be sure you have added the pathdata polyfill to your page before use getPathData
var pathdata = path.getPathData();
console.log(pathdata);
//you will get an Array object contains all path data details
//like this:
[
{
"type": "M",
"values": [ 50, 50 ]
},
{
"type": "L",
"values": [ 58.33333333333332, 58.33333333333332 ]
},
{
"type": "C",
"values": [ 66.66666666666666, 66.66666666666666, 83.33333333333331, 83.33333333333331, 99.99999999999999, 99.99999999999999 ]
},
{
"type": "C",
"values": [ 116.66666666666666, 116.66666666666666, 133.33333333333331, 133.33333333333331, 150, 150 ]
},
{
"type": "C",
"values": [ 166.66666666666666, 166.66666666666666, 183.33333333333331, 183.33333333333331, 191.66666666666663, 191.66666666666663 ]
},
{
"type": "L",
"values": [ 200, 200 ]
}
]
I found this question by Google. What I needed was simply the pathSegList property of a SVG path object:
var points = pathElement.pathSegList;
Every point looks like
y: 57, x: 109, pathSegTypeAsLetter: "L", pathSegType: 4, PATHSEG_UNKNOWN: 0…}
See
www.w3.org/TR/SVG/paths
https://msdn.microsoft.com/en-us/library/ie/ff971976(v=vs.85).aspx
I've successfully used this to render a list of x,y points:
https://shinao.github.io/PathToPoints/
Code is more than what I could fit into this textbox, but here is probably a good start:
https://github.com/Shinao/PathToPoints/blob/master/js/pathtopoints.js#L209
Expanding on #cuixiping answer: getPathData() also includes a normalization option:
getPathData({normalize:true}) that will convert
relative and shorthand commands to use only M, L, C and z.
So you don't have to worry about highly optimized/minified d strings (containing relative commands, shorthands etc).
let pathData = path1.getPathData({ normalize: true });
let lineData = pathDataToPoints(pathData);
pointsOut.value=JSON.stringify(lineData, null, '\t')
/**
* create point array
* from path data
**/
function pathDataToPoints(pathData) {
let points = [];
pathData.forEach((com) => {
let values = com.values;
let valuesL = values.length;
// the last 2 coordinates represent a segments end point
if (valuesL) {
let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
points.push(p);
}
});
return points;
}
/**
* render points from array
* just for illustration
**/
renderPoints(svg, lineData);
function renderPoints(svg, points) {
points.forEach(point=>{
renderPoint(svg, point);
})
}
function renderPoint(svg, coords, fill = "red", r = "2") {
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
svg{
width:20em;
border:1px solid red;
overflow:visible;
}
path{
stroke:#000;
stroke-width:1
}
textarea{
width:100%;
min-height:20em
}
<svg id="svg" viewBox='0 0 250 250'>
<path id="path1" d="M50 50l8.33 8.33c8.33 8.33 25 25 41.67 41.67s33.33 33.33 50 50s33.33 33.33 41.67 41.67l8.33 8.33" stroke="#000" />
</svg>
<h3>Points</h3>
<textarea id="pointsOut"></textarea>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
I am trying to scale/move an SVG path created with the Raphael api. I want the path to fit neatly within a container, no matter the size of the container. I have searched the reference, the web and I'm still struggling to get this to work.
If anyone can tell me why this isn't working, I would be very happy.
This fiddle shows you what I'm doing: http://jsfiddle.net/tolund/3XPxL/5/
JavaScript:
var draw = function(size) {
var $container = $("#container").empty();
$container.css("height",size+"px").css("width",size+"px");
var paper = Raphael("container");
var pathStr = "M11.166,23.963L22.359,17.5c1.43-0.824,1.43-2.175,"+
"0-3L11.166,8.037c-1.429-0.826-2.598-0.15-2.598,"+
"1.5v12.926C8.568,24.113,9.737,24.789,11.166,23.963z";
// set the viewbox to same size as container
paper.setViewBox(0, 0, $container.width(), $container.height(), true);
// create the path
var path = paper.path(pathStr)
.attr({ fill: "#000", "stroke-width": 0, "stroke-linejoin": "round", opacity: 1 });
// get the path outline box (may be different size than view box.
var box = path.getBBox();
// move the path as much to the top/left as possible within container
path.transform("t" + 0 + "," + 0);
// get the width/height based on path box relative to paper (same size as container)
var w = (paper.width) / box.width;
var h = (paper.height) / box.height;
// scale the path to the container (use "..." to compound transforms)
path.transform('...S' + w + ',' + h + ',0,0');
}
$(function() {
var currentSize = 30;
draw(currentSize);
$("#smaller").click(function(){
currentSize = currentSize < 10 ? currentSize : currentSize * 0.5;
draw(currentSize);
});
$("#bigger").click(function(){
currentSize = 300 < currentSize ? currentSize : currentSize * 2;
draw(currentSize);
});
});
HTML:
<button id="smaller">-</button>
<button id="bigger">+</button>
<div id="container" style="border: 1px #ddd solid; margin: 30px">
</div>
Thanks,
Torgeir.
I think your problem is a fundamental misunderstanding of what the viewbox is useful for. In your code, you're attempting to set the viewbox of the svg element so that it matches the coordinate space of the screen, and then transform the path to match that coordinate space. There's no technical reason you can't do this, but it does effectively take the "Scalable" out of "Scalable Vector Graphics." The entire point of the viewbox is to make the translation between the vector coordinate space and the screen relative.
The best way to solve your problem, then, is not to transform the path to match the SVG element, but to use the viewbox to let SVG's intrinsic scalability do this for you.
First things first: create your path so we have an object to work with. We don't care what the viewbox is at this point.
var pathStr = "..."; // The content of the path and its coordinates are completely immaterial
var path = paper.path(pathStr)
.attr({ fill: "#000", "stroke-width": 0, "stroke-linejoin": "round", opacity: 1 });
Now all we need to do is use the viewbox to "focus" the SVG on the coordinate space we're interested in.
var box = path.getBBox();
var margin = Math.max( box.width, box.height ) * 0.1; // because white space always looks nice ;-)
paper.setViewBox(box.x - margin, box.y - margin, box.width + margin * 2, box.height + margin * 2);
And that's it. The SVG (regardless of its size) will translate from the internal coordinates specified in the viewbox to its physical coordinates on screen.
Here's a fork of your fiddle as proof-of-concept.
I want to animate a path (actually a set of paths, but I'll get to that) along a curved path.
RaphaelJS 2 removed the animateAlong method, for reasons I haven't been able to discern. Digging into the Raphael documentation's gears demo as abstracted by Zevan, I have got this far:
//adding a custom attribute to Raphael
(function() {
Raphael.fn.addGuides = function() {
this.ca.guide = function(g) {
return {
guide: g
};
};
this.ca.along = function(percent) {
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "t" + [point.x, point.y]
};
return t;
};
};
})();
var paper = Raphael("container", 600, 600);
paper.addGuides();
// the paths
var circ1 = paper.circle(50, 150, 40);
var circ2 = paper.circle(150, 150, 40);
var circ3 = paper.circle(250, 150, 40);
var circ4 = paper.circle(350, 150, 40);
var arc1 = paper.path("M179,204c22.667-7,37,5,38,9").attr({'stroke-width': '2', 'stroke': 'red'});
// the animation
// works but not at the right place
circ3.attr({guide : arc1, along : 1})
.animate({along : 0}, 2000, "linear");
http://jsfiddle.net/hKGLG/4/
I want the third circle to animate along the red path. It is animating now, but at a distance from the red path equal to the third circle's original coordinates. The weird thing is that this happens whether the transform translate in the along object is relative (lowercase "t") or absolute (uppercase "T"). It also always animates in the same spot, even if I nudge it with a transform translation just before the animate call.
Any help very appreciated. I just got off the boat here in vector-land. Pointers are helpful--a working fiddle is even better.
You're just a hop, skip, and jump away from the functionality that you want. The confusion here concerns the interaction between transformations and object properties -- specifically, that transformations do not modify the original object properties. Translating simply adds to, rather than replaces, the original coordinates of your circles.
The solution is extremely straightforward. In your along method:
this.ca.along = function(percent) {
var box = this.getBBox( false ); // determine the fundamental location of the object before transformation occurs
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "...T" + [point.x - ( box.x + ( box.width / 2 ) ), point.y - ( box.y + ( box.height / 2 ) )] // subtract the center coordinates of the object from the translation offset at this point in the guide.
};
return t;
Obviously, there's some room for optimization here (i.e., it might make sense to create all your circles at 0,0 and then translate them to the display coordinates you want, avoiding a lot of iterative math). But it's functional... see here.
One other caveat: the ...T translation won't effect any other transforms that have already been applied to a given circle. This implementation is not guaranteed to play nicely with other transforms.