我想在SVG中显示一个<text>
,它会像HTML文本填充<div>
元素一样自动换行到容器<rect>
。有没有办法做到这一点?我不想使用<tspan>
分别定位每行。
SVG1.1规范中并没有包含文字环绕功能。
如果您要在Web上使用SVG图形,可以通过<foreignObject/>
元素嵌入SVG内的HTML。示例:
<svg ...>
<switch>
<foreignObject x="20" y="90" width="150" height="200">
<p xmlns="http://www.w3.org/1999/xhtml">Text goes here</p>
</foreignObject>
<text x="20" y="20">Your SVG viewer cannot display html.</text>
</switch>
</svg>
如果你的目标是纯SVG渲染器且不支持HTML,或者希望使用专业的矢量图形处理软件(Adobe Illustrator,Inkscape等)来编辑你的图像,则这个解决方案可能不适合你。
这里是另一种选择:
<svg ...>
<switch>
<g requiredFeatures="http://www.w3.org/Graphics/SVG/feature/1.2/#TextFlow">
<textArea width="200" height="auto">
Text goes here
</textArea>
</g>
<foreignObject width="200" height="200"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<p xmlns="http://www.w3.org/1999/xhtml">Text goes here</p>
</foreignObject>
<text x="20" y="20">No automatic linewrapping.</text>
</switch>
</svg>
请注意,即使foreignObject被报告为支持该featurestring,也不能保证HTML可以显示,因为这不是SVG 1.1规范要求的。目前没有html-in-foreignobject支持的featurestring。然而,在许多浏览器中仍然支持它,所以将来可能会成为必需品,也许会有相应的featurestring。
请注意,SVG Tiny 1.2中的'textArea'元素支持所有标准的svg功能,例如高级填充等,您可以指定width或height中的任何一个为auto,表示文本可以自由流动。ForeignObject作为剪辑视口。
注意:虽然上面的示例是有效的SVG 1.1内容,在SVG 2中已经删除了'requiredFeatures'属性,这意味着'switch'元素将尝试呈现第一个'g'元素,而不管是否支持SVG 1.2 'textArea'元素。请参见SVG2 switch element spec。
xhtml:div
代替div
,但这可能是由于d3.js。我找不到关于TextFlow的有用参考资料,它是否(仍然)存在或者只是在某些草案中? - johndodotextPath可能在某些情况下很有用。
<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<!-- define lines for text lies on -->
<path id="path1" d="M10,30 H190 M10,60 H190 M10,90 H190 M10,120 H190"></path>
</defs>
<use xlink:href="#path1" x="0" y="35" stroke="blue" stroke-width="1" />
<text transform="translate(0,35)" fill="red" font-size="20">
<textPath xlink:href="#path1">This is a long long long text ......</textPath>
</text>
</svg>
xmlns:xlink="http://www.w3.org/1999/xlink"
属性对其正常工作至关重要。 - Klesunfunction wraptorect(textnode, boxObject, padding, linePadding) {
var x_pos = parseInt(boxObject.getAttribute('x')),
y_pos = parseInt(boxObject.getAttribute('y')),
boxwidth = parseInt(boxObject.getAttribute('width')),
fz = parseInt(window.getComputedStyle(textnode)['font-size']); // We use this to calculate dy for each TSPAN.
var line_height = fz + linePadding;
// Clone the original text node to store and display the final wrapping text.
var wrapping = textnode.cloneNode(false); // False means any TSPANs in the textnode will be discarded
wrapping.setAttributeNS(null, 'x', x_pos + padding);
wrapping.setAttributeNS(null, 'y', y_pos + padding);
// Make a copy of this node and hide it to progressively draw, measure and calculate line breaks.
var testing = wrapping.cloneNode(false);
testing.setAttributeNS(null, 'visibility', 'hidden'); // Comment this out to debug
var testingTSPAN = document.createElementNS(null, 'tspan');
var testingTEXTNODE = document.createTextNode(textnode.textContent);
testingTSPAN.appendChild(testingTEXTNODE);
testing.appendChild(testingTSPAN);
var tester = document.getElementsByTagName('svg')[0].appendChild(testing);
var words = textnode.textContent.split(" ");
var line = line2 = "";
var linecounter = 0;
var testwidth;
for (var n = 0; n < words.length; n++) {
line2 = line + words[n] + " ";
testing.textContent = line2;
testwidth = testing.getBBox().width;
if ((testwidth + 2*padding) > boxwidth) {
testingTSPAN = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
testingTSPAN.setAttributeNS(null, 'x', x_pos + padding);
testingTSPAN.setAttributeNS(null, 'dy', line_height);
testingTEXTNODE = document.createTextNode(line);
testingTSPAN.appendChild(testingTEXTNODE);
wrapping.appendChild(testingTSPAN);
line = words[n] + " ";
linecounter++;
}
else {
line = line2;
}
}
var testingTSPAN = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
testingTSPAN.setAttributeNS(null, 'x', x_pos + padding);
testingTSPAN.setAttributeNS(null, 'dy', line_height);
var testingTEXTNODE = document.createTextNode(line);
testingTSPAN.appendChild(testingTEXTNODE);
wrapping.appendChild(testingTSPAN);
testing.parentNode.removeChild(testing);
textnode.parentNode.replaceChild(wrapping,textnode);
return linecounter;
}
document.getElementById('original').onmouseover = function () {
var container = document.getElementById('destination');
var numberoflines = wraptorect(this,container,20,1);
console.log(numberoflines); // In case you need it
};
boxwidth = parseInt(boxObject.getAttribute('width'))
,它只接受像素宽度,而boxwidth = parseInt(boxObject.getBBox().width)
则可以接受任何类型的度量单位。 - massic80以下代码正常运行。 运行代码片段查看其作用。
也许可以清理一下或使其自动适用于SVG中的所有文本标签。
function svg_textMultiline() {
var x = 0;
var y = 20;
var width = 360;
var lineHeight = 10;
/* get the text */
var element = document.getElementById('test');
var text = element.innerHTML;
/* split the words into array */
var words = text.split(' ');
var line = '';
/* Make a tspan for testing */
element.innerHTML = '<tspan id="PROCESSING">busy</tspan >';
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var testElem = document.getElementById('PROCESSING');
/* Add line in testElement */
testElem.innerHTML = testLine;
/* Messure textElement */
var metrics = testElem.getBoundingClientRect();
testWidth = metrics.width;
if (testWidth > width && n > 0) {
element.innerHTML += '<tspan x="0" dy="' + y + '">' + line + '</tspan>';
line = words[n] + ' ';
} else {
line = testLine;
}
}
element.innerHTML += '<tspan x="0" dy="' + y + '">' + line + '</tspan>';
document.getElementById("PROCESSING").remove();
}
svg_textMultiline();
body {
font-family: arial;
font-size: 20px;
}
svg {
background: #dfdfdf;
border:1px solid #aaa;
}
svg text {
fill: blue;
stroke: red;
stroke-width: 0.3;
stroke-linejoin: round;
stroke-linecap: round;
}
<svg height="300" width="500" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text id="test" y="0">GIETEN - Het college van Aa en Hunze is in de fout gegaan met het weigeren van een zorgproject in het failliete hotel Braams in Gieten. Dat stelt de PvdA-fractie in een brief aan het college. De partij wil opheldering over de kwestie en heeft schriftelijke
vragen ingediend. Verkeerde route De PvdA vindt dat de gemeenteraad eerst gepolst had moeten worden, voordat het college het plan afwees. "Volgens ons is de verkeerde route gekozen", zegt PvdA-raadslid Henk Santes.</text>
</svg>
我已经发布了一个添加虚假换行到SVG “text”元素的指南,具体内容请参见:
你只需要添加一个简单的JavaScript函数,将你的字符串拆分成较短的“tspan”元素。下面是一个示例:
希望这可以帮助你!
正如Erik Dahlström和其他人所指出的:
<foreignObject>
目前可能是在SVG中实现多行文本最简单的方法。
不幸的是,大多数桌面图形编辑器,如inkscape、Adobe Illustrator或预览应用程序无法呈现或转换<foreignObject>
内容。
我想出了一个笨拙的辅助函数,试图重建HTML文本元素以成为本机svg <text>
。
免责声明:以下函数支持大多数块和内联文本元素,但不支持例如列表或表格等。
function foreignObjectToNativeSvgtext(foreignObject) {
let body = foreignObject.children[0];
let children = [...body.children];
// children - will become svg <text> elements
children.forEach((child, i) => {
// clone text el
let type = child.nodeName.toLowerCase();
let clone;
clone = document.createElement(type);
body.insertBefore(clone, body.children[i]);
let textNodes = textNodesInEl(child);
let textL = textNodes.length;
for (let n = 0; n < textL; n++) {
let textNode = textNodes[n];
// get computed styles for parent
let style = window.getComputedStyle(textNode.parentNode);
copyStyleProps(child, clone)
// split to words
let words = textNode.textContent.split(' ');
words.forEach((word, w) => {
let newtextNode = document.createTextNode(word);
let span = document.createElement('span');
span.classList.add('span2tspan', 'span2tspan' + type);
// get computed styles for child
copyStyleProps(textNode.parentNode, span)
span.textContent = word + ' ';
clone.appendChild(span);
})
}
// convert to svg tspan
let nativeText = htmlTextEl2Svg(clone);
//insert before foreignObject
foreignObject.parentNode.insertBefore(nativeText, foreignObject);
// delete original foreign object elements
child.remove();
})
//preserve whitespace
let svg = foreignObject.closest('svg');
svg.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve')
foreignObject.remove();
output.value = new XMLSerializer().serializeToString(svg)
}
function htmlTextEl2Svg(el) {
const ns = "http://www.w3.org/2000/svg";
let parentSVG = el.closest('svg');
let newText = document.createElementNS(ns, 'text');
let parentProps = copyStyleProps(el, newText);
let bb = el.getBoundingClientRect();
let {
x,
y
} = bb;
let point = parentSVG.createSVGPoint();
point.x = x;
point.y = y;
let ctm = parentSVG.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
// round
[x, y] = [x, y].map(val => {
return +(val).toFixed(3)
})
newText.setAttribute('x', x);
newText.setAttribute('y', y);
// children
let children = el.querySelectorAll('.span2tspan');
/**
* add x and y only for vertical shifts
* (new line breaks)
*/
let xPrev = 0;
let yPrev = 0;
let prevStyle = '';
children.forEach(child => {
let bb = child.getBoundingClientRect();
let {
x,
y,
width,
height
} = bb;
let tspan = document.createElementNS(ns, 'tspan');
let style = window.getComputedStyle(child);
let currentProps = copyStyleProps(child, tspan);
// convert coordinates to svg
let point = parentSVG.createSVGPoint();
point.x = x;
point.y = y;
let ctm = parentSVG.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
x = point.x;
// add fontsize to baseline shift
y = point.y + (parseFloat(style.fontSize) * 1);
// round
[x, y] = [x, y].map(val => {
return +(val).toFixed(3)
});
if (x !== xPrev && y !== yPrev) {
tspan.setAttribute('x', x)
}
if (y !== yPrev) {
tspan.setAttribute('y', y)
}
// text color to fill
if (currentProps.color !== 'rgb(0, 0, 0)') {
tspan.style.fill = currentProps.color;
}
// remove superfluous inherited props
tspan.style.removeProperty('margin');
tspan.style.removeProperty('padding');
if (parentProps.fontFamily == currentProps.fontFamily) {
tspan.style.removeProperty('font-family')
}
if (parentProps.fontSize == currentProps.fontSize) {
tspan.style.removeProperty('font-size')
}
if (parentProps.fontWeight == currentProps.fontWeight) {
tspan.style.removeProperty('font-weight')
}
if (parentProps.lineHeight == currentProps.lineHeight) {
tspan.style.removeProperty('line-height')
}
// copy content
tspan.textContent = child.textContent;
// stringify current style
let currentStyle = JSON.stringify(currentProps);
// add to svg
if (child.textContent.trim()) {
let prevTspans = newText.querySelectorAll('tspan');
let prev = prevTspans[prevTspans.length - 1];
if (prev && !tspan.getAttribute('x') &&
!tspan.getAttribute('y') &&
currentStyle == prevStyle
) {
prev.textContent += tspan.textContent;
} else {
newText.appendChild(tspan)
prevStyle = currentStyle;
}
}
xPrev = x;
yPrev = y;
})
return newText;
}
/**
* helper copy computed styles
*/
function copyStyleProps(el, target, styleProps = []) {
let defaultvaluesToExclude = {
'color': 'rgb(0, 0, 0)',
'fontStyle': 'normal',
'letterSpacing': 'normal',
'verticalAlign': 'baseline',
}
let currentProps = {};
// defaults
if (!styleProps.length) {
styleProps = [
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'lineHeight',
'letterSpacing',
'verticalAlign',
'margin',
'padding',
'color'
];
}
let style = window.getComputedStyle(el);
for (prop in style) {
let val = style[prop];
if (styleProps.includes(prop) && val !== defaultvaluesToExclude[prop]) {
target.style[prop] = val;
currentProps[prop] = val;
}
}
return currentProps;
}
/**
* Get text nodes in element
* based on:
* https://dev59.com/DWgv5IYBdhLWcg3wQ-qd
*/
function textNodesInEl(el) {
let textNodes = [];
for (el = el.firstChild; el; el = el.nextSibling) {
if (el.nodeType == 3) {
textNodes.push(el);
} else {
textNodes = textNodes.concat(textNodesInEl(el));
}
}
// filter empty text nodes
textNodes = textNodes.filter((node) => node.textContent.trim());
return textNodes;
}
textarea {
width: 100%;
min-height: 30em;
white-space: pre;
font-family: monospace;
font-size: 0.75em;
}
<p><button onclick="foreignObjectToNativeSvgtext(foreignObject)">Convert foreignObject</button></p>
<h3>SVG with foreignObject – text is editable</h3>
<svg id="svg" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
.foreignBody {
font-family: Georgia, serif;
font-size: 1em;
line-height: 1.5em;
}
h1 {
font-family: sans-serif;
font-size: 2em;
line-height: 1.2em;
margin: 0 0 1rem 0;
}
.author {
line-height: 1.2em;
font-style: italic;
margin-bottom: 0em;
}
p {
margin: 0 0 1rem 0;
}
sup {
line-height: 0px;
font-size: 0.5em;
}
ul li:marker{
content:'•';
color:red;
}
</style>
<foreignObject id="foreignObject" x="5" y="5" width="90%" height="90%">
<div class="foreignBody" xmlns="http://www.w3.org/1999/xhtml" contenteditable>
<p class="author">Franz Kafka</p>
<h1>The Metamorphosis</h1>
<p>One morning, when <strong>Gregor Samsa</strong> woke from troubled dreams, he found himself
transformed in
his bed into <em style="color:red; letter-spacing:0.1em">a horrible</em> vermin.<sup>1</sup></em>
</p>
<p>He lay on his armour-like back, and if he lifted his head a little he could see his brown belly,
slightly
domed and divided by arches <strong><em> into stiff sections.</em></strong> The bedding was hardly
able to
cover it and seemed ready to slide off any moment.</p>
</div>
</foreignObject>
</svg>
<h3>Output</h3>
<textarea id="output"></textarea>
循环遍历所有块元素,如<p>
,<h1>
将文本内容拆分为单独的文本节点
通过getComputedStyle()
检索每个文本节点的样式信息
通过getBoundingClientRect()
获取位置
将HTML坐标转换为SVG用户单位:
let point = parentSVG.createSVGPoint();
point.x = x;
point.y = y;
let ctm = parentSVG.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
用<text>
和<span>
元素替换文本节点
一致的呈现空格敏感呈现。 Firefox 如果将 whitespace:pre
应用于 <text>
,则呈现换行符-另一方面,这对于优化/缩小的SVG不起作用(当过多的空格被删除时)。
text{
white-space: pre;
word-break: break-word;
}
svg{
width:20em;
border: 1px solid #ccc;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" xml:space="preserve">
<text x="0" y="20" font-size="10">The Metamorphosis
One morning, when
Gregor Samsa woke
from troubled dreams,
he found himself transformed
in his bed into a
horrible vermin.1
</text>
</svg>
if (data['clinic']['cicovidcliniccity'].length > 35 && data['clinic']['cicovidcliniccity'].length < 75) {
const cname = data['clinic']['cicovidcliniccity'];
const ctext2_shodow = document.querySelector("#c_text2_shdow");
ctext2.textContent = cname.substring(1, 35)
ctext2_shodow.textContent = cname.substring(35, cname.length);
}
if (data['clinic']['cicovidcliniccity'].length > 75 && data['clinic']['cicovidcliniccity'].length < 110) {
const cname1 = data['clinic']['cicovidcliniccity'];
const ctext2_shodow = document.querySelector("#c_text2_shdow");
const ctext3_shodow = document.querySelector("#c_text3_shdow");
ctext2.textContent = cname1.substring(1, 35)
ctext2_shodow.textContent = cname1.substring(35, 75);
ctext3_shodow.textContent = cname1.substring(75, cname1.length);
}
const myTextContent = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's s";
const lineLength = 20;
const maxLineHeight = 15;
const mySVG = document.querySelector("svg");
let loopMax = Math.round(myTextContent.length/lineLength);
const txts = [];
for (let i=1; i<=loopMax; i++){
txts.push(myTextContent.substring((lineLength*i)-lineLength,lineLength*i));
}
txts.forEach( (txt,i)=>{
const newTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
newTxt.setAttribute("x", "0");
newTxt.setAttribute("y", `${maxLineHeight * (i+1)}`);
newTxt.setAttribute("fill", "red");
newTxt.textContent = txt;
mySVG.appendChild(newTxt);
});
<svg height="90" width="200">
</svg>
2023年Web版(在所有主要浏览器中测试):
<switch>
<foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<p id="ModernText">My really long text</p>
</foreignObject>
<text id="FallbackText">Fallback Description</text>
</switch>