I would like to build a site where the text is not displayed in straight lines but on curved lines instead. I work with a single SVG path at the moment, that spans over multiple rows and put the text on that path with the textPath-element. I then scale the fontsize with JavaScript, to make it "responsive".
Here is what I have so far:
$(function() {
var myFontsize = window.innerWidth * 0.015 ((window.innerWidth - 1300) / -30);
$( "text" ).css("font-size", myFontsize)
});
$( window ).resize(function() {
var myFontsize = window.innerWidth * 0.015 ((window.innerWidth - 1300) / -30);
$( "text" ).css("font-size", myFontsize)
});
body {
margin: 1vw;
}
path {
fill: none;
}
#line-box {
position: fixed;
left: 1vw;
top: 0;
width: 101vw;
}
text {
fill: black;
font-size: 1.3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="line-box" viewBox="0 -30 1200 1200" preserveAspectRatio="none">
<path vector-effect="non-scaling-stroke" id="curve" d="M0 0C449.6667 0 857 0 1160 0M0 35C292 35 584 35 876 55 953.6667 55 1031.3333 55 1160 35"/>
<text text-anchor="start">
<textPath xlink:href="#curve">
Are you a human being? We apologize for the confusion, but we can't quite tell if you're a person or a script. Please don't take this personally. Bots and scripts can be remarkably lifelike these days!
</textPath>
</text>
</svg>
(Change the width of the view box to see the behavior I am talking about)
Now the rendered text on the path of course doesn't behave like a text in a p-element for example, so where there is a "line-break" it wraps around the edges in the middle of the words (which are not words but single rendered letters).
I thought about putting each word into a tspan-element, thinking I could get a grip on each word this way, but the rows of text are put on one consecutive path, so I don't really know how to calculate or detect wether a word is leaving or entering a specific row of the path.
Is there a way to continue a text over multiple paths instead of having one path drawing multiple rows? Maybe this way it could be somehow calculated when a tspan-element is leaving a path and must enter the next?
I basically want the text and it's "line-breaks" to work similarly to a normal paragraph without hyphenation, just that the lines move on paths. That idea doesn't sound so complicated to me, but I don't really know what the best way is to make it work.
Maybe SVG-paths are just not the right tool for this. In that case, does anyone have a better Idea where I should look? I am not hesitant to look into some js-libraries for example.
Thank you so much :)
CodePudding user response:
You'll definitely need Javascript to implement this. SVG is a relatively low-level format for describing vector graphics, not a text layout format. Responsive line breaks is a feature in the pipeline, but not fully implemented yet AFAIK. Responsive line breaks on text paths? That is far too specialised.
That idea doesn't sound so complicated to me
Well, it is for various reasons. Here is a somewhat naive implementation:
const nssvg = "http://www.w3.org/2000/svg";
const nsxlink = "http://www.w3.org/1999/xlink";
const text = document.querySelector('text');
const originalText = text.textContent.trim();
function nextCurve(curves) {
const currentCurve = curves.shift();
const currentLength = currentCurve.getTotalLength();
return [currentCurve, currentLength, /\s /g, 0];
}
function makeTextPath(content, id) {
const textPath = document.createElementNS(nssvg, 'textPath');
textPath.setAttributeNS(nsxlink, 'xlink:href', '#' id);
textPath.textContent = content;
text.appendChild(textPath);
}
function onResize() {
text.style.fontSize = (window.innerWidth * 0.015 ((window.innerWidth - 1300) / -30)) 'px';
// flush old layout
text.innerHTML = "";
let currentText = originalText;
// set text in a provisorial tspan so it can be measured
const tspan = document.createElementNS(nssvg, 'tspan');
tspan.textContent = currentText;
text.appendChild(tspan);
const curves = [...document.querySelectorAll('.curve')];
let [currentCurve, currentLength, re, previousIndex] = nextCurve(curves);
// search for spaces
while (re.exec(currentText)) {
// measure the position of the space
const length = tspan.getSubStringLength(0, re.lastIndex)
if ( (length < currentLength) ) {
previousIndex = re.lastIndex;
} else {
// if the space is beyond the end, add a textPath with one word less,
// provided there is still a curve left to use
if (currentCurve) {
makeTextPath(currentText.substring(0, previousIndex), currentCurve.id);
}
// remove the already used part from the tspan
// and initialize for next line
tspan.textContent = currentText = currentText.substring(previousIndex).trim();
[currentCurve, currentLength, re, previousIndex] = nextCurve(curves);
}
}
// there is a rest that still needs to be layed out
if (currentCurve) {
makeTextPath(currentText, currentCurve.id);
}
//the provisorial tspan can be removed now
tspan.remove();
}
onResize();
window.addEventListener('resize', onResize);
#line-box {
left: 1vw;
width: 98vw;
}
text {
text-anchor: start;
fill: black;
font-size: 1.3rem;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="line-box" viewBox="0 -30 1200 1200" preserveAspectRatio="none">
<defs>
<path id="c1" d="M0 0C449.6667 0 857 0 1160 0"/>
<path id="c2" d="M0 35C292 35 584 35 876 55 953.6667 55 1031.3333 55 1160 35"/>
<path id="c3" d="M 0,70 C 292,70 584,90 876,110 953.67,110 1031.3,90 1160,70"/>
</defs>
<text>
Are you a human being? We apologize for the confusion, but we can't quite tell if you're a person or a script. Please don't take this personally. Bots and scripts can be remarkably lifelike these days!
</text>
</svg>
One simplification about this you mentioned yourself: the potential line breaks are found with the regular expression /\s /g
, which will identify various space characters. This is even less sophisticated than the algorithms used for line breaks in HTML (with the default hyphens: none
).
The big problem is identifying what is fitting on the path used in <textPath>
. There are a few interfaces defined for SVGTextContentElement that look like they could be helpful. But in practice, they fall short. The algorithm for describing how to position glyphs is quite longish, but it doesn't say much about glyphs that cannot be positioned:
- Glyphs whose midpoint-on-the-path are off the path are not rendered.
- Continue rendering glyphs until there are no more glyphs (or no more space on path).
Suppose the nth character is not rendered. The quote above doesn't say anything about the return values of textPath.getSubStringLength(0, n)
or textPath.getStartPositionOfChar(n)
. Firefox, for example, returns values as if they were all lumped up somewhere near the end of the path. Other browsers may return something completely different, there is no guarantee what.
I decided to circumvent this problem by first laying out the string in a straight line, then measuring where the spaces end up, and break at the last space whose position is still smaller than the length of the path it will be laid out with. But this is only a rough estimate. In reality the character spacing differs between straight and curved layout. Especially for narrow curves the results will be wrong, and when the text is positioned on its inside, some letters may disappear.
Another problem arises from the concept of "addressable characters" used for producing character indices in SVG:
A character that is addressable by text positioning attributes and SVG DOM text methods. Characters discarded during layout such as collapsed white space characters are not addressable, neither are characters within an element with a value of none for the display property. Addressable characters are addressed by their index measured in UTF-16 code units (thus, a single Unicode code point above U FFFF will map to two addressable characters as a UTF-16 code unit consists of 16 bits).
If you have a string as returned from the textContent
property, and apply methods of the Javascript String
prototype, the same indices may point to completely different characters.
To completely solve both problems, a lot more code will be needed, and all the special cases to be considered are quite beyond the scope of this answer. Maybe there is a library out there somewhere that attempts this, but I am not aware of it.