XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

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

Big Lists in Small Spaces

SVG At the Movies

Mobile SVG

SVG and Typography: Animation

Going Mobile With SVG: Standards

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.



1 to 9 of 9
  1. Ref to parent element missing
    2006-08-20 16:47:55 ErikBorgen
  2. Firefox 1.5
    2006-08-16 11:45:50 richbk
  3. scaling
    2006-01-28 12:40:03 couloir
  4. Converting the content in text:wrap to batik doesn't support
    2006-01-27 05:28:42 JKEOTSVG
  5. Broken example file
    2005-12-09 13:04:29 andoporfe
  6. Setting line advance
    2004-10-15 10:15:45 RobMelville
  7. Making this work!!!
    2004-02-09 13:16:05 Nicolas Jones
  8. Simple Text Wrapping
    2004-01-29 17:20:04 John Freeman
  9. Newby question.
    2002-12-13 00:31:03 julien bloit
1 to 9 of 9