Keep stroke-dasharray consistent when path length is changing - javascript

I have a path that is being drawn by the user, with D3.js.
I would like to have a dasharray defined on my user drawn path, however, as it changes its shape and length, the dash behaves inconsistently and the gaps are moving and becoming smaller.
Here is a codepen:
https://codepen.io/Richacinas/pen/YjXpBE
Feel free to fork.. the function that is drawing the user path is this one:
DrawItGraph.prototype.draw = function() {
...
var pos = this.d3.mouse(event.target)
if (pos[1] > this.chartHeight) return
var date = this.clamp(this.middleValue + 2629743, this.maxValue, this.convection.x.invert(pos[0]))
var value = this.clamp(0, this.convection.y.domain()[1], this.convection.y.invert(pos[1]))
this.userData.forEach(function (d) {
if (Math.round(Math.abs(d.date - date) / 60000000) < 50) {
d.value = value
d.defined = true
}
})
this.yourDataSel.at({d: this.line.defined(this.format('defined'))(this.userData)})
if (this.d3.mean(this.userData, this.format('defined')) === 1) {
this.graphCompleted = true
}
}
I suspect that I have to dynamically change the stroke-dashoffset and/or stroke-dasharray depending on path length, however, I have no idea what would the proper formula...
Thanks a lot

Your problem is not the dash array or the dash offset. Lets look at an example generated path:
.your-line {
stroke: #ffb531;
stroke-width: 10;
stroke-dasharray: 15;
}
<svg width="600" viewBox="1000 0 800 440">
<path class="your-line" d="M1031.9776744186047,214L1099.9652386780906,189L1170.1216156670746,188L1238.0148837209304,188L1308.1712607099143,153.00000000000006L1378.3276376988983,174.00000000000003L1446.1266095471235,163.00000000000006L1516.2829865361077,218.99999999999997L1584.1762545899633,201L1654.3326315789475,201L1724.4890085679315,114.99999999999994L1787.85605875153,195L1858.012435740514,195L1858.012435740514,195L1787.85605875153,195L1724.4890085679315,114.99999999999994L1654.3326315789475,201L1584.1762545899633,201L1516.2829865361077,218.99999999999997L1446.1266095471235,163.00000000000006L1378.3276376988983,174.00000000000003L1308.1712607099143,153.00000000000006L1238.0148837209304,188L1170.1216156670746,188L1099.9652386780906,189L1031.9776744186047,214Z"></path>
</svg>
If we pull out the first few coordinates and the last few, the problem should become obvious:
M 1031, 214
L 1099, 189
L 1170, 188
...
L 1170, 188
L 1099, 189
L 1031, 214
Z
Your problem is that you are not drawing a single line, you are drawing a path that travels to the left and then back again, over the same coordinates, to the start.
As the length of the path varies, the dash pattern is interfering with itself and making the dashes appear to grow and shrink.
You need to update your line drawing code to draw a path in one direction only. Not a closed shape.

Related

Fabric.Path Object's object.path does not give updated path when scaled or changed position in Fabric js

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

Does the Fabric.js Path Array have a size limit?

I am trying to plot a line graph using Fabric.js. The fabric.Path seems the way to go but it stops drawing after 8 segments.
I've tried loops and individually coding each segment and it always stops drawing after 8 segments
const canvas = Controller.Canvas;
line = new fabric.Path('M 90 104', { stroke: 'black', fill: '' });
lastLeft = 90;
for (i = 1; i < 20; i++) {
lastLeft += 20;
line.path[i] = ['L', lastLeft, 104];
}
canvas.add(line);
I would expect the code to draw a line of 20 segments. It stops at 8. The canvas is plenty large enough.
Looking at the fabric.js code, Line's path property is not really supposed to be modified like that - it's more of an internal property. After the path is parsed from the path data passed into the constructor, fabric calculates the Line's dimensions and position, which it then uses during a render call. This means that if you've modified path after constructor is run, you're going to end up with wrong dimensions, hence the missing lines, etc.
There is a way to make fabric re-calculate dimensions, although it uses a private call. Which means that it could change between fabric versions (the snippet below works with 3.4.0) and is therefore not recommended, unless you really want to mess with path for some reason:
const canvas = new fabric.Canvas('c')
const initialX = 10
const initialY = 10
const line = new fabric.Path(`M ${initialX} ${initialY}`, { stroke: 'black', fill: '' })
for (let i = 1; i < 20; i++) {
line.path[i] = ['L', initialX + i * 20, initialY + Math.pow(i, 2)]
}
/* WARNING: Hacky stuff start */
fabric.Polyline.prototype._setPositionDimensions.call(line, {})
/* Hacky stuff end */
canvas.add(line)
body {
background: ivory;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.4.0/fabric.min.js"></script>
<canvas id="c" width="500" height="400"></canvas>

Set center point for svg group with javascript

In a game I am currently making you are in a map which is build from nodes and you are warping to different nodes when you click on them. My current map looks like that (created with two.js it's a svg group where each circle and line is svg path element, ex:
<path transform="matrix(1 0 0 1 553 120)" d="M 8 0 C 8 2.02 7.085 4.228 5.656 5.656 C 4.228 7.085 2.02 8 0 8 C -2.021 8 -4.229 7.085 -5.657 5.656 C -7.086 4.228 -8 2.02 -8 0 C -8 -2.021 -7.086 -4.229 -5.657 -5.657 C -4.229 -7.086 -2.021 -8 -0.001 -8 C 2.02 -8 4.228 -7.086 5.656 -5.657 C 7.085 -4.229 8 -2.021 8 0 Z " fill="#003251" stroke="#ebebeb" stroke-width="1" stroke-opacity="1" fill-opacity="1" visibility="visible" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" id="19"></path>
And the map looks like that:
The marked in red node is indicator for your current position on the zone/map. How can I re-position the svg group so the current player position to be always in the center of the container shifting the whole map to left/right/top/bottom and become like that:
Using the following function I am able to change x,y coordinates of the svg group:
function shiftMap(id_attr, offsetX, offsetY) {
let elem = $(id_attr);
if (elem) {
let params = ' translate(' + offsetX + ',' + offsetY + ')';
elem.attr('transform', params);
}
}
I also have a function to detect current player position node coordinates, so I tried to do:
//where playerPosX = X of the current player node location (red mark)
//and playerPosY = Y of the current player node location (red mark)
shiftMap("svgMapID", playerPosX, playerPosY);
Unfortunately the position is not accurate and map shifts/moves everywhere, goes out of the container bounds, etc. Any help is much appreciated. Thank you!
Hint: It may be possible to do via twojs anchor point, but I am not sure how. More info here: https://two.js.org/#two-anchor
You have to calculate bounding box of your svg-group and move the group, considering outer offsets of the group itself and inner offsets relative to your center node.
Let's say you want this new random node to be at center of your group, highlighted in red.
Then you calculate its X and Y related to the container. Let's say that X = 400, Y = 200.
Then you calculate the bounding box of all your map group related to the container.
In this example, Y1 is probably going to be 0, where X1 you have to find. Try using group.getBoundingClientRect(shallow), as described in Two.js docs. Let's say that 'X1 = 300', 'Y1 = 0'.
Having your container W(width) = 1000 and H(height) = 700, let's calculate your final coordinates:
finalX= (W/2) - (X + X1) = 500 - 700 =-200
finalY= (H/2) - (Y + Y1) = 350 - 200 =150
That means that you have to move your map group -200 by X (200 to the left) and 150 by Y (150 to the bottom)

Snap.svg APIs to help find the intersection point of a line and a quadratic curve

How to find the intersection point of a line and a quadratic curve?
Here is the code to generate the figure of choice:
var s = Snap(300, 300);
var path = s.path("M 35 50 h 100 v 50 q -25 -20 -50 0 q -25 20 -50 0 z")
path.attr({
fill:'none',
stroke: 'black'
});
var bbox = Snap.path.getBBox(path);
console.log(bbox);
var pbox = path.getBBox();
console.log(pbox);
s.circle(bbox.x, bbox.y, 3).attr('fill', 'red');
s.circle(bbox.x2, bbox.y2, 3).attr('fill', 'red');
s.circle(bbox.cx, bbox.cy, 3).attr('fill', 'magenta');
var l = s.line(bbox.cx, bbox.cy, 250, 200).attr('stroke', 'black');
var lbox = l.getBBox();
console.log(lbox);
The image looks like this:
I am trying to find the point highlighted by the blue circle.
Plunk: http://plnkr.co/edit/ZFo381tZfG4SHWHKyINZ?p=preview
Snap has a method Snap.path.intersection. So if you can use a path instead of a line, you could use that method. Just be aware that if the paths can change, there may be multiple or no intersections, so you may want to loop through the intersections, rather than just taking the first.
Changed bits of code...
var l = s.path('M'+bbox.cx+','+bbox.cy+'L250,200').attr({ stroke: 'black'})
var intersection = Snap.path.intersection( path.attr('d'), l.attr('d'))
s.circle( intersection[0].x, intersection[0].y,5 )
example

Is it possible to draw an svg path to the periphery of a circle [duplicate]

Short question: using SVG path, we can draw 99.99% of a circle and it shows up, but when it is 99.99999999% of a circle, then the circle won't show up. How can it be fixed?
The following SVG path can draw 99.99% of a circle: (try it below and see if you see 4 arcs or only 2 arcs, but note that if it is IE, it is rendered in VML, not SVG, but have the similar issue)
var paper = Raphael(0, 0, 300, 800);
// Note that there are supposed to be 4 arcs drawn, but you may see only 1, 2, or 3 arcs depending on which browser you use
paper.path("M 100 100 a 50 50 0 1 0 35 85").attr({stroke: "#080", opacity: 1, "stroke-width" : 6}) // this is about 62.5% of a circle, and it shows on most any browsers
paper.path("M 100 210 a 50 50 0 1 0 0.0001 0").attr({stroke: "#080", opacity: 1, "stroke-width" : 6}) // this one won't show anything if it is IE 8's VML, but will show if it is Chrome or Firefox's SVG. On IE 8, it needs to be 0.01 to show
paper.path("M 100 320 a 50 50 0 1 0 0.0000001 0").attr({stroke: "#080", opacity: 1, "stroke-width" : 6}) // this one won't draw anything at all, unless you change the 0.0000001 to 0.0001 on Chrome or Firefox... Safari will show it though...
paper.path("M 100 430 a 50 50 0 1 0 0 0").attr({stroke: "#080", opacity: 1, "stroke-width" : 6}) // this is 100% of a circle... even Safari won't show it
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
M 100 100 a 50 50 0 1 0 0.00001 0
But when it is 99.99999999% of a circle, then nothing will show at all?
M 100 100 a 50 50 0 1 0 0.00000001 0
And that's the same with 100% of a circle (it is still an arc, isn't it, just a very complete arc)
M 100 100 a 50 50 0 1 0 0 0
How can that be fixed? The reason is I use a function to draw a percentage of an arc, and if I need to "special case" a 99.9999% or 100% arc to use the circle function, that'd be kind of silly.
Again, a test case is above
(and if it is VML on IE 8, even the second circle won't show... you have to change it to 0.01)
Update:
This is because I am rendering an arc for a score in our system, so 3.3 points get 1/3 of a circle. 0.5 gets half a circle, and 9.9 points get 99% of a circle. But what if there are scores that are 9.99 in our system? Do I have to check whether it is close to 99.999% of a circle, and use an arc function or a circle function accordingly? Then what about a score of 9.9987? Which one to use? It is ridiculous to need to know what kind of scores will map to a "too complete circle" and switch to a circle function, and when it is "a certain 99.9%" of a circle or a 9.9987 score, then use the arc function.
I know it's a bit late in the game, but I remembered this question from when it was new and I had a similar dillemma, and I accidently found the "right" solution, if anyone is still looking for one:
<path
d="
M cx cy
m -r, 0
a r,r 0 1,0 (r * 2),0
a r,r 0 1,0 -(r * 2),0
"
/>
In other words, this:
<circle cx="100" cy="100" r="75" />
can be achieved as a path with this:
<path
d="
M 100, 100
m -75, 0
a 75,75 0 1,0 150,0
a 75,75 0 1,0 -150,0
"
/>
The trick is to have two arcs, the second one picking up where the first left off and using the negative diameter to get back to the original arc start point.
The reason it can't be done as a full circle in one arc (and I'm just speculating) is because you would be telling it to draw an arc from itself (let's say 150,150) to itself (150,150), which it renders as "oh, I'm already there, no arc necessary!".
The benefits of the solution I'm offering are:
it's easy to translate from a circle directly to a path, and
there is no overlap in the two arc lines (which may cause issues if you are using markers or patterns, etc). It's a clean continuous line, albeit drawn in two pieces.
None of this would matter if they would just allow textpaths to accept shapes. But I think they are avoiding that solution since shape elements like circle don't technically have a "start" point.
snippet demo:
circle, path {
fill: none;
stroke-width: 5;
stroke-opacity: .5;
}
circle {
stroke: red;
}
path {
stroke: yellow;
}
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
width="220px" height="220px">
<circle cx="100" cy="100" r="75" />
<path
d="
M 100, 100
m -75, 0
a 75,75 0 1,0 150,0
a 75,75 0 1,0 -150,0
"
/>
</svg>
Update:
If you are using the path for a textPath reference and you are wanting the text to render on the outer edge of the arc, you would use the exact same method but change the sweep-flag from 0 to 1 so that it treats the outside of the path as the surface instead of the inside (think of 1,0 as someone sitting at the center and drawing a circle around themselves, while 1,1 as someone walking around the center at radius distance and dragging their chalk beside them, if that's any help). Here is the code as above but with the change:
<path
d="
M cx cy
m -r, 0
a r,r 0 1,1 (r * 2),0
a r,r 0 1,1 -(r * 2),0
"
/>
Same for XAML's arc. Just close the 99.99% arc with a Z and you've got a circle!
In reference to Anthony’s solution, here is a function to get the path:
function circlePath(cx, cy, r){
return 'M '+cx+' '+cy+' m -'+r+', 0 a '+r+','+r+' 0 1,0 '+(r*2)+',0 a '+r+','+r+' 0 1,0 -'+(r*2)+',0';
}
A totally different approach:
Instead of fiddling with paths to specify an arc in svg, you can also take a circle element and specify a stroke-dasharray, in pseudo code:
with $score between 0..1, and pi = 3.141592653589793238
$length = $score * 2 * pi * $r
$max = 7 * $r (i.e. well above 2*pi*r)
<circle r="$r" stroke-dasharray="$length $max" />
Its simplicity is the main advantage over the multiple-arc-path method (e.g. when scripting you only plug in one value and you're done for any arc length)
The arc starts at the rightmost point, and can be shifted around using a rotate transform.
Note: Firefox has an odd bug where rotations over 90 degrees or more are ignored. So to start the arc from the top, use:
<circle r="$r" transform="rotate(-89.9)" stroke-dasharray="$length $max" />
Building upon Anthony and Anton's answers I incorporated the ability to rotate the generated circle without affecting it's overall appearance. This is useful if you're using the path for an animation and you need to control where it begins.
function(cx, cy, r, deg){
var theta = deg*Math.PI/180,
dx = r*Math.cos(theta),
dy = -r*Math.sin(theta);
return "M "+cx+" "+cy+"m "+dx+","+dy+"a "+r+","+r+" 0 1,0 "+-2*dx+","+-2*dy+"a "+r+","+r+" 0 1,0 "+2*dx+","+2*dy;
}
i made a jsfiddle to do it in here:
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
function describeArc(x, y, radius, startAngle, endAngle){
var start = polarToCartesian(x, y, radius, endAngle);
var end = polarToCartesian(x, y, radius, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y
].join(" ");
return d;
}
console.log(describeArc(255,255,220,134,136))
link
all you need to do is to change the input of console.log and get the result in console
For those like me who were looking for an ellipse attributes to path conversion:
const ellipseAttrsToPath = (rx,cx,ry,cy) =>
`M${cx-rx},${cy}a${rx},${ry} 0 1,0 ${rx*2},0a${rx},${ry} 0 1,0 -${rx*2},0 Z`
Adobe Illustrator uses bezier curves like SVG, and for circles it creates four points. You can create a circle with two elliptical arc commands...but then for a circle in SVG I would use a <circle /> :)
Written as a function, it looks like this:
function getPath(cx,cy,r){
return "M" + cx + "," + cy + "m" + (-r) + ",0a" + r + "," + r + " 0 1,0 " + (r * 2) + ",0a" + r + "," + r + " 0 1,0 " + (-r * 2) + ",0";
}
It's a good idea that using two arc command to draw a full circle.
usually, I use ellipse or circle element to draw a full circle.
Another way would be to use two Cubic Bezier Curves. That's for iOS folks using pocketSVG which doesn't recognize svg arc parameter.
C x1 y1, x2 y2, x y (or c dx1 dy1, dx2 dy2, dx dy)
The last set of coordinates here (x,y) are where you want the line to end. The other two are control points. (x1,y1) is the control point for the start of your curve, and (x2,y2) for the end point of your curve.
<path d="M25,0 C60,0, 60,50, 25,50 C-10,50, -10,0, 25,0" />
These answers are much too complicated.
A simpler way to do this without creating two arcs or convert to different coordinate systems..
This assumes your canvas area has width w and height h.
`M${w*0.5 + radius},${h*0.5}
A${radius} ${radius} 0 1 0 ${w*0.5 + radius} ${h*0.5001}`
Just use the "long arc" flag, so the full flag is filled. Then make the arcs 99.9999% the full circle. Visually it is the same. Avoid the sweep flag by just starting the circle at the rightmost point in the circle (one radius directly horizontal from the center).

Categories

Resources