@@ -46,34 +46,34 @@ function getColor(t) { | |||
SVG('rect') | |||
.clone().show() | |||
.animate() | |||
.move(200, 200) | |||
// for (let i = 0 ; i < 15; i++) { | |||
// for (let j = 0 ; j < 10; j++) { | |||
// | |||
// // Make the rect | |||
// let o = i + j | |||
// let rect = SVG('rect').clone().show() | |||
// .width(40).height(40) | |||
// .x(50 * i).y(50 * j) | |||
// .attr('fill', getColor(o * 0.1)) | |||
// | |||
// // Move the rect | |||
// let {cx, cy} = rect.bbox() | |||
// | |||
// // Animate the rect | |||
// rect.animate(3000, Math.random() * 2000) | |||
// .during(t => rect.transform({rotate: 720 * t, origin: [cx, cy]})) | |||
// .during(t => rect.attr('fill', getColor(o * 0.1 + t))) | |||
// } | |||
// } | |||
// SVG('rect') | |||
// .clone().show() | |||
// .animate() | |||
// .move(200, 200) | |||
for (let i = 0 ; i < 15; i++) { | |||
for (let j = 0 ; j < 10; j++) { | |||
// Make the rect | |||
let o = i + j | |||
let rect = SVG('rect').clone().show() | |||
.width(40).height(40) | |||
.x(50 * i).y(50 * j) | |||
.attr('fill', getColor(o * 0.1)) | |||
// Move the rect | |||
let {cx, cy} = rect.bbox() | |||
// Animate the rect | |||
rect.animate(3000, Math.random() * 2000) | |||
.during(t => rect.transform({rotate: 720 * t, origin: [cx, cy]})) | |||
.during(t => rect.attr('fill', getColor(o * 0.1 + t))) | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,273 @@ | |||
describe('SVG.Runner', function () { | |||
var initFn = jasmine.createSpy('initFn') | |||
var runFn = jasmine.createSpy('runFn') | |||
describe('())', function () { | |||
it('creates a runner with defaults', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner instanceof SVG.Runner).toBe(true) | |||
expect(runner._duration).toBe(SVG.timeline.duration) | |||
expect(runner._ease).toBe(SVG.timeline.ease) | |||
expect(runner._stepper).toBe(null) | |||
}) | |||
it('creates a runner with duration set', function () { | |||
var runner = new SVG.Runner(1000) | |||
expect(runner instanceof SVG.Runner).toBe(true) | |||
expect(runner._duration).toBe(1000) | |||
expect(runner._ease).toBe(SVG.timeline.ease) | |||
expect(runner._stepper).toBe(null) | |||
}) | |||
it('creates a runner with controller set', function () { | |||
var runner = new SVG.Runner(runFn) | |||
expect(runner instanceof SVG.Runner).toBe(true) | |||
expect(runner._duration).toBe(null) | |||
expect(runner._ease).toBe(SVG.timeline.ease) | |||
expect(runner._stepper).toBe(runFn) | |||
}) | |||
}) | |||
describe('constructors', function () { | |||
describe('animate()', function () { | |||
it('creates a runner with the element set and schedules it on the timeline', function () { | |||
spyOn(SVG, 'Runner').and.callTrough() | |||
var element = SVG('<rect>') | |||
var runner = element.animate() | |||
expect(SVG.Runner).toHaveBeenCalled(); | |||
expect(runner instanceof SVG.Runner) | |||
expect(runner.element()).toBe(element) | |||
expect(element.timeline()._runners.length).toBe(1) | |||
}) | |||
}) | |||
describe('loop()', function () { | |||
it('calls animate with correct parameters', function () { | |||
var element = SVG('<rect>') | |||
spyOn(element, 'animate') | |||
element.loop() | |||
expect(element.animate).toHaveBeenCalledWith(objectContaining({ | |||
duration: SVG.defaults.timeline.duration, | |||
times: Infinity, | |||
swing: false | |||
})); | |||
}) | |||
}) | |||
describe('delay()', function () { | |||
it('calls animate with correct parameters', function () { | |||
var element = SVG('<rect>') | |||
spyOn(element, 'animate') | |||
element.delay(100, 'now') | |||
expect(element.animate).toHaveBeenCalledWith(0, 100, 'now') | |||
}) | |||
}) | |||
}) | |||
describe('queue()', function () { | |||
it('adds another closure to the runner', function () { | |||
var runner = new SVG.Runner() | |||
runner.queue(initFn, runFn, true) | |||
expect(runner.functions[0]).toEqual(jasmine.objectContaining({ | |||
alwaysInitialise: true, | |||
initialiser: initFn, | |||
runner: runFn | |||
})) | |||
}) | |||
}) | |||
describe('tag()', function () { | |||
it('acts as a getter', function () { | |||
var runner = new SVG.Runner() | |||
runner.tags = {foo: true} | |||
expect(runner.tag()).toEqual(jasmine.arrayContaining(['foo'])) | |||
}) | |||
it('sets one tag with a string given', function () { | |||
var runner = new SVG.Runner() | |||
runner.tag('foo') | |||
expect(runner.tags).toEqual(jasmine.objectContaining({foo: true})) | |||
}) | |||
it('sets multiple tags with an array given', function () { | |||
var runner = new SVG.Runner() | |||
runner.tag(['foo', 'bar', 'baz']) | |||
expect(runner.tags).toEqual(jasmine.objectContaining({foo: true, bar: true, baz: true})) | |||
}) | |||
}) | |||
describe('step()', function () { | |||
it('calls initFn once and runFn at every step when alwaysInitialise is false', function() { | |||
var runner = new SVG.Runner() | |||
runner.queue(initFn, runFn, false) | |||
runner.step() | |||
expect(initFn).toHaveBeenCalled() | |||
expect(runFn).toHaveBeenCalled() | |||
runner.step() | |||
expect(init.calls.count()).toBe(1) | |||
expect(runFn.calls.count()).toBe(2) | |||
}) | |||
it('calls initFn and runFn at every step when alwaysInitialise is true', function() { | |||
var runner = new SVG.Runner() | |||
runner.queue(initFn, runFn, true) | |||
runner.step() | |||
expect(initFn).toHaveBeenCalled() | |||
expect(runFn).toHaveBeenCalled() | |||
runner.step() | |||
expect(initFn.calls.count()).toBe(2) | |||
expect(runFn.calls.count()).toBe(2) | |||
}) | |||
it('returns false if not finished', function () { | |||
var runner = new SVG.Runner() | |||
runner.queue(initFn, runFn, false) | |||
expect(runner.step()).toBe(false) | |||
}) | |||
it('returns true if finished', function () { | |||
var runner = new SVG.Runner() | |||
runner.queue(initFn, runFn, false) | |||
expect(runner.step()).toBe(true) | |||
}) | |||
}) | |||
describe('active()', function () { | |||
it('acts as a getter without parameters', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.active()).toBe(true) | |||
}) | |||
it('disabled the runner when false is passed', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.active(false)).toBe(runner) | |||
expect(runner.active()).toBe(false) | |||
}) | |||
it('enables the runner when true is passed', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.active(false)).toBe(runner) | |||
expect(runner.active(true)).toBe(runner) | |||
expect(runner.active()).toBe(true) | |||
}) | |||
}) | |||
describe('time()', function () { | |||
it('returns itself', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.time(0)).toBe(runner) | |||
}) | |||
it('acts as a getter with no parameter passed', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.time()).toBe(0) | |||
}) | |||
it('reschedules the runner to a new time', function () { | |||
var runner = new SVG.Runner() | |||
runner.time(10) | |||
expect(runner.time()).toBe(10) | |||
}) | |||
it('calls step to reschedule', function () { | |||
var runner = new SVG.Runner() | |||
spyOn(runner, 'step') | |||
runner.time(10) | |||
expect(runner.step).toHaveBeenCalledWith(10) | |||
}) | |||
}) | |||
describe('element()', function () { | |||
it('returns the element bound to this runner if any', function () { | |||
var runner1 = new SVG.Runner() | |||
expect(runner1.element()).toBe(null) | |||
var element = SVG('<rect>') | |||
var runner2 = element.animate() | |||
expect(runner1.element()).toBe(element) | |||
}) | |||
it('sets an element to be bound to the runner', function () { | |||
var runner = new SVG.Runner() | |||
var element = SVG('<rect>') | |||
runner.element(element) | |||
expect(runner.element()).toBe(element) | |||
}) | |||
}) | |||
describe('during()', function () { | |||
it('returns itself', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.during(runFn)).toBe(runner) | |||
}) | |||
it('calls queue giving only a function to call on every step', function () { | |||
var runner = new SVG.Runner() | |||
spyOn(runner.queue) | |||
runner.during(runFn) | |||
expect(runner.queue).toHaveBeenCalledWith(null, runFn) | |||
}) | |||
}) | |||
describe('finish()', function () { | |||
it('returns itself', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.finish()).toBe(runner) | |||
}) | |||
it('calls step with Infinity as arument', function () { | |||
var runner = new SVG.Runner() | |||
spyOn(runner.step) | |||
runner.finish() | |||
expect(runner.step).toHaveBeenCalledWith(Infinity) | |||
}) | |||
}) | |||
describe('reverse()', function () { | |||
it('returns itself', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.reverse()).toBe(runner) | |||
}) | |||
it('reverses the runner by setting the time to the end and going backwards', function () { | |||
var runner = new SVG.Runner() | |||
spyOn(runner.time) | |||
runner.reverse() | |||
expect(runner.time).toHaveBeenCalledWith(SVG.defaults.timeline.duration) | |||
}) | |||
}) | |||
describe('ease()', function () { | |||
it('returns itself', function () { | |||
var runner = new SVG.Runner() | |||
expect(runner.ease(function () {})).toBe(runner) | |||
}) | |||
it('creates an easing Controller from the easing function', function () { | |||
var runner = new SVG.Runner() | |||
runner.ease(function () {}) | |||
expect(runner._stepper instanceof SVG.Stepper).toBe(true) | |||
}) | |||
}) | |||
}) |
@@ -9,7 +9,7 @@ Base Class | |||
The base stepper class that will be | |||
***/ | |||
SVG.Stepper = SVG.Invent ({ | |||
SVG.Stepper = SVG.invent ({ | |||
create: function (fn) { | |||
@@ -32,22 +32,27 @@ Easing Functions | |||
================ | |||
***/ | |||
SVG.Ease = SVG.Invent ({ | |||
SVG.Ease = SVG.invent ({ | |||
inherit: SVG.Stepper, | |||
create: function (fn) { | |||
SVG.Stepper.call(this, fn) | |||
this.ease = SVG.easing[fn || SVG.defaults.timeline.ease] || fn | |||
}, | |||
extend: { | |||
step: function (current, target, dt, c) { | |||
step: function (from, to, pos) { | |||
if(typeof from !== 'number') { | |||
return pos < 1 ? from : to | |||
} | |||
return from + (to - from) * this.ease(pos) | |||
}, | |||
isComplete: function (dt, c) { | |||
return false | |||
}, | |||
}, | |||
}) | |||
@@ -70,7 +75,7 @@ Controller Types | |||
================ | |||
***/ | |||
SVG.Controller = SVG.Invent ({ | |||
SVG.Controller = SVG.invent ({ | |||
inherit: SVG.Stepper, | |||
@@ -107,7 +112,7 @@ SVG.Spring = function spring(duration, overshoot) { | |||
var K = wn * wn | |||
// Return the acceleration required | |||
return SVG.Controller( | |||
return new SVG.Controller( | |||
function (current, target, dt, c) { | |||
if(dt == Infinity) return target |
@@ -94,6 +94,11 @@ SVG.Morphable = SVG.invent({ | |||
this._stepper = stepper | |||
}, | |||
// FIXME: we can call this._stepper.isComplete directly | |||
// no need for this wrapper here | |||
isComplete: function () { | |||
return this._stepper && this._stepper.isComplete() | |||
}, | |||
at: function (pos) { | |||
var _this = this | |||
@@ -105,11 +110,11 @@ SVG.Morphable = SVG.invent({ | |||
return this._type.prototype.fromArray( | |||
this.modifier( | |||
this._from.map(function (i, index) { | |||
return _this._stepper.step(i, _this._to[index], pos, _this.context[i]) | |||
return _this._stepper.step(i, _this._to[index], pos, _this._context[i]) | |||
}) | |||
) | |||
) | |||
} | |||
}, | |||
valueOf: function () { | |||
return this._value |
@@ -6,23 +6,25 @@ SVG.easing = { | |||
'<': function (pos) { return -Math.cos(pos * Math.PI / 2) + 1 } | |||
} | |||
SVG.Runner = SVG.Invent({ | |||
SVG.Runner = SVG.invent({ | |||
parent: SVG.Element, | |||
create: function (options) { | |||
options = options || SVG.defaults.timeline.duration | |||
// Declare all of the variables | |||
this._element = null | |||
this._functions = [] | |||
this.done = false | |||
// Work out the stepper and the duration | |||
this._stepper = typeof options === 'function' && (options) | |||
this._duration = typeof options === 'number' && options | |||
this._isDeclaritive = options instanceof SVG.Controller | |||
this._stepper = this._isDeclaritive ? options : new SVG.Ease() | |||
// We copy the current values from the timeline because they can change | |||
this._ease = SVG.defaults.timeline.ease | |||
this._morphers = {} | |||
// Store the state of the runner | |||
@@ -51,9 +53,11 @@ SVG.Runner = SVG.Invent({ | |||
waits = duration.waits || [] | |||
} | |||
// FIXME: take care of looping here because loop is a constructor | |||
// alternatively disallow loop as constructor | |||
// Construct a new runner and setup its looping behaviour | |||
var runner = new SVG.Runner(duration) | |||
.loop(times, swing, waits) | |||
//.loop(times, swing, waits) | |||
.element(this) | |||
// Attach this animation to a timeline | |||
@@ -95,6 +99,12 @@ SVG.Runner = SVG.Invent({ | |||
return this | |||
}, | |||
timeline: function () { | |||
return this._element.timeline() | |||
}, | |||
// FIXME: It makes totally sense to call this, when the runner is attached to a timeline. | |||
// So maybe we should attach runners to timelines and timelines to elements instead of the other way round | |||
animate: function () { | |||
if(this._element) { | |||
return this._element.animate.apply(this._element, arguments) | |||
@@ -196,10 +206,12 @@ SVG.Runner = SVG.Invent({ | |||
// Changes the animation easing function | |||
ease: function (fn) { | |||
this._stepper = SVG.Ease(fn) | |||
return this | |||
}, | |||
enable: function (enabled) { | |||
active: function (enabled) { | |||
if(active == null) return this._enabled | |||
this._enabled = enabled | |||
return this | |||
}, | |||
@@ -307,15 +319,12 @@ SVG.extend(SVG.Runner, { | |||
var morpher = new Morphable(this._stepper).to(val) | |||
this.queue( | |||
function () { | |||
morpher = morpher.from(this[type](name)) | |||
}, | |||
function () { | |||
this[type](name, morpher.at(pos)) | |||
return morpher.isComplete() | |||
} | |||
) | |||
this.queue(function () { | |||
morpher = morpher.from(this[type](name)) | |||
}, function () { | |||
this[type](name, morpher.at(pos)) | |||
return morpher.isComplete() | |||
}, this._isDeclarative) | |||
return this | |||
}, | |||
@@ -328,7 +337,7 @@ SVG.extend(SVG.Runner, { | |||
}, function (pos) { | |||
this.zoom(morpher.at(pos), point) | |||
return morpher.isComplete() | |||
}) | |||
}, this._isDeclarative) | |||
return this | |||
}, | |||
@@ -378,7 +387,8 @@ SVG.extend(SVG.Runner, { | |||
return this.queue(function() {}, function (pos) { | |||
this.pushRightTransform(new Matrix(morpher.at(pos))) | |||
}) | |||
return morpher.isComplete() | |||
}, this._isDeclarative) | |||
} | |||
@@ -420,7 +430,7 @@ SVG.extend(SVG.Runner, { | |||
} | |||
return morpher.isComplete() | |||
}) | |||
}, this._isDeclarative) | |||
return this | |||
}, | |||
@@ -458,7 +468,7 @@ SVG.extend(SVG.Runner, { | |||
}, function (pos) { | |||
this[method](morpher.at(pos)) | |||
return morpher.isComplete() | |||
}, this._declarative) | |||
}, this._isDeclarative) | |||
// Register the morpher so that if it is changed again, we can retarget it | |||
this._saveMorpher(method, morpher) | |||
@@ -477,7 +487,7 @@ SVG.extend(SVG.Runner, { | |||
}, function (pos) { | |||
this[method](morpher.at(pos)) | |||
return morpher.isComplete() | |||
}, this._declarative) | |||
}, this._isDeclarative) | |||
// Register the morpher so that if it is changed again, we can retarget it | |||
this._saveMorpher(method, morpher) |
@@ -124,21 +124,74 @@ SVG.Timeline = SVG.invent({ | |||
} else { | |||
return this._runner | |||
} | |||
} | |||
}, | |||
schedule (runner, delay, when) { | |||
// The start time for the next animation can either be given explicitly, | |||
// derived from the current timeline time or it can be relative to the | |||
// last start time to chain animations direclty | |||
var absoluteStartTime | |||
schedule () { | |||
// Work out when to start the animation | |||
if ( when == null || when === 'last' || when === 'relative' ) { | |||
// Take the last time and increment | |||
// FIXME: How to figue out the relative time? Maybe use runner.endTime() | |||
absoluteStartTime = this._startTime + delay | |||
} else if (when === 'absolute' || when === 'start' ) { | |||
} else if (when === '' ) { | |||
absoluteStartTime = delay | |||
} else if (when === 'now') { | |||
absoluteStartTime = this._time + delay | |||
} else { | |||
// TODO: Throw error | |||
} | |||
} | |||
runner.time(absoluteStartTime) | |||
this._startTime = absoluteStartTime + runner._duration | |||
this._runners.push(runner) | |||
this._step() | |||
return this | |||
// Clear the controller and the looping parameters | |||
this._controller = duration instanceof Function ? duration : null | |||
this._backwards = false | |||
this._swing = false | |||
this._loops = 0 | |||
// If we have an object we are declaring imperative animations | |||
if (typeof duration === 'object') { | |||
duration = duration.duration | |||
delay = duration.delay | |||
nowOrAbsolute = duration.absolute || duration.now | |||
} | |||
// The start time for the next animation can either be given explicitly, | |||
// derived from the current timeline time or it can be relative to the | |||
// last start time to chain animations direclty | |||
var absoluteStartTime = typeof nowOrAbsolute === 'number' ? nowOrAbsolute | |||
: nowOrAbsolute ? this._time | |||
: this._startTime + this._duration | |||
// We start the next animation after the delay required | |||
this._startTime = absoluteStartTime + (delay || 0) | |||
this._duration = duration instanceof Function ? null | |||
: (duration || SVG.defaults.timeline.duration) | |||
// Make a new runner to queue all of the animations onto | |||
this._runner = new Runner(this._time - this._startTime, this.duration) | |||
this._runners.push(this._runner) | |||
// Step the animation | |||
this._step() | |||
// Allow for chaining | |||
return this | |||
}, | |||
play () { | |||
@@ -159,23 +212,27 @@ SVG.Timeline = SVG.invent({ | |||
stop () { | |||
// Cancel the next animation frame for this object | |||
this._nextFrame = null | |||
return this | |||
}, | |||
finish () { | |||
return this | |||
}, | |||
speed (newSpeed) { | |||
this._speed = newSpeed | |||
return this | |||
}, | |||
seek (dt) { | |||
this._time += dt | |||
return this | |||
}, | |||
time (t) { | |||
this._time = t | |||
} | |||
return this | |||
}, | |||
persist (dtOrForever) { | |||
// 0 by default | |||
@@ -266,43 +323,42 @@ SVG.Timeline = SVG.invent({ | |||
}, | |||
}, | |||
// Only elements are animatable | |||
parent: SVG.Element, | |||
// These methods will be added to all SVG.Element objects | |||
parent: SVG.Element, | |||
construct: { | |||
timeline: function () { | |||
this.timeline = (this.timeline || new SVG.Timeline(this)) | |||
return this.timeline | |||
}, | |||
animate: function(o, delay, now) { | |||
// Get the current timeline or construct a new one | |||
this.timeline = (this.timeline || new SVG.Timeline(this)) | |||
.animate(o, delay, now) | |||
this.timeline._loops = null | |||
return this.timeline | |||
this._timeline = (this._timeline || new SVG.Timeline(this)) | |||
return this._timeline | |||
}, | |||
loop: function(o) { | |||
/* | |||
{ | |||
swing: wether or not the animation should repeat when its done | |||
times: the number of times to loop the animation | |||
wait: [array] a buffer of times to wait between successive animations | |||
delay: defaults.timeline to wait | |||
} | |||
*/ | |||
this.timeline = (this.timeline || new SVG.Timeline(this)) | |||
// REFACTOR this into an init function | |||
this.timeline._waits = [].concat(o.wait || o.delay || 0) | |||
this.timeline._loops = o.times || Infinity | |||
this.timeline._swing = o.swing || false | |||
return this.timeline | |||
} | |||
// animate: function(o, delay, now) { | |||
// | |||
// // Get the current timeline or construct a new one | |||
// this.timeline = (this.timeline || new SVG.Timeline(this)) | |||
// .animate(o, delay, now) | |||
// this.timeline._loops = null | |||
// return this.timeline | |||
// }, | |||
// | |||
// loop: function(o) { | |||
// | |||
// /* | |||
// { | |||
// swing: wether or not the animation should repeat when its done | |||
// times: the number of times to loop the animation | |||
// wait: [array] a buffer of times to wait between successive animations | |||
// delay: defaults.timeline to wait | |||
// } | |||
// */ | |||
// this.timeline = (this.timeline || new SVG.Timeline(this)) | |||
// | |||
// // REFACTOR this into an init function | |||
// this.timeline._waits = [].concat(o.wait || o.delay || 0) | |||
// this.timeline._loops = o.times || Infinity | |||
// this.timeline._swing = o.swing || false | |||
// return this.timeline | |||
// } | |||
} | |||
}) |