I am trying to change "d" attribute of all lines in certain SVG map to make straigh lines curved.
d="M514 222L488 66"
Is there any universal algorithm to change any straigt line "d" attribute (like this one above) and get curved line as result?
This is how I would do it: For the curve (a quadratic Bézier curve Q) I need to calculate the position of the control point. In this case I want the control point in the middle of the line at a distance R.
Please read the comments in the code to understand it.
// a variable to define the curvature
let R = 50;
// the points of the original line
let linePoints = [
{x:514,y:222},
{x:488,y:66}
]
//the length of the line
let length = thePath.getTotalLength();
//a point in the middle of the line
let point = thePath.getPointAtLength(length/2);
// calculate the angle of the line
let dy = linePoints[1].y - linePoints[0].y;
let dx = linePoints[1].x - linePoints[0].x;
let angle = Math.atan2(dy,dx);
let cp = {//control point for the bézier as a perpendicular line to thePath
x:point.x + R*Math.cos(angle + Math.PI/2),
y:point.y + R*Math.sin(angle + Math.PI/2)
}
//the new d attribute for the path
let d = `M${linePoints[0].x}, ${linePoints[0].y} Q${cp.x},${cp.y} ${linePoints[1].x}, ${linePoints[1].y}`;
//set the new d attribute
thePath.setAttributeNS(null,"d",d)
svg {
border: 1px solid;
width: 100vh;
}
path {
stroke: black;
fill: none;
}
<svg viewBox = "300 0 400 300">
<path id="thePath" d="M514, 222L488, 66" />
</svg>
Related
The essence of the problem is that when I move an SVG element around, the widths of the lines varies.
I'm working in a browser environment and I want crisp lines (no anti-aliasing), so shape-rendering is set to crispEdges. For example, you might have
"d M 0 0 L 10 0 L 10 10 L 0 10 z"
to define a square, with style set to
'fill: none; stroke: black; stroke-width: 1px; shape-rendering: crispEdges'
Then use translate() to drag it around with the mouse. It works, but as the square moves, the thickness of the lines jumps around by a pixel, and I want the thickness to remain constant.
I think I know why it's happening, but I can't find any way to prevent it. It happens (I think) due to the way the coordinates relative to the viewBox are mapped to coordinates relative to the physical monitor's pixels. When a one pixel wide (in SVG terms) line is mapped to the monitor, it may or may not straddle several pixels, so it may end up a (screen) pixel thicker or thinner.
I've tried messing with the ratio of the viewBox size to the size of the drawing area, rounding the translation amount to be a whole number or to take the form integer + 0.50, but no solution like this seems likely to work because it comes down to the level of zoom in the browser. vector-effect: non-scaling-stroke doesn't fix it either.
Is there a way to tell SVG to treat all paths as 1d geometric shapes, and to use a particular pen for all of them? Put another way, I want the pen used to draw everything to "scale with the zoom", rather than having the lines scale after they've been drawn.
Please, vanilla JS only.
You can test what I mean by running the code below. Click on the square and drag it around (no touch devices). Note that at some levels of browser zoom, it may behave nicely and not do what I've described.
There's something odd going on. The svg element (the square) starts out looking uniform, with all edges of the same width. It only moves in response to a mousemove, and that event (presumably) happens when the mouse moves by a whole (never fractional) screen pixel. Therefore, one would expect the square to move by a whole screen pixel and remain aligned to the screen pixels if it started off that way.
Follow-up...The problem is not with the screen CTM. I tested that the inverse really is an inverse; that is, CTM x CTM^{-1} is the identity for every value I tried. And the values that matrixTransform(getScreenCTM().inverse()) provides are linear in the inputs (or as linear as floating-point numbers allow).
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svgtest"
style = "border: 1px solid; display: block; margin-left: auto; margin-right: auto; shape-rendering: crispEdges;"
width = "400" height = "400" viewBox = "0 0 100 100" >
</svg>
<script>
var theSquare = {
x : 10,
y : 10
};
var initialSquare = {
x : 10,
y : 10
};
var SideLength = 10;
var initialX = 0;
var initialY = 0;
var draggingSquare = 0;
function pointInRect(p, r) {
return p.x > r.xLeft && p.x < r.xRight && p.y > r.yTop && p.y < r.yBottom;
}
function mouseToLocal(theEvent) {
var screenPt = svgArea.createSVGPoint();
screenPt.x = theEvent.clientX;
screenPt.y = theEvent.clientY;
var svgPt = screenPt.matrixTransform(svgArea.getScreenCTM().inverse());
return {
x : svgPt.x,
y : svgPt.y
};
}
function doMouseDown(event) {
var localCoord = mouseToLocal(event);
initialX = localCoord.x;
initialY = localCoord.y;
var pt = {x: initialX, y: initialY};
var rect = {xLeft: theSquare.x, xRight: theSquare.x + SideLength,
yTop: theSquare.y, yBottom: theSquare.y + SideLength};
if (pointInRect(pt,rect))
{
draggingSquare = 1;
initialSquare.x = theSquare.x;
initialSquare.y = theSquare.y;
}
else
draggingSquare = 0;
}
function doMouseUp(event) {
draggingSquare = 0;
}
function doMouseMove(event) {
if (draggingSquare == 0)
return;
var localCoord = mouseToLocal(event);
zeroTranslate.setTranslate(initialSquare.x + localCoord.x - initialX,
+initialSquare.y + localCoord.y - initialY);
theSquare.x = initialSquare.x + localCoord.x - initialX;
theSquare.y = initialSquare.y + localCoord.y - initialY;
}
var svgArea = document.getElementById('svgtest');
var theSVGSquare = document.createElementNS("http://www.w3.org/2000/svg", 'path' );
theSVGSquare.setAttributeNS(null, "d", "M 0 0" +
" L " + SideLength + " 0" +
" L " + SideLength + " " + SideLength +
" L 0 " +SideLength +
" z");
theSVGSquare.setAttributeNS(null, 'style', 'fill: none; stroke: black; stroke-width: 0.5px; shape-rendering: crispEdges' );
var tforms = theSVGSquare.transform;
var zeroTranslate = svgArea.createSVGTransform();
zeroTranslate.setTranslate(initialSquare.x,initialSquare.y);
theSVGSquare.transform.baseVal.insertItemBefore(zeroTranslate,0);
svgArea.appendChild(theSVGSquare);
svgArea.addEventListener("mousedown",doMouseDown,false);
svgArea.addEventListener("mouseup",doMouseUp,false);
svgArea.addEventListener("mousemove",doMouseMove,false);
</script>
</body>
</html>
I'm posting this "answer" as a way to officially close the question, and to thank Robert Longson for his attention.
In short, what I want to do can't be done in a browser, which is surprising since all I want to do is draw a line. Using the HTML canvas tag produces something blurry due to anti-aliasing, and SVG produces jittery lines. It comes down to the fact that the browser doesn't provide the information needed to work at a device-pixel level of accuracy.
I posted a discussion of this problem, with examples, on my personal website:
Canvas drawing is blurry and SVG is jittery
I have a set of points through which I have drawn a path.
let path=svg.append("path").attr("id","path")
.data([points])
.attr("d", d3.line()
.curve(d3.curveCatmullRom));
I now want to get the distance of the path between the points so that I can break it into segments. I have tried incrementing the distance and checking if the points (rounded off to 5) match with that of the initial set of points and thereby getting the distance whenever there is a match. I then save the points until there as a list.
Here, xyArray has the list of points to which I append d and the list seg as well.
function distanceBetween2Points(path, xyArray) {
let distance = 0;
xyArray.forEach(function(d,i)
{
let flag=1;
seg=[];
while(flag)
{let pt=path.getPointAtLength(distance);
if(round5(pt.x)==round5(d.x) && round5(pt.y)==round5(d.y))
{console.log("d",i);
d.d=distance;
d.seg=seg;
flag=0;
break;}
seg.push([pt.x,pt.y]);
distance++;}
return 0;
});
}
It sometimes works (even though not accurately) but sometimes does not, depending on the data. Is there a better way to get the distance?
This is a demo using vanilla javascript not d3 but I hope you'll find it useful.
The function getLengthForPoint(p,thePath)is calculating the distance on the path for a given point p.
I'm setting a variable let precision = 100;. Depending on the length of the path you may want to change this value to something else.
Also keep in mind that a path can pass through the same point multiple times. This can be tricky and can give you an error.
Also as you may know you will get the approximated distance to a point. In this example the point p1 = {x:93.5,y:60}. The point at the calculated length has this coords: {x:93.94386291503906,y: 59.063079833984375}
// some points on the path
let p1 = {x:93.5,y:60}
let p2 = {x:165,y:106}
//the total length of the path
let pathLength = thePath.getTotalLength();
let precision = 100;
let division = pathLength / precision;
function getLengthForPoint(p,thePath){
let theRecord = pathLength;
let theSegment;
for (let i = 0; i < precision; i++) {
// get a point on the path for thia distance
let _p = thePath.getPointAtLength(i * division);
// get the distance between the new point _p and the point p
let theDistance = dist(_p, p);
if (theDistance < theRecord) {
// if the distance is smaller than the record set the new record
theRecord = theDistance;
theSegment = i;
}
}
return(theSegment * division);
}
let theDistanceOnThePath = getLengthForPoint(p1,thePath);
//if you calculate the coords of a point at the calculated distance you'll see that is very near the point
console.log(thePath.getPointAtLength(theDistanceOnThePath));
let theDistanceBetween2PointsOnThePath = getLengthForPoint(p2,thePath) - getLengthForPoint(p1,thePath);
// a helper function to measure the distance between 2 points
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
svg{border:solid}
<svg viewBox="0 10 340 120">
<path id="thePath" fill="none" stroke="black" d="M10, 24Q10,24,40,67Q70,110,93.5,60Q117,10,123.5,76Q130,142,165,106Q200,70,235,106.5Q270,143, 320,24"></path>
<circle cx="93.5" cy="60" r="2" fill="red"/>
<circle cx="165" cy="106" r="2" fill="red"/>
</svg>
To get the distance between 2 points on the path you can do:
let theDistanceBetween2PointsOnThePath = getLengthForPoint(p2,thePath) - getLengthForPoint(p1,thePath);
I made two triangle divs with html.
Im using jquery Ui to make them draggable.
Now I want to overlap these two triangles and color the overlaying part with another color like this:
Do you have any clues how I can do this with jQuery?
$(document).ready(function() {
$(".triangle").draggable();
})
#triangle_1 {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid #00ff00;
}
#triangle_2 {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class = "triangle"
id = "triangle_1" > </div>
<div class="triangle" id="triangle_2"></div >
This is my working solution with rectangles instead of triangles (link).
The jQuery API has no method to find the intersection of 2 triangularly-styled DIVs.
You can find your "red intersecting triangle" using mathematics:
Part I: Find the vertex points of your red polygon
Find the 3 vertex points of both triangle-divs.
For both triangles, use #1 points to find the find the 3 line segments that form the triangle's sides.
Find each intersection point (if any) of each triangle-A line segment with each triangle-B line segment. See the utility below on how to find these intersections.
If you found exactly 3 intersection points in #3 above then the 2 original triangles intersect to form a new triangle -- then continue to Part II...
Part II: Create a triangle polygon from the 3 intersection points found in Part I
Find the "centerpoint" of your new triangle. This is the arithmetic mean of its x's and y's: centerX = (vertex1.x + vertex2.x + vertex3.x)/3 and centerY = (vertex1.y + vertex2.y + vertex3.y)/3
Calculate all the angles from the centerpoint to each of your user's points. You can do this using Math.atan2( (vertex1.y-centerY), (vertext1.x-centerX) ) ... and the same for vertex2 & vertex3.
Sort the points in ascending order by their angles calculated in #2.
This set of 3 points are the vertices of your red triangle.
Part III: Style a new red div into a triangle using the vertices from Part II
Utilities
Here's a javascript function that finds the intersection of 2 line segments (if any):
// Get interseting point of 2 line segments (if any)
// Attribution: http://paulbourke.net/geometry/pointlineplane/
function line2lineIntersection(p0,p1,p2,p3) {
var unknownA = (p3.x-p2.x) * (p0.y-p2.y) - (p3.y-p2.y) * (p0.x-p2.x);
var unknownB = (p1.x-p0.x) * (p0.y-p2.y) - (p1.y-p0.y) * (p0.x-p2.x);
var denominator = (p3.y-p2.y) * (p1.x-p0.x) - (p3.x-p2.x) * (p1.y-p0.y);
// Test if Coincident
// If the denominator and numerator for the ua and ub are 0
// then the two lines are coincident.
if(unknownA==0 && unknownB==0 && denominator==0){return(null);}
// Test if Parallel
// If the denominator for the equations for ua and ub is 0
// then the two lines are parallel.
if (denominator == 0) return null;
// If the intersection of line segments is required
// then it is only necessary to test if ua and ub lie between 0 and 1.
// Whichever one lies within that range then the corresponding
// line segment contains the intersection point.
// If both lie within the range of 0 to 1 then
// the intersection point is within both line segments.
unknownA /= denominator;
unknownB /= denominator;
var isIntersecting=(unknownA>=0 && unknownA<=1 && unknownB>=0 && unknownB<=1)
if(!isIntersecting){return(null);}
return({
x: p0.x + unknownA * (p1.x-p0.x),
y: p0.y + unknownA * (p1.y-p0.y)
});
}
I've got 4 arcs of a circle which are generated using SnapSVG.js
The problem is that between the upper arcs the gap is higher than the gap of lower arcs
So my question is can I fix the gap between arcs and obtain a perfect circle using only CSS to cosistency with resize?
This is a raw JS [I intend to clean it a bit]
http://jsfiddle.net/LtLafp2r/
var canvasSize = 200,
centre = canvasSize/2,
radius = canvasSize*0.8/2,
s = Snap('#svg'),
path = "",
arc = s.path(path),
startY = centre-radius;
var d = 0;
var dr =0;
radians = Math.PI*(dr)/180,
endx = centre + radius*Math.cos(radians),
endy = centre + radius * Math.sin(radians),
largeArc = d>180 ? 1 : 0;
var s = Snap("#svg");
// Lets create big circle in the middle:
path = "M"+centre+","+startY+" A"+radius+","+radius+" 0 "+largeArc+",1 "+endx+","+endy;
var arc = s.path(path);
// By default its black, lets change its attributes
arc.attr({
stroke: '#3da08d',
fill: 'none',
strokeWidth: 25
});
Maybe I'm misunderstanding but using this CSS does a perfect circle with your arcs :
svg{position:fixed;}
#svg3{left:-72px; top:88px;}
#svg4{left:88px; top:88px;}
http://jsfiddle.net/LtLafp2r/3/
Ps: there is a bug in rendering path arcs in Chrome : check this question
I've got a Raphael.js image that I've done, which consists of a set of circles, like so
var paper = Raphael(0,0,100,100);
var circle1 = paper.circ(20,20,10);
var circle2 = paper.circ(40,40,10);
I also have an svg icon in Raphael format (thanks to the lovely site http://readysetraphael.com/) that I'd like to place inside of each circle. Problem is... all the paths in the converted svg icon are now relative to the point (0,0) ! By this is mean all the strings are written like this
paper.path('M 1 1 L 2 0 L 0,2 z')
So my question ... is there some clever way to 'relativise' this path to make it sit inside each circle, without just going in and changing every single element of the path string by hand to make it draw the same path twice inside the two circles?
Try this. Replace the contents of shp with any other valid path.
var shape,
circleHalfWidth,
circleHalfHeight,
shpHalfHeight,
shpHalfWidth,
targetLeft,
targetTop,
paper,
circle1,
circBBox,
shpBBox,
shp,
radius = 20,
b2,
c2,
b,
s;
shape = "M25.979,12.896,5.979,12.896,5.979,19.562,25.979,19.562z";
paper = new Raphael(0,0,500,500);
circle1 = paper.circle(100,100,radius);
shp = paper.path( shape );
// get objects that contain dimensions of circle and shape
circBBox = circle1.getBBox( );
shpBBox = shp.getBBox( );
// get dimensions that will help us calculate coordinates to centre of circle
circleHalfWidth = circBBox.width / 2;
circleHalfHeight = circBBox.height / 2;
shpHalfWidth = shpBBox.width / 2;
shpHalfHeight = shpBBox.height / 2;
// calculate coordinates to position shape on top left corner of circle
targetLeft = circle1.getBBox( ).x - shp.getBBox( ).x;
targetTop = circle1.getBBox( ).y - shp.getBBox( ).y;
//Calculate how wide is shape allowed to be in circle using pythagoras
c2 = Math.pow( radius, 2 );
b2 = c2 / 2;
b = Math.sqrt( b2 );
// Calculate ratio so that both height and width fit in circle
s = shpBBox.width > shpBBox.height ? ( shpBBox.width / b ) : ( shpBBox.height / b );
// change coordinates so shape will be moved to centre of circle
targetLeft += circleHalfWidth - shpHalfWidth;
targetTop += circleHalfHeight - shpHalfHeight;
// Remember uppercase T so that scaling is ignored when moving shape
shp.transform( "s" + s + "T" + targetLeft + "," + targetTop );
fiddle here