SVG textpath text layout on complex, changing paths - svg

I am not sure the problem I have is solvable using the current SVG standard but I thought I would ask here anyway if anyone knows an answer
I have a constantly changing svg path (vertices defined by forming a hull around nodes made in d3, force driven so the nodes constantly move and the bounding hull moves to accomodate the nodes)
Because I can't predict the vertices nor do I know what the text will be (as it depends on the grouping of the nodes in that situation, which changes) all i can do is blindly apply text on a textpath to the path. The problemis sometimes the text does not display nicely.
Problem 1: upside down text - I don't mind where on the path the text goes but its annoying that it often ends up upside down
For example (image):
[NB Problem 2 branched into SVG textpath rendering breaks words up badly on a textpath with sharp corners as suggested in answer]
Problem 2: broken up text - when a corner forms, text has a tendency to split. up. I don't think my use of dy to push the text outside the boundary helps (the path is actually tight to the nodes and I apply a 40 stroke-width to give some padding: the dy pushed the text outside that stroke)
For example (image):
Any ideas on what I can do to fix this?
--Chris
svg code for reference:
Problem 1:
<g id="hull_elements">
<path class="boundary" id="Secure" d="M219.31353652066463,309.7274362305448L199.3259715998452,277.60331505353355L54.5215284230899,92.9756148805194L29.418010605669316,64.72387260525474Z" style="fill: #b0c4de; stroke: #b0c4de; stroke-width: 40px; stroke-linejoin: round;"></path>
<path class="boundary" id="DMZ" d="M234.7675515627913,79.25604751762172L122.76947855325542,190.1418483839412L271.90702281166267,76.40758102069142Z" style="fill: #b0c4de; stroke: #b0c4de; stroke-width: 40px; stroke-linejoin: round;"></path>
</g>
<g id="hull_text">
<text dy="30"><textPath startOffset="0" text-anchor="start" method="align" spacing="auto" xlink:href="#Secure">Secure</textPath></text>
<text dy="30"><textPath startOffset="0" text-anchor="start" method="align" spacing="auto" xlink:href="#DMZ">DMZ</textPath></text>
</g>
Problem 2:
<g id="hull_elements"><path class="boundary" id="Secure" d="M30.716331539726912,88.02778447495649L66.8405337274694,100.01086904278971L251.78816229874747,53.214214251587265L277.8704519199028,25.642491075146587Z" style="fill: #b0c4de; stroke: #b0c4de; stroke-width: 40px; stroke-linejoin: round;"></path>
<path class="boundary" id="DMZ" d="M177.8575710153683,149.56053657599713L251.04637461899244,245.55658992744486L277.76418020025847,271.7261370009561L159.53295211932644,118.0340968521715Z" style="fill: #b0c4de; stroke: #b0c4de; stroke-width: 40px; stroke-linejoin: round;"></path>
</g>
<g id="hull_text">
<text dy="30"><textPath startOffset="0" text-anchor="start" method="align" spacing="auto" xlink:href="#Secure">Secure</textPath></text>
<text dy="30"><textPath startOffset="0" text-anchor="start" method="align" spacing="auto" xlink:href="#DMZ">DMZ</textPath></text>
</g>
jsfiddle to play with that shows this (move the nodes to see the issues)
http://jsfiddle.net/zuzzy/GC2C2/
[edited to add the NB of the branch of problem 2 - zuzzy]

For problem 1 I think you need to detect when the x co-ordinates are moving to the left and draw the path back to front in that case.
If you have
M 0,0 L 100, 0
that's OK 100 > 0 so leave it as it is. But
M 100, 0 L 0,0
has 0 < 100 so that would need reversing. In this case reversing would give us the path in the first case.
Here's a complete example.
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="MyPath"
d="M 300 200
L 100 200" />
<path id="MyPathReversed"
d="M 100 200
L 300 200" />
</defs>
<desc>Example toap01 - simple text on a path</desc>
<g transform="translate(0, 100)">
<text font-family="Verdana" font-size="42.5" fill="blue" >
<textPath xlink:href="#MyPath">
upside down
</textPath>
</text>
</g>
<text font-family="Verdana" font-size="42.5" fill="blue" >
<textPath xlink:href="#MyPathReversed">
right way up
</textPath>
</text>
</svg>
BTW I suggest you ask problem 2 as a separate question.

Related

Reference SVG text element in path coordinates

Is it possible to programmatically reference the extents of a text bounding box while creating a path? For example, depending on the chosen font, scale, and glyphs, a specific text might be larger or smaller. I always want, for example, a path drawn exactly under the text. Or over the text (not using the text-decoration: underline/overline attributes, but by using a path). Alternatively, if I need to guess the width of the text, is it possible to at least center a path horizontally relative to a text block without knowing its exact extents?
I played around with the "50%" attributes, but those always seem to be relative to the page, not individual SVG elements.
Example:
<svg height="200" width="300">
<text x="0" y="50" style="fill: red">Blue line on top, green line on right</text>
<path d="m 0 0 h 50" style="stroke-width: 2px; stroke: blue" />
<path d="m 50 0 v 15" style="stroke-width: 2px; stroke: green" />
</svg>
How can the path of the blue line be made to be exactly on the top boundingbox, the green line to be exactly on the righthand side of the text element?
Maybe real SVG gurus can chime in and create exactly what you want.
The JavaScript/Web Component is not required, I just did not want to copy/paste SVG code
customElements.define("svg-text-border", class extends HTMLElement{
connectedCallback(){
setTimeout(()=>{ // wait till innerHTML is parsed
this.innerHTML =
`<svg viewbox="0 0 220 30">
<path d="M10 20h200" stroke="red" pathLength="100" id="R" />
<path d="M210 20v-15" stroke="green"/>
<path d="M210 5h-200" stroke="blue"/>
<text textLength="90%">
<textPath href="#R" startoffset="100" text-anchor="end">
${this.innerHTML.trim()}
</textPath>
</text>
</svg>`;
});
}
})
<style>svg{ max-height:60px }</style>
<svg-text-border>The quick brown fox jumps over the lazy dog</svg-text-border>
<svg-text-border>Hello World!</svg-text-border>
<svg-text-border>Web Components are Cool!</svg-text-border>

Positioning rotated letters inside viewBox in svg

I have code which generates svg (by means of producing the XML DOM). It takes input text and randomly scatters it's letters on page as shown below.
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- ... styles omitted -->
  <text x="20" y="150" rotate="45">M</text>
<text x="100" y="80" rotate="45">W</text>
<text x="90" y="50" rotate="270">X</text>
<!-- etc ... -->
</svg>
I have problem how to fill the page efficiently without having letters extend outside of the view box. Either I limit the random values for the x and y, but then there is lot of space left empty around borders. Or I use wider ranges for x,y but then the letters bleed outside. As show the red letters in the jsfiddle example (https://jsfiddle.net/5zqrugx1/1/).
I tried to adjust the x, y ranges based on rotation, but still it does not help much because of different letter shapes.
I am looking for way to style/position these letters in svg in a way which would force them to be completely inside the view port while being able to fill the space border-to-border (this second condition added later to clarify). Something like giving 0-100% where 0% would mean "touching left border" and 100% would be "touching right border". Is there any way to do it?
Below is example which I hand-edited to achieve more-less desired result.
What this probably amounts to is controling the center of rotation in such a way that it is in the center of the glyph. This way, all you need is a 0.5em padding at each edge.
You can start out with positioning the text control point at the middle both horizontally and vertically:
text {
text-anchor:middle;
dominant-baseline:middle;
}
Unfortunately, using the rotate attribute of the text element does not work as expected (at least in Firefox). But you can get around that by adding a post-rotation via a transform attribute. The best way to formulate it would be to also position the glyph with a translation:
<text transform="translate(40 100) rotate(60)">A</text>
Order is important - translate must come before rotate.
The following example rotates all glyphs around the center of the circles they are sitting in. It turns out the font-defined middle is a bit off, so you have to tweak with a dy attribute. If it shows still a bit wrong on your screen, this is because the font used by your system might define or compute a different middle line. For a system-independent experience, you would need to use a web font for you to have complete control.
circle {
fill: none;
stroke: blue;
}
text {
text-anchor: middle;
dominant-baseline: middle;
font-family: sans-serif;
font-size: 50px;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350 100">
<circle r="25" cx="50" cy="50" />
<circle r="25" cx="100" cy="50" />
<circle r="25" cx="150" cy="50" />
<circle r="25" cx="200" cy="50" />
<circle r="25" cx="250" cy="50" />
<circle r="25" cx="300" cy="50" />
<text dy="4" transform="translate(50 50) rotate(60)">A</text>
<text dy="4" transform="translate(100 50) rotate(120)">B</text>
<text dy="4" transform="translate(150 50) rotate(180)">C</text>
<text dy="4" transform="translate(200 50) rotate(240)">D</text>
<text dy="4" transform="translate(250 50) rotate(300)">E</text>
<text dy="4" transform="translate(300 50) rotate(360)">F</text>
</svg>

How to trace one edge of an open path in SVG

I am trying to make a web page that allows the user to draw lines in an SVG image. The drawing part is fine, but each line needs to carry a label that fills the width of the line (the lines are 15px wide).
I have tried to use a <textpath> referencing the line they drew, but the baseline of the label ends up running down the middle of the line. Here is a screenshot to show what I mean.
I have tried various ways to nudge the text over slightly using CSS and properties, but the only success I have had is to use a transform, which will often result in the text 'spilling out' if the direction of the line takes a sudden turn.
The other solution I have tried is to generate a second path that runs down one edge of the user-drawn path and using that for the <textpath>, but I'm struggling to find a way to translate the user-drawn path points into points that correspond to the rendered edge of the line.
Does anybody know a way to make either one of these methods work?
I understand that the lines need to carry a label that fills the width of the line (the lines are 15px wide).
In order to move the text I use dy="4"
text{fill:white;stroke:none;font-family:consolas;}
path{stroke-width:15px;fill:none;}
<svg viewBox="50 150 350 150">
<defs>
<path id="path" d="M70,180Q100,330 195,225Q290,120 380,250"></path>
</defs>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path" stroke="#000000"></use>
<text stroke="#000000" font-size="12" dy="4">
<textPath id="tp" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path" startOffset="30%">
just some words I wrote
</textPath>
</text>
</svg>
An other option is using dominant-baseline="middle"
text{fill:white;stroke:none;font-family:consolas;}
path{stroke-width:15px;fill:none;}
<svg viewBox="50 150 350 150">
<defs>
<path id="path" d="M70,180Q100,330 195,225Q290,120 380,250" ></path>
</defs>
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path" stroke="#000000"></use>
<text stroke="#000000" font-size="12" dominant-baseline="middle">
<textPath id="tp" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#path" startOffset="30%">
just some words I wrote
</textPath>
</text>
</svg>
I hope this is what you were asking.
You can use the dy attribute to move glyphs in a string - either individually or together - in a vertical direction relative to their orientation.
The spec chapter on <tspan> elements has a lot of practical examples on how to use the various positioning attributes (dx, dy, rotate); I'd recomend to read it.
path {
fill:none;
stroke: red;
stroke-width: 15px;
}
text {
font-family: sans-serif;
font-size: 20px;
}
<svg>
<path id="p1" d="M 25,60 60,30 H 80 V 120" />
<text dy="-7.5px">
<textPath href="#p1">abcdefghijklmn</textPath>
</text>
</svg>

svg draw circle with curved text inside

I need to draw red cirle with two curved string inside like that:
upper string always be 3 chars length
lower string can be from 1 to 20 chars length
UPDATE1:
I try to use textpath and circle tags, but I think I need to change some coordinates:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="40" cy="40" r="24" style="stroke:#006600; fill:none"/>
<defs>
<path id="myTextPath"
d="M75,20
a1,1 0 0,0 150,0"
/>
</defs>
<text x="5" y="50" style="stroke: #000000;">
<textPath xlink:href="#myTextPath" >
string
</textPath>
</text>
</svg>
Also I didnt clear understand <path> 'd' atrribute , but I found out that I can change starting point to M10,20 but how I can change text curve orientation?
d="M10,20 a1,1 0 0,0 150,0"
To have text that "hangs" from a line nicely, the best way right now is to use a path with a smaller radius. There is an attribute to adjust the text's baseline, but that doesn't work reliably.
So you need two arcs. One for the bottom half of the circle, and one with a smaller radius for the top half. They also need to both start from the left. That means one will go clockwise, and the other will go anti-clockwise. You control that with the arc command's "sweep" flag.
We need to also use startOffset="50%" and text-anchor="middle" to centre the text on the paths.
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 80 80">
<defs>
<path id="tophalf" d="M26,40 a14,14 0 0,1 28,0" />
<path id="lowerhalf" d="M16,40 a24,24 0 0,0 48,0" />
</defs>
<circle cx="40" cy="40" r="24" style="stroke:#006600; fill:none"/>
<path d="M16,40 a24,24 0 0,0 48,0" style="stroke:#600; fill:none"/>
<text x="5" y="50" style="stroke: #000000;"
text-anchor="middle">
<textPath xlink:href="#tophalf" startOffset="50%">str</textPath>
</text>
<text x="5" y="50" style="stroke: #000000;"
text-anchor="middle">
<textPath xlink:href="#lowerhalf" startOffset="50%">second st</textPath>
</text>
</svg>
This works fine in FF, but unfortunately, it seems there are bugs in Chrome and IE right now that is causing the text to not be centred properly on those browsers.

If two partially opaque shapes overlap, can I show only one shape where they overlap?

For example if you have a simple "spy glass" shape made of a circle and a rectangle and the outlines of both are partially transparent, can you stop the opacity effectively being decreased where the two shapes overlap?
You can use a filter to tweak the opacity value. Say, both shapes have an opacity value of .5, then you want to make the area where both overlap to have an opacity value of .5 as well.
<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px">
<filter id="constantOpacity">
<feComponentTransfer>
<!-- This transfer function leaves all alpha values of the unfiltered
graphics that are lower than .5 at their original values.
All higher alpha above will be changed to .5.
These calculations are derived from the values in
the tableValues attribute using linear interpolation. -->
<feFuncA type="table" tableValues="0 .5 .5" />
</feComponentTransfer>
</filter>
<line x2="300" y2="300" stroke="black" stroke-width="10"/>
<path d="M0 150h300" stroke="blue" stroke-width="10"/>
<g filter="url(#constantOpacity)">
<rect x="50" y="50" width="150" height="150" opacity=".5" fill="green" id="rect1"/>
<rect width="150" height="150" opacity=".5" fill="red" id="rect2"
x="100" y="100"/>
</g>
</svg>
You can see that adding the filter lets the background shine through so to say with constant intensity. However, the color of the shapes gets a paler, more grayish appearance (unless both colors are identical). Maybe you can go with a compromise, reducing the alpha value slightly less with a tableValues attribute like 0 .5 .75 instead of 0 .5 .5.
If the shape is constructed as a single SVG path element, the overlaps don't sum up into a darker result.
If the shape is constructed of multiple SVG elements, the overlaps do sum up into a darker result.
<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px">
<!-- No summing here -->
<g>
<path d="M10,10 L100,100 M10,100 L100,10" style="stroke: #000000; stroke-width: 10px; opacity: 0.5" />
</g>
<!-- Summing here -->
<g>
<path d="M200,200 L290,290" style="stroke: #000000; stroke-width: 10px; opacity: 0.5" />
<path d="M200,290 L290,200" style="stroke: #000000; stroke-width: 10px; opacity: 0.5" />
</g>
</svg>

Resources