SVG - create path for textpath dynamically? - svg

I want to have a curved text along a path (half circle) in SVG. I have followed this tutorial, which is great: https://css-tricks.com/snippets/svg/curved-text-along-path/
The problem is that the path presented there works only for this specific text - Dangerous Curves Ahead. If you leave only Dangerous word that's what happens: https://codepen.io/anon/pen/pqqVGa - it no longer works (the text is no more evenly spreaded across the path).
I want to have it work regardless of text length. How to achieve that?

Using the attributes lengthAdjust and textLength you can adjust the length of the text and the height of the letters, thereby placing the text of the desired length on a segment of a fixed length
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="1%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
Using the attribute startOffset =" 10% " you can adjust the position of the first character of the phrase
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300" >
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="400" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="15%" xlink:href="#path1">
<tspan dy="-10"> very long text very long text very long text </tspan>
</textPath>
</text>
</svg>
and make animation using this attribute (click canvas)
<svg id="svg1" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="300" viewBox="0 0 500 300">
<path id="path1" fill="none" stroke="black" d="M30,151 Q215,21 443,152 " />
<text id="txt1" lengthAdjust="spacingAndGlyphs" textLength="200" font-size="24">
<textPath id="result" method="align" spacing="auto" startOffset="-100%" xlink:href="#path1">
<tspan dy="-10"> Very long text Very long text Very long text </tspan>
<animate
begin="svg1.click"
dur="15s"
attributeName="startOffset"
values="-100%;1%;1%;100%;1%;1%;-100%"
repeatCount="5"/>
</textPath>
</text>
<text x="200" y="150" font-size="24" fill="orange" >Click me </text>
</svg>

This is assuming that the initial text size (35) is too small.
let curveLength = curve.getTotalLength();
let fs = 35;//the initial font size
test.setAttributeNS(null, "style", `font-size:${fs}px`)
while(test.getComputedTextLength() < curveLength){
fs++
test.setAttributeNS(null, "style", `font-size:${fs}px`)
}
body {
background-color: #333;
}
text {
fill: #FF9800;
}
<svg viewBox="0 0 500 500">
<path id="curve" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous
</textPath>
</text>
</svg>
UPDATE
The OP is commenting:
Thanks for the response. Instead of adjusting the font size, I would prefer to create a new path that is longer / smaller and matches the text width. Not sure how to do this tho. – feerlay
Please read the comments in the code. In base of the length of the text I'm calculating the new path, but I'm assuming a lot of things: I'm assuming the new path starts in the same point as the old one.
let textLength = test.getComputedTextLength();
// the center of the black circle
let c = {x:250,y:266}
// radius of the black circle
let r = 211;
// the black arc starts at point p1
let p1 = {x:73.2,y:150}
// the black arc ends at point p2
let p2 = {x:426.8,y:150}
// distance between p1 and p2
let d = dist(p1, p2);
// the angle of the are begining at p1 and ending at p2
let angle = Math.asin(.5*d/r);
// the radius of the new circle
let newR = textLength / angle;
// the distance between p1 and the new p2
let newD = 2 * Math.sin(angle/2) * newR;
// the new attribute c for the path #curve
let D = `M${p1.x},${p1.y} A`
D += `${newR}, ${newR} 0 0 1 ${p1.x + newD},${p1.y} `
document.querySelector("#curve").setAttributeNS(null,"d",D);
// a function to calculate the distance between two points
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
body {
background-color: #333;
}
text {
fill: #FF9800;
};
<svg viewBox="0 0 500 500">
<path id="black_circle" d="M73.2,148.6c4-6.1,65.5-96.8,178.6-95.6c111.3,1.2,170.8,90.3,175.1,97" />
<path id ="curve" d="M73.2,150 A 211,211 0 0 1 426.8,150" fill="#777" />
<text id="test">
<textPath xlink:href="#curve">
Dangerous curves
</textPath>
</text>
</svg>

Related

Better way to add dashed border to a path element in an svg [duplicate]

This question already has an answer here:
Declaratively stroke a line with a composite line symbol using SVG
(1 answer)
Closed 7 months ago.
Is there a better solution to add to a path element a dashed border (lets say the border should have an offset of 2px in each direction)
I am looking for a general solution for a lot of path elements
For example my initial path element would be
<path stroke="black" fill="none" d="M10 10 L 50 10 L 50 80 L 10 80 Z"></path>
and at the moment I am creating another path element to add the border around the initial path element
<path stroke="black" stroke-dasharray="3" fill="none" d="M6 6 L 54 6 L 54 84 L 6 84 Z"></path>
<svg height="1000" width="1000">
<path stroke="black" fill="none"
d="M10 10 L 50 10 L 50 80 L 10 80 Z"></path>
<path stroke="black" stroke-dasharray="3"
fill="none" d="M6 6 L 54 6 L 54 84 L 6 84 Z"></path>
</svg>
"a general solution for a lot of path elements" is pretty vague. I'll handle simple closed paths in this answer.
This way may actually be more of an example how not to do it, but I think it shows a general problem with what you try to achieve. It uses only one place to define a path, and then re-uses it in three other places:
first, to draw the inner border,
then, to draw a much wider dashed border,
and finally, as a mask to hide that part of the dashed border that would otherwise overlap the inner one.
This has the advantage of not having to create extra paths, but the dashed border looks strange. The corners either show gaps or exta-long dashes, and in curved sections the length of the dashes are differing.
.distance {
stroke-width: 6;
}
.inner {
fill:none;
stroke: black;
stroke-width: 2;
}
.outer {
stroke: black;
stroke-width: 10;
stroke-dasharray: 4;
stroke-dashoffset: 2;
}
<svg viewBox="0 0 100 100" width="200" height="200">
<defs>
<path id="src" d="M 32,13 20,42 Q 30,90 85,90 L 92,53 Q 60,53 60,13 Z" />
<mask id="mask">
<rect width="100%" height="100%" fill="white" />
<use class="distance" href="#src" stroke="black" />
</mask>
</defs>
<use class="inner" href="#src" />
<use class="outer" href="#src" mask="url(#mask)" />
</svg>
Why is that so? The dashes are computed in relation to where the original path is, but what is shown is only the outer fringe of the whole stroke, at an offset. (or to put it the other way round, the path defining where dashes start and end is at an offset from the middle of the shown dashed line.) For concave sections, dashes get longer, and for convex sections, shorter.
The only way the dash length can be stable is when the path used to compute dashes sits in the middle of the dashes. You could change the order around and define the dashes on the inner border:
.distance {
stroke-width: 6;
}
.inner {
fill:none;
stroke: black;
stroke-width: 2;
stroke-dasharray: 4;
}
.outer {
stroke: black;
stroke-width: 10;
}
<svg viewBox="0 0 100 100" width="200" height="200">
<defs>
<path id="src" d="M 32,13 20,42 Q 30,90 85,90 L 92,53 Q 60,53 60,13 Z" />
<mask id="mask">
<rect width="100%" height="100%" fill="white" />
<use class="distance" href="#src" stroke="black" />
</mask>
</defs>
<use class="inner" href="#src" />
<use class="outer" href="#src" mask="url(#mask)" />
</svg>
..but that is as far as you get. The bottom line remains: you need to have a path where the dashed line is, not at an offset.
Use a native JavaScript Web Component <svg-outline> (you define once)
to do the work on an <svg>
<svg-outline>
<svg viewBox="0 0 100 100" height="180">
<path outline="blue" fill="none" d="M5 5 L 50 30 L 50 40 L 10 80 Z"/>
</svg>
</svg-outline>
The Web Component clones your original shapes (marked with "outline" attribute)
sets a stroke and stroke-dasharray on it
removes any existing fill
transforms clone
translates clone to account for scale(1.2)
scales clone to 1.2 size
then corrects translate
SO snippet output:
See JSFiddle: https://jsfiddle.net/WebComponents/2goahcqv/
<svg-outline>
<style> circle[outline] { stroke: blue } </style>
<svg viewBox="0 0 100 100" height="180">
<rect outline="green" x="15" y="15" width="50%" height="50%" stroke="blue" fill="teal"/>
<circle outline fill="lightcoral" cx="50" cy="50" r="10"/>
</svg>
</svg-outline>
<svg-outline>
<svg viewBox="0 0 100 100" height="180">
<path outline="blue" fill="pink" d="M15 10 L 50 30 L 50 40 L 20 70 Z"/>
</svg>
</svg-outline>
<script>
customElements.define("svg-outline", class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML is parsed
let svg = this.querySelector("svg");
svg.querySelectorAll('[outline]').forEach(original => {
let outlined = svg.appendChild(original.cloneNode(true));
original.after(outlined); // so we don't create "z-index" issues
let outline_stroke = outlined.getAttribute("outline") || false;
if (outline_stroke) outlined.setAttribute("stroke", outline_stroke );
original.removeAttribute("outline"); // so we can use CSS on outlines
let {x,y,width,height} = original.getBBox();
let cx = x + width/2;
let cy = y + height/2;
outlined.setAttribute("fill", "none"); // outlines never filled
outlined.setAttribute("stroke-dasharray", 3); // or read from your own attribute, like "outline"
outlined.setAttribute("transform", `translate(${cx} ${cy}) scale(${1.2}) translate(-${cx} -${cy})`);
});
// (optional) whack everything into shadowDOM so styles don't conflict
this.attachShadow({mode:"open"}).append(...this.children);
})
}
})
</script>

How to center text in an SVG region

I'm attempting to center the text in my SVG region but am not succeeding. Does anyone have any insight on how to optimize this?
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Usa Mo" viewBox="0 0 70.389999 62.16">
<g>
<path tabindex="0" aria-label="Sainte Genevieve" fill="red" id="sainte genevieve" name="Sainte Genevieve"
d="m 57.466419,33.749401
2.854,2.605
-1.578,2.754
-0.463,-0.343
-0.441,0.418
-1.953,-1.762
-0.824,-0.796
1.293,-1.417
-0.982,-0.73
1.582,-1.112
0.512,0.383"
> </path>
<text font-family="Verdana" font-size="0.75" fill="blue">
<textPath href="#sainte genevieve" startOffset="50%" text-anchor="middle">Sainte Genevieve</textPath>
</text>
</g>
</svg>
The OP is commenting:
I just want the text to be inside the colored region and right side up.
In this case you don't need to use a textPath. You need to get the center of the path. For this you first get the bounding box of the path: let bb = thePath.getBBox()
Next you get the center of the path:
let x = bb.x + bb.width/2;
let y = bb.y + bb.height/2;
Finally you set the x and y attributes of the text as x and y making shure that the text is centered around this point: text-anchor="middle" dominant-baseline="middle"
let bb = thePath.getBBox();
theText.setAttribute("x", bb.x + bb.width/2);
theText.setAttribute("y", bb.y + bb.height/2);
svg{border:1px solid; width:300px}
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Usa Mo" viewBox="50 30 15 12">
<path id="thePath" aria-label="Sainte Genevieve" fill="red" id="sainte_genevieve" name="Sainte Genevieve"
d="m 57.466419,33.749401
2.854,2.605
-1.578,2.754
-0.463,-0.343
-0.441,0.418
-1.953,-1.762
-0.824,-0.796
1.293,-1.417
-0.982,-0.73
1.582,-1.112
0.512,0.383"
> </path>
<text id="theText" font-family="Verdana" font-size=".75" fill="blue" text-anchor="middle" dominant-baseline="middle">
Sainte Genevieve
</text>
</svg>
Observation: I've changed the viewBox of the svg element because I wanted to have thePath in the center. You can change it back to what it was.

SVG path hops on intersection in Leaflet map

Having a SVG path, what would be the easiest SVG way to draw hops on intersections, to make paths crossing each other more UX friendly? Both intersections with other paths, and within the path itself.
Something like this:
or
Computing intersections and drawing each path segment separately is an option, but I'm afraid about impact on performance, because the SVG path is drawn internally by Leaflet polyline, and there can be many polylines on the map.
In the first SVG canvas I'm using an other wider white line to mark the intersection. In the second I'm using javascript to calculate the intersection and I'm drawing a white circle to mark it. The formula for the intersecting lines is from Intersection point of two line segments in 2 dimensions - written by Paul Bourke
function Intersect(p1,p2,p3,p4){
var denominator = (p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y - p1.y);
var ua = ((p4.x - p3.x)*(p1.y - p3.y) - (p4.y - p3.y)*(p1.x - p3.x))/denominator;
var ub = ((p2.x - p1.x)*(p1.y - p3.y) - (p2.y - p1.y)*(p1.x - p3.x))/denominator;
var x = p1.x + ua*(p2.x - p1.x);
var y = p1.y + ua*(p2.y - p1.y);
if(ua > 0 && ua < 1 && ub > 0 && ub < 1){
return {x:x,y:y};
}else{return false;}
}
let p1={x:50,y:90}
let p2={x:50,y:10}
let p3={x:10,y:50}
let p4={x:90,y:50}
let _int = Intersect(p1,p2,p3,p4);
int.setAttributeNS(null,"cx", _int.x);
int.setAttributeNS(null,"cy", _int.y);
svg{border:1px solid; width:60vh}
line{stroke-linecap:round;}
.white{stroke:#fff;stroke-width:6}
.black{stroke:#000;stroke-width:2}
<svg viewBox="0 0 100 100">
<defs>
<line id="l1" x2="50" y2="10" x1="50" y1="90" />
<line id="l2" x1="50" y1="10" x2="10" y2="50" />
<line id="l3" x1="10" y1="50" x2="90" y2="50" />
</defs>
<use xlink:href="#l1" class="black" />
<use xlink:href="#l3" class="white" />
<use xlink:href="#l2" class="black" />
<use xlink:href="#l3" class="black" />
</svg>
<svg viewBox="0 0 100 100">
<use xlink:href="#l1" class="black" />
<use xlink:href="#l2" class="black" />
<circle id="int" cx="0" cy="0" r="3" fill="white" />
<use xlink:href="#l3" class="black" />
</svg>

Label on pie chart slice using textPath not displaying

I'm writing an Amcharts plugin that puts a pie chart slice label on the slice's path. I do this by adding IDs to the slice <path>, moving the <text> in the label inside a <textPath> that references that <path>. The output looks correct but the text is not visible. It doesn't seem to be a browserism because several SVG validators do the same thing. Any idea why the <textPath> isn't being displayed?
I manipulate the chart data like so:
var chart = event.chart;
var div = chart.div;
var divId = div.id;
var chartData = chart.chartData;
chart.container.container.setAttribute("xmlns","http://www.w3.org/2000/svg");
chart.container.container.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
for(var i = 0; i < chartData.length; i++) {
if(chartData[i].dataContext.wrapLabel) {
/**
* Create an ID for the <path> that the label will wrap onto
*/
chartData[i].wedge.node.firstChild.setAttribute("id",divId + "-" + i);
// create the textPath element and set its href to the id we added to the path
var n = document.createElement('textPath');
n.setAttribute("xlink:href","#" + divId + "-" + i);
// Now move all of the tspan nodes underneath the textPath node
while(chartData[i].label.node.hasChildNodes()) {
n.appendChild(chartData[i].label.node.firstChild);
}
// and then append the textPath node to the <text> node
chartData[i].label.node.appendChild(n);
}
}
This produces the following SVG:
<svg version="1.1" style="position: absolute; width: 1000px; height: 1000px; top: -0.457382px; left: -0.002841px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g opacity="1" aria-label=": 24.38% 84 " visibility="visible" transform="translate(0,0)">
<path cs="1000,1000" d=" M450.03754782613004,498.06263767618003 L375.0938695653251,495.1565941904501 A125,125,0,0,1,499.99999999999994,375 L500,450 A50,50,0,0,0,450.03754782613004,498.06263767618003 Z" fill="#741010" stroke="#000" stroke-width="2" stroke-opacity="1" fill-opacity="1" id="chart2-0"></path>
</g>
<g visibility="visible" transform="translate(0,0)" opacity="1">
<text y="5" fill="#fff" font-family="Verdana" font-size="9px" opacity="1" text-anchor="middle" transform="translate(552,554)" style="cursor: default;" visibility="visible">
<textpath xlink:href="#chart2-0">
<tspan y="5" x="0">$260.5B</tspan>
<tspan y="16" x="0">Satellite</tspan>
<tspan y="27" x="0">Industry</tspan>
</textpath>
</text>
</g>
</svg>
(Edited down for brevity.)
You'd be wanting
n.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href","#" + divId + "-" + i);
because you can only set attributes in the null namespace with setAttribute.
Nor can you set a namespace with setAttribute so I'm not sure what you're trying to achieve with
chart.container.container.setAttribute("xmlns","http://www.w3.org/2000/svg");
chart.container.container.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
The fact that the textPath element is entirely lower case in the output suggests that something has gone wrong in its creation. Either you incorrectly wrote it in lower case or you've created it (and possibly all your other elements) in the wrong namespace.
See the MDN article for more details about namespaces.

SVG circle with diameter = odd number

I would like to draw a circle with it's diameter being an odd number (7px), so I can draw a 1px line exactly through the center of the circle.
<svg width="600" height="600">
<circle cx="30" cy="30" r="3.5" fill="red" />
</svg>
Strangely, the diameter is always even, no matter what is r value.
Shift the cx and cy coordinates by .5:
<svg width="600" height="600">
<circle cx="30.5" cy="30.5" r="3.5" fill="red" />
</svg>

Resources