I'm attempting to write a RaphaelJS function that will take existing text nodes within a Raphael paper instance and convert them into paths.
The goal is to replicate the position, size and attribute of the text exactly as it appears on the page, but have it rendered using paths instead of text. I cannot initially render the text using the Raphael paper.print() function because the text is updated dynamically and requires "text" based attributes to do so. Converting existing text nodes to paths will occur as the "final" step in the process (after the text modifications are complete).
I am doing this to eliminate the need for having fonts installed to view or handle the SVG later.
The challenges I face are:
Text nodes may include tspans with
x
anddy
definitions. The paths created must line it perfectly witch each of the childNode letters (tspans).Retrieving the actual position data of text node, and each tspan. This is where I'm having trouble and hopefully someone with more experience can assist me. Since stroke widths and other attributes affect the positioning/bbox values, I'm not sure what's the most efficient method of obtaining the correct positioning data for the text.
What I have tried so far:
A simple breakdown of my code.
I wrote a custom attribute function, textFormat, that formats the text in a staggered formation. This function parses the text node, splits it by each letter adding a new line \n
character, and adjusts the positioning to look staggered.
The textToPaths function is a paper function that is supposed to loop through the paper nodes, and convert all found text nodes into path using the Raphael paper.print() function. This is the function I am having trouble with.
View the Complete JSFiddle Example Here
The problem code
I'm not sure how to obtain accurate and consistent x
and y
values to pass into the paper.print()
function. Right now, I am using getBoundingClientRect()
but it's still off and skewed. My assumption is the stroke widths are affecting the x and y calculations.
//Loop through each tspan and print the path for each.
var i,
children = node.node.childNodes,
len = children.length;
for (i = 0; i < len; i++) {
var tspan = children[i],
tspanText = tspan.innerHTML,
x = tspan.getBoundingClientRect().left - node.node.getBoundingClientRect().left, //How do I get the correct x value?
y = tspan.getBoundingClientRect().top - node.node.getBoundingClientRect().top; //How do I get the correcy y value?
var path = paper.print(x, y, tspanText, font, fontSize),
attrs = node.attrs;
delete attrs.x;
delete attrs.y;
path.attr(attrs);
path.attr('fill', '#ff0000'); //Red, for testing purposes.
}
Complete Code View the JSFiddle Example
//Register Cufon Font
var paper = Raphael(document.getElementById('paper'), '600', '600');
var text1 = paper.text(100, 100, 'abc').attr({fill: 'none',stroke: '#000000',"stroke-width": '12',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text2 = paper.text(100, 100, 'abc').attr({fill: 'none',stroke: '#ffffff',"stroke-width": '8',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text3 = paper.text(100, 100, 'abc').attr({fill: '#000000',stroke: '#ffffff',"stroke-width": '0',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text = paper.set(text1, text2, text3);
text.attr('textFormat', 'stagger');
/* paper.textToPaths
* Description: Converts all text nodes to paths within a paper.
*
* Example: paper.textToPaths();
*/
(function(R) {
R.fn.textToPaths = function() {
var paper = this;
//Loop all nodes in the paper.
for (var node = paper.bottom; node != null; node = node.next ) {
if ( node.node.style.display === 'none' || node.type !== "text" || node.attrs.opacity == "0") continue; //skip non-text and hidden nodes.
//Get the font config for this text node.
var text = node.attr('text'),
fontFamily = node.attr('font-family'),
fontSize = parseInt(node.attr('font-size')),
fontWeight = node.attr('font-weight'),
font = paper.getFont(fontFamily, fontWeight);
//Loop through each tspan and print the path for each.
var i,
children = node.node.childNodes,
len = children.length;
for (i = 0; i < len; i++) {
var tspan = children[i],
tspanText = tspan.innerHTML,
x = tspan.getBoundingClientRect().left - node.node.getBoundingClientRect().left, //How do I get the correct x value?
y = tspan.getBoundingClientRect().top - node.node.getBoundingClientRect().top; //How do I get the correcy y value?
var path = paper.print(x, y, tspanText, font, fontSize),
attrs = node.attrs;
delete attrs.x;
delete attrs.y;
path.attr(attrs);
path.attr('fill', '#ff0000'); //Red, for testing purposes.
}
}
};
})(window.Raphael);
textToPaths = function() {
//Run textToPaths
paper.textToPaths();
};
/* Custom Element Attribute: textFormat
* Description: Formats a text element to either staggered or normal text.
*
* Example: element.attr('textFormat, 'stagger');
*/
paper.customAttributes.textFormat = function( value ) {
// Sets the SVG dy attribute, which Raphael doesn't control
var selector = Raphael.svg ? 'tspan' : 'v:textpath',
has = "hasOwnProperty",
$node = $(this.node),
text = $node.text(),
$tspans = $node.find(selector);
console.log('format');
switch(value)
{
case 'stagger' :
var stagger = function(el) {
var R = Raphael,
letters = '',
newline = '\n';
for (var c=0; c < text.length; c++) {
var letter = text[c],
append = '';
if(c < text.length - 1)
append = newline;
letters += letter+append;
}
el.attr('text', letters);
var children = el.node.childNodes;
var i,
a = el.attrs,
node = el.node,
len = children.length,
letterOffset = 0,
tspan,
tspanHeight,
tspanWidth,
tspanX,
prevTspan,
prevTspanRight = 0,
tspanDiff = 0,
tspanTemp,
fontSize,
leading = 1.2,
tempText;
for (i = 0; i < len; i++) {
tspan = children[i];
tspanHeight = tspan.getComputedTextLength();
tspanWidth = tspan.getComputedTextLength();
tspanX = tspan.getAttribute('x'),
prevTspanRight = tspan.getBoundingClientRect().right
if(tspanX !== null)
{
tspanDiff = tspanDiff + prevTspanRight - tspan.getBoundingClientRect().left;
var setX = parseInt(tspanX) + parseInt(tspanDiff);
tspan.setAttribute('x', setX);
tspan.setAttribute('dy', 15);
}
prevTspan = tspan;
}
}
stagger(this);
break;
case 'normal' :
this.attr('text', text);
break;
default :
this.attr('text', text);
break;
}
eve("raphael.attr.textFormat." + this.id, this, value);
// change no default Raphael attributes
return {};
};
staggerText = function() {
//Run textToPaths
text.attr('textFormat', 'stagger');
};
If anyone can help me solve this problem I would greatly appreciate it. Thanks!