訪問http://caniuse.com/#search=svg
IE對SVG動畫支持很差。
那咱們就說服老闆不支持IE。
老闆任性,硬着頭皮上。javascript
訪問 https://github.com/FakeSmile/FakeSmile 這個項目。
它用模擬的方式實現SVG動畫。
使用demo以下:html
<!DOCTYPE HTML> <head> </head> <script type="text/javascript" src="./svg.js"></script> <body> <meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" /> <svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" viewBox="0 0 120 40" xml:space="preserve"> <rect x="0" y="0" width="10" height="10" style="fill:blue"> <animate attributeName="x" from="0" to="100" dur="5s" repeatCount="indefinite" /> </rect> </svg> <svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg"> <circle cx="100" cy="100" r="30" stroke='red' strokeWidth= 1 fill = 'none'> <animate attributeType="XML" attributeName="r" from="20" to="60" dur="2s" repeatCount="indefinite"> </animate> </circle> <circle cx="100" cy="100" r="20" stroke='red' strokeWidth= 1 fill = 'none'> <animate attributeType="XML" attributeName="r" from="10" to="40" dur="2s" repeatCount="indefinite"> </animate> </circle> <circle cx="100" cy="100" r="15" stroke='red' strokeWidth= 1 fill = 'none'> <animate attributeType="XML" attributeName="r" from="5" to="20" dur="2s" repeatCount="indefinite"> </animate> </circle> <circle cx="100" cy="100" r="5" stroke='red' strokeWidth= 1 fill = 'red'></circle> </svg> </body> </html>
svg.jsjava
/* @id {7eeff186-cfb4-f7c3-21f2-a15f210dca49} @name FakeSmile @version 0.3.0 @description SMIL implementation in ECMAScript @creator David Leunen (leunen.d@gmail.com) @homepageURL http://leunen.me/fakesmile/ @ff_min_version 2.0 @ff_max_version 3.* */ // ==UserScript== // @name smil // @namespace svg.smil // ==/UserScript== /* MIT or GPLv3 Licenses */ /* Copyright 2008 David Leunen Copyright 2012 Helder Magalhaes */ /** * Milliseconds Per Frame - relation between animation smoothness and resources usage: * 83 for ~12fps (standard quality web animation; low CPU usage; slightly jumpy; recommended for discrete or slow-motion animations); * 67 for ~15fps (high quality web animation; reasonable resources usage; recommended for most use-cases); * 40 for 25fps ("cine"-look; recommended for good quality animations on television systems); * 33 for ~30fps (half LCD refresh rate; recommended for high quality animations on desktop systems); * 25 for 40fps (very smooth animation; recommended for high quality animations on dedicated desktop systems); * 17 for ~60fps (LCD refresh rate; high CPU and system overhead; only recommended for very high quality animations running on high-end systems). * Lower values are *not* recommended - may cause an overall negative impact on the Operating System and a relevant energy consumption increase! * References: * http://animation.about.com/od/faqs/f/faq_fpsnumber.htm * http://en.wikipedia.org/wiki/Frame_rate#Frame_rates_in_film_and_television * https://www.nczonline.net/blog/2011/12/14/timer-resolution-in-browsers/ */ var mpf = 67; var splinePrecision = 25; var svgns="http://www.w3.org/2000/svg"; var smilanimns="http://www.w3.org/2001/smil-animation"; var smil2ns="http://www.w3.org/2001/SMIL20"; var smil21ns="http://www.w3.org/2005/SMIL21"; var smil3ns="http://www.w3.org/ns/SMIL30"; var timesheetns="http://www.w3.org/2007/07/SMIL30/Timesheets"; var xlinkns="http://www.w3.org/1999/xlink"; var animators = new Array(); // all animators var id2anim = new Object(); // id -> animation elements (workaround a Gecko bug) var animations = new Array(); // running animators var timeZero; // timeline start-up timestamp var prevTime; // previous render timestamp var animTimer; // render loop timer id, when active /** * If declarative animations are not supported, * the document animations are fetched and registered. */ function initSMIL() { if (document.documentElement.getAttribute("smiling")=="fake") return; document.documentElement.setAttribute("smiling", "fake"); smile(document); timeZero = new Date(); prevTime = new Date(0); // not yet rendered // I schedule them (after having instantiating them, for sync-based events) // (it doesn't work either: first 0s animation don't trigger begin event to the following -> make it asynchronous) for (var i=0, j=animators.length; i<j; ++i) animators[i].register(); } function getURLCallback(data) { if (data.success) smile(parseXML(data.content, document)); } function xhrCallback() { if (this.readyState==4 && this.status==200 && this.responseXML!=null) smile(this.responseXML); } function smile(animating) { var request = null; var src = null; var impl = document.implementation; // namespace-to-process cache // ("process" in the sense of "feature check states that support by script is needed") // (map is initialized this way to avoid variables names being picked up as key instead of their value) var ns2proc = {}; // NOTE: feature strings are broken in ASV - apparently only declarative switch declarations work // (we have already filter this implementation, though, during the loading phase) // http://tech.groups.yahoo.com/group/svg-developers/message/61236 ns2proc[svgns] = !impl.hasFeature("http://www.w3.org/TR/SVG11/feature#Animation", "1.1"); //&& !impl.hasFeature("org.w3c.svg.animation", "1.0"); ns2proc[smilanimns] = !impl.hasFeature(smilanimns, "1.1"); ns2proc[smil2ns] = !impl.hasFeature(smil2ns, "2.0"); ns2proc[smil21ns] = !impl.hasFeature(smil21ns, "2.1"); ns2proc[smil3ns] = !impl.hasFeature(smil3ns, "3.0"); ns2proc[timesheetns] = !impl.hasFeature(timesheetns, "1.0"); var animates = animating.getElementsByTagName("*"); for (var i=0, j=animates.length; i<j; ++i) { var anim = animates.item(i); var nodeName = anim.localName; var namespaceURI = anim.namespaceURI; switch (nodeName.length) { case 4: // "link".length if ((nodeName=="link" || nodeName=="LINK") && anim.getAttribute("rel")=="timesheet" && anim.getAttribute("type")=="application/smil+xml") { src = anim.getAttribute("src"); if (src) break; } continue; case 9: // "timesheet".length if (nodeName=="timesheet" && ns2proc[anim.namespaceURI]) { src = anim.getAttribute("href"); if (src) break; } continue; case 3: // "set".length if (nodeName=="set") { break; } continue; case 7: // "animate".length if (nodeName=="animate") { break; } continue; case 12: // "animateColor".length if (nodeName=="animateColor") { break; } continue; case 13: // "animateMotion".length if (nodeName=="animateMotion") { break; } continue; case 16: // "animateTransform".length if (nodeName=="animateTransform") { break; } continue; default: continue; } // deal with external timesheets if (src && src.length > 0) { if (!request){ // lazy initialization of XHR request = window.XMLHttpRequest ? new XMLHttpRequest() : window.ActiveXObject ? new ActiveXObject("MSXML2.XMLHTTP.3.0") : null; if (request) { if (request.overrideMimeType) request.overrideMimeType('text/xml'); request.onreadystatechange = xhrCallback; } } if (request) { request.open("GET", src, false); request.send(null); } else if (window.getURL && window.parseXML) { getURL(src, getURLCallback); } // reset variable src = null; continue; } // deal with animations if (ns2proc[anim.namespaceURI]) { var targets = getTargets(anim); var elAnimators = new Array(); for (var k=0; k<targets.length; ++k) { var target = targets[k]; var animator = new Animator(anim, target, k); animators.push(animator); elAnimators[k] = animator; } anim.animators = elAnimators; var id = anim.getAttribute("id"); if (id) id2anim[id] = anim; } } } function getTargets(anim) { if (anim.hasAttribute("select")) return select(anim); var href = anim.getAttributeNS(xlinkns, "href"); if (href!=null && href!="") return [document.getElementById(href.substring(1))]; else { var target = anim.parentNode; if (target.localName=="item" && (target.namespaceURI==timesheetns || target.namespaceURI==smil3ns)) return select(target); return [target]; } } function select(element) { var selector = element.getAttribute("select"); var parent = element.parentNode; while(parent && parent.nodeType==1) { if (parent.localName=="item" && (parent.namespaceURI==timesheetns || parent.namespaceURI==smil3ns)) selector = parent.getAttribute("select")+" "+selector; parent = parent.parentNode; } return document.querySelectorAll(selector); } function getEventTargetsById(id, ref) { var element = null; if (id=="prev") { element = ref.previousSibling; while(element && element.nodeType!=1) element = element.previousSibling; } if (element==null) element = document.getElementById(id); if (element==null) element = id2anim[id]; // because getElementById doesn't returns SMIL elements in Gecko if (element==null) return null; if (element.animators) return element.animators; return [element]; } /** * Corresponds to one <animate>, <set>, <animateTransform>, ... * (there can be more than one Animator for each element) */ Animator.prototype = { /** * Registers the animation. * It schedules the beginnings and endings. */ register : function() { var begin = this.anim.getAttribute("begin"); if (begin) this.schedule(begin, this.begin); else this.begin(0); var end = this.anim.getAttribute("end"); if (end) this.schedule(end, this.finish); }, /** * Schedules the starts or ends of the animation. */ schedule : function(timeValueList, func) { var me = this; // I do that because if I use "this", the addEventListener understands the event source var timeValues = timeValueList.split(";"); for (var i=0; i<timeValues.length; ++i) { var time = timeValues[i].trim(); if (time.length>11 && time.substring(0,10)=="wallclock(") { var wallclock = new Date(); wallclock.setISO8601(time.substring(10,time.length-1)); if (!isNaN(wallclock.getTime())) { var now = new Date(); var diff = wallclock-now; func.call(me, diff); } } else if (isNaN(parseInt(time))) { var offset = 0; var io = time.indexOf("+"); if (io==-1) io = time.indexOf("-"); if (io!=-1) { offset = toMillis(time.substring(io).replace(/ /g, "")); time = time.substring(0, io).trim(); } io = time.indexOf("."); var elements; if (io==-1) { elements = [this.target]; } else { var id = time.substring(0, io); if (id.indexOf("index(")==0) id = id.substring(6,id.length-1)+this.index; elements = getEventTargetsById(id, this.anim); } var event = time.substring(io+1); var call = funk(func, me, offset); for (var j=0; j<elements.length; ++j) { var element = elements[j]; if (element==null) continue; element.addEventListener(event, call, false); } } else { time = toMillis(time); func.call(me, time); } } }, /** * Remembers the initial value of the animated attribute. * This function is overridden. */ getCurVal : function() { if (this.attributeType=="CSS") { // should use this.target.getPresentationAttribute instead return this.target.style.getPropertyValue(this.attributeName); } else { //var animAtt = this.target[this.attributeName]; //if (animAtt && animAtt.animVal) // return animAtt.animVal.value; //else return this.target.getAttributeNS(this.namespace, this.attributeName); } }, /** * Starts the animation. * I mean the very beginning of it. * Not called when repeating. */ begin : function(offset) { if (this.restart=="never" || (this.running && this.restart=="whenNotActive")) return; if (this.running) this.finish(); if (offset && offset>0) { var me = this; var myself = this.begin; var call = function() {myself.call(me)}; window.setTimeout(call, offset); return; } this.startTime = new Date(); if (offset && offset<0) { this.startTime.setTime(this.startTime.getTime()+offset); if (this.startTime<timeZero) return; } this.stop(); this.running = true; var initVal = this.getCurVal(); this.realInitVal = initVal; // TODO // I should get the inherited value here (getPresentationAttribute is not supported) if (!initVal && propDefaults[this.attributeName] ) initVal = propDefaults[this.attributeName]; if (this.anim.nodeName=="set") this.step(this.to); this.iteration = 0; if (this.values) { this.animVals = this.values.split(";"); for (var i=0; i<this.animVals.length; ++i) this.animVals[i] = this.animVals[i].trim(); } else { this.animVals = new Array(); if (this.from) this.animVals[0] = this.from; else this.animVals[0] = initVal; if (this.by && this.animVals[0]) this.animVals[1] = this.add(this.normalize(this.animVals[0]), this.normalize(this.by)); else this.animVals[1] = this.to; } if (this.animVals[this.animVals.length-1]) { this.freezed = this.animVals[this.animVals.length-1]; if (this.animVals[0]) { if ( (this.animVals[0][0]=="#" || colors[this.animVals[0]] || (this.animVals[0].length>5 && this.animVals[0].trim().substring(0,4)=="rgb(")) && (this.freezed[0]=="#" || colors[this.freezed] || (this.freezed.length>5 && this.freezed.trim().substring(0,4)=="rgb(")) ) this.color(); else { var cp = new Array(); var oneVal = this.animVals[0]; var qualified = getUnit(oneVal); cp[0] = qualified[0]; this.unit = qualified[1]; for (var i=1; i<this.animVals.length; ++i) { var oneVal = this.animVals[i]; var qualified = getUnit(oneVal); if (qualified[1]==this.unit) cp[i] = qualified[0]; else { cp = this.animVals; break; } } this.animVals = cp; } } } this.iterBegin = this.startTime; animations.push(this); // if this is the first running animator, start the rendering loop if (!animTimer) { // asynchronous to render all animators, listeners, etc. starting at this frame window.setTimeout(animate, 0); // schedules the rendering loop animTimer = window.setInterval(animate, mpf); } for (var i=0; i<this.beginListeners.length; ++i) this.beginListeners[i].call(); var onbegin = this.anim.getAttribute("onbegin"); if (onbegin) eval(onbegin); }, /** * This function is overridden for multiple values attributes (scale, rotate, translate). */ normalize : function(value) { return value; }, /** * Sums up two normalized values. */ add : function(a, b) { return ""+(parseFloat(a)+parseFloat(b)); }, /** * Computes and applies the animated value for a given time. * Returns false if this animation has been stopped (removed from the running array). */ f : function(curTime) { var dur = this.computedDur; if (isNaN(dur)) return true; var beginTime = this.iterBegin; var diff = curTime-beginTime; var percent = diff/dur; if (percent>=1) return this.end(curTime); var iteration = this.iteration; if (this.repeatCount && this.repeatCount!="indefinite" && (iteration+percent)>=this.repeatCount) { if (this.fill=="freeze") this.freezed = this.valueAt(this.repeatCount-iteration); return this.end(curTime); } if (this.repeatDur && this.repeatDur!="indefinite" && (curTime-this.startTime)>=toMillis(this.repeatDur)) { if (this.fill=="freeze") { var div = toMillis(this.repeatDur)/dur; this.freezed = this.valueAt(div-Math.floor(div)); } return this.end(curTime); } var anim = this.anim; if (anim.localName=="set") return true; var curVal = this.valueAt(percent); this.step(curVal); return true; }, isInterpolable : function(from, to) { var areN = (!isNaN(from) && !isNaN(to)); if (!areN && from.trim().indexOf(" ")!=-1 && to.trim().indexOf(" ")!=-1) { var tfrom = from.trim().split(" "); var tto = to.trim().split(" "); areN = true; if (tfrom.length==tto.length) for (var i=0; i<tto.length; ++i) if (!this.isInterpolable(tfrom[i], tto[i])) return false; } return areN; }, valueAt : function(percent) { var tValues = this.animVals; if (percent==1) return tValues[tValues.length-1]; if (this.calcMode=="discrete" || !this.isInterpolable(tValues[0],tValues[1])) { if (this.keyTimes) { for (var i=1; i<this.keyTimes.length; ++i) if (this.keyTimes[i]>percent) return tValues[i-1]; return tValues[tValues.length-1]; } var parts = tValues.length; var div = Math.floor(percent*parts); return tValues[div]; } else { var index; if (this.keyTimes) { for (var i=1; i<this.keyTimes.length; ++i) if (this.keyTimes[i]>percent) { index = i-1; var t1 = this.keyTimes[index]; percent = (percent-t1)/(this.keyTimes[i]-t1); break; } if (i>=this.keyTimes.length) index = i-2; } else { var parts = tValues.length-1; index = Math.floor(percent*parts); percent = (percent%(1/parts))*parts; } if (this.calcMode=="spline") percent = this.spline(percent, index); return this.interpolate(this.normalize(tValues[index]), this.normalize(tValues[index+1]), percent); } }, spline : function(percent, index) { var path = this.keySplines[index]; var tot = path.getTotalLength(); var step = tot/splinePrecision; for (var i=0; i<=tot; i+=step) { var pt = path.getPointAtLength(i); if (pt.x>percent) { var pt1 = path.getPointAtLength(i-step); percent -= pt1.x; percent /= pt.x-pt1.x; return pt1.y+((pt.y-pt1.y)*percent); } } var pt = path.getPointAtLength(tot); var pt1 = path.getPointAtLength(tot-step); percent -= pt1.x; percent /= pt.x-pt1.x; return pt1.y+((pt.y-pt1.y)*percent); }, /** * Performs the interpolation. * This function is overridden. */ interpolate : function(from, to, percent) { if (!this.isInterpolable(from, to)) { if (percent<.5) return from; else return to; } if (from.trim().indexOf(" ")!=-1) { var tfrom = from.split(" "); var tto = to.split(" "); var ret = new Array(); for (var i=0; i<tto.length; ++i) ret[i] = parseFloat(tfrom[i])+((tto[i]-tfrom[i])*percent); return ret.join(" "); } return parseFloat(from)+((to-from)*percent); }, /** * Apply a value to the attribute the animator is linked to. * This function is overridden. */ step : function(value) { var attributeName = this.attributeName; var attributeType = this.attributeType; if (attributeType=="CSS") { // workaround a Gecko and WebKit bug if (attributeName=="font-size" && !isNaN(value)) value += "px"; else if (this.unit) value += this.unit; this.target.style.setProperty(attributeName, value, ""); } else { if (this.unit) value += this.unit; //var animAtt = this.target[attributeName]; //if (animAtt && animAtt.animVal) // animAtt.animVal.value = value; //else this.target.setAttributeNS(this.namespace, attributeName, value); } }, /** * Normal end of the animation: * it restarts if repeatCount. */ end : function(now) { if (!this.repeatCount && !this.repeatDur) return this.finish(); else { ++this.iteration; if (this.repeatCount && this.repeatCount!="indefinite" && this.iteration>=this.repeatCount) return this.finish(); else if (this.repeatDur && this.repeatDur!="indefinite" && (now-this.startTime)>=toMillis(this.repeatDur)) return this.finish(); else { if (this.accumulate=="sum") { var curVal = this.getCurVal(); if (!curVal && propDefaults[this.attributeName] ) curVal = propDefaults[this.attributeName]; if (this.by && !this.from) { this.animVals[0] = curVal; this.animVals[1] = this.add(this.normalize(curVal), this.normalize(this.by)); } else { for (var i=0; i<this.animVals.length; ++i) this.animVals[i] = this.add(this.normalize(curVal), this.normalize(this.animVals[i])); } this.freezed = this.animVals[this.animVals.length-1]; } this.iterBegin = now; for (var i=0; i<this.repeatIterations.length; ++i) { if (this.repeatIterations[i]==this.iteration) this.repeatListeners[i].call(); } var onrepeat = this.anim.getAttribute("onrepeat"); if (onrepeat) eval(onrepeat); } } return true; }, /** * Really stop of the animation (it doesn't repeat). * Freezes or removes the animated value. */ finish : function(offset) { if (this.min && this.min!="indefinite") { var now = new Date(); if ((now-this.startTime)>=this.computedMin) return true; } if (offset && offset>0) { var me = this; var myself = this.finish; var call = function() {myself.call(me)}; window.setTimeout(call, offset); return true; } if (offset && offset<0) { var now = new Date(); now.setTime(now.getTime()+offset); if (now<this.startTime) return true; } var fill = this.fill; var kept = true; if (fill=="freeze") { this.freeze(); } else { this.stop(); this.step(this.realInitVal); kept = false; } if (this.running) { for (var i=0; i<this.endListeners.length; ++i) this.endListeners[i].call(); var onend = this.anim.getAttribute("onend"); if (onend) eval(onend); this.running = false; } return kept; }, /** * Removes this animation from the running array. */ stop : function() { for (var i=0, j=animations.length; i<j; ++i) if (animations[i]==this) { animations.splice(i, 1); // if this is the last running animator, stop the rendering loop if (!animations.length && animTimer) { window.clearInterval(animTimer); animTimer = null; } break; } }, /** * Freezes the attribute value to the ending value. */ freeze : function() { this.step(this.freezed); }, /** * Adds a listener to this animation beginning or ending. */ addEventListener : function(event, func, b) { if (event=="begin") this.beginListeners.push(func); else if (event=="end") this.endListeners.push(func); else if (event.length>7 && event.substring(0,6)=="repeat") { var iteration = event.substring(7,event.length-1); this.repeatListeners.push(func); this.repeatIterations.push(iteration); } }, /** * Returns the path linked to this animateMotion. */ getPath : function() { var mpath = this.anim.getElementsByTagNameNS(svgns,"mpath")[0]; if (mpath) { var pathHref = mpath.getAttributeNS(xlinkns, "href"); return document.getElementById(pathHref.substring(1)); } else { var d = this.anim.getAttribute("path"); if (d) { var pathEl = createPath(d); //pathEl.setAttribute("display", "none"); //this.anim.parentNode.appendChild(pathEl); return pathEl; } } return null; }, /** * Initializes this animator as a translation (x,y): * <animateTransform type="translate"> or * <animateMotion> without a path. */ translation : function() { if (this.by && this.by.indexOf(",")==-1) this.by = this.by+",0"; this.normalize = function(value) { var coords = value.replace(/,/g," ").replace(/ +/," ").split(/ /); coords[0] = parseFloat(coords[0]); if (coords.length==1) coords[1] = 0; //coords[1] = this.initVal.split(",")[1]; else coords[1] = parseFloat(coords[1]); return coords; }; this.add = function(a, b) { var x = a[0]+b[0]; var y = a[1]+b[1]; return x+","+y; }; this.isInterpolable = function(from, to) { return true; }; this.interpolate = function(from, to, percent) { var x = from[0]+((to[0]-from[0])*percent); var y = from[1]+((to[1]-from[1])*percent); return x+","+y; }; }, /** * Initializes this animator as a color animation: * <animateColor> or * <animate> on a color attribute. */ color : function() { this.isInterpolable = function(from, to) { return true; }; this.interpolate = function(from, to, percent) { var r = Math.round(from[0]+((to[0]-from[0])*percent)); var g = Math.round(from[1]+((to[1]-from[1])*percent)); var b = Math.round(from[2]+((to[2]-from[2])*percent)); var val = "rgb("+r+","+g+","+b+")"; return val; }; this.normalize = function(value) { var rgb = toRGB(value); if (rgb==null) return toRGB(propDefaults[this.attributeName]); return rgb; }; this.add = function(a, b) { var ret = new Array(); for (var i=0; i<a.length; ++i) ret.push(Math.min(a[i],255)+Math.min(b[i],255)); return ret.join(","); }; }, d : function() { this.isInterpolable = function(from, to) { return true; }; this.interpolate = function(from, to, percent) { var path = ""; var listFrom = from.myNormalizedPathSegList; var listTo = to.myNormalizedPathSegList; var segFrom, segTo, typeFrom, typeTo; for (var i=0, j=Math.min(listFrom.numberOfItems, listTo.numberOfItems); i<j; ++i) { segFrom = listFrom.getItem(i); segTo = listTo.getItem(i); typeFrom = segFrom.pathSegType; typeTo = segTo.pathSegType; // NOTE: in 'normalizedPathSegList', only 'M', 'L', 'C' and 'z' path data commands are expected if (typeFrom==1 || typeTo==1) // PATHSEG_CLOSEPATH path += " z "; else { var x = segFrom.x+((segTo.x-segFrom.x)*percent); var y = segFrom.y+((segTo.y-segFrom.y)*percent); if (typeFrom==2 || typeTo==2) // PATHSEG_MOVETO_ABS path += " M "; else if (typeFrom==4 || typeTo==4) // PATHSEG_LINETO_ABS path += " L "; // NOTE: need to be more strict here, as interpolating a 'C' command with an 'M' or an 'L' isn't yet supported // (additional trickery is required for dealing with different DOM interfaces and interpolating them) else if (typeFrom==6 && typeTo==6) { // PATHSEG_CURVETO_CUBIC_ABS var x1 = segFrom.x1+((segTo.x1-segFrom.x1)*percent); var y1 = segFrom.y1+((segTo.y1-segFrom.y1)*percent); var x2 = segFrom.x2+((segTo.x2-segFrom.x2)*percent); var y2 = segFrom.y2+((segTo.y2-segFrom.y2)*percent); path += " C "+x1+","+y1+" "+x2+","+y2+" "; } else // "unexpected" type found, which means that 'pathSegList' is being used // (incomplete support for segment interpolation therefore switch to a discrete approach) return (percent<.5? from: to).getAttribute("d"); path += x+","+y; } } return path; }; this.normalize = function(value) { var path = createPath(value); return path; }; } }; /** * Constructor: * - initializes * - gets the attributes * - corrects and precomputes some values * - specializes some functions */ function Animator(anim, target, index) { this.anim = anim; this.target = target; this.index = index; anim.targetElement = target; this.attributeType = anim.getAttribute("attributeType"); this.attributeName = anim.getAttribute("attributeName"); if (this.attributeType!="CSS" && this.attributeType!="XML") { // attributeType not specified, default stands for "auto" // "The implementation must first search through the list of CSS properties for a matching property name" // http://www.w3.org/TR/SVG11/animate.html#AttributeTypeAttribute if (propDefaults[this.attributeName] && this.target.style.getPropertyValue(this.attributeName)) this.attributeType = "CSS"; else this.attributeType = "XML"; } if (this.attributeType=="XML" && this.attributeName) { this.namespace = null; var chColon = this.attributeName.indexOf(":"); if (chColon != -1) { var prefix = this.attributeName.substring(0,chColon); this.attributeName = this.attributeName.substring(chColon+1); var node = target; while(node && node.nodeType==1) { var ns = node.getAttributeNS("http://www.w3.org/2000/xmlns/", prefix); if (ns) { this.namespace = ns; break; } node = node.parentNode; } } } if (this.attributeName=="d") this.d(); else if (this.attributeName=="points") { this.isInterpolable = function(from, to) { return true; }; this.interpolate = function(from, to, percent) { var ret = new Array(); var xyFrom, xyTo, x, y; for (var i=0, j=Math.min(from.length, to.length); i<j; ++i) { xyFrom = from[i].split(","); xyTo = to[i].split(","); x = parseFloat(xyFrom[0])+((parseFloat(xyTo[0])-xyFrom[0])*percent); y = parseFloat(xyFrom[1])+((parseFloat(xyTo[1])-xyFrom[1])*percent); ret.push(x+","+y); } return ret.join(" "); }; this.normalize = function(value) { var ar = value.split(" "); for (var i=ar.length-1; i>=0; --i) if (ar[i]=="") ar.splice(i,1); return ar; }; } this.from = anim.getAttribute("from"); this.to = anim.getAttribute("to"); this.by = anim.getAttribute("by"); this.values = anim.getAttribute("values"); if (this.values) { this.values = this.values.trim(); if (this.values[this.values.length-1]==";") this.values = this.values.substring(0, this.values.length-1); } this.calcMode = anim.getAttribute("calcMode"); this.keyTimes = anim.getAttribute("keyTimes"); if (this.keyTimes) { this.keyTimes = this.keyTimes.split(";"); for (var i=0; i<this.keyTimes.length; ++i) this.keyTimes[i] = parseFloat(this.keyTimes[i]); this.keyPoints = anim.getAttribute("keyPoints"); if (this.keyPoints) { this.keyPoints = this.keyPoints.split(";"); for (var i=0; i<this.keyPoints.length; ++i) this.keyPoints[i] = parseFloat(this.keyPoints[i]); } } this.keySplines = anim.getAttribute("keySplines"); if (this.keySplines) { this.keySplines = this.keySplines.split(";"); for (var i=0; i<this.keySplines.length; ++i) this.keySplines[i] = createPath("M 0 0 C "+this.keySplines[i]+" 1 1"); } this.dur = anim.getAttribute("dur"); if (this.dur && this.dur!="indefinite") this.computedDur = toMillis(this.dur); this.max = anim.getAttribute("max"); if (this.max && this.max!="indefinite") { this.computedMax = toMillis(this.max); if (!isNaN(this.computedMax) && this.computedMax>0 && (!this.computedDur || this.computedDur>this.computedMax)) this.computedDur = this.computedMax; } this.min = anim.getAttribute("min"); if (this.min) { this.computedMin = toMillis(this.min); if (!this.computedDur || this.computedDur<this.computedMin) this.computedDur = this.computedMin; } this.fill = anim.getAttribute("fill"); this.type = anim.getAttribute("type"); this.repeatCount = anim.getAttribute("repeatCount"); this.repeatDur = anim.getAttribute("repeatDur"); this.accumulate = anim.getAttribute("accumulate"); this.additive = anim.getAttribute("additive"); this.restart = anim.getAttribute("restart"); if (!this.restart) this.restart = "always"; this.beginListeners = new Array(); this.endListeners = new Array(); this.repeatListeners = new Array(); this.repeatIterations = new Array(); var nodeName = anim.localName; if (nodeName=="animateColor") { this.color(); } else if (nodeName=="animateMotion") { this.isInterpolable = function(from, to) { return true; }; this.getCurVal = function() { var curTrans = this.target.transform; if (curTrans && curTrans.animVal.numberOfItems>0) { var transList = curTrans.animVal; return decompose(transList.getItem(0).matrix, "translate"); } else return "0,0"; }; this.path = this.getPath(); if (this.path) { this.valueAt = function(percent) { var length = this.path.getTotalLength(); var point = this.path.getPointAtLength(percent*length); return point.x+","+point.y; }; } else { this.translation(); } this.freeze = function() { var val = this.valueAt(1); this.step(val); }; if (this.keyPoints && this.keyTimes) { this.pathKeyTimes = this.keyTimes; this.keyTimes = null; this.superValueAt = this.valueAt; this.valueAt = function(percent) { for (var i=1; i<this.keyPoints.length; ++i) { var fakePC = this.keyPoints[this.keyPoints.length-1] if (this.pathKeyTimes[i]>percent) { var pt = this.keyPoints[i-1]; if (this.calcMode=="discrete") fakePC = pt; else { var t1 = this.pathKeyTimes[i-1]; percent = (percent-t1)/(this.pathKeyTimes[i]-t1); fakePC = pt+((this.keyPoints[i]-pt)*percent) } break; } } return this.superValueAt(fakePC); }; } this.step = function(value) { value = "translate("+value+")"; this.target.setAttribute("transform", value); }; } else if (nodeName=="animateTransform") { this.isInterpolable = function(from, to) { return true; }; this.getCurVal = function() { var type = this.type; var curTrans = this.target.transform; if (curTrans && curTrans.animVal.numberOfItems>0) { var transList = curTrans.animVal; return decompose(transList.getItem(0).matrix, type); } else { if (type=="scale") return "1,1"; else if (type=="translate") return "0,0"; else if (type=="rotate") return "0,0,0"; else return 0; } }; if (this.type=="scale") { this.normalize = function(value) { value = value.replace(/,/g," "); var coords = value.split(" "); coords[0] = parseFloat(coords[0]); if (coords.length==1) coords[1] = coords[0]; else coords[1] = parseFloat(coords[1]); return coords; }; this.add = function(a, b) { var ret = new Array(); for (var i=0; i<a.length; ++i) ret.push(a[i]*b[i]); return ret.join(","); }; } else if (this.type=="translate") { this.translation(); } else if (this.type=="rotate") { this.normalize = function(value) { value = value.replace(/,/g," "); var coords = value.split(" "); coords[0] = parseFloat(coords[0]); if (coords.length<3) { coords[1] = 0; coords[2] = 0; } else { coords[1] = parseFloat(coords[1]); coords[2] = parseFloat(coords[2]); } return coords; }; this.add = function(a, b) { var ret = new Array(); for (var i=0; i<a.length; ++i) ret.push(a[i]+b[i]); return ret.join(","); }; } if (this.type=="scale" || this.type=="rotate") { if (this.from) this.from = this.normalize(this.from).join(","); if (this.to) this.to = this.normalize(this.to).join(","); if (this.by) this.by = this.normalize(this.by).join(","); if (this.values) { var tvals = this.values.split(";"); for (var i=0; i<tvals.length; ++i) tvals[i] = this.normalize(tvals[i]).join(","); this.values = tvals.join(";"); } this.interpolate = function(from, to, percent) { var ret = new Array(); for (var i=0; i<from.length; ++i) ret.push(from[i]+((to[i]-from[i])*percent)); return ret.join(","); }; } this.step = function(value) { var attributeName = this.attributeName; value = this.type+"("+value+")"; this.target.setAttribute(attributeName, value); }; } var me = this; this.anim.beginElement = function() { me.begin(); return true; }; this.anim.beginElementAt = function(offset) { me.begin(offset*1000); return true; }; this.anim.endElement = function() { me.finish(); return true; }; this.anim.endElementAt = function(offset) { me.finish(offset*1000); return true; }; this.anim.getStartTime = function() { return (me.iterBegin-timeZero)/1000; }; this.anim.getCurrentTime = function() { var now = new Date(); return (now-me.iterBegin)/1000; }; } /** * Can be called at any time. * It's the main loop. */ function animate() { var curTime = new Date(); if (curTime<=prevTime) return; for (var i=0, j=animations.length; i<j; ++i) { try { if (!animations[i].f(curTime)) { // animation was removed therefore we need to adjust both the iterator and the auxiliary variable --i; --j; } } catch(exc) { if (exc.message!=="Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIDOMSVGPathElement.getTotalLength]") { // NOTE: in IE, console object is only available when Developer tools are open if (window.console && console.log) { console.log(exc); // uncomment to force error display //} else { // alert(exc); } } } } prevTime = curTime; // it would be cool if the attributes would be computed only, in the previous loop // and then the last values applied after the loop // for that, f(t) must return the value, and we must have a map for object(?).attributeType.attributeName -> value // then f(t) cannot return false when autostopping -> we must find another mechanism } /** * Converts a clock-value to milliseconds. * Supported: "s" | "ms" | "min" | "h" | no-units */ function toMillis(time) { time = time.trim(); var len = time.length; var io = time.indexOf(":"); if (io!=-1) { var clockVal = time.split(":"); len = clockVal.length; time = 0; if (len==3) time += parseInt(clockVal[0])*3600000; time += parseInt(clockVal[len-2])*60000; time += parseFloat(clockVal[len-1])*1000; } else if (len>2 && time.substring(len-2)=="ms") { time = parseInt(time.substring(0, time.length-2)); } else if (len>1 && time[len-1]=="s") { time = time.substring(0, time.length-1); time *= 1000; } else if (len>3 && time.substring(len-3)=="min") { time = time.substring(0, time.length-3); time *= 60000; } else if (len>1 && time[len-1]=="h") { time = time.substring(0, time.length-1); time *= 3600000; } else { time *= 1000; } return time; } /** * Decompose a matrix into its scale, translate, rotate or skew. */ function decompose(matrix, type) { if (type=="translate") return matrix.e+","+matrix.f; var a = matrix.a; var b = matrix.b; var c = matrix.c; var d = matrix.d; if (type=="rotate") return Math.atan2(c,a)+",0,0"; var ModA = Math.sqrt(a*a+c*c); var ModB = Math.sqrt(b*b+d*d); if (type=="scale") { var AxB = a*d-b*c; var scaleX = AxB==0?0:(AxB/ModA); var scaleY = ModB; return scaleX+","+scaleY; } var AdotB = a*b+c*d; if (AdotB==0) return 0; var shear = Math.PI/2-Math.acos(AdotB/(ModB*ModA)); return (shear*180)/Math.PI; } /** * Convert an rgb(), #XXX, #XXXXXX or named color * into an [r,g,b] array. */ function toRGB(color) { if (color.substring(0, 3)=="rgb") { color = color.replace(/ /g, ""); color = color.replace("rgb(", ""); color = color.replace(")", ""); var rgb = color.split(","); for (var i=0; i<rgb.length; ++i) { var len = rgb[i].length-1; if (rgb[i][len]=="%") rgb[i] = Math.round((rgb[i].substring(0,len))*2.55); else rgb[i] = parseInt(rgb[i]); } return rgb; } else if (color.charAt(0)=="#") { color = color.trim(); var rgb = new Array(); if (color.length==7) { rgb[0] = parseInt(color.substring(1,3),16); rgb[1] = parseInt(color.substring(3,5),16); rgb[2] = parseInt(color.substring(5,7),16); } else { rgb[0] = color.substring(1,2); rgb[1] = color.substring(2,3); rgb[2] = color.substring(3,4); rgb[0] = parseInt(rgb[0]+rgb[0],16); rgb[1] = parseInt(rgb[1]+rgb[1],16); rgb[2] = parseInt(rgb[2]+rgb[2],16); } return rgb; } else { return colors[color]; } } function createPath(d) { var path = document.createElementNS(svgns, "path"); path.setAttribute("d", d); try { if (path.normalizedPathSegList) path.myNormalizedPathSegList = path.normalizedPathSegList; } catch(exc) {} if (!path.myNormalizedPathSegList) { // TODO : normalize the path path.myNormalizedPathSegList = path.pathSegList; } return path; } // NOTE: units which aren't valid variable names are enclosed in quotes var units = {grad: 1, deg: 1, rad: 1, kHz: 1, Hz: 1, em: 1, ex: 1, px: 1, pt: 1, pc: 1, mm: 1, cm: 1, in: 1, ms: 1, s: 1, "%": 1}; function getUnit(str) { if (str && str.substring && str.length > 1) { for (var i=1; i<4; ++i) { // loop through units string length var vlen = str.length-i; if (vlen>0) { var unit = str.substring(vlen); if (units[unit]) { var val = str.substring(0, vlen); if (!isNaN(val)) return [val,unit]; } } } } return [str,null]; } var colors = { aliceblue : [240, 248, 255], antiquewhite : [250, 235, 215], aqua : [0, 255, 255], aquamarine : [127, 255, 212], azure : [240, 255, 255], beige : [245, 245, 220], bisque : [255, 228, 196], black : [0, 0, 0], blanchedalmond : [255, 235, 205], blue : [0, 0, 255], blueviolet : [138, 43, 226], brown : [165, 42, 42], burlywood : [222, 184, 135], cadetblue : [95, 158, 160], chartreuse : [127, 255, 0], chocolate : [210, 105, 30], coral : [255, 127, 80], cornflowerblue : [100, 149, 237], cornsilk : [255, 248, 220], crimson : [220, 20, 60], cyan : [0, 255, 255], darkblue : [0, 0, 139], darkcyan : [0, 139, 139], darkgoldenrod : [184, 134, 11], darkgray : [169, 169, 169], darkgreen : [0, 100, 0], darkgrey : [169, 169, 169], darkkhaki : [189, 183, 107], darkmagenta : [139, 0, 139], darkolivegreen : [85, 107, 47], darkorange : [255, 140, 0], darkorchid : [153, 50, 204], darkred : [139, 0, 0], darksalmon : [233, 150, 122], darkseagreen : [143, 188, 143], darkslateblue : [72, 61, 139], darkslategray : [47, 79, 79], darkslategrey : [47, 79, 79], darkturquoise : [0, 206, 209], darkviolet : [148, 0, 211], deeppink : [255, 20, 147], deepskyblue : [0, 191, 255], dimgray : [105, 105, 105], dimgrey : [105, 105, 105], dodgerblue : [30, 144, 255], firebrick : [178, 34, 34], floralwhite : [255, 250, 240], forestgreen : [34, 139, 34], fuchsia : [255, 0, 255], gainsboro : [220, 220, 220], ghostwhite : [248, 248, 255], gold : [255, 215, 0], goldenrod : [218, 165, 32], gray : [128, 128, 128], grey : [128, 128, 128], green : [0, 128, 0], greenyellow : [173, 255, 47], honeydew : [240, 255, 240], hotpink : [255, 105, 180], indianred : [205, 92, 92], indigo : [75, 0, 130], ivory : [255, 255, 240], khaki : [240, 230, 140], lavender : [230, 230, 250], lavenderblush : [255, 240, 245], lawngreen : [124, 252, 0], lemonchiffon : [255, 250, 205], lightblue : [173, 216, 230], lightcoral : [240, 128, 128], lightcyan : [224, 255, 255], lightgoldenrodyellow : [250, 250, 210], lightgray : [211, 211, 211], lightgreen : [144, 238, 144], lightgrey : [211, 211, 211], lightpink : [255, 182, 193], lightsalmon : [255, 160, 122], lightseagreen : [32, 178, 170], lightskyblue : [135, 206, 250], lightslategray : [119, 136, 153], lightslategrey : [119, 136, 153], lightsteelblue : [176, 196, 222], lightyellow : [255, 255, 224], lime : [0, 255, 0], limegreen : [50, 205, 50], linen : [250, 240, 230], magenta : [255, 0, 255], maroon : [128, 0, 0], mediumaquamarine : [102, 205, 170], mediumblue : [0, 0, 205], mediumorchid : [186, 85, 211], mediumpurple : [147, 112, 219], mediumseagreen : [60, 179, 113], mediumslateblue : [123, 104, 238], mediumspringgreen : [0, 250, 154], mediumturquoise : [72, 209, 204], mediumvioletred : [199, 21, 133], midnightblue : [25, 25, 112], mintcream : [245, 255, 250], mistyrose : [255, 228, 225], moccasin : [255, 228, 181], navajowhite : [255, 222, 173], navy : [0, 0, 128], oldlace : [253, 245, 230], olive : [128, 128, 0], olivedrab : [107, 142, 35], orange : [255, 165, 0], orangered : [255, 69, 0], orchid : [218, 112, 214], palegoldenrod : [238, 232, 170], palegreen : [152, 251, 152], paleturquoise : [175, 238, 238], palevioletred : [219, 112, 147], papayawhip : [255, 239, 213], peachpuff : [255, 218, 185], peru : [205, 133, 63], pink : [255, 192, 203], plum : [221, 160, 221], powderblue : [176, 224, 230], purple : [128, 0, 128], red : [255, 0, 0], rosybrown : [188, 143, 143], royalblue : [65, 105, 225], saddlebrown : [139, 69, 19], salmon : [250, 128, 114], sandybrown : [244, 164, 96], seagreen : [46, 139, 87], seashell : [255, 245, 238], sienna : [160, 82, 45], silver : [192, 192, 192], skyblue : [135, 206, 235], slateblue : [106, 90, 205], slategray : [112, 128, 144], slategrey : [112, 128, 144], snow : [255, 250, 250], springgreen : [0, 255, 127], steelblue : [70, 130, 180], tan : [210, 180, 140], teal : [0, 128, 128], thistle : [216, 191, 216], tomato : [255, 99, 71], turquoise : [64, 224, 208], violet : [238, 130, 238], wheat : [245, 222, 179], white : [255, 255, 255], whitesmoke : [245, 245, 245], yellow : [255, 255, 0], yellowgreen : [154, 205, 50] }; // NOTE: variables cannot contain dashes, as they are seen as a subtraction expression // (therefore, in those cases, enclosing in quotes is required) var propDefaults = { font : "see individual properties", "font-family" : "Arial", "font-size" : "medium", "font-size-adjust" : "none", "font-stretch" : "normal", "font-style" : "normal", "font-variant" : "normal", "font-weight" : "normal", direction : "ltr", "letter-spacing" : "normal", "text-decoration" : "none", "unicode-bidi" : "normal", "word-spacing" : "normal", clip : "auto", color : "depends on user agent", cursor : "auto", display : "inline", overflow : "hidden", visibility : "visible", "clip-path" : "none", "clip-rule" : "nonzero", mask : "none", opacity: 1, "enable-background" : "accumulate", filter : "none", "flood-color" : "black", "flood-opacity" : 1, "lighting-color" : "white", "stop-color" : "black", "stop-opacity" : 1, "pointer-events" : "visiblePainted", "color-interpolation" : "sRGB", "color-interpolation-filters" : "linearRGB", "color-profile" : "auto", "color-rendering" : "auto", fill : "black", "fill-opacity" : 1, "fill-rule" : "nonzero", "image-rendering" : "auto", "marker-end" : "none", "marker-mid" : "none", "marker-start" : "none", "shape-rendering" : "auto", stroke : "none", "stroke-dasharray" : "none", "stroke-dashoffset" : 0, "stroke-linecap" : "butt", "stroke-linejoin" : "miter", "stroke-miterlimit" : 4, "stroke-opacity" : 1, "stroke-width" : 1, "text-rendering" : "auto", "alignment-baseline" : 0, "baseline-shift" : "baseline", "dominant-baseline" : "auto", "glyph-orientation-horizontal" : 0, "glyph-orientation-vertical" : "auto", kerning : "auto", "text-anchor" : "start", "writing-mode" : "lr-tb" }; function funk(func, obj, arg) { return function() {func.call(obj, arg);}; } /** * Removes the leading and trailing spaces chars from the string. * NOTE: part of ES5, so use feature detection * http://stackoverflow.com/questions/2308134/trim-in-javascript-not-working-in-ie/#2308157 * NOTE: the regular expression used in fallback is placed in global namespace for performance * (as it's far better having a "singleton" than bloating every string instance) */ if (typeof String.prototype.trim !== "function") { window._trimRegExp = new RegExp("^\\s+|\\s+$", "g"); String.prototype.trim = function() { return this.replace(window._trimRegExp, ""); }; } /** * Set an ISO 8601 timestamp to a Date object. * NOTE: as ES5 doesn't define precisely what "parse" should do, we run a sample to test for feasibility * http://stackoverflow.com/questions/2479714/does-javascript-ecmascript3-support-iso8601-date-parsing/#2481375 * NOTE: the regular expression used in fallback is placed in global namespace for performance * (as it's far better having a "singleton" than bloating every date instance) */ if (!isNaN(Date.parse("2012-04-22T19:53:32Z"))){ // parse did well, use the native implementation Date.prototype.setISO8601 = function (string) { this.setTime(Date.parse(string)); }; }else{ window._setISO8601RegExp = new RegExp( "([0-9]{4})(?:-([0-9]{2})(?:-([0-9]{2})" + "(?:T([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\.([0-9]+))?)?" + "(?:Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" ); Date.prototype.setISO8601 = function (string) { var d = window._setISO8601RegExp.exec(string); // check that RegExp was applied successfully and that at least year is present if (d && d.length>1) { var date = new Date(d[1], 0, 1); if (d[2]) { date.setMonth(d[2] - 1); } if (d[3]) { date.setDate(d[3]); } if (d[4]) { date.setHours(d[4]); } if (d[5]) { date.setMinutes(d[5]); } if (d[6]) { date.setSeconds(d[6]); } // NOTE: ISO 8601 "decimal fraction of a second" needs to be converted to milliseconds if (d[7]) { date.setMilliseconds(parseFloat("0." + d[7]) * 1000); } if (d[8]) { var offset = (parseInt(d[10]) * 60) + parseInt(d[11]); if (d[9]!='-') { offset = -offset; } } else var offset = 0; offset -= date.getTimezoneOffset(); this.setTime(date.getTime() + (offset * 60 * 1000)); } else this.setTime(NaN); }; } try { // NOTE: ASV skips triggering the library here, as 'addEventListener' is not supported // (but that's not an issue as most popular versions, ASV3 and ASV6 beta, both support SMIL) window.addEventListener("load", initSMIL, false); } catch(exc) {}