How to use a transformed path in textPath? - svg

Consider following code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="2000" height="2000" viewBox="-1000 -1000 2000 2000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style>text { font-size: 60px; }</style>
<style>path { fill: none; stroke: black; }</style>
<style>textPath { text-anchor: middle; }</style>
<path d="M -258.8190, -965.9258 a 1000, 1000 0 0,1 517.6381, 0" id="ttO"/>
<use xlink:href="#ttO" transform="scale(.85)" id="ttI"/>
</defs>
<use xlink:href="#ttO"/>
<text dy="55"><textPath xlink:href="#ttO" startOffset="50%">Hello</textPath></text>
<use xlink:href="#ttI"/>
<text dy="25"><textPath xlink:href="#ttI" startOffset="50%">World!</textPath></text>
</svg>
... and the resulting image:
.
The explicitly defined path (ttO) and the textPath (Hello) using that path works fine. To define the smaller concentric arc (ttI), rather than calculating coordinates, I like to use transform. This works fine. However, I cannot use this second arc 'ttI' in the second textPath, at least Chrome doesn't render the second textPath. Is this because 'use' cannot be used to define new ids? What is the best way to do this?
If it was just this example, probably I can define one path halfway between two arcs and use that to draw textPaths with +/- offsets and draw the two arcs as transforms of the defied path. My goal is to extend the pattern I attempted to create more paths/textPaths at other angles / distances from the center with different languages to form a circular graphic. Without defining transformed paths, I have to explicitly specify at least one arc for each language.

This is how I would do it: I would create a text element using the same text path and I would scale the text - in this case transform="scale(.7,.7)". Also since I want the text to be the same size as the unscaled one I would use a different font size: 1em for the scaled text and .7em for the unscaled. Of coarse this is just an example. You can pick your font size making sure the font size for the scaled text is bigger than the font-size of the unscaled one by the same amount as the transformation.
Please observe that for the path I'm using vector-effect="non-scaling-stroke"so that that stroke appears the same in both the scaled and unscaled one.
svg{border:solid}
path{fill:none;stroke:black;}
<svg viewBox="-110 -110 220 220" >
<g>
<path id="path0" d="M90,0A90,90 0 0 1 -90,0A90,90 0 0 1 90,0" vector-effect="non-scaling-stroke"/>
<text font-size=".7em" >
<textPath xlink:href="#path0" startOffset="75%" text-anchor="middle">Hello World!</textPath>
</text>
</g>
<g transform="scale(.7,.7)">
<text font-size="1em" >
<textPath xlink:href="#path0" startOffset="75%" text-anchor="middle">Hello World!</textPath>
</text>
<use xlink:href="#path0"/>
</g>
</svg>

Related

SVG circle where the stroke width is bigger than the diameter

I have two identical paths, but stroked differently: https://jsfiddle.net/vzbdcupf/
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" width="300" height="300">
<style>
.s0 { fill: none; stroke: red ; stroke-width: 80 }
.s1 { fill: none; stroke: black; stroke-width: 4 }
</style>
<path class="s0" d="m100 100c20 7 33-1 36-25 2-13-71 13-36 25z"/>
<path class="s1" d="m100 100c20 7 33-1 36-25 2-13-71 13-36 25z"/>
</svg>
When I stroke the path with red, the stroke-width is huge, and there should not be any "hole" inside it. Why is there a hole?
I think it is related to the rendering algorithm (stroking is converted into filling paths, and the inner path gets "reflected"). But how do you explain it in terms of the SVG specification, to be able to say the rendering is correct?
Not an explanation why this happens but a possible workaround:
Some observations on the occurrence of this rendering:
appears on paths containing curve commands (c, s, q etc.)
won't appear on primitives like <circle>, <polygon> or paths using only line commands like l, h, v
Applying a dashed stroke seems to fix this rendering issue:
pathLength="100"
stroke-dasharray="100 0"
stroke-linecap="round"
Example:
function applyDash() {
const svg = document.querySelector('svg');
const paths = svg.querySelectorAll('path');
paths.forEach(function(path) {
path.setAttribute('pathLength', 100);
path.setAttribute('stroke-dasharray', '100 0');
path.setAttribute('stroke-linecap', 'round');
})
}
<p><button type="button" onclick="applyDash()"> Apply dash fix</button></p>
<svg xmlns="http://www.w3.org/2000/svg" width="300px" height="300px" viewBox="0 0 300 300">
<path fill="none" stroke="#FF1746" stroke-width="80" d="M58,100c10,3.5,18.3,3.3,24.4-0.9S92.5,87,94,75 c1-6.5-16.8-3.3-30.4,3.1S40.5,94,58,100z" />
<path fill="none" stroke="#FF1746" stroke-width="80" d="M211.6,78.1l-14.5,11.1L206,100l13.6,2.4l10.7-3.3
l7.5-9.1l4.1-15l-9.7-3.3L211.6,78.1L211.6,78.1z" />
<circle fill="none" stroke="#FF1746" stroke-width="80" cx="230" cy="232.5" r="9.8" />
<path fill="none" stroke="#FF1746" stroke-width="80" d="M77.1,232.5c0,5.4-4.4,9.8-9.8,9.8
s-9.8-4.4-9.8-9.8s4.4-9.8,9.8-9.8S77.1,227.1,77.1,232.5z" />
</svg>
Your guess as to what is happening is basically correct. When 2D graphics engines "stroke" a path, what they actually do is effectively convert the stroke into a filled path of its own. When the path that is being stroked has tight corners, larger stroke widths can cause the inside borders of the stroke path to overlap each other. The result is the same as if any path intersects with itself - it can cause holes. With actual fills, you can control how those holes are rendered with the fill-rule property. Unfortunately, there is no such property for strokes.
The SVG 2 specification briefly mentions this phenomenon here, but ultimately leaves it up to implementers how they want to deal this situation.

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>

Programmatically Fix text into viewBox

I have this svg:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
viewBox="0 0 ' + size + ' ' + size +'" width="'+ boxW +'" height="'+ boxH +'">
<text>Sample Text</text>
</svg>
size: A parameter that is needed to the viewBox in order to create the wrapper.
width & height: the width and height of the container of the text.
I have a function that generate this svg. The problem is that the text is not fitting into the box; the result is like this:
(Is blue due to the Chrome inspector, you can see up in the top-left corner the text being small instead of full size.
The SVG resulted is this:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
viewBox="0 0 580 532">
<text x="0" y="15" style="font-family:Arial;fill:%230000ff;fill-opacity:1;font-weight:normal;font-style:normal;"
>test</text>
</svg>
The whole img is this:
<img src="data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 580 532"><text x="0" y="15" style="font-family:Arial;fill:%230000ff;fill-opacity:1;font-weight:normal;font-style:normal;">test</text></svg>" class="leaflet-marker-icon leaflet-zoom-animated leaflet-interactive" alt="" tabindex="0" style="margin-left: -298px; margin-top: -291px; width: 309px; height: 295px; transform: translate3d(683px, 317px, 0px); z-index: 317; outline: none;">
So my quesiton is: How to fit the text into the main wrapper?
You either have to:
fit the viewBox to the text, or
fit the text to the viewBox.
You are not doing either. You are not even setting a font-size.
Option 1 is not really available to you. You can measure the text if you have access to the SVG DOM, but you can't do that if you aren't in a rendering environment, like a browser.
Perhaps you could use a font loading library to get metadata about the glyphs in the font. Then calculate the size of a piece of text that way. You don't mention which language you are using to produce these SVGs, so I can't advise further on that.
So you are left with option 2. The only option that SVG has to let you fit text to a particular size, is the textLength and lengthAdjust attributes on the <text> element.
textLength
Sets a length to which you want the text to be fitted
lengthAdjust
Sets the method to be used to adjust the length. You can either stretch just the spacing between the letters, or you can stretch the letter glyphs
See the <text> section in the spec for more information
There are no options for adjusting the text height.
svg {
width: 400px;
background-color: linen;
}
<svg viewBox="0 0 200 40">
<line x1="10" y1="30" x2="190" y2="30" stroke="black" opacity="0.2"/>
<text x="10" y="30"
textLength="180"
lengthAdjust="spacing">Sample Text</text>
</svg>
<br/>
<svg viewBox="0 0 200 40">
<line x1="10" y1="30" x2="190" y2="30" stroke="black" opacity="0.2"/>
<text x="10" y="30"
textLength="180"
lengthAdjust="spacingAndGlyphs">Sample Text</text>
</svg>
If you want the font size to be a better match, then you are going to have to work out a method of calculating an approximate font size. Eg.
var numChars = text.length()
var fontSize = (desiredTextWidth / numChars) * someScalingFactor
The scaling factor will depend on your font.
This is my solution: I'm putting the text inside a <symbol>. I get the size of the text with getBBox() and use it to set the viewBox for the <symbol>. Please note that the <use> element has a width of 100%.
let bb = text.getBBox();
test.setAttributeNS(null, "viewBox", `${bb.x} ${bb.y} ${bb.width} ${bb.height}`);
*{font-size:16px;}
svg{border:1px solid;width:90vh}
body{font-family:Arial;fill:#0000ff;fill-opacity:1;font-weight:normal;font-style:normal;}
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 580 532">
<symbol id="test">
<text id="text" dominant-baseline="central" text-anchor="middle" >test</text>
</symbol>
<use id="_use" xlink:href="#test" width="100%" />
</svg>

SVG or HTML text that scales to fully fit a container so it stretches, bot vertically and horizontally, disregarding aspect ratio

I need to make text automatically stretch in both dimensions, to fill a container. It will distort.
This shows the the container space in red
This shows what a long name would normally resize to put in that space and maintaining aspect ratio
.
This shows what my client wants to happen
.
I would prefer to use SVG but I will work with what works.
I have searched for a solution to the best of my abilities but all seem to either refer to maintaining aspect ratio or stretching text when the page or viewbox changes dimensions.
That's quite a broad question, but yes you can do it with svg, I'll let you implement it though since you didn't provided anything to chew on.
The key point is to set your svg's preserveAspectRatio to "none":
svg{
height: 100vh;
width: 50vw;
}
body{
margin:0;
}
<div>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 300 40" preserveAspectRatio="none">
<text x="0" y="35" font-family="Verdana" font-size="35">
Hello, out there
</text>
</svg>
</div>
If your text is already part of an SVG (as it appears in your example), you will probably need to use a nested <svg> element.
<svg width="400" height="400">
<rect width="400" height="400" fill="rebeccapurple"/>
<!-- rect representing area that our text has to squeeze into -->
<rect x="20" y="50" width="200" height="50" fill="white"/>
<!-- x y width height match above rect -->
<!-- viewBox values need to match text bounds -->
<svg x="20" y="50" width="200" height="50"
viewBox="0 8 244 28" preserveAspectRatio="none">
<text x="0" y="35" font-family="Verdana" font-size="35">
HELLO THERE
</text>
</svg>
</svg>
The hardest part is workoing out the correct values for viewBox. It needs to match the bounds of the (normal unsqueezed) text.

Multiple preserveAspectRatio in svg?

I'm looking to have an svg fill a particular space in my layout, so preserveAspectRatio="none" seems like a good first approach.
However, within the svg, there is a mask that I do not want to stretch / warp. Rather, it should occupy 100% the width, with the height scaling according to its ratio. The two images illustrate the mask's behaviour when the parent svg is in either landscape or portrait. (Note: the grey in the image is the rest of the <svg>, which should stretch to fit)
Can the mask have its own aspectRatio settings? Is there a better way to achieve this? Or, is it even possible?
```
<!-- this should scale according to its bounding parent -->
<svg class="fix" viewbox="0 0 2880 1620" preserveAspectRatio="none..?">
<!-- this should scale according to some intrinsic ratio -->
<mask id="mask"
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse">
<rect fill="white" x="0" y="0" width="2880" height="1620" />
<path fill="black" d="M57.59,60h409c344.17,.... ...."/>
</mask>
<rect mask="url(#mask)" x="0" y="0" width="100%" height="100%" />
</svg>
```
edit: using mask-image instead of just mask seems possible (as it has additional positioning options), but this does not seem to work with svg elements.
You don't need to use preserveAspectRatio="none" to have the rectangle and mask fill the page. Just extend the <rect> and <mask> past the boundaries of the SVG in all directions. Root <svg> elements have overflow: visible by default, so the extended rect will fill SVGs parent container - as long as you extend far enough of course.
<rect mask="url(#mask)" x="-1000%" y="-1000%" width="3000%" height="3000%" />
I've used 1000% here, but you can adjust that if you need more or less (than 10x).
And note that we are just using the standard default SVG preserveAspectRatio. So we still get the automatic centerinng and scaling of the SVG.
body {
margin: 0;
padding: 0;
}
svg {
width: 100vw;
height: 100vh;
}
<svg viewbox="0 0 2880 1620">
<!-- this should scale according to some intrinsic ratio -->
<mask id="mask"
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse"
x="-1000%" y="-1000%" width="3000%" height="3000%">
<rect fill="white" x="-1000%" y="-1000%" width="3000%" height="3000%" />
<circle cx="1440" cy="810" r="400" fill="black"/>
</mask>
<rect mask="url(#mask)" x="-1000%" y="-1000%" width="3000%" height="3000%" />
</svg>
Demo in fiddle format

Resources