SVG.js TextPath not aligned with path (Verticaly) - javascript

I'm trying to place a text onto a path with svg.js
Here is my fiddle:
https://jsfiddle.net/Byteschmiede/ytz67egn/1/
var draw = SVG().addTo('body').size(500, 500)
draw.svg(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="100%" height="100%" viewBox="0 0 4725 2363" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path id="text1326" d="M2362.2,952.109C2548.36,773.615 2727.44,622.128 2899.46,497.647C3071.47,370.823 3216.39,286.272 3334.21,243.993C3452.02,201.721 3580.45,180.583 3719.47,180.58C4011.66,180.583 4247.3,278.051 4426.39,472.986C4607.82,665.577 4698.54,899.267 4698.55,1174.06C4698.54,1361.95 4658.48,1535.75 4578.37,1695.46C4498.25,1855.16 4383.97,1976.12 4235.52,2058.32C4089.42,2140.53 3920.94,2181.63 3730.08,2181.62C3482.66,2181.63 3265.87,2128.78 3079.72,2023.09C2895.92,1917.4 2656.75,1717.77 2362.2,1424.19C2055.87,1727.16 1811.99,1929.15 1630.55,2030.14C1449.11,2131.13 1238.21,2181.63 997.866,2181.62C691.537,2181.63 452.365,2086.51 280.351,1896.27C110.692,1706.03 25.862,1465.29 25.863,1174.06C25.862,901.616 115.404,667.926 294.489,472.986C475.929,278.051 712.744,180.583 1004.94,180.58C1146.32,180.583 1275.92,201.721 1393.74,243.993C1511.55,286.272 1655.29,370.823 1824.95,497.647C1996.97,622.128 2176.05,773.615 2362.2,952.109M2591.95,1170.53C2841.72,1417.14 3046.73,1585.07 3206.96,1674.32C3369.55,1761.22 3530.96,1804.67 3691.2,1804.67C3891.49,1804.67 4048.18,1745.95 4161.29,1628.52C4274.4,1508.74 4330.95,1364.3 4330.95,1195.19C4330.95,1009.65 4274.4,856.992 4161.29,737.208C4050.54,615.082 3903.27,554.017 3719.47,554.014C3615.79,554.017 3515.64,572.806 3419.04,610.382C3322.42,645.614 3206.96,709.027 3072.65,800.622C2938.33,889.873 2778.1,1013.18 2591.95,1170.53M2132.46,1170.53C1962.8,1027.27 1810.81,909.836 1676.5,818.237C1542.19,724.294 1424.37,657.357 1323.05,617.428C1221.72,577.503 1110.97,557.54 990.797,557.537C818.78,557.54 676.22,617.43 563.115,737.208C450.009,856.992 393.456,1009.65 393.457,1195.19C393.456,1324.37 422.911,1433.58 481.82,1522.83C540.729,1612.08 612.598,1681.37 697.428,1730.68C784.613,1780.01 893.006,1804.67 1022.61,1804.67C1192.27,1804.67 1357.21,1760.05 1517.45,1670.8C1677.68,1581.55 1882.68,1414.79 2132.46,1170.53" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve3" d="M294.489,472.986C475.929,278.051 712.744,180.583 1004.94,180.58C1146.32,180.583 1275.92,201.721 1393.74,243.993C1511.55,286.272 1655.29,370.823 1824.95,497.647" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve2" d="M2899.46,497.647C3071.47,370.823 3216.39,286.272 3334.21,243.993C3452.02,201.721 3580.45,180.583 3719.47,180.58C4011.66,180.583 4247.3,278.051 4426.39,472.986" style="fill:none;fill-rule:nonzero;stroke:black;stroke-width:1px;"/>
<path id="textCurve1" d="M564.216,1310.73C609.732,1475.4 770.154,1619.22 909.116,1635.49C993.881,1645.42 1152.78,1644.6 1262.96,1605.82C1479.72,1529.53 1626.78,1408.31 1802.89,1258.49" style="fill:none;fill-rule:nonzero;stroke:rgb(177,0,52);stroke-width:1px;"/>
</svg>
`)
let textCurve1 = draw.find("#textCurve1")[0]
let textPath = textCurve1.text("Forever").font({
size: textCurve1.height()
})
.attr({
startOffset: '50%',
'text-anchor': 'middle'
})
.fill({
opacity: 0
}).stroke({
color: "#000",
width: 1
})
The red line is the target path.
The Text needs to follow the path.
As you can see, the text curve is correct but the placement is way to low on the y axis.
I'm not sure how to solve this, since there aren't much opportunities.
I tried attributes like dominant-baseline and baseline-alignment but nothing workend
Here is a picture of the svg

You can use a tspan with a dx attribute to move specific text up or down.
const text = draw.text((text) => {
text.tspan('Some Text').dx(10)
}).path(textCurve1)

Related

How to fit svg path into svg viewbox in React Native

I am trying to implement barcode scanner viewFinder and I want to use svg icon to make it look nice, but I have a problem with forcing the path element inside the svg to take up the full svg width and height. I am using react native and to generate icon i use SVGR https://react-svgr.com/playground/?native=true&typescript=true in the scan handler I set the dimensions of the svg like so:
const handleBarCodeScanned = ({ type, data, bounds }: BarCodeEvent) => {
if (!bounds) return;
const { origin, size } = bounds;
setX(origin.x);
setY(origin.y);
setWidth(size.width);
setHeight(size.height);
};
and the I ise them inside the svg which looks like so
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
export const ViewFinder = (props: SvgProps & { top: number; left: number }) => {
const { width, height, top, left } = props;
return (
<Svg
width={width}
height={height}
style={{
borderColor: "green",
borderWidth: 2,
position: "absolute",
left: 0,
top: 0,
width: "100%",
height: "100%",
}}
fill="none"
stroke="green"
preserveAspectRatio="none"
viewBox={`0 0 ${width} ${height}`}
>
<Path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></Path>
<Path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></Path>
</Svg>
);
};
original icon is a featerIcons crop icon https://feathericons.com/ and the original code of the icon is:
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg>
as you can see I set the border color and borderWidth on the svg itself, and it scales to fit the container so here everything seems to be ok. I have viewBox and preserveAspectRatio set up its just the inner path not scaling with the svg, and it is not just this icon I have tries several and the issue is still this same so there must be something wrong with my understanding of svg.
Thanks a lot for any help.
Normally a viewBox would be 4 fixed numbers, i.e. unrelated to width and height. That should give you the result you want.
Your content doesn't change in size so your viewBox shouldn't change either.

How to get the exact BBox for svg <tspan>

I am trying to figure out why getBBox() for tspan element of a svg does not return the dimension.
To demonstrate this with an example, if I run BBox on both tsp1 and rect1, it returns the correct dimension for rect1 but not for tsp1
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log(tspBBox);
console.log(rectBBox);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>
I was expecting BBox to return the exact x and y for tsp1 but it does not.
I don't know why. I need to pass on the exact values to the succeeding class dynamically.
How can javascript return the exact dimension for the tspan element?
There are a number of methods for measuring text, and they are a bit more complex than defining a simple box. This is because with the dx, dy and rotate attributes, each addressable character can be be positioned individually - moved and rotated in every direction. Therefore, it makes more sense to answer the question where a single character is positioned, and where, after completing one sequence, the next character would be positioned.
In your case none of the above attributes are set ( on the <tspan> or <text> element). In this case is is possible to retrieve the start position of the <tspan> with .getStartPositionOfChar(0) and the horizontal width with .getComputedTextLength().* The height according to the font metrics is the same for all characters in the tspan, so it is enough to return one .getExtentOfChar(0) - 0 refers to the first character within the sequence of addressable characters.
As chrwahl pointed out in his answer, the start position refers to the font-specific baseline and normally will not be identical to the top left corner of a bounding box.
*There is a subtle trick here: if the letter-spacing or word-spacing CSS properties were defined, the "length" returned would not only return the width from the start of the first character to the end of the last, but also would add (or subtract) a spacing value that is defined after the end of the string. In other words: despite its name, the method returns the relative horizontal start position of the next character after the string examined.
var tsp = document.getElementById('tsp1');
var tspPos = tsp.getStartPositionOfChar(0);
console.log('start position', tspPos.x, tspPos.y);
console.log('horizontal advance', tsp.getComputedTextLength());
console.log('vertical extent', tsp.getExtentOfChar(0).height);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
<text class="t1" id="t1" font-size="20" font-family="PT Mono" text-decoration="underline">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
</svg>
It is all about the dominant-baseline. So, there is a differences between where the text is placed according to the dominant-baseline and the box that the text takes up. The value text-before-edge will place the text according to the upper left corner of the box.
var tsp = document.getElementById('tsp1');
var tspBBox = tsp.getBBox();
var rect = document.getElementById('rect1');
var rectBBox = rect.getBBox();
console.log('tspBBox', tspBBox.x, tspBBox.y);
console.log('rectBBox', rectBBox.x, rectBBox.y);
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 100 400 200">
<text class="t1" id="t1" font-size="20" font-family="PT Mono"
text-decoration="underline" dominant-baseline="text-before-edge">
<tspan class="tsp1" id="tsp1" x="10.23" y="135.05">Abc ef ghi</tspan>
</text>
<rect class="rect1" id="rect1" x="9.23" y="112.73368530273439" height="31.546314697265625" width="1" fill="orange" />
</svg>

Is it possible to make SVG equal to path?

Helo I have a problem. I am using npm package which provides pack of weather icons. The problem is the SVG box is big and it breaks positioning that is a lot of white space on website, it does not look nice. Is negative margin the way>
import {wiCloudy} from 'weather-icons-react'
const WeatherDataBox = () => {
return (
icon=<WiCloudy size={256} />
)}
<svg stroke="currentColor" fill="currentColor"
stroke-width="0" viewBox="0 0 30 30"
attr="[object Object]" size="256" height="256" width="256">
<path>*long code*</path>
</svg>
I changed viewbox and it reduced empty background
let ICON_SIZE = 256
let VIEWBOX = "5 5 20 20"
<WiCloudy viewBox={VIEWBOX} size={ICON_SIZE} />

Add text to SVG path dynamically

I have an SVG exported from Adobe Illustrator with several paths like this, which produces a small polygon I intend to use as a text box
<svg viewbox="387 390 74 20">
<g>
<path class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>
I'd like to dynamically add text to it. I've seen many similar questions here, but most of them involed specifying a x and y property for a text element based on the x and y property the path element. My path, however, does not have such properties.
I've tried to use a textPath element with xlink:href pointing to my path. I gets attached to the path, but the text wraps my path element instead of being inside it.
So, is there a way to achieve this? I'm open to different solutions here. My dream would be to use javascript to get the path element and add the text from there. But I could also edit the base SVG file to add a text or any other relevant element and attributes to make this work, as long as I can change the text dynamically from javascript later. And since this SVG is produced by Illustrator, I could also try different export options there in order to get a proper output for my goal.
Here's some sample code that takes a label path and adds a <text> element after it with whatever text you choose.
let label1 = document.querySelector("#label1");
addLabelText(label1, "Something");
function addLabelText(bgPath, labelText)
{
let bbox = bgPath.getBBox();
let x = bbox.x + bbox.width / 2;
let y = bbox.y + bbox.height / 2;
// Create a <text> element
let textElem = document.createElementNS(bgPath.namespaceURI, "text");
textElem.setAttribute("x", x);
textElem.setAttribute("y", y);
// Centre text horizontally at x,y
textElem.setAttribute("text-anchor", "middle");
// Give it a class that will determine the text size, colour, etc
textElem.classList.add("label-text");
// Set the text
textElem.textContent = labelText;
// Add this text element directly after the label background path
bgPath.after(textElem);
}
.st37 {
fill: linen;
}
.label-text {
font-size: 10px;
fill: rebeccapurple;
transform: translate(0, 3px); /* adjust vertical position to centre text */
}
<svg viewbox="387 390 74 20">
<g>
<path id="label1" class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>
Since you can edit your base SVG align
create a proper SVG to work with
Your path is a background label starting (red circle) at a large offset x=452 y=408
I would recreate it,
starting at the green circle, (editor: https://yqnn.github.io/svg-path-editor/)
in a viewBox="0 0 80 20"
To get single coordinates for both backgroundlabel and (blue) textPath line
after that use JavaScript to add text dynamically.
No need for text x,y calculations, pathLength and startoffset do the work
Or if you go fancy you can create the blue line dynamically from getBBox()
That will also work with your current path; just more calculations required
It is all about coordinates,
and positioning the blue line (with stroke="transparent" then):
playground:
<svg viewbox="387 390 74 20">
<path fill="lightgreen" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0C460,404.42,456.42,408,452,408z" />
<circle cx="452" cy="408" r="2" fill="red"/>
<circle cx="388" cy="400" r="2" fill="green"/>
<path id="P" pathLength="100" d="M388 400h72" stroke="blue"/>
<text>
<textPath href="#P" startoffset="50" text-anchor="middle" dominant-baseline="middle"
fill="green" font-size="14px">My Text</textPath>
</text>
</svg>
Thanks for the answers! I end up using a tweaked version of Paul LeBeau's function to take into account the structure suggested by Danny '365CSI' Engelman so I don't have to use translate to center the text vertically.
let label = document.querySelector("#mylabel");
addLabelTextPath(label, "Something");
function addLabelTextPath(bgPath, labelText) {
let bbox = bgPath.getBBox();
let x = bbox.x + bbox.width / 2;
let y = bbox.y + bbox.height / 2;
// Create the path line
let pathElem = document.createElementNS(bgPath.namespaceURI, "path");
pathElem.setAttribute("pathLength", 100);
pathElem.setAttribute("d", `M${bbox.x} ${y}h${bbox.width}`);
pathElem.id = `baseline-${bgPath.id}`;
// Create a <text> element
let textElem = document.createElementNS(bgPath.namespaceURI, "text");
// Create a <textPath> element
let textPath = document.createElementNS(bgPath.namespaceURI, "textPath");
textPath.setAttribute("href", `#${pathElem.id}`);
//Center text
textPath.setAttribute("dominant-baseline", "Middle");
textPath.setAttribute("startOffset", 50);
textPath.setAttribute("text-anchor", "middle");
// Give it a class that will determine the text size, colour, etc
textPath.classList.add("label-text");
// Set the text
textPath.textContent = labelText;
// Add the elements directly after the label background path
bgPath.after(pathElem);
pathElem.after(textElem);
textElem.appendChild(textPath);
}
.st37 {
fill: lightblue;
}
.label-text {
font-size: 10px;
fill: darkblue;
}
<svg viewbox="387 390 74 20">
<g>
<path id="mylabel" class="st37" d="M452,408h-56c-4.42,0-8-3.58-8-8l0,0c0-4.42,3.58-8,8-8h56c4.42,0,8,3.58,8,8l0,0 C460,404.42,456.42,408,452,408z" />
</g>
</svg>

Checking text overflow on SVG textpath & jquery

First of all I looked at all possible related answers here but none of them seem to bring the answer I need so here I am.
Given a svg text path:
<svg viewBox="0 0 900 900"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
id="mysvg"
>
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>
At this point the text exceeds the textpath
I can't find a way to check for possible overflow through jquery. This command won't actually return undefined:
alert($("text#names").attr("textLength") );
I am trying to check for overflows in order to fit the text to the maximum length or so.
I had the same problem when adjusting font size so that the given text will be drawn with the largest possible font size without overflow. Its quite simple using plain JS.
1) Determine the np. of characters in the text element with a minimum font size:
textElement.css('font-size', 1);
var allCharCount = textElement[0].getNumberOfChars();
2) Then set font size to any value and determine the length again
var hasOverflow = allCharCount != textElement[0].getNumberOfChars();
getNumberOfChars() will only return the no. of chars drawn. If there is an overflow this number will be smaller then from the original whole string.
It looks like text.getNumberOfChars() has changed since the other answer was written, and now returns the total number of characters in the string, regardless of if they're rendered or not.
My approach to this problem is to:
Change the <textPath> element to draw on a much longer path, then calculate the text length using text.getComputedLength()
Change the <textPath> back to the original path and calculate length again
If the length on the original path is shorter than the length on the longer path, you know there's an overflow.
const textPath = document.querySelector('textPath');
const checkClipped = () => {
textPath.setAttribute('xlink:href', '#fullWidthPath');
const fullLength = textPath.getComputedTextLength();
textPath.setAttribute('xlink:href', '#myPath');
const curvedLength = textPath.getComputedTextLength();
return fullLength > curvedLength;
}
const findLongestString = () => {
const text = textPath.innerHTML;
if (checkClipped()) {
const newText = text.substring(0, text.length - 1);
textPath.innerHTML = newText;
return findLongestString(newText);
} else {
return text;
}
}
console.log(findLongestString())
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="mysvg">
<defs>
<path id="myPath" d="M70 110 C 70 140, 110 140, 110 110" stroke="black" fill="transparent"/>
<path id="fullWidthPath" d="M 0 0 L 0 10000" />
</defs>
<use xlink:href="#myPath" fill="none" stroke="red" />
<text id="names" font-family="Verdana" font-size="10" text-anchor="middle" >
<textPath xlink:href="#myPath" startOffset="50%">
My text is going to exceed at some point…
</textPath>
</text>
</svg>

Categories