UNPKG

svg.textmorph.js

Version:

Enables textmorphing / textanimation in svg.js

302 lines (204 loc) 6.59 kB
/*! svg.textmorph.js - v0.1.0 - 2015-10-23 * Copyright (c) 2015 Ulrich-Matthias Schäfer; Licensed MIT */ ;(function () { function parseXML(xmlStr){ return ( new window.DOMParser() ).parseFromString(xmlStr, "text/xml"); } function loadFont(filename, cb){ var ajax = new XMLHttpRequest(); ajax.onreadystatechange = function() { if (ajax.readyState === 4) { if (ajax.status === 200) { cb(ajax.responseXML); }else{ cb() } } }; ajax.open('GET', filename); ajax.send(); } SVG.fonts = {} SVG.SVGFont = SVG.invent({ create: function(source, cb, lazy, undefined){ if(typeof cb == 'boolen'){ lazy = cb; cb = function(){} }else{ cb = cb || function(){} } cb = cb.bind(this) this.lazy = lazy === undefined ? true : false var node; var fontLoadedCb = function(node, cb){ if(!node) throw('Given Parameter is neither XML nor ID nor a fontfile with this name was found. "'+source+'" given') this.source = node.getElementsByTagName('font')[0]; this.family = this.source.firstElementChild.getAttribute('font-family') SVG.fonts[this.family] = this this.lazy || this.loadAll() cb() }.bind(this) try{ fontLoadedCb(parseXML(source)) }catch(e){ node = document.getElementById(source) node ? fontLoadedCb(node) : loadFont(source, function(node){ fontLoadedCb(node, cb) }) } }, extend: { lazy:true, cache:{}, loadLazy: function(glyphs){ if(!this.lazy) return var i = glyphs.length while(i--){ if(this.cache[glyphs[i]]) continue this.cache[glyphs[i]] = getPathAttributes(this.source.querySelector('glyph[unicode="'+glyphs[i]+'"]')) } return this }, loadAll: function(){ this.lazy = false var glyphs = this.source.querySelectorAll('glyph') , i = glyphs.length while(i--) { var attr = getPathAttributes(glyphs[i]) this.cache[attr.unicode] = attr } return this }, fontSize: function(size){ this.size = parseFloat(size) return this }, getPathArray: function(glyphs, cb){ // load needed glyphs into cache this.loadLazy(glyphs) // helper variables var uPerEm = this.source.firstElementChild.getAttribute('units-per-em') , cache = this.cache , h = this.source.getAttribute('horiz-adv-x') , x = 0 , scale = this.size / uPerEm for(var i = 0, len = glyphs.length; i < len; ++i){ if(glyphs[i-1]){ hkern = this.source.querySelector('hkern[u1="'+glyphs[i-1]+'"][u2="'+glyphs[i]+'"]') if(hkern){ x -= parseFloat(hkern.getAttribute('k')) * scale } } var p = new SVG.PathArray(cache[glyphs[i]].d) , box = p.bbox() if(box.height && box.width) p.size(box.width * scale, -box.height * scale) p.move(x,(uPerEm - box.y - box.height)*scale) cb.call(this, p, i) x += parseFloat(cache[glyphs[i]]['horiz-adv-x'] || h) * scale; } return { x:x, uPerEm:uPerEm, scale:scale } } } }) function getPathAttributes(a){ a = a.attributes var i = a.length , b = {} while(i--){ b[a[i].nodeName] = SVG.regex.isNumber.test(a[i].nodeValue) ? parseFloat(a[i].nodeValue) : a[i].nodeValue } // ensure that the glyph has a path if(!b['d'])b['d'] = 'M0 0' return b } SVG.MorphText = SVG.invent({ // Initialize node create: 'g' // Inherit from , inherit: SVG.G // Add class methods , extend: { glyphs:[] , family:SVG.defaults.attrs['font-family'].split(',')[0].trim() , size:SVG.defaults.attrs['font-size'] , font: function(k, v){ if(v == null){ for(var i in k) this.font(i, k[i]) return this } this[k] = v this.text(this.content) return this } , text: function(glyphs){ this.content = glyphs this.clear() this.glyphs = [] var svgFont = this.family instanceof SVG.SVGFont ? this.family : SVG.fonts[this.family] if(!svgFont) return this var font = svgFont .fontSize(this.size) .getPathArray(glyphs, function(pathArray, index){ var p = this.path(pathArray) this.glyphs.push(p) p.glyph = glyphs[index] }.bind(this)) this.remember('font', font) return this } } , construct: { // Create a group element morphText: function(text) { return this.put(new SVG.MorphText).text(text) } } }) function fixTextLength(glyphs){ var a = this.remember('font') , p while(this.glyphs.length < glyphs.length){ p = this.path('M0 0').move(a.x, a.uPerEm*a.scale) p.glyph = ' ' this.glyphs.push(p) } while(this.glyphs.length > glyphs.length){ glyphs += ' ' } // maybe move that to the top? while(glyphs[glyphs.length - 1] == ' ' && this.glyphs[glyphs.length - 1].glyph == ' '){ this.glyphs.pop().remove() glyphs = glyphs.slice(0, - 1) } return glyphs } SVG.extend(SVG.FX, { text: function(glyphs){ if(!this.target.glyphs) throw 'Text not animatable' var svgFont = this.target.family instanceof SVG.SVGFont ? this.target.family : SVG.fonts[this.target.family] if(!svgFont) throw('SVG font "'+svgFont.family+'" not (yet) loaded') // this.target.glyphs are changed per reference, glyphs is returned glyphs = fixTextLength.call(this.target, glyphs) var font = svgFont .fontSize(this.target.size) .getPathArray(glyphs, function(pathArray, index){ var bbox = this.glyphs[index].bbox() var box = pathArray.bbox() var scale = this.remember('font').scale if(!bbox.width || !bbox.height){ this.glyphs[index].move(0, -(box.y + box.height/2)*scale) }else if(!box.height || !box.width){ pathArray.move(0,-bbox.cy * scale) } // update glyph representation this.glyphs[index].glyph = glyphs[index] // animate glyph this.glyphs[index].animate().plot(pathArray) }.bind(this.target)) return this } }) }).call(this);