diff options
author | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2016-03-23 22:50:30 +0100 |
---|---|---|
committer | Ulrich-Matthias Schäfer <ulima.ums@googlemail.com> | 2016-03-23 22:50:30 +0100 |
commit | 8205de6087bd6fe48706f598c49a111f52740c97 (patch) | |
tree | 965fd228f650007bd6ded8835885d89b2e19554b /src | |
parent | 2b26d66fc4686c249ef27b2b8ca1bb20df7bb95d (diff) | |
download | svg.js-8205de6087bd6fe48706f598c49a111f52740c97.tar.gz svg.js-8205de6087bd6fe48706f598c49a111f52740c97.zip |
Implementation new fx module
Diffstat (limited to 'src')
-rw-r--r-- | src/element.js | 3 | ||||
-rw-r--r-- | src/event.js | 11 | ||||
-rw-r--r-- | src/fxnew.js | 179 | ||||
-rw-r--r-- | src/fxnew2.js | 813 | ||||
-rw-r--r-- | src/group.js | 4 | ||||
-rw-r--r-- | src/helpers.js | 4 | ||||
-rw-r--r-- | src/matrix.js | 5 | ||||
-rw-r--r-- | src/transform.js | 248 | ||||
-rw-r--r-- | src/viewbox.js | 170 |
9 files changed, 1297 insertions, 140 deletions
diff --git a/src/element.js b/src/element.js index 75d77dc..552fd5b 100644 --- a/src/element.js +++ b/src/element.js @@ -256,5 +256,8 @@ SVG.Element = SVG.invent({ this.dom = o return this } + , is: function(obj){ + return is(this, obj) + } } }) diff --git a/src/event.js b/src/event.js index 66cf6c3..e94b6e1 100644 --- a/src/event.js +++ b/src/event.js @@ -30,6 +30,7 @@ // Initialize listeners stack SVG.listeners = [] SVG.handlerMap = [] +SVG.listenerId = 0 // Add event binder in the SVG namespace SVG.on = function(node, event, listener, binding) { @@ -45,8 +46,11 @@ SVG.on = function(node, event, listener, binding) { SVG.listeners[index][ev] = SVG.listeners[index][ev] || {} SVG.listeners[index][ev][ns] = SVG.listeners[index][ev][ns] || {} + if(!listener._svgjsListenerId) + listener._svgjsListenerId = ++SVG.listenerId + // reference listener - SVG.listeners[index][ev][ns][listener] = l + SVG.listeners[index][ev][ns][listener._svgjsListenerId] = l // add listener node.addEventListener(ev, l, false) @@ -59,8 +63,11 @@ SVG.off = function(node, event, listener) { , ns = event && event.split('.')[1] if(index == -1) return - + if (listener) { + if(typeof listener == 'function') listener = listener._svgjsListenerId + if(!listener) return + // remove listener reference if (SVG.listeners[index][ev] && SVG.listeners[index][ev][ns || '*']) { // remove listener diff --git a/src/fxnew.js b/src/fxnew.js index 324ab53..732a366 100644 --- a/src/fxnew.js +++ b/src/fxnew.js @@ -38,7 +38,7 @@ SVG.FX = SVG.invent({ } this.transforms = [ - // holds all transformations of teh form: + // holds all transformations of the form: // [A, B, C] or, [A, [25, 0, 0]] where ABC are matrixes and the array represents a rotation ] @@ -173,7 +173,7 @@ SVG.FX = SVG.invent({ this.init || this.initAnimations() - this.timeout = setTimeout(function(){ this.startAnimFrame() }.bind(this), this.delay) + this.timeout = setTimeout(function(){ this.startAnimFrame() }.bind(this), this._delay) } return this @@ -187,6 +187,12 @@ SVG.FX = SVG.invent({ for(i in this.animations){ // TODO: this is not a clean clone of the array. We may have some unchecked references this.animations[i].value = (i == 'plot' ? this.target.array().value : this.target[i]()) + + // sometimes we get back an object and not the real value, fix this + if(this.animations[i].value.value){ + this.animations[i].value = this.animations[i].value.value + } + if(this.animations[i].relative) this.animations[i].destination.value = this.animations[i].destination.value + this.animations[i].value } @@ -212,22 +218,27 @@ SVG.FX = SVG.invent({ } // resets the animation to the initial state - // TODO: maybe rename to reset , stop: function(){ - if(!this.active) return false + if(!this.active) return this this.active = false this.stopAnimFrame() clearTimeout(this.timeout) - return this.seek(0) + return this + } + + // do we need this one? + , reset: function(){ + return this.stop().seek(0) } // finish off the animation - // TODO: does it kickoff the next animation in the queue? - // global finish or fx specific finish? - , finish: function(){ + // param next: true if next animation should be started + , finish: function(next){ this.finished = true - return this.stop().seek(1) + this.loop = false + if(!next)this.stop() + return this.seek(1) } // set the internal animation pointer to the specified position and updates the visualisation @@ -245,6 +256,14 @@ SVG.FX = SVG.invent({ this._end = this._start + this._duration return this.seek(this.pos) } + // Make loopable + , loop: function(times, reverse) { + // store current loop and total loops + this.loop = times || true + + if(reverse) return this.reverse() + return this + } // pauses the animation , pause: function(){ @@ -268,7 +287,7 @@ SVG.FX = SVG.invent({ , reverse: function(){ if(!this.shared.reversed){ this.shared.reversed = true - this.seek(1-this.pos) + //this.seek(1-this.pos) } return this } @@ -389,11 +408,15 @@ SVG.FX = SVG.invent({ // convert current time to position if(!ignoreTime) this.pos = this.timeToPos(+new Date) + if(this.pos >= 1 && (this.loop === true || (typeof this.loop == 'number' && --this.loop))){ + return this.seek(this.pos-1) + } + if(this.shared.reversed) this.pos = 1 - this.pos // correct position - if(this.pos > 1) this.pos = 1 - if(this.pos < 0) this.pos = 0 + if(this.pos > 1)this.pos = 1 + if(this.pos < 0)this.pos = 0 // apply easing var eased = this.easing(this.pos) @@ -413,7 +436,7 @@ SVG.FX = SVG.invent({ this.eachAt() // do final code when situation is finished - if(this.pos == 1){ + if(this.pos == 1 && !this.shared.reversed){ // stop animation callback cancelAnimationFrame(this.animationFrame) @@ -436,6 +459,7 @@ SVG.FX = SVG.invent({ // todo: this is more or less duplicate code. has to be removed }else if(this.shared.reversed && this.pos == 0){ + // stop animation callback cancelAnimationFrame(this.animationFrame) @@ -507,11 +531,72 @@ SVG.FX = SVG.invent({ if(this.transforms.length){ at = this.transformations for(i in this.transforms){ - if(Array.isArray(this.transforms[i])){ + + // we have to deal with absolute transformations + // very stupid stuff + //if(!this.transforms[i].relative){ + + var a = this.transforms[i] + + if(a instanceof SVG.Matrix){ + + if(a.relative){ + at = at.multiply(a.at(pos)) + }else{ + at = at.morph(a).at(this.easing(pos)) + } + continue + } + + if(!a.relative) + a.undo(at.extract()) + + if(a instanceof SVG.Translate && this.pos == 1){ + //console.log(at, a.at(this.easing(this.pos)), at.multiply(a.at(this.easing(this.pos)))) + } + + at = at.multiply(a.at(this.easing(this.pos))) + continue; + // get the current transformation + /*var current = at.extract() + + var undoArgs = [] + var absArgs = [] + + // collect the arguments for the method we have to call + for(var j in this.transforms[i].order){ + if(this.transforms[i].args[this.transforms[i].order[j]] == null)undoArgs.push(0) + else undoArgs.push(current[this.transforms[i].order[j]]) + absArgs.push(this.transforms[i].args[this.transforms[i].order[j]]) + } + + + + // create a new matrix and call this method with its arguments on it + var diff = new SVG.Matrix() + diff = diff[this.transforms[i].method].apply(diff, undoArgs) + + // build the inverse (we just undo the changes) and apply the new absolute ones + diff = diff.inverse() + if(this.transforms[i].method == 'rotate'){ + diff = diff.rotate(new SVG.Number().morph(absArgs[0]).at(this.easing(this.pos)), absArgs[1], absArgs[2]) + }else{ + diff = diff[this.transforms[i].method].apply(diff, absArgs) + } + + // the resulting matrix is the matrix with the value set absolute + at = at.multiply(new SVG.Matrix().morph(diff).at(this.easing(this.pos))) + //this.transforms[i] = new SVG.Matrix().morph(diff) + continue*/ + + //} + + + /*if(Array.isArray(this.transforms[i])){ at = at.rotate(this.transforms[i][0].at(this.easing(this.pos)), this.transforms[i][1], this.transforms[i][2]) }else{ at = at.multiply(this.transforms[i].at(this.easing(this.pos))) - } + }*/ } target.matrix(at) @@ -561,7 +646,7 @@ SVG.FX = SVG.invent({ , construct: { // Get fx module or create a new one, then animate with given duration and ease animate: function(o) { - return (this.fx || (this.fx = new SVG.FX(this))).animate(o) + return (this.fx || (this.fx = new SVG.FX(this))).stop().animate(o) } , delay: function(delay){ return (this.fx || (this.fx = new SVG.FX(this))).animate({delay:delay}) @@ -598,7 +683,7 @@ SVG.MorphObj = SVG.invent({ SVG.extend(SVG.FX, { // Add animatable attributes - attr: function(a, v) { + attr: function(a, v, relative) { // apply attributes individually if (typeof a == 'object') { for (var key in a) @@ -609,41 +694,26 @@ SVG.extend(SVG.FX, { var from = this.search('attr', a) // detect format - if (a == 'transform') { - // merge given transformation with an existing one - //if (this.attrs[a]) - // v = this.attrs[a].multiply(v) + /*if (a == 'transform') { + // we have to calculate the rotation on our own to make sure we animate it correctly. + // thats why we just add the parameters instead of the matrix if(v.rotation && !v.a){ this.transforms.push([new SVG.Number(0).morph(v.rotation), v.cx || 0, v.cy || 0]) this.start() }else{ - this.transforms.push(new SVG.Matrix(this.target).morph(v)) + if(!relative){ + this.transforms.push(v) + }else{ + this.transforms.push(new SVG.Matrix().morph(v)) + } this.start() } - // prepare matrix for morphing - //this.push(a, (new SVG.Matrix(this.target)).morph(v), 'transforms') - - // add parametric rotation values - /*if (this.param) { - // get initial rotation - v = this.target.transform('rotation') - - // add param - this.attrs[a].param = { - from: this.target.param || { rotation: v, cx: this.param.cx, cy: this.param.cy } - , to: this.param - } - }*/ - - } else { - /*if(typeof this[a] == 'function'){ - return this[a](v) - }*/ - + } else {*/ + // FIXME: from is obsolete this.push(a, new SVG.MorphObj(from, v), 'attrs') - } + //} } return this @@ -661,13 +731,25 @@ SVG.extend(SVG.FX, { } // Animatable x-axis , x: function(x, relative) { - var num = new SVG.Number(/*this.search('x')*/0).morph(x) + if(this.target instanceof SVG.G){ + this.transform({x:x}, relative) + return this + } + + var num = new SVG.Number(/*this.search('x')*/).morph(x) + //var num = new SVG.Number(this.target.x()).morph(x) num.relative = relative return this.push('x', num) } // Animatable y-axis , y: function(y, relative) { + if(this.target instanceof SVG.G){ + this.transform({y:y}, relative) + return this + } + var num = new SVG.Number(/*this.search('y')*/).morph(y) + //var num = new SVG.Number(this.target.y()).morph(y) num.relative = relative return this.push('y', num) } @@ -734,4 +816,13 @@ SVG.extend(SVG.FX, { return this } +, update: function(o) { + if (this.target instanceof SVG.Stop) { + if (o.opacity != null) this.attr('stop-opacity', o.opacity) + if (o.color != null) this.attr('stop-color', o.color) + if (o.offset != null) this.attr('offset', new SVG.Number(o.offset)) + } + + return this + } })
\ No newline at end of file diff --git a/src/fxnew2.js b/src/fxnew2.js new file mode 100644 index 0000000..7514a40 --- /dev/null +++ b/src/fxnew2.js @@ -0,0 +1,813 @@ +SVG.easing = { + '-': function(pos){return pos} +, '<>':function(pos){return -Math.cos(pos * Math.PI) / 2 + 0.5} +, '>': function(pos){return Math.sin(pos * Math.PI / 2)} +, '<': function(pos){return -Math.cos(pos * Math.PI / 2) + 1} +} + +var someVar = 0 + +SVG.Situation = SVG.invent({ + + create: function(o){ + this.init = false + this.reversed = false + + this.duration = o.duration + this.delay = o.delay + + this.start = +new Date() + this.delay + this.end = this.start + this.duration + this.easing = o.easing + + this.animations = { + // functionToCall: [list of morphable objects] + // e.g. move: [SVG.Number, SVG.Number] + } + + this.attrs = { + // holds all attributes which are not represented from a function svg.js provides + // e.g. someAttr: SVG.Number + } + + this.styles = { + // holds all styles which should be animated + // e.g. fill-color: SVG.Color + } + + this.transforms = [ + // holds all transformations of the form: + // [A, B, C] or, [A, [25, 0, 0]] where ABC are matrixes and the array represents a rotation + ] + + this.once = { + // functions to fire at a specific position + // e.g. "0.5": function foo(){} + } + + } + +}) + +SVG.Delay = function(delay){ + this.delay = delay +} + +SVG.FX = SVG.invent({ + + create: function(element) { + this._target = element + this.situations = [] + this.active = false + this.current = null + this.paused = false + this.lastPos = 0 + this.pos = 0 + } + +, extend: { + + /** + * sets or returns the target of this animation + * @param o object || number In case of Object it holds all parameters. In case of number its the duration of the animation + * @param easing function || string Function which should be used for easing or easing keyword + * @param delay Number indicating the delay before the animation starts + * @return target || this + */ + animate: function(o, easing, delay){ + + if(typeof o == 'object'){ + easing = o.ease + delay = o.delay + o = o.duration + } + + var situation = new SVG.Situation({ + duration: o || 1000, + delay: delay || 0, + easing: SVG.easing[easing || '-'] || easing + }) + + this.queue(situation) + + return this + } + + /** + * sets a delay before the next element of the queue is called + * @param delay Duration of delay in milliseconds + * @return this.target() + */ + // FIXME: the function needs to get a delay property to make sure, that the totalProgress can be calculated + , delay: function(delay){ + var delay = new SVG.Delay(delay) + + return this.queue(delay) + } + + /** + * sets or returns the target of this animation + * @param null || target SVG.Elemenet which should be set as new target + * @return target || this + */ + , target: function(target){ + if(target && target instanceof SVG.Element){ + this._target = target + return this + } + + return this._target + } + + // returns the position at a given time + , timeToPos: function(timestamp){ + return (timestamp - this.current.start) / (this.current.duration) + } + + // returns the timestamp from a given positon + , posToTime: function(pos){ + return this.current.duration * pos + this.current.start + } + + // starts the animationloop + // TODO: It may be enough to call just this.step() + , startAnimFrame: function(){ + this.stopAnimFrame() + this.animationFrame = requestAnimationFrame(function(){ this.step() }.bind(this)) + } + + // cancels the animationframe + // TODO: remove this in favour of the oneliner + , stopAnimFrame: function(){ + cancelAnimationFrame(this.animationFrame) + } + + // kicks off the animation - only does something when the queue is curretly not active and at least one situation is set + , start: function(){ + // dont start if already started + if(!this.active && this.current){ + this.current.start = +new Date + this.current.delay + this.current.end = this.current.start + this.current.duration + + this.initAnimations() + this.active = true + this.startAnimFrame() + } + + return this + } + + /** + * adds a function / Situation to the animation queue + * @param fn function / situation to add + * @return this + */ + , queue: function(fn){ + if(typeof fn == 'function' || fn instanceof SVG.Situation || fn instanceof SVG.Delay) + this.situations.push(fn) + + if(!this.current) this.current = this.situations.shift() + + return this + } + + /** + * pulls next element from the queue and execute it + * @return this + */ + , dequeue: function(){ + // stop current animation + this.current && this.current.stop && this.current.stop() + + // get next animation from queue + this.current = this.situations.shift() + + if(this.current){ + + var fn = function(){ + if(this.current instanceof SVG.Situation) + this.initAnimations().seek(0) + else if(this.current instanceof SVG.Delay) + this.dequeue() + else + this.current.call(this) + }.bind(this) + + // start next animation + if(this.current.delay){ + setTimeout(function(){fn()}, this.current.delay) + }else{ + fn() + } + + } + + return this + } + + // updates all animations to the current state of the element + // this is important when one property could be changed from another property + , initAnimations: function() { + var i + var s = this.current + + if(s.init) return this + + for(i in s.animations){ + + if(i == 'viewbox'){ + s.animations[i] = this.target().viewbox().morph(s.animations[i]) + }else{ + + // TODO: this is not a clean clone of the array. We may have some unchecked references + s.animations[i].value = (i == 'plot' ? this.target().array().value : this.target()[i]()) + + // sometimes we get back an object and not the real value, fix this + if(s.animations[i].value.value){ + s.animations[i].value = s.animations[i].value.value + } + + if(s.animations[i].relative) + s.animations[i].destination.value = s.animations[i].destination.value + s.animations[i].value + + } + + } + + for(i in s.attrs){ + if(s.attrs[i] instanceof SVG.Color){ + var color = new SVG.Color(this.target().attr(i)) + s.attrs[i].r = color.r + s.attrs[i].g = color.g + s.attrs[i].b = color.b + }else{ + s.attrs[i].value = this.target().attr(i)// + s.attrs[i].value + } + } + + for(i in s.styles){ + s.styles[i].value = this.target().style(i) + } + + s.transformations = this.target().matrixify() + + s.init = true + return this + } + , clearQueue: function(){ + this.situations = [] + return this + } + , clearCurrent: function(){ + this.current = null + return this + } + /** stops the animation immediately + * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. + * @param clearQueue A Boolean indicating whether to remove queued animation as well. + * @return this + */ + , stop: function(jumpToEnd, clearQueue){ + if(!this.active) this.start() + + if(clearQueue){ + this.clearQueue() + } + + this.active = false + + if(jumpToEnd){ + this.seek(1) + } + + this.stopAnimFrame() + clearTimeout(this.timeout) + + return this.clearCurrent() + } + + /** resets the element to the state where the current element has started + * @return this + */ + , reset: function(){ + if(this.current){ + var temp = this.current + this.stop() + this.current = temp + this.seek(0) + } + return this + } + + // Stop the currently-running animation, remove all queued animations, and complete all animations for the element. + , finish: function(){ + + this.stop(true, false) + + while(this.dequeue().current && this.stop(true, false)); + + return this.clearQueue().clearCurrent() + } + + // set the internal animation pointer to the specified position and updates the visualisation + , seek: function(pos){ + this.pos = pos + this.current.start = +new Date - pos * this.current.duration + this.current.end = this.current.start + this.current.duration + return this.step(true) + } + + // speeds up the animation by the given factor + // this changes the duration of the animation + , speed: function(speed){ + this.current.duration = this.current.duration * this.pos + (1-this.pos) * this.current.duration / speed + this.current.end = this.current.start + this.current.duration + return this.seek(this.pos) + } + // Make loopable + , loop: function(times, reverse) { + // store current loop and total loops + this.current.loop = times || true + + if(reverse) return this.reverse() + return this + } + + // pauses the animation + , pause: function(){ + this.paused = true + this.stopAnimFrame() + clearTimeout(this.timeout) + return this + } + + // unpause the animation + , play: function(){ + if(!this.paused) return this + this.paused = false + return this.seek(this.pos) + } + + /** toggle or set the direction of the animation + * true sets direction to backwards while false sets it to forwards + * @param reversed Boolean indicating whether to reverse the animation or not (default: toggle the reverse status) + * @return this + */ + , reverse: function(reversed){ + var c = this.last() + + if(typeof reversed == 'undefined') c.reversed = !c.reversed + else c.reversed = reversed + + return this + } + + + /** + * returns a float from 0-1 indicating the progress of the current animation + * @param eased Boolean indicating whether the returned position should be eased or not + * @return number + */ + , progress: function(easeIt){ + return easeIt ? this.current.easing(this.pos) : this.pos + } + + /** + * adds a callback function which is called when the current animation is finished + * @param fn Function which should be executed as callback + * @return number + */ + , after: function(fn){ + var c = this.last() + , wrapper = function wrapper(e){ + if(e.detail.situation == c){ + fn.call(this, c) + this.off('finished.fx', wrapper) // prevent memory leak + } + } + + this.target().on('finished.fx', wrapper) + return this + } + + // adds a callback which is called whenever one animation step is performed + , during: function(fn){ + var c = this.last() + , wrapper = function(e){ + if(e.detail.situation == c){ + fn.call(this, e.detail.pos, e.detail.eased, c) + } + } + + // see above + this.target().off('during.fx', wrapper).on('during.fx', wrapper) + + return this.after(function(){ + this.off('during.fx', wrapper) + }) + } + + // calls after ALL animations in the queue are finished + , afterAll: function(fn){ + var wrapper = function wrapper(e){ + fn.call(this) + this.off('allfinished.fx', wrapper) + } + + // see above + this.target().off('allfinished.fx', wrapper).on('allfinished.fx', wrapper) + return this + } + + // calls on every animation step for all animations + , duringAll: function(fn){ + var wrapper = function(e){ + fn.call(this, e.detail.pos, e.detail.eased, e.detail.fx, e.detail.situation) + } + + this.target().off('during.fx', wrapper).on('during.fx', wrapper) + + return this.afterAll(function(){ + this.off('during.fx', wrapper) + }) + } + + /** + * returns a float from 0-1 indicating the progress of the whole animation queue + * we recalculate the end time because it may be changed from methods like seek() + * @return number + */ + // FIXME: current start always changes so the progress get a reset whenever one situation finishes. We need a global start which is only modified on pause and stop + , totalProgress: function(){ + var start = this.current.start + , end = this.current + + for(var i = 0, len = this.situations.length; i < len; ++i){ + end += (situations[i].duration || 0) + (situations[i].delay || 0) + } + + return (this.pos * this.current.duration + this.start - start) / (end - start) + } + + , last: function(){ + return this.situations.length ? this.situations[this.situations.length-1] : this.current + } + + // adds one property to the animations + , add: function(method, args, type){ + //if(this.situations.length){ + // this.situations[this.situations.length-1][type || 'animations'][method] = args + //}else{ + // this.current[type || 'animations'][method] = args + //} + this.last()[type || 'animations'][method] = args + setTimeout(function(){this.start()}.bind(this), 0) + return this + } + + /** perform one step of the animation + * @param ignoreTime Boolean indicating whether to ignore time and use position directly or recalculate position based on time + * @return this + */ + , step: function(ignoreTime){ + + // convert current time to position + if(!ignoreTime) this.pos = this.timeToPos(+new Date) + + if(this.pos >= 1 && (this.current.loop === true || (typeof this.current.loop == 'number' && --this.current.loop))){ + return this.seek(this.pos-1) + } + + if(this.current.reversed) this.pos = 1 - this.pos + + // correct position + if(this.pos > 1)this.pos = 1 + if(this.pos < 0)this.pos = 0 + + // apply easing + var eased = this.current.easing(this.pos) + + // call once-callbacks + for(var i in this.current.once){ + if(i > this.lastPos && i <= eased){ + this.current.once[i].call(this.target(), this.pos, eased) + delete this.current.once[i] + } + } + + // fire during callback with position, eased position and current situation as parameter + this.target().fire('during', {pos: this.pos, eased: eased, fx: this, situation: this.current}) + + // apply the actual animation to every property + this.eachAt() + + // do final code when situation is finished + if((this.pos == 1 && !this.current.reversed) || (this.current.reversed && this.pos == 0)){ + + // stop animation callback + cancelAnimationFrame(this.animationFrame) + + // fire finished callback with current situation as parameter + this.target().fire('finished', {fx:this, situation: this.current}) + + if(!this.situations.length && !this.current && this.active){ + this.target().fire('allfinished') + this.target().off('.fx') + this.active = false + } + + // start next animation + if(this.active) this.dequeue() + else this.clearCurrent() + + }else if(!this.paused && this.active){ + // we continue animating when we are not at the end + this.startAnimFrame() + } + + // save last eased position for once callback triggering + this.lastPos = eased + return this + + } + + // calculates the step for every property and calls block with it + // todo: include block directly cause it is used only for this purpose + , eachAt: function(){ + var i, at, self = this, target = this.target(), c = this.current + + // apply animations which can be called trough a method + for(i in c.animations){ + + at = [].concat(c.animations[i]).map(function(el){ + return el.at ? el.at(c.easing(self.pos), self.pos) : el + }) + + target[i].apply(target, at) + + } + + // apply animation which has to be applied with attr() + for(i in c.attrs){ + + at = [i].concat(c.attrs[i]).map(function(el){ + return el.at ? el.at(c.easing(self.pos), self.pos) : el + }) + + target.attr.apply(target, at) + + } + + // apply animation which has to be applied with style() + for(i in c.styles){ + + at = [i].concat(c.styles[i]).map(function(el){ + return el.at ? el.at(c.easing(self.pos), self.pos) : el + }) + + target.style.apply(target, at) + + } + + // animate transformations which has to be chained + if(c.transforms.length){ + + // get inital transformations + at = c.transformations + for(i in c.transforms){ + + // get next transformation in chain + var a = c.transforms[i] + + // multiply matrix directly + if(a instanceof SVG.Matrix){ + + if(a.relative){ + at = at.multiply(a.at(this.pos)) + }else{ + at = at.morph(a).at(c.easing(this.pos)) + } + continue + } + + // when transformation is absolute we have to reset the needed transformation first + if(!a.relative) + a.undo(at.extract()) + + // and reapply it after + at = at.multiply(a.at(c.easing(this.pos))) + continue; + + } + + // set new matrix on element + target.matrix(at) + } + + return this + + } + + + // adds an once-callback which is called at a specific position and never again + , once: function(pos, fn, isEased){ + + if(!isEased)pos = this.current.easing(pos) + + this.current.once[pos] = fn + + return this + } + + } + +, parent: SVG.Element + + // Add method to parent elements +, construct: { + // Get fx module or create a new one, then animate with given duration and ease + animate: function(o, easing, delay) { + return (this.fx || (this.fx = new SVG.FX(this))).animate(o, easing, delay) + } + , delay: function(delay){ + return (this.fx || (this.fx = new SVG.FX(this))).delay(delay) + } + , stop: function(jumpToEnd, clearQueue) { + if (this.fx) + this.fx.stop(jumpToEnd, clearQueue) + + return this + } + // Pause current animation + , pause: function() { + if (this.fx) + this.fx.pause() + + return this + } + // Play paused current animation + , play: function() { + if (this.fx) + this.fx.play() + + return this + } + } + +}) + +// MorphObj is used whenever no morphable object is given +SVG.MorphObj = SVG.invent({ + + create: function(to){ + // prepare color for morphing + if(SVG.Color.isColor(to)) return new SVG.Color().morph(to) + // prepare number for morphing + if(SVG.regex.numberAndUnit.test(to)) return new SVG.Number().morph(to) + + // prepare for plain morphing + this.value = 0 + this.destination = to + } + +, extend: { + at: function(pos, real){ + return real < 1 ? this.value : this.destination + }, + + valueOf: function(){ + return this.value + } + } + +}) + +SVG.extend(SVG.FX, { + // Add animatable attributes + attr: function(a, v, relative) { + // apply attributes individually + if (typeof a == 'object') { + for (var key in a) + this.attr(key, a[key]) + + } else { + // the MorphObj takes care about the right function used + this.add(a, new SVG.MorphObj(v), 'attrs') + } + + return this + } + // Add animatable styles +, style: function(s, v) { + if (typeof s == 'object') + for (var key in s) + this.style(key, s[key]) + + else + this.add(s, new SVG.MorphObj(v), 'styles') + + return this + } + // Animatable x-axis +, x: function(x, relative) { + if(this.target() instanceof SVG.G){ + this.transform({x:x}, relative) + return this + } + + var num = new SVG.Number().morph(x) + num.relative = relative + return this.add('x', num) + } + // Animatable y-axis +, y: function(y, relative) { + if(this.target() instanceof SVG.G){ + this.transform({y:y}, relative) + return this + } + + var num = new SVG.Number().morph(y) + num.relative = relative + return this.add('y', num) + } + // Animatable center x-axis +, cx: function(x) { + return this.add('cx', new SVG.Number().morph(x)) + } + // Animatable center y-axis +, cy: function(y) { + return this.add('cy', new SVG.Number().morph(y)) + } + // Add animatable move +, move: function(x, y) { + return this.x(x).y(y) + } + // Add animatable center +, center: function(x, y) { + return this.cx(x).cy(y) + } + // Add animatable size +, size: function(width, height) { + if (this.target() instanceof SVG.Text) { + // animate font size for Text elements + this.attr('font-size', width) + + } else { + // animate bbox based size for all other elements + var box + + if(!width || !height){ + box = this.target().bbox() + } + + if(!width){ + width = box.width / box.height * height + } + + if(!height){ + height = box.height / box.width * width + } + + this.add('width' , new SVG.Number().morph(width)) + .add('height', new SVG.Number().morph(height)) + + } + + return this + } + // Add animatable plot +, plot: function(p) { + return this.add('plot', this.target().array().morph(p)) + } + // Add leading method +, leading: function(value) { + return this.target().leading ? + this.add('leading', new SVG.Number().morph(value)) : + this + } + // Add animatable viewbox +, viewbox: function(x, y, width, height) { + if (this.target() instanceof SVG.Container) { + this.add('viewbox', new SVG.ViewBox(x, y, width, height)) + } + + return this + } +, update: function(o) { + if (this.target() instanceof SVG.Stop) { + if (typeof o == 'number' || o instanceof SVG.Number) { + return this.update({ + offset: arguments[0] + , color: arguments[1] + , opacity: arguments[2] + }) + } + + if (o.opacity != null) this.attr('stop-opacity', o.opacity) + if (o.color != null) this.attr('stop-color', o.color) + if (o.offset != null) this.attr('offset', o.offset) + } + + return this + } +})
\ No newline at end of file diff --git a/src/group.js b/src/group.js index 05d144c..9ec89f2 100644 --- a/src/group.js +++ b/src/group.js @@ -17,11 +17,11 @@ SVG.G = SVG.invent({ } // Move by center over x-axis , cx: function(x) { - return x == null ? this.tbox().cx : this.x(x - this.tbox().width / 2) + return x == null ? this.gbox().cx : this.x(x - this.gbox().width / 2) } // Move by center over y-axis , cy: function(y) { - return y == null ? this.tbox().cy : this.y(y - this.tbox().height / 2) + return y == null ? this.gbox().cy : this.y(y - this.gbox().height / 2) } , gbox: function() { diff --git a/src/helpers.js b/src/helpers.js index ee86dc7..4de813f 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,3 +1,7 @@ +function is(el, obj){ + return el instanceof obj +} + // tests if a given selector matches an element function matches(el, selector) { return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector); diff --git a/src/matrix.js b/src/matrix.js index 9b3c28e..e727b2f 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -14,7 +14,7 @@ SVG.Matrix = SVG.invent({ source : base // merge source - for (i = abcdef.length - 1; i >= 0; i--) + for (i = abcdef.length - 1; i >= 0; --i) this[abcdef[i]] = source && typeof source[abcdef[i]] === 'number' ? source[abcdef[i]] : base[abcdef[i]] } @@ -32,6 +32,8 @@ SVG.Matrix = SVG.invent({ // translation x: this.e , y: this.f + , transformedX:(this.e * Math.cos(skewX * Math.PI / 180) + this.f * Math.sin(skewX * Math.PI / 180)) / Math.sqrt(this.a * this.a + this.b * this.b) + , transformedY:(this.f * Math.cos(skewX * Math.PI / 180) + this.e * Math.sin(-skewX * Math.PI / 180)) / Math.sqrt(this.c * this.c + this.d * this.d) // skew , skewX: -skewX , skewY: 180 / Math.PI * Math.atan2(py.y, py.x) @@ -46,6 +48,7 @@ SVG.Matrix = SVG.invent({ , d: this.d , e: this.e , f: this.f + , matrix: new SVG.Matrix(this) } } // Clone matrix diff --git a/src/transform.js b/src/transform.js index 2b6e9df..e6ad9ae 100644 --- a/src/transform.js +++ b/src/transform.js @@ -1,8 +1,8 @@ -SVG.extend(SVG.Element, SVG.FX, { +SVG.extend(SVG.Element, { // Add transformations transform: function(o, relative) { // get target in case of the fx module, otherwise reference this - var target = this.target || this + var target = this , matrix // act as a getter @@ -10,20 +10,11 @@ SVG.extend(SVG.Element, SVG.FX, { // get current matrix matrix = new SVG.Matrix(target).extract() - // add parametric rotation - if (typeof this.param === 'object') { - matrix.rotation = this.param.rotation - matrix.cx = this.param.cx - matrix.cy = this.param.cy - } - return typeof o === 'string' ? matrix[o] : matrix } // get current matrix - matrix = /*this instanceof SVG.FX && this.attrs.transform ? - this.attrs.transform :*/ - new SVG.Matrix(target) + matrix = new SVG.Matrix(target) // ensure relative flag relative = !!relative || !!o.relative @@ -41,26 +32,12 @@ SVG.extend(SVG.Element, SVG.FX, { // ensure centre point ensureCentre(o, target) - // relativize rotation value - /*if (relative) { - o.rotation += this.param && this.param.rotation != null ? - this.param.rotation : - matrix.extract().rotation - }*/ - - // store parametric values - //this.param = o - - if(this instanceof SVG.FX) return this.attr('transform', o) - // apply transformation - //if (this instanceof SVG.Element) { - matrix = relative ? - // relative - matrix.rotate(o.rotation, o.cx, o.cy) : - // absolute - matrix.rotate(o.rotation - matrix.extract().rotation, o.cx, o.cy) - //} + matrix = relative ? + // relative + matrix.rotate(o.rotation, o.cx, o.cy) : + // absolute + matrix.rotate(o.rotation - matrix.extract().rotation, o.cx, o.cy) // act on scale } else if (o.scale != null || o.scaleX != null || o.scaleY != null) { @@ -116,7 +93,84 @@ SVG.extend(SVG.Element, SVG.FX, { } } - return this.attr(this instanceof SVG.Pattern ? 'patternTransform' : this instanceof SVG.Gradient ? 'gradientTransform' : 'transform', matrix) + return this.attr('transform', matrix) + } +}) + +SVG.extend(SVG.FX, { + transform: function(o, relative) { + // get target in case of the fx module, otherwise reference this + var target = this.target() + , matrix + + // act as a getter + if (typeof o !== 'object') { + // get current matrix + matrix = new SVG.Matrix(target).extract() + + return typeof o === 'string' ? matrix[o] : matrix + } + + // ensure relative flag + relative = !!relative || !!o.relative + + // act on matrix + if (o.a != null) { + matrix = new SVG.Matrix(o) + + // act on rotation + } else if (o.rotation != null) { + // ensure centre point + ensureCentre(o, target) + + // apply transformation + matrix = new SVG.Rotate(o.rotation, o.cx, o.cy) + + // act on scale + } else if (o.scale != null || o.scaleX != null || o.scaleY != null) { + // ensure centre point + ensureCentre(o, target) + + // ensure scale values on both axes + o.scaleX = o.scale != null ? o.scale : o.scaleX != null ? o.scaleX : 1 + o.scaleY = o.scale != null ? o.scale : o.scaleY != null ? o.scaleY : 1 + + matrix = new SVG.Scale(o.scaleX, o.scaleY, o.cx, o.cy) + + // act on skew + } else if (o.skewX != null || o.skewY != null) { + // ensure centre point + ensureCentre(o, target) + + // ensure skew values on both axes + o.skewX = o.skewX != null ? o.skewX : 0 + o.skewY = o.skewY != null ? o.skewY : 0 + + matrix = new SVG.Skew(o.skewX, o.skewY, o.cx, o.cy) + + // act on flip + } else if (o.flip) { + matrix = new SVG.Matrix().morph(new SVG.Matrix().flip( + o.flip + , o.offset == null ? target.bbox()['c' + o.flip] : o.offset + )) + + // act on translate + } else if (o.x != null || o.y != null) { + matrix = new SVG.Translate(o.x, o.y) + } + + if(!matrix) return this + + matrix.relative = relative + + var situation = this.situations.length ? this.situations[this.situations.length-1] : this.current + + situation.transforms.push(matrix) + + setTimeout(function(){this.start()}.bind(this), 0) + + return this } }) @@ -163,3 +217,133 @@ SVG.extend(SVG.Element, { } }) + +SVG.Transformation = SVG.invent({ + + create: function(source, inversed){ + + if(arguments.length > 1 && typeof inversed != 'boolean'){ + return this.create([].slice.call(arguments)) + } + + if(typeof source == 'object'){ + for(var i = 0, len = this.arguments.length; i < len; ++i){ + this[this.arguments[i]] = source[this.arguments[i]] + } + } + + if(Array.isArray(source)){ + for(var i = 0, len = this.arguments.length; i < len; ++i){ + this[this.arguments[i]] = source[i] + } + } + + this.inversed = false + + if(inversed === true){ + this.inversed = true + } + + } + +, extend: { + + at: function(pos){ + + var params = [] + + for(var i = 0, len = this.arguments.length; i < len; ++i){ + params.push(this[this.arguments[i]]) + } + + var m = this._undo || new SVG.Matrix() + + m = new SVG.Matrix().morph(SVG.Matrix.prototype[this.method].apply(m, params)).at(pos) + + return this.inversed ? m.inverse() : m + + } + + , undo: function(o){ + this._undo = new SVG[capitalize(this.method)](o, true).at(1) + return this + } + + } + +}) + +SVG.Translate = SVG.invent({ + + parent: SVG.Matrix +, inherit: SVG.Transformation + +, create: function(source, inversed){ + if(typeof source == 'object') this.constructor.call(this, source, inversed) + else this.constructor.call(this, [].slice.call(arguments)) + } + +, extend: { + arguments: ['transformedX', 'transformedY'] + , method: 'translate' + } + +}) + +SVG.Rotate = SVG.invent({ + + parent: SVG.Matrix +, inherit: SVG.Transformation + +, create: function(source, inversed){ + if(typeof source == 'object') this.constructor.call(this, source, inversed) + else this.constructor.call(this, [].slice.call(arguments)) + } + +, extend: { + arguments: ['rotation', 'cx', 'cy'] + , method: 'rotate' + , at: function(pos){ + var m = new SVG.Matrix().rotate(new SVG.Number().morph(this.rotation - (this._undo ? this._undo.rotation : 0)).at(pos), this.cx, this.cy) + return this.inversed ? m.inverse() : m + } + , undo: function(o){ + this._undo = o + } + } + +}) + +SVG.Scale = SVG.invent({ + + parent: SVG.Matrix +, inherit: SVG.Transformation + +, create: function(source, inversed){ + if(typeof source == 'object') this.constructor.call(this, source, inversed) + else this.constructor.call(this, [].slice.call(arguments)) + } + +, extend: { + arguments: ['scaleX', 'scaleY', 'cx', 'cy'] + , method: 'scale' + } + +}) + +SVG.Skew = SVG.invent({ + + parent: SVG.Matrix +, inherit: SVG.Transformation + +, create: function(source, inversed){ + if(typeof source == 'object') this.constructor.call(this, source, inversed) + else this.constructor.call(this, [].slice.call(arguments)) + } + +, extend: { + arguments: ['skewX', 'skewY', 'cx', 'cy'] + , method: 'skew' + } + +}) diff --git a/src/viewbox.js b/src/viewbox.js index ce9c902..bb106b0 100644 --- a/src/viewbox.js +++ b/src/viewbox.js @@ -1,63 +1,115 @@ -SVG.ViewBox = function(element) { - var x, y, width, height - , wm = 1 // width multiplier - , hm = 1 // height multiplier - , box = element.bbox() - , view = (element.attr('viewBox') || '').match(/-?[\d\.]+/g) - , we = element - , he = element - - // get dimensions of current node - width = new SVG.Number(element.width()) - height = new SVG.Number(element.height()) - - // find nearest non-percentual dimensions - while (width.unit == '%') { - wm *= width.value - width = new SVG.Number(we instanceof SVG.Doc ? we.parent().offsetWidth : we.parent().width()) - we = we.parent() - } - while (height.unit == '%') { - hm *= height.value - height = new SVG.Number(he instanceof SVG.Doc ? he.parent().offsetHeight : he.parent().height()) - he = he.parent() - } - - // ensure defaults - this.x = box.x - this.y = box.y - this.width = width * wm - this.height = height * hm - this.zoom = 1 - - if (view) { - // get width and height from viewbox - x = parseFloat(view[0]) - y = parseFloat(view[1]) - width = parseFloat(view[2]) - height = parseFloat(view[3]) - - // calculate zoom accoring to viewbox - this.zoom = ((this.width / this.height) > (width / height)) ? - this.height / height : - this.width / width - - // calculate real pixel dimensions on parent SVG.Doc element - this.x = x - this.y = y - this.width = width - this.height = height - +SVG.ViewBox = SVG.invent({ + + create: function(source) { + var i, base = [1, 0, 0, 1] + + var x, y, width, height, box, view, we, he + , wm = 1 // width multiplier + , hm = 1 // height multiplier + , reg = /-?[\d\.]+/g + + if(source instanceof SVG.Element){ + + we = source + he = source + view = (source.attr('viewBox') || '').match(reg) + box = source.bbox + + // get dimensions of current node + width = new SVG.Number(source.width()) + height = new SVG.Number(source.height()) + + // find nearest non-percentual dimensions + while (width.unit == '%') { + wm *= width.value + width = new SVG.Number(we instanceof SVG.Doc ? we.parent().offsetWidth : we.parent().width()) + we = we.parent() + } + while (height.unit == '%') { + hm *= height.value + height = new SVG.Number(he instanceof SVG.Doc ? he.parent().offsetHeight : he.parent().height()) + he = he.parent() + } + + // ensure defaults + this.x = 0 + this.y = 0 + this.width = width * wm + this.height = height * hm + this.zoom = 1 + + if (view) { + // get width and height from viewbox + x = parseFloat(view[0]) + y = parseFloat(view[1]) + width = parseFloat(view[2]) + height = parseFloat(view[3]) + + // calculate zoom accoring to viewbox + this.zoom = ((this.width / this.height) > (width / height)) ? + this.height / height : + this.width / width + + // calculate real pixel dimensions on parent SVG.Doc element + this.x = x + this.y = y + this.width = width + this.height = height + + } + + }else{ + // ensure source as object + source = typeof source === 'string' ? + source.match(reg).map(function(el){ return parseFloat(el) }) : + Array.isArray(source) ? + source : + typeof source == 'object' ? + [source.x, source.y, source.width, source.height] : + arguments.length == 4 ? + [].slice.call(arguments) : + base + + this.x = source[0] + this.y = source[1] + this.width = source[2] + this.height = source[3] + } + + } - -} - -// -SVG.extend(SVG.ViewBox, { - // Parse viewbox to string - toString: function() { - return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height + +, extend: { + + toString: function() { + return this.x + ' ' + this.y + ' ' + this.width + ' ' + this.height + } + , morph: function(v){ + + var v = arguments.length == 1 ? + [v.x, v.y, v.width, v.height] : + [].slice.call(arguments) + + this.destination = new SVG.ViewBox(v) + + return this + + } + + , at: function(pos) { + + if(!this.destination) return this + + return new SVG.ViewBox([ + this.x + (this.destination.x - this.x) * pos + , this.y + (this.destination.y - this.y) * pos + , this.width + (this.destination.width - this.width) * pos + , this.height + (this.destination.height - this.height) * pos + ]) + + } + } - + })
\ No newline at end of file |