How to vertically center a character within a svg path? - html

I have an svg path with a text and a character like this:
<svg id="svg1" style="overflow:visible">
<path id="path1" stroke="black" d="M 0 50 L 100 50" stroke-width="10" />
<text text-anchor="middle" dy="-30" >
<textPath href="#path1" startOffset="50%" fill="red">Shape</textPath>
<textPath href="#path1" startOffset="50%" fill="red" dominant-baseline="central" >
<tspan font-size="30" dy="-5">⬤</tspan>
</textPath>
</text>
</svg>
And I want the circle (or any character in general) to align perfectly at the center of the line regardless of font-size or any other attribute
I tried changing dy but it's really isn't a universal solution.
Here's what it looks like for different browsers:
for font-size="50" in firefox
for font-size="50" in Brave
for font-size="10" in firefox & Brave
So how do I universally align the character to the vertical center for any given style and/or attribute?
Edit
as mentioned in the comments, I tried this solution but it really doesn't solve the problem for varying font-size nor does it perfectly align to the center cross browser
<svg id="svg1" style="overflow:visible">
<path id="path1" stroke="black" d="M 0 50 L 100 50" stroke-width="10" />
<text text-anchor="middle" dy="-30" >
<textPath href="#path1" startOffset="50%" fill="red">Shape</textPath>
<textPath href="#path1" startOffset="50%" fill="red" dominant-baseline="central"
alignment-baseline="central">
<tspan font-size="30" dy="-5">⬤</tspan>
</textPath>
</text>
</svg>

Unfortunately, there is no reliable way to center text vertically.
Firefox and chrome will interpret dominant-baseline quite differently.
For a consistent rendering, you could use some javaScript.
This approach also requires to measure some character/glyph proportions to calculate an ideal baseline shift.
So we need to get a ratio between font size and character height.
E.g write a capital in Arial at 100 points in inkscape, Illustrator etc. and convert it to paths/outlines and check it's height: 71.582 pt
So the capital to font-size ratio is: 100/71.582 = 0.71582
Example 1: emulate dominant-baseline:central; font-size 10 and 20
<style>
svg{
height:90vmin;
border:1px solid #ccc
}
</style>
<svg viewBox="0 0 50 20">
<line x1="0" x2="100%" y1="50%" y2="50%" stroke="#000" stroke-width="10" /></line>
<text font-size="10" style="font-family:Arial" x="0" y="50%" fill="red" dy="3.5791">I⬤</text>
<text font-size="20" style="font-family:Arial" x="20" y="50%" fill="red" dy="7.1582">I⬤</text>
</svg>
The dy values are calculated like so:
10 (1. font-size) * 0.71582 (capheight ratio) / 2 = 3.5791
20 (2. font-size) * 0.71582 (capheight ratio) / 2 = 7.1582
The bad news: you need to get different ratios for each type of character e.g lowercase letters.
Albeit, it might be enough to check the x-height for lowercase characters.
Example 2: save ratios to data-attribute; calculate dy via js
let texts = document.querySelectorAll('text');
texts.forEach(function(text){
let offsetRatio = parseFloat(text.getAttribute('data-offsetratio'));
let style = window.getComputedStyle(text);
let fontSize = parseFloat(style.fontSize);
let dy = fontSize * offsetRatio /2;
text.setAttribute('dy', dy );
})
svg{
width:100%;
border:1px solid #ccc
}
<svg id="svg1" style="overflow:visible" viewBox="0 30 100 40">
<path id="path1" stroke="black" d="M 0 50 L 100 50" stroke-width="10" />
<path id="path2" stroke="#fff" d="M 0 50 L 100 50" stroke-width="0.25" />
<g font-size="10" style="font-family:Arial,Georgia,'Segoe UI'">
<text data-offsetratio="0.71582" x="0" y="50" fill="red">I⬤</text>
<text data-offsetratio="0.51855" x="15" y="50" fill="red">x</text>
</g>
<g font-size="25" style="font-family:Arial,Georgia,'Segoe UI'">
<text data-offsetratio="0.71582" x="30" y="50" fill="red">I⬤</text>
<text data-offsetratio="0.56409" x="60" y="50" fill="red">●</text>
<text data-offsetratio="0.70081" x="75" y="50" fill="red">•</text>
</g>
</svg>

Vertically aligning a text is done using dominant-baseline, but you can see from this example that it is not easy to do. Here I align ⬤Gga using the values middle and central.
I added a viewBox to <svg> so that it is easier to control the position. But I guess it all depends on the font used.
<svg overflow="hidden" viewBox="0 0 100 65" width="250">
<rect width="100" height="65" fill="#eee" />
<path id="path1" stroke="black" d="M 0 50 L 100 50" stroke-width="10" />
<text text-anchor="middle" dy="-30" fill="red">
<textPath href="#path1" startOffset="50%">Shape</textPath>
<textPath href="#path1" startOffset="50%" dominant-baseline="middle" font-size="30">⬤Gga</textPath>
</text>
</svg>
<svg overflow="hidden" viewBox="0 0 100 65" width="250">
<rect width="100" height="65" fill="#eee" />
<path id="path1" stroke="black" d="M 0 50 L 100 50" stroke-width="10" />
<text text-anchor="middle" dy="-30" fill="red">
<textPath href="#path1" startOffset="50%">Shape</textPath>
<textPath href="#path1" startOffset="50%" dominant-baseline="central" font-size="30">⬤Gga</textPath>
</text>
</svg>

Related

SVG text not horizontally center aligning

I generated a SVG using Adobe XD. They use transform for positioning things but the text in my mini computer screen is not always the same width (it is dynamically generated). I have tried anchored, anything I could find but it still didn't work. This is how it looks with the current code:
Here is the code:
<svg xmlns="http://www.w3.org/2000/svg" width="903.5" height="860.5" viewBox="0 0 1200 1041">
<g transform="translate(-397)">
<g
transform="translate(507 975)"
fill="#fff"
stroke="#707070"
strokeWidth="1"
>
<rect width="907" height="66" rx="33" stroke="none" />
<rect x="0.5" y="0.5" width="906" height="65" rx="32.5" fill="none" />
</g>
<rect width="119" height="395" transform="translate(901 613)" fill="#fff" />
<g
transform="translate(397)"
fill="#232323"
stroke="#fff"
stroke-width="30"
>
<rect width="1127" height="627" rx="103" stroke="none" />
<rect x="15" y="15" width="1097" height="597" rx="88" fill="none" />
</g>
<text
fill="white"
fontSize="96"
fontFamily="Fredoka"
>
{screenText}
</text>
</g>
</svg>
You can use transform/translate, text-anchor and dominant-baseline to place a text in the middle of something.
body {
background: gray;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1041">
<g
transform="translate(600 975)"
fill="#fff"
stroke="#707070"
stroke-width="1">
<rect x="-453.5" width="907" height="66" rx="33" stroke="none" />
<rect x="-454" y="0.5" width="906" height="65" rx="32.5" fill="none" />
</g>
<rect width="119" height="395" transform="translate(545.5 613)" fill="#fff" />
<g transform="translate(50)"
fill="#232323"
stroke="#fff"
stroke-width="30">
<rect width="1127" height="627" rx="103" stroke="none" />
<rect x="15" y="15" width="1097" height="597" rx="88" fill="none" />
</g>
<text
fill="#fff"
font-size="96"
font-family="Fredoka"
transform="translate(600 300)"
text-anchor="middle"
dominant-baseline="middle">
{screenText}
</text>
</svg>
Thanks to Buhan Yu's comment I learned that you need to specify x and y to center align it. I set x="50%" and it worked!

Unable to make text bold in SVG

I have something like this:
.speed-description {
font-size: calculate-rem(9px);
}
<svg #speedAnimation class="{{ speedAnimationClass }}" viewBox="-10 50 140 80" preserveAspectRatio="xMinYMax meet">
<path class="loader" d="m 0 120 a 1 1 0 0 1 120 0" fill="none" stroke-linecap="round" stroke-width="11" />
<text text-anchor="middle" x="60" y="110" font-weight="900" ></text>
<text text-anchor="middle" x="60" y="120" font-weight="bold" class="speed-description">
{{ speedDescription }}
</text>
</svg>
Whatever I try I cannot make the 2 texts in bold.
What am I doing wrong?
Currently it's something like this
As I've commented: in SVG you can add a stroke to the text. This will make the text look like bold. You can use a stroke-width to set it as bold as you need.
text{stroke:black; stroke-width:.5}
<svg #speedAnimation class="speedAnimationClass" viewBox="-10 50 140 80" preserveAspectRatio="xMinYMax meet">
<text text-anchor="middle" x="60" y="100" >some text</text>
<text text-anchor="middle" x="60" y="120" class="speed-description">speed Description</text>
</svg>
Alternatively you can use <feMorphology> for this. In this case you will need to play with the value of the radius attribute of the <feMorphology>
<svg #speedAnimation class="speedAnimationClass" viewBox="-10 50 140 80" preserveAspectRatio="xMinYMax meet">
<filter id="dilate">
<feMorphology operator="dilate" radius=".5"/>
</filter>
<text text-anchor="middle" x="60" y="100" filter="url(#dilate)" >some text</text>
<text text-anchor="middle" x="60" y="120" class="speed-description" filter="url(#dilate)">speed Description</text>
</svg>

SVG rect as background to text [duplicate]

I want to color the background of svg text similar to background-color in css
I was only able to find documentation on fill, which colors the text itself
Is it even possible?
You could use a filter to generate the background.
<svg width="100%" height="100%">
<defs>
<filter x="0" y="0" width="1" height="1" id="solid">
<feFlood flood-color="yellow" result="bg" />
<feMerge>
<feMergeNode in="bg"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<text filter="url(#solid)" x="20" y="50" font-size="50">solid background</text>
</svg>
No this is not possible, SVG elements do not have background-... presentation attributes.
To simulate this effect you could draw a rectangle behind the text attribute with fill="green" or something similar (filters). Using JavaScript you could do the following:
var ctx = document.getElementById("the-svg"),
textElm = ctx.getElementById("the-text"),
SVGRect = textElm.getBBox();
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", SVGRect.x);
rect.setAttribute("y", SVGRect.y);
rect.setAttribute("width", SVGRect.width);
rect.setAttribute("height", SVGRect.height);
rect.setAttribute("fill", "yellow");
ctx.insertBefore(rect, textElm);
The solution I have used is:
<svg>
<line x1="100" y1="100" x2="500" y2="100" style="stroke:black; stroke-width: 2"/>
<text x="150" y="105" style="stroke:white; stroke-width:0.6em">Hello World!</text>
<text x="150" y="105" style="fill:black">Hello World!</text>
</svg>
A duplicate text item is being placed, with stroke and stroke-width attributes. The stroke should match the background colour, and the stroke-width should be just big enough to create a "splodge" on which to write the actual text.
A bit of a hack and there are potential issues, but works for me!
Instead of using a <text> tag, the <foreignObject> tag can be used, which allows for XHTML content with CSS.
No, you can not add background color to SVG elements. You can do it programmatically with d3.
var text = d3.select("text");
var bbox = text.node().getBBox();
var padding = 2;
var rect = self.svg.insert("rect", "text")
.attr("x", bbox.x - padding)
.attr("y", bbox.y - padding)
.attr("width", bbox.width + (padding*2))
.attr("height", bbox.height + (padding*2))
.style("fill", "red");
Answer by Robert Longson (#RobertLongson) with modifications:
<svg width="100%" height="100%">
<defs>
<filter x="0" y="0" width="1" height="1" id="solid">
<feFlood flood-color="yellow"/>
<feComposite in="SourceGraphic" operator="xor"/>
</filter>
</defs>
<text filter="url(#solid)" x="20" y="50" font-size="50"> solid background </text>
<text x="20" y="50" font-size="50">solid background</text>
</svg>
and we have no bluring and no heavy "getBBox" :)
Padding is provided by white spaces in text-element with filter.
It's worked for me
Going further with #dbarton_uk answer, to avoid duplicating text you can use paint-order=stroke style:
<svg>
<line x1="100" y1="100" x2="350" y2="100" style="stroke:grey; stroke-width: 100"/>
<text x="150" y="105" style="stroke:white; stroke-width:0.5em; fill:black; paint-order:stroke; stroke-linejoin:round">Hello World!</text>
</svg>
Note the stroke-linejoin:round which is needed to avoid seeing spikes for the W sharp angle.
You can combine filter with the text.
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>SVG colored patterns via mask</title>
</head>
<body>
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter x="0" y="0" width="1" height="1" id="bg-text">
<feFlood flood-color="white"/>
<feComposite in="SourceGraphic" operator="xor" />
</filter>
</defs>
<!-- something has already existed -->
<rect fill="red" x="150" y="20" width="100" height="50" />
<circle cx="50" cy="50" r="50" fill="blue"/>
<!-- Text render here -->
<text filter="url(#bg-text)" fill="black" x="20" y="50" font-size="30">text with color</text>
<text fill="black" x="20" y="50" font-size="30">text with color</text>
</svg>
</body>
</html>
this is my favorite hack (not sure it should work). It refer an element that is not yet displayed, and it works pretty well
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 620 40" preserveAspectRatio="xMidYMid meet">
<defs>
<filter x="-0.02" y="0" width="1.04" height="1.1" id="removebackground">
<feFlood flood-color="#00ffff"/>
</filter>
</defs>
<!--Draw the text-->
<use xlink:href="#mygroup" filter="url(#removebackground)" />
<g id="mygroup">
<text id="text1" x="9" y="20" style="text-anchor:start;font-size:14px;">custom text with background</text>
<line x1="200" y1="18" x2="200" y2="36" stroke="#000" stroke-width="5"/>
<line x1="120" y1="27" x2="203" y2="27" stroke="#000" stroke-width="5"/>
</g>
</svg>
For those wondering how to apply padding to a text element when it has a background like in the Robert's answer, do the following:
<svg>
<defs>
<filter x="-0.1" y="-0.1" width="1.2" height="1.2" id="solid">
<feFlood flood-color="#171717"/>
<feComposite in="SourceGraphic" operator="xor" />
</filter>
</defs>
<text filter="url(#solid)" x="20" y="50" font-size="50">Hello</text>
</svg>
In the example above, filter's x and y positions can be used as transform: translate(-10%, -10%) would, and width and height values can be read as 120% and 120%. So we made background 20% bigger, and offsetted it -10%, so background is now 10% bigger on each side of the text.
The previous answers relied on doubling up text and lacked sufficient whitespace.
By using atop and I was able to get the results I wanted.
This example also includes arrows, a common use case for SVG text labels:
<svg viewBox="-105 -40 210 234">
<title>Size Guide</title>
<defs>
<filter x="0" y="0" width="1" height="1" id="solid">
<feFlood flood-color="white"></feFlood>
<feComposite in="SourceGraphic" operator="atop"></feComposite>
</filter>
<marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z"></path>
</marker>
</defs>
<g id="garment">
<path id="right-body" fill="none" stroke="black" stroke-width="1" stroke-linejoin="round" d="M0 0 l30 0 l0 154 l-30 0"></path>
<path id="right-sleeve" d="M30 0 l35 0 l0 120 l-35 0" fill="none" stroke-linejoin="round" stroke="black" stroke-width="1"></path>
<use id="left-body" href="#right-body" transform="scale(-1,1)"></use>
<use id="left-sleeve" href="#right-sleeve" transform="scale(-1,1)"></use>
<path id="collar-right-top" fill="none" stroke="black" stroke-width="1" stroke-linejoin="round" d="M0 -6.5 l11.75 0 l6.5 6.5"></path>
<use id="collar-left-top" href="#collar-right-top" transform="scale(-1,1)"></use>
<path id="collar-left" fill="white" stroke="black" stroke-width="1" stroke-linejoin="round" d="M-11.75 -6.5 l-6.5 6.5 l30 77 l6.5 -6.5 Z"></path>
<path id="front-right" fill="white" stroke="black" stroke-width="1" d="M18.25 0 L30 0 l0 154 l-41.75 0 l0 -77 Z"></path>
<line x1="0" y1="0" x2="0" y2="154" stroke="black" stroke-width="1" stroke-dasharray="1 3"></line>
<use id="collar-right" href="#collar-left" transform="scale(-1,1)"></use>
</g>
<g id="dimension-labels">
<g id="dimension-sleeve-length">
<line marker-start="url(#arrow)" marker-end="url(#arrow)" x1="85" y1="0" x2="85" y2="120" stroke="black" stroke-width="1"></line>
<text font-size="10" filter="url(#solid)" fill="black" x="85" y="60" class="dimension" text-anchor="middle" dominant-baseline="middle"> 120 cm</text>
</g>
<g id="dimension-length">
<line marker-start="url(#arrow)" marker-end="url(#arrow)" x1="-85" y1="0" x2="-85" y2="154" stroke="black" stroke-width="1"></line>
<text font-size="10" filter="url(#solid)" fill="black" x="-85" y="77" text-anchor="middle" dominant-baseline="middle" class="dimension"> 154 cm</text>
</g>
<g id="dimension-sleeve-to-sleeve">
<line marker-start="url(#arrow)" marker-end="url(#arrow)" x1="-65" y1="-20" x2="65" y2="-20" stroke="black" stroke-width="1"></line>
<text font-size="10" filter="url(#solid)" fill="black" x="0" y="-20" text-anchor="middle" dominant-baseline="middle" class="dimension"> 130 cm </text>
</g>
<g title="Back Width" id="dimension-back-width">
<line marker-start="url(#arrow)" marker-end="url(#arrow)" x1="-30" y1="174" x2="30" y2="174" stroke="black" stroke-width="1"></line>
<text font-size="10" filter="url(#solid)" fill="black" x="0" y="174" text-anchor="middle" dominant-baseline="middle" class="dimension"> 60 cm </text>
</g>
</g>
</svg>
An obvious workaround to the problem of the blur produced by the filter effect is to render the <text> two times: once for the background (with transparent characters) and once for the characters (without a background filter).
For me, this was the only way to make the text readable in Safari.
<svg width="100%" height="100%">
<filter x="0" y="0" width="1" height="1" id="solid">
<feFlood flood-color="yellow" />
</filter>
<g transform="translate(20, 50)" font-size="50">
<text aria-hidden="true" fill="none" filter="url(#solid)">solid background</text>
<text fill="blue">solid background</text>
</g>
</svg>
The aria-hidden="true" attribute is there to prevent screen readers from speaking the text twice, if the user uses a screen reader.
You can add style to your text:
style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
text-shadow: rgb(255, 255, 255) -2px -2px 0px, rgb(255, 255, 255) -2px 2px 0px,
rgb(255, 255, 255) 2px -2px 0px, rgb(255, 255, 255) 2px 2px 0px;"
White, in this example.
Does not work in IE :)

Centering an SVG element Chrome vs Safari

I'm trying to center an SVG element in a parent element. However, I'm finding discrepancies between Chrome and Safari. The following code centers the text nicely inside the square on Chrome, but not on Safari:
<svg width="200px" height="200px">
<g transform="translate(70,70)">
<path d="M -40,-40 l 80,0 l 0,80 l -80,0 l 0,-80 z" style="fill: gray"></path>
<g>
<text text-anchor="middle" dominant-baseline="middle" style="fill: white" transform="scale(2)">
<tspan>test</tspan>
</text>
</g>
</g>
</svg>
Result:
I created a jsFiddle with this test case:
https://jsfiddle.net/yq11jot0/
How do I vertically center the text inside the square?
How about just using dy instead of dominant-baseline?
<svg width="200px" height="200px">
<g transform="translate(70,70)">
<path d="M -40,-40 l 80,0 l 0,80 l -80,0 l 0,-80 z" style="fill: gray"></path>
<g>
<text text-anchor="middle" dy="0.25em" style="fill: white" transform="scale(2)">
<tspan>test</tspan>
</text>
</g>
</g>
</svg>
Apparently, Safari wants the inner tspan have the dominant baseline set to middle. So this also works on Safari:
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(50,50)">
<path class="node" d="M -40,-40 l 80,0 l 0,80 l -80,0 l 0,-80" style="fill: rgb(247, 61, 0);"></path>
<g>
<text text-anchor="middle" fill="white"><tspan dominant-baseline="middle">test</tspan></text>
</g>
</g>
</svg>
Try removing all the scaling and translating (can have issues in browsers) ... does this work in Safari?
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0 H200 V200 H0 z" fill="gray" />
<text x="100" y="100" text-anchor="middle" dominant-baseline="middle" fill="white" font-size="100" >test</text>
</svg>
Note the following:
Not using px units
Text positioned to the center of the square using x and y attributes.

SVG: right-align text at the end of a textPath

I'd like to right-lign text at the end of a textPath:
<svg height="300" width="400">
<defs>
<path id="myTextPath" d="M30,160 A175,175 0 0,1 370,160" />
</defs>
<path d="M30,160 A175,175 0 0,1 370,160" style="stroke: black; stroke-width:1; fill:none;" />
<g fill="black" stroke="none">
<text x="0%" y="0" text-anchor="start">
<textPath xlink:href="#myTextPath">Start</textPath>
</text>
<text x="50%" y="0" text-anchor="middle">
<textPath xlink:href="#myTextPath">Middle</textPath>
</text>
<text x="100%" y="0" text-anchor="end">
<textPath xlink:href="#myTextPath">End</textPath>
</text>
</g>
</svg>
You can see this working here: http://jsfiddle.net/7sqdxw11/
The Start text begins right where I'd expect - at the beginning of the textPath.
However, the end text ends well short of the end of the textPath.
(The Middle is also well shy of the middle of the textPath).
Here's a screenshot:
What am I doing wrong? How to I get the End to end at the right end of the textPath arc?
In SVG percentage coordinates generally refer to the either the width of the SVG, or in some cases the width of the parent object.
So in your example the "100%" will result in a value of 400px - the width of your SVG. However your path actually has a length of 466. You can get the length by experimentation, or by using some Javascript:
var len = document.getElementById("myTextPath").getTotalLength();
So if you change the "100%" to "466" you get the effect you wanted.
<svg height="300" width="400">
<defs>
<path id="myTextPath" d="M30,160 A175,175 0 0,1 370,160" />
</defs>
<path d="M30,160 A175,175 0 0,1 370,160" style="stroke: black; stroke-width:1; fill:none;" />
<g fill="black" stroke="none">
<text x="0" y="0" text-anchor="start">
<textPath xlink:href="#myTextPath">Start</textPath>
</text>
<text x="233" y="0" text-anchor="middle">
<textPath xlink:href="#myTextPath">Middle</textPath>
</text>
<text x="466" y="0" text-anchor="end">
<textPath xlink:href="#myTextPath">End</textPath>
</text>
</g>
</svg>