Simple Text Wrapping
by Antoine Quint
|
Pages: 1, 2
Core Functionalities
Let's turn to the design of the resulting SVG
code. First off, I want a container SVG
<text> element where I will put
the text. You can have as many
<tspan> elements as you want,
but you only want a single
<text>. In our case we only
deal with one single paragraph per
<text:wrap> element -- I was
too lazy to do more -- so we're going to end up
with one <text> element for
each of our wrapped paragraphs and that element
will have one <tspan> child
element for each line it is broken into. On a more
pragmatic side, having all your text wrapped up in
a single <text> element will
allow the user to actually select it all in one
go. So our _build() method is
supposed to handle creating that container
<text> element:
TextWrap.prototype._build = function () {
var element = document.createElementNS(SVG.ns, 'text');
var node = this._node;
var nextElement = null;
while (node.nextSibling) {
if (node.nextSibling.nodeType == 1) {
nextElement = node.nextSibling;
break;
} else {
node = node.nextSibling;
}
}
if (nextElement) {
var test = this._node.parentNode.insertBefore(element, nextElement);
} else {
this._node.parentNode.appendChild(element);
}
element.appendChild(document.createTextNode(''));
this._svg = element;
}
The only challenge so far was to append our new
<text> element right after our
original <text:wrap>
element. In order to do so we ended up writing
some code that could be taken into a new method
called Node.insertAfter(). We need to
start from our current node (the
<text:wrap> element) and look
for the next element node, not just any node. Once
we find it, we can call
insertBefore. If we don't find one,
then we will just have to append our new
<text> element to
<text:wrap>'s parent.
How about splitting the string into lines?
That shows off the power of SVG nicely. The real
headache here is knowing the pixel-length of a
bunch of words so I can come up with nice little
lines that take up a maximum of 440 pixels. We can
add the words as we find them to an SVG
<text> element and query its
bounding box to find out its width:
TextWrap.prototype._splitString = function () {
this._hide();
this._clear();
var words = this._string.split(' ');
var lines = new Array();
var line = new Array();
var length = 0;
var prevLength = 0;
while (words.length) {
var word = words[0];
this._svg.firstChild.data = line.join(' ') + ' ' + word;
length = this._svg.getComputedTextLength();
if (length > this._width) {
if (!words.length) {
line.push(words[0]);
}
lines.push( new Line(this, prevLength, line) );
line = new Array();
} else {
line.push(words.shift());
}
prevLength = length;
if (words.length == 0) {
lines.push( new Line(this, 0, line) );
}
}
this._lines = lines;
}
So we took the simple route by only breaking
lines at spaces. Recall that the design is biased
against vertically-oriented languages because it
has a width attribute, but no
length attribute. Actually I don't
care if the input text is horizontal or vertical
in SVG, the SVG DOM method
getComputedTextLength() method gives
me the length of the text, whatever its
direction. That's much better than getting the
width of the bounding box. So now we have the line
breaking bit figured out, how about having the
layout done? We decided to handle four types of
alignment. Our _layout() method does
all the work of printing things on the screen:
TextWrap.prototype._layout = function () {
this._clear();
var lines = (new Array(0)).concat(this._lines);
var anchor = 'start';
if (this._align == 'center') {
anchor = 'middle';
} else if (this._align == 'right') {
anchor = 'end';
}
for (var i=0; i<lines.length; i++) {
var x = 0;
line = lines[i];
this._svg.appendChild( document.createTextNode(' ') );
var tspan = document.createElementNS(SVG.ns, 'tspan');
tspan.appendChild( document.createTextNode(line._words.join(' ')) );
if (this._align == 'justify') {
var space = (this._width - line._width) / (line._words.length - 1);
space = (i != lines.length - 1) ? space : 0;
tspan.style.setProperty('word-spacing', space + 'px');
} else if (this._align == 'center') {
anchor = 'middle';
x = this._width / 2;
} else if (this._align == 'right') {
anchor = 'end';
x = this._width;
}
tspan.setAttribute('x', x);
tspan.setAttribute('dy', i ? this._interval : '1em');
this._svg.appendChild(tspan);
}
this._svg.style.setProperty('text-anchor', anchor);
this._show();
}
First we need to clear whatever text contents
have been displayed in our placeholder
<text> element. We're going to
start from scratch to display the new text
according to the latest property or data
changes. Then we clone our _lines
array; we might want to do a different layout with
the same line splitting. All we have to do now is
process each line in the cloned array and build a
corresponding <tspan> element
with the right properties. You'll notice that the
rest of our _layout() method is not a
lot of code because SVG 1.0 a lot for text
wrapping. First thing we will want to use is the
text-anchor CSS property that will
allow us to specify how the line text is spread in
regards to its anchor point.
For a left-alignment we use its default value
of start which will basically have
our text positioned
exactly where we said with the
transform attribute on the
<text> element. This default
value will work for both left-aligned and
justified text as text will flow naturally from
left to right starting at that point. For centered
alignment we want to use the middle
value assigned to that property: that means the
text will be spread equally on both sides of the
starting point. Now we must make sure our starting
point is translated by half of the desired box
width. Setting text-anchor to
end will make the text flow from
right-to-left in the case of right-to-left
languages, but we have to translate our
anchor-point by the desired width of the wrapping
box. In order to do those translations, we use the
x attribute of the
<tspan> element.
We use the
dy attribute as well: this one helps us set the line
interval; it specifies a vertical offset between this one and the
previous <tspan> element. When we are handling
the first line, we set dy differently to
1em. Why? The position of a <text>
element is the lower-left corner of its first rendered line. In my
case, I wanted the provided x and y
attributes on the <text:wrap> element to say
where it will be positioned according to its upper-right
position. Setting dy to 1em will have my
lines starting one line down, as if there was a previous invisible
one (as 1em is equal to whatever our a glyph's height
is).
|
Also in Sacré SVG |
There's one last thing we take care of in the
case of justified alignment. When we were breaking
the lines, we made sure to check out how long
lines were in pixels before they grew too long
(more than 440 in our demo). We then saved that
length in the Line object we created
and pushed onto our _lines array. It
comes in handy in justification since we're going
to say what the interval is between each word, so
that our line length is exactly 440 pixels. We
compute that interval simply with
(this._width - line._width) /
(line._words.length - 1). This value is
then used to set another great CSS property,
word-spacing, in order to control the
length of a space between two words. That's all
there is to justification.
Wrapping It All Up
We've come up with a pretty neat and useful extension to SVG by using it as a 2D Graphics API. But the great thing is that it's more than an API. It's also got an XML front-end and allows us to build higher-level blocks with higher level of semantics. We will explore all of this further with XForms-related work in the coming months.
Have you been exploring advanced SVG techniques? Share your experience in our forums.
(* You must be a member of XML.com to use this feature.)
Comment on this Article
| Titles Only | Titles Only | Newest First |
- Car Locked Out Open Door Car Los Angeles 1-818-386-1022 Locksmith
2008-10-24 23:09:04 orellytos [Reply]
AA Locksmith Los Angeles 1-818-386-1022 all locks install rekeying locks and locked out services 24 hour.
- Ref to parent element missing
2006-08-20 16:47:55 ErikBorgen [Reply]
I use SVG in combination with normal HTML and implements the SVG in iframes (with the embed).
I need to make reference outside the SVG using java scripts and this works for normal SVG's, but not if I use the text wrapper.
Does anybody know which modifications are needed in the textwrap.es scripts?
- Firefox 1.5
2006-08-16 11:45:50 richbk [Reply]
Hello,
Excellent article. Anyone get this working in Firefox 1.5? Anyone know of any reason why it wouldn't work in Firefox 1.5?
In the example included below I get the error:
Error: tspan.style has no properties
Source File: file:TextWrap.js
Line: 150
Thanks,
Rick
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:text="http://xmlns.graougraou.com/svg/text/"
onload="TextWrap._init()"
>
<defs>
<script type="text/javascript" xlink:href="TextWrap.js" />
</defs>
<text:wrap x="10.5" y="47.5" width="440" style=" font-family: arial;font-size: 11px;text-rendering: optimizeLegibility;text-align: justify;line-interval: 1.5em">SVG is a language for describing two-dimensional graphics in XML. SVG allows for three types of graphic objects</text:wrap>
</svg>
- Firefox 1.5
2008-05-12 05:09:14 ebbeebbe [Reply]
To make it work in Firefox 2, native SVG, you should do the following:
- add "important" as the third argument to all setPropertyValue
- find the line which says tspan.setAttribute('dy', i ? this._interval : '1em'); and change that into tspan.setAttribute('dy',15);
In Firefox 2 there is also an issue with getComputedTextLength(). This will only work when the element it is called on is rendered _and_ visible. For the onload function you can make sure everything is rendered by setting a timeout: setTimeout('TextWrap._init()',1000); Firefox 3 should not have this issue anymore. See https://bugzilla.mozilla.org/show_bug.cgi?id=345635 for more info about the getComputedTextLength() issue in Firefox 2.
- Firefox 1.5
- scaling
2006-01-28 12:40:03 couloir [Reply]
I have a document that is structured like this:
<svg id="outer" x="0" y="0" width="100%" height="100%" viewBox="0 0 700 585"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg">
<svg id="textBox" x="0" y="0" width="300" height="400" onload="init(evt)">
</svg>
</svg>
Setting width and height to 100% causes problems with the scaling of the text. The text frequently extends beyond what I set the width limit to. I've uploaded an example at the yahoo svg group. Thanks.
Sean
- Converting the content in text:wrap to batik doesn't support
2006-01-27 05:28:42 JKEOTSVG [Reply]
Hi,
Its a great work, But the tag element couldn't able to understand by bakik or xslfo, Is there any solution for this.
Thanks and Regards
Sathya
- Broken example file
2005-12-09 13:04:29 andoporfe [Reply]
http://www.xml.com/2002/09/11/examples/TextWrap.svg ...is currently broken. Error message: "prefix not bound to a namespace", for the script element in Firefox 1.5. I haven't checked it with another browser so I don't know if it's a Mozilla problem or an author problem.
- Setting line advance
2004-10-15 10:15:45 RobMelville [Reply]
I found I had to include:
style="line-advance:1em" in the <text:wrap> tag pair
to get this to work.
This stuff has saved me a lot of work, so many thanks!
- Making this work!!!
2004-02-09 13:16:05 Nicolas Jones [Reply]
Hi,
I spent many hours trying to make this work with no success!! But by chance, I tried again a few months later (tonight) and finally got it. So I thought I might as well post a small message here for those of you in a similare situation. My problem was simply that I wasn't understanding what is needed in a minimal way to make all this work. Here is how I see all this, and how it has allowed me to make this work.
1) get the javascript file http://www.xml.com/2002/09/11/examples/TextWrap.es and put it in the same folder as your svg file where you are tying to do text wrap
2a) If you want to test the tutorial file, just download it, stick it in the same folder as the javascript file of point 1) and launch the svg file -> it works.
2b) If you want to try using the text wrap in another svg doc, you need to include the following things in your svg doc:
- you will need to add the following namespaces to the first <svg> tag of your document:
xmlns="http://www.w3.org/2000/svg" xmlns:text="http://xmlns.graougraou.com/svg/text/" xmlns:a3="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" a3:scriptImplementation="Adobe" onload="TextWrap._init()"
- you will need to add a definition to your code: <defs>
<script type="text/ecmascript" xlink:href="TextWrap.es" a3:scriptImplementation="Adobe" />
</defs>
With these to things, you can simply use:
<text:wrap x="20" y="20" width="100">your text</text:wrap> and it works great!!! That's how I made it work. Now you can start playing around with all the rest.
Hope this can help someone who is stuck like I was...
Dj Jay
PS
One problem I have already seen: if a word is longer than the width of the wrap-box, then Adobe's SVG viewer (v3.0) just get's stuck, and therefore youre browser too... :(
- Making this work!!!
2005-08-07 17:59:34 adrianbj [Reply]
Hi All, Thanks for this script - but for some reason I keep getting a "TypeError: style has no properties" error. I have tried for hours to debug this. Would really appreciate any input on what I need to change.
I get the rest of the output for the document (not included in the code below), but don't see anything at all of the text that is within the <text:wrap>
Thanks,
Adrian
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20000303 Stylable//EN"
"http://www.w3.org/TR/2000/03/WD-SVG-20000303/DTD/svg-20000303-stylable.dtd">
<svg a3:scriptImplementation="Adobe" onload="TextWrap._init()" width="670" height="550" xmlns:a3="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" xmlns:text="http://xmlns.graougraou.com/svg/text/" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg-20000303-stylable" zoomAndPan="magnify">
<defs>
<script type="text/ecmascript" xlink:href="TextWrap.es" a3:scriptImplementation="Adobe"/>
</defs>
<text:wrap x="250" y="25" width="100" style="font-family: arial; font-size: 10px; font-weight: normal; fill: #000000; text-rendering: optimizeLegibility; text-align: justify; line-interval: 1.5em"> High secondary symptoms indicate serious problems, but low primary indicates other factors may also be involved in causing the conditions.</text:wrap>
</svg>
- Making this work!!!
2006-08-20 16:40:13 ErikBorgen [Reply]
Hi,
Have just joined the SVG world and got same problem. With some patience I found the reason: The text string is not allowed to start with a space.
- Making this work!!!
- Making this work!!!
- Simple Text Wrapping
2004-01-29 17:20:04 John Freeman [Reply]
Antoine,
I have been looking all over for something like this, great job!!
What steps should be taken if a single word overflows the width of the textbox. As it stands now the browser (IE6) locks up. It would make sense to chop the word at the textbox width and continue on to the next line, but how would you implement that?
thanks
John
- Newby question.
2002-12-13 00:31:03 julien bloit [Reply]
Hi,
I got really excited with this article, think it will help saving a few tspan tags for the purpose of text layout. Great job Antoine!
I'm now trying to go my own way with this example, but it's only a few days since my first SVG, and I'm getting trouble figuring out where is the TextWrap class defined. Is it done in an external file? I've tried to copy/paste the SVG source of the demo, and obviously, some file is missing for the class definition.
Another remark/question: what difference do you make between TextFlow and TextWrap? It seemed to me you're talking about the same class.
Sorry for that kind of beginner's question, and thanks for the articles!
- Newby question.
2003-08-04 04:41:30 Bruno - [Reply]
It does not look like you got an answer.
to make it work, download the javascript file where the textwrap object is implemented.
http://www.xml.com/2002/09/11/examples/TextWrap.es
Bruno
- Newby question.
