javascriptsvgraphael

Convert all SVG text nodes into path nodes with Raphael JS


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:

  1. Text nodes may include tspans with x and dy definitions. The paths created must line it perfectly witch each of the childNode letters (tspans).

  2. 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!


Solution

  • You can convert fonts to SVG/Canvas path commands using Opentype.js.

    The lib will return to you a series of path drawing commands; these are intended for drawing on an HTML5 <canvas> element.

    However it is trivial to build an SVG path with those commands since the font-conversion does not include any commands that are compatible with Canvas path drawing that would be incompatible with an SVG path command.