From 59f09a1a2317e57d13bbe8f60e1949cc82199ead Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ulrich-Matthias=20Sch=C3=A4fer?= Date: Sun, 3 May 2020 12:32:34 +1000 Subject: [PATCH] 99% line coverage - BAAAAM --- CHANGELOG.md | 3 + spec/setupBrowser.js | 2 +- spec/spec/animation/Morphable.js | 32 +- spec/spec/animation/Runner.js | 2423 ++++++++++++++++++++++-------- src/animation/Morphable.js | 88 +- src/animation/Runner.js | 83 +- src/types/Color.js | 2 +- 7 files changed, 1919 insertions(+), 714 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9789900..90bd0f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http: - fixed `Timeline.reverse()` which did exactly the opposite of what you would expect when passing `true/false` - fixed cancelAnimationFrame-mock for tests - fixed animate when=after to be really "now" when no runner is on the timeline + - fixed animate attr which is also retargetable now + - fixed internals of ObjectBag which can hold other Morphable values now + - fixed animate transform which didnt change its origin on retarget for declaritive animations ### Added - added second Parameter to `SVG(el, isHTML)` which allows to explicitely create elements in the HTML namespace (#1058) diff --git a/spec/setupBrowser.js b/spec/setupBrowser.js index b2441c1..429fa88 100644 --- a/spec/setupBrowser.js +++ b/spec/setupBrowser.js @@ -1,7 +1,7 @@ /* globals beforeEach, afterEach, jasmine */ import { buildCanvas, clear } from './helpers.js' -jasmine.DEFAULT_TIMEOUT_INTERVAL = 200 +jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 beforeEach(() => { // buildFixtures() diff --git a/spec/spec/animation/Morphable.js b/spec/spec/animation/Morphable.js index c1f27e8..f703f97 100644 --- a/spec/spec/animation/Morphable.js +++ b/spec/spec/animation/Morphable.js @@ -313,20 +313,39 @@ describe('Morphable.js', () => { describe('ObjectBag', () => { describe('()', () => { it('wraps an object into a morphable object by passing an array', () => { - const bag = new ObjectBag([ 'foo', 1, 'bar', 2, 'baz', 3 ]) - expect(bag.values).toEqual([ 'foo', 1, 'bar', 2, 'baz', 3 ]) + const bag = new ObjectBag([ 'foo', SVGNumber, 2, 1, '', 'bar', SVGNumber, 2, 2, '', 'baz', SVGNumber, 2, 3, '' ]) + expect(bag.values).toEqual([ 'foo', SVGNumber, 2, 1, '', 'bar', SVGNumber, 2, 2, '', 'baz', SVGNumber, 2, 3, '' ]) }) it('wraps an object into a morphable object by passing an object', () => { const bag = new ObjectBag({ foo: 1, bar: 2, baz: 3 }) - expect(bag.values).toEqual([ 'bar', 2, 'baz', 3, 'foo', 1 ]) + expect(bag.values).toEqual([ 'bar', SVGNumber, 2, 2, '', 'baz', SVGNumber, 2, 3, '', 'foo', SVGNumber, 2, 1, '' ]) + }) + + it('wraps an object with morphable values in an ObjectBag', () => { + const bag = new ObjectBag({ fill: new Color(), bar: 2 }) + expect(bag.values).toEqual([ 'bar', SVGNumber, 2, 2, '', 'fill', Color, 5, 0, 0, 0, 0, 'rgb' ]) + }) + + it('wraps an array with morphable representation in an ObjectBag', () => { + const bag = new ObjectBag([ 'bar', SVGNumber, 2, 2, '', 'fill', Color, 5, 0, 0, 0, 0, 'rgb' ]) + expect(bag.toArray()).toEqual([ 'bar', SVGNumber, 2, 2, '', 'fill', Color, 5, 0, 0, 0, 0, 'rgb' ]) }) }) describe('toArray()', () => { it('creates an array out of the object', () => { const bag = new ObjectBag({ foo: 1, bar: 2, baz: 3 }) - expect(bag.toArray()).toEqual([ 'bar', 2, 'baz', 3, 'foo', 1 ]) + expect(bag.toArray()).toEqual( + [ 'bar', SVGNumber, 2, 2, '', 'baz', SVGNumber, 2, 3, '', 'foo', SVGNumber, 2, 1, '' ] + ) + }) + + it('creates a flattened array out of the object with morphable values', () => { + const bag = new ObjectBag({ fill: new Color(), bar: 2 }) + expect(bag.toArray()).toEqual( + [ 'bar', SVGNumber, 2, 2, '', 'fill', Color, 5, 0, 0, 0, 0, 'rgb' ] + ) }) }) @@ -335,6 +354,11 @@ describe('Morphable.js', () => { const bag = new ObjectBag({ foo: 1, bar: 2, baz: 3 }) expect(bag.valueOf()).toEqual({ foo: 1, bar: 2, baz: 3 }) }) + + it('creates also morphable objects from the stored values', () => { + const bag = new ObjectBag({ fill: new Color(), bar: 2 }) + expect(bag.valueOf()).toEqual({ fill: objectContaining(new Color()), bar: 2 }) + }) }) }) }) diff --git a/spec/spec/animation/Runner.js b/spec/spec/animation/Runner.js index 63d1bb8..08fdbdc 100644 --- a/spec/spec/animation/Runner.js +++ b/spec/spec/animation/Runner.js @@ -1,842 +1,1947 @@ /* globals describe, expect, it, beforeEach, afterEach, spyOn, jasmine */ -import { Runner, defaults, Ease, Controller, SVG, Timeline } from '../../../src/main.js' +import { Runner, defaults, Ease, Controller, SVG, Timeline, Rect, Morphable, Animator, Queue, Matrix, Color, Box, Polygon, PathArray, PointArray } from '../../../src/main.js' +import { FakeRunner, RunnerArray } from '../../../src/animation/Runner.js' import { getWindow } from '../../../src/utils/window.js' +import SVGNumber from '../../../src/types/SVGNumber.js' -const { createSpy, objectContaining, arrayContaining } = jasmine +const { any, createSpy, objectContaining, arrayContaining } = jasmine describe('Runner.js', () => { - var initFn = createSpy('initFn') - var runFn = createSpy('runFn') - - beforeEach(() => { - jasmine.RequestAnimationFrame.install(getWindow()) - initFn.calls.reset() - runFn.calls.reset() - }) - - afterEach(() => { - jasmine.RequestAnimationFrame.uninstall(getWindow()) - }) + describe('Runner', () => { + var initFn = createSpy('initFn') + var runFn = createSpy('runFn') + + beforeEach(() => { + jasmine.RequestAnimationFrame.install(getWindow()) + Animator.timeouts = new Queue() + Animator.frames = new Queue() + Animator.immediates = new Queue() + Animator.nextDraw = null + initFn.calls.reset() + runFn.calls.reset() + }) - describe('sanitise()', () => { - it('can handle all form of input', () => { - var fn = Runner.sanitise - - expect(fn(200, 200, 'now')).toEqual(objectContaining({ - duration: 200, - delay: 200, - when: 'now', - times: 1, - wait: 0, - swing: false - })) - - expect(fn(200, 200)).toEqual(objectContaining({ - duration: 200, - delay: 200, - when: 'last', - times: 1, - wait: 0, - swing: false - })) - - expect(fn(200)).toEqual(objectContaining({ - duration: 200, - delay: defaults.timeline.delay, - when: 'last', - times: 1, - wait: 0, - swing: false - })) - - expect(fn(runFn)).toEqual(objectContaining({ - duration: runFn, - delay: defaults.timeline.delay, - when: 'last', - times: 1, - wait: 0, - swing: false - })) - - expect(fn({ delay: 200 })).toEqual(objectContaining({ - duration: defaults.timeline.duration, - delay: 200, - when: 'last', - times: 1, - wait: 0, - swing: false - })) - - expect(fn({ times: 3, delay: 200, when: 'now', swing: true, wait: 200 })).toEqual(objectContaining({ - duration: defaults.timeline.duration, - delay: 200, - when: 'now', - times: 3, - wait: 200, - swing: true - })) + afterEach(() => { + jasmine.RequestAnimationFrame.uninstall(getWindow()) }) - }) - describe('())', () => { - it('creates a runner with defaults', () => { - var runner = new Runner() - expect(runner instanceof Runner).toBe(true) - expect(runner._duration).toBe(defaults.timeline.duration) - expect(runner._stepper instanceof Ease).toBe(true) + describe('sanitise()', () => { + it('can handle all form of input', () => { + var fn = Runner.sanitise + + expect(fn(200, 200, 'now')).toEqual(objectContaining({ + duration: 200, + delay: 200, + when: 'now', + times: 1, + wait: 0, + swing: false + })) + + expect(fn(200, 200)).toEqual(objectContaining({ + duration: 200, + delay: 200, + when: 'last', + times: 1, + wait: 0, + swing: false + })) + + expect(fn(200)).toEqual(objectContaining({ + duration: 200, + delay: defaults.timeline.delay, + when: 'last', + times: 1, + wait: 0, + swing: false + })) + + expect(fn(runFn)).toEqual(objectContaining({ + duration: runFn, + delay: defaults.timeline.delay, + when: 'last', + times: 1, + wait: 0, + swing: false + })) + + expect(fn({ delay: 200 })).toEqual(objectContaining({ + duration: defaults.timeline.duration, + delay: 200, + when: 'last', + times: 1, + wait: 0, + swing: false + })) + + expect(fn({ times: 3, delay: 200, when: 'now', swing: true, wait: 200 })).toEqual(objectContaining({ + duration: defaults.timeline.duration, + delay: 200, + when: 'now', + times: 3, + wait: 200, + swing: true + })) + }) }) - it('creates a runner with duration set', () => { - var runner = new Runner(1000) - expect(runner instanceof Runner).toBe(true) - expect(runner._duration).toBe(1000) - expect(runner._stepper instanceof Ease).toBe(true) + describe('())', () => { + it('creates a runner with defaults', () => { + var runner = new Runner() + expect(runner instanceof Runner).toBe(true) + expect(runner._duration).toBe(defaults.timeline.duration) + expect(runner._stepper instanceof Ease).toBe(true) + }) + + it('creates a runner with duration set', () => { + var runner = new Runner(1000) + expect(runner instanceof Runner).toBe(true) + expect(runner._duration).toBe(1000) + expect(runner._stepper instanceof Ease).toBe(true) + }) + + it('creates a runner with controller set', () => { + var runner = new Runner(runFn) + expect(runner instanceof Runner).toBe(true) + expect(runner._duration).toBeFalsy() + expect(runner._stepper instanceof Controller).toBe(true) + }) }) - it('creates a runner with controller set', () => { - var runner = new Runner(runFn) - expect(runner instanceof Runner).toBe(true) - expect(runner._duration).toBeFalsy() - expect(runner._stepper instanceof Controller).toBe(true) + describe('queue()', () => { + it('adds another closure to the runner', () => { + var runner = new Runner() + runner.queue(initFn, runFn, true) + + expect(runner._queue[0]).toEqual(objectContaining({ + initialiser: initFn, + initialised: false, + runner: runFn, + finished: false + })) + }) }) - }) - describe('constructors', () => { - // FIXME: Not possible to spy like this in es6 - // describe('animate()', () => { - // it('creates a runner with the element set and schedules it on the timeline', () => { - // var orginalRunner = Runner - // spyOn(SVG, 'Runner').and.callFake(() =>{ - // return new orginalRunner() - // }) - // - // var element = SVG('') - // var runner = element.animate() - // expect(Runner).toHaveBeenCalled(); - // expect(runner instanceof Runner) - // expect(runner.element()).toBe(element) - // expect(runner.timeline()).toBe(element.timeline()) - // }) - // }) + describe('step()', () => { - describe('delay()', () => { - it('calls animate with correct parameters', () => { - var element = SVG('') + it('returns itself', () => { + var runner = new Runner() + expect(runner.step()).toBe(runner) + }) - spyOn(element, 'animate') - element.delay(100, 'now') - expect(element.animate).toHaveBeenCalledWith(0, 100, 'now') + it('does nothing when not active', () => { + const runner = new Runner().active(false) + const frozen = Object.freeze(runner) + expect(frozen.step()).toEqual(runner) }) - }) - }) - describe('queue()', () => { - it('adds another closure to the runner', () => { - var runner = new Runner() - runner.queue(initFn, runFn, true) + it('calls initFn once and runFn at every step', () => { + var runner = new Runner() + runner.queue(initFn, runFn, false) - expect(runner._queue[0]).toEqual(objectContaining({ - initialiser: initFn, - initialised: false, - runner: runFn, - finished: false - })) - }) - }) + runner.step() + expect(initFn).toHaveBeenCalled() + expect(runFn).toHaveBeenCalled() - describe('step()', () => { + runner.step() + expect(initFn.calls.count()).toBe(1) + expect(runFn.calls.count()).toBe(2) + }) - it('returns itself', () => { - var runner = new Runner() - expect(runner.step()).toBe(runner) - }) + it('calls initFn on every step if its declaritive', () => { + var runner = new Runner(new Controller()) + runner.queue(initFn, runFn, true) - it('calls initFn once and runFn at every step', () => { - var runner = new Runner() - runner.queue(initFn, runFn, false) + runner.step() + expect(initFn).toHaveBeenCalled() + expect(runFn).toHaveBeenCalled() - runner.step() - expect(initFn).toHaveBeenCalled() - expect(runFn).toHaveBeenCalled() + runner.step() + expect(initFn.calls.count()).toBe(2) + expect(runFn.calls.count()).toBe(2) + }) - runner.step() - expect(initFn.calls.count()).toBe(1) - expect(runFn.calls.count()).toBe(2) - }) + function getLoop (r) { + var loopDuration = r._duration + r._wait + var loopsDone = Math.floor(r._time / loopDuration) + return loopsDone + } + + // step in time + it('steps forward a certain time', () => { + var spy = createSpy('stepper') + var r = new Runner(1000).loop(10, false, 100) + r.queue(null, spy) + + r.step(300) // should be 0.3s + expect(spy).toHaveBeenCalledWith(0.3) + expect(getLoop(r)).toBe(0) + + r.step(300) // should be 0.6s + expect(spy).toHaveBeenCalledWith(0.6) + expect(getLoop(r)).toBe(0) + + r.step(600) // should be 0.1s + expect(spy).toHaveBeenCalledWith(0.1) + expect(getLoop(r)).toBe(1) + + r.step(-300) // should be 0.9s + expect(spy).toHaveBeenCalledWith(0.9) + expect(getLoop(r)).toBe(0) + + r.step(2000) // should be 0.7s + expect(spy).toHaveBeenCalledWith(0.7) + expect(getLoop(r)).toBe(2) + + r.step(-2000) // should be 0.9s + expect(spy).toHaveBeenCalledWith(0.9) + expect(getLoop(r)).toBe(0) + }) - it('calls initFn on every step if its declaritive', () => { - var runner = new Runner(new Controller()) - runner.queue(initFn, runFn, true) + it('handles dts which are bigger than the animation time', () => { + var runner = new Runner(1000) + runner.queue(initFn, runFn, true) - runner.step() - expect(initFn).toHaveBeenCalled() - expect(runFn).toHaveBeenCalled() + runner.step(1100) + expect(initFn).toHaveBeenCalled() + expect(runFn).toHaveBeenCalledWith(1) + }) - runner.step() - expect(initFn.calls.count()).toBe(2) - expect(runFn.calls.count()).toBe(2) - }) + describe('looping', () => { + describe('without wait', () => { + describe('unreversed', () => { + describe('nonswinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, false) + runner.queue(null, spy) + + runner.step(5750) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, false) + runner.queue(null, spy) + + runner.step(4750) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + }) - function getLoop (r) { - var loopDuration = r._duration + r._wait - var loopsDone = Math.floor(r._time / loopDuration) - return loopsDone - } + describe('swinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, true) + runner.queue(null, spy) + + runner.step(5750) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true) + runner.queue(null, spy) + + runner.step(4750) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + }) + }) - // step in time - it('steps forward a certain time', () => { - var spy = createSpy('stepper') - var r = new Runner(1000).loop(10, false, 100) - r.queue(null, spy) + describe('reversed', () => { + describe('nonswinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, false).reverse() + runner.queue(null, spy) + + runner.step(5750) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, false).reverse() + runner.queue(null, spy) + + runner.step(4750) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + }) - r.step(300) // should be 0.3s - expect(spy).toHaveBeenCalledWith(0.3) - expect(getLoop(r)).toBe(0) + describe('swinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, true).reverse() + runner.queue(null, spy) + + runner.step(5750) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true).reverse() + runner.queue(null, spy) + + runner.step(4750) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + }) + }) + }) - r.step(300) // should be 0.6s - expect(spy).toHaveBeenCalledWith(0.6) - expect(getLoop(r)).toBe(0) + describe('with wait', () => { + describe('unreversed', () => { + describe('nonswinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, false, 100) + runner.queue(null, spy) + + runner.step(5450) + expect(spy).toHaveBeenCalledWith(1) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, false, 100) + runner.queue(null, spy) + + runner.step(4350) + expect(spy).toHaveBeenCalledWith(1) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + }) - r.step(600) // should be 0.1s - expect(spy).toHaveBeenCalledWith(0.1) - expect(getLoop(r)).toBe(1) + describe('swinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, true, 100) + runner.queue(null, spy) + + runner.step(5450) + expect(spy).toHaveBeenCalledWith(1) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true, 100) + runner.queue(null, spy) + + runner.step(4350) + expect(spy).toHaveBeenCalledWith(0) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.75) + + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + }) + }) - r.step(-300) // should be 0.9s - expect(spy).toHaveBeenCalledWith(0.9) - expect(getLoop(r)).toBe(0) + describe('reversed', () => { + describe('nonswinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, false, 100).reverse() + runner.queue(null, spy) + + runner.step(5450) + expect(spy).toHaveBeenCalledWith(0) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, false, 100).reverse() + runner.queue(null, spy) + + runner.step(4350) + expect(spy).toHaveBeenCalledWith(0) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + }) - r.step(2000) // should be 0.7s - expect(spy).toHaveBeenCalledWith(0.7) - expect(getLoop(r)).toBe(2) + describe('swinging', () => { + it('does behave correctly at the end of an even loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(6, true, 100).reverse() + runner.queue(null, spy) + + runner.step(5450) + expect(spy).toHaveBeenCalledWith(0) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.75) + runner.step(250) + expect(spy).toHaveBeenCalledWith(1) + }) + + it('does behave correctly at the end of an uneven loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true, 100).reverse() + runner.queue(null, spy) + + runner.step(4350) + expect(spy).toHaveBeenCalledWith(1) + spy.calls.reset() + + runner.step(800) + expect(spy).toHaveBeenCalledWith(0.25) + runner.step(250) + expect(spy).toHaveBeenCalledWith(0) + }) + }) + }) + }) + }) - r.step(-2000) // should be 0.9s - expect(spy).toHaveBeenCalledWith(0.9) - expect(getLoop(r)).toBe(0) }) - it('handles dts which are bigger than the animation time', () => { - var runner = new Runner(1000) - runner.queue(initFn, runFn, true) + describe('active()', () => { + it('acts as a getter without parameters', () => { + var runner = new Runner() + expect(runner.active()).toBe(true) + }) + + it('disables the runner when false is passed', () => { + var runner = new Runner() + expect(runner.active(false)).toBe(runner) + expect(runner.active()).toBe(false) + }) - runner.step(1100) - expect(initFn).toHaveBeenCalled() - expect(runFn).toHaveBeenCalledWith(1) + it('enables the runner when true is passed', () => { + var runner = new Runner() + expect(runner.active(false)).toBe(runner) + expect(runner.active(true)).toBe(runner) + expect(runner.active()).toBe(true) + }) }) - describe('looping', () => { - describe('without wait', () => { - describe('unreversed', () => { - describe('nonswinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, false) - runner.queue(null, spy) + describe('duration()', () => { + it('return the full duration of the runner including all loops and waits', () => { + var runner = new Runner(800).loop(10, true, 200) + expect(runner.duration()).toBe(9800) + }) + }) - runner.step(5750) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) + describe('loop()', () => { + it('makes this runner looping', () => { + var runner = new Runner(1000).loop(5) + expect(runner.duration()).toBe(5000) + }) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, false) - runner.queue(null, spy) + it('makes this runner indefinitey by passing true', () => { + var runner = new Runner(1000).loop(true) + expect(runner.duration()).toBe(Infinity) + }) - runner.step(4750) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) - }) + it('makes this runner indefinitey by passing nothing', () => { + var runner = new Runner(1000).loop() + expect(runner.duration()).toBe(Infinity) + }) + }) - describe('swinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, true) - runner.queue(null, spy) + describe('time()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.time(0)).toBe(runner) + }) - runner.step(5750) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) + it('acts as a getter with no parameter passed', () => { + var runner = new Runner() + expect(runner.time()).toBe(0) + }) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true) - runner.queue(null, spy) + it('reschedules the runner to a new time', () => { + var runner = new Runner() + runner.time(10) - runner.step(4750) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) - }) - }) + expect(runner.time()).toBe(10) + }) - describe('reversed', () => { - describe('nonswinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, false).reverse() - runner.queue(null, spy) + it('calls step to reschedule', () => { + var runner = new Runner() + spyOn(runner, 'step') + runner.time(10) - runner.step(5750) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) + expect(runner.step).toHaveBeenCalledWith(10) + }) + }) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, false).reverse() - runner.queue(null, spy) + describe('loops()', () => { + it('get the loops of a runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - runner.step(4750) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) - }) + runner.step(300) + expect(spy).toHaveBeenCalledWith(0.3) - describe('swinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, true).reverse() - runner.queue(null, spy) + expect(runner.loops()).toBe(0.3) + }) + it('sets the loops of the runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - runner.step(5750) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) + expect(runner.loops(0.5).loops()).toBe(0.5) + expect(spy).toHaveBeenCalledWith(0.5) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true).reverse() - runner.queue(null, spy) + expect(runner.loops(0.1).loops()).toBe(0.1) + expect(spy).toHaveBeenCalledWith(0.1) - runner.step(4750) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) - }) - }) + expect(runner.loops(1.5).loops()).toBe(1) + expect(spy).toHaveBeenCalledWith(1) }) + it('sets the loops of the runner in a loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true, 500).queue(null, spy) - describe('with wait', () => { - describe('unreversed', () => { - describe('nonswinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, false, 100) - runner.queue(null, spy) + expect(runner.loops(1.3).loops()).toBe(1.3) + expect(spy).toHaveBeenCalledWith(0.7) - runner.step(5450) - expect(spy).toHaveBeenCalledWith(1) - spy.calls.reset() + expect(runner.loops(0.3).loops()).toBe(0.3) + }) + }) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) + describe('progress()', () => { + it('gets the progress of a runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, false, 100) - runner.queue(null, spy) + runner.step(300) + expect(spy).toHaveBeenCalledWith(0.3) - runner.step(4350) - expect(spy).toHaveBeenCalledWith(1) - spy.calls.reset() + expect(runner.progress()).toBe(0.3) + }) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) - }) + it('gets the progress of a runner when looping', () => { + var spy = createSpy('stepper') + var runner = new Runner(800).queue(null, spy).loop(10, false, 200) // duration should be 9800 - describe('swinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, true, 100) - runner.queue(null, spy) + // middle of animation, in the middle of wait time + runner.step(4900) + expect(runner.progress()).toBe(0.5) + expect(spy).toHaveBeenCalledWith(1) - runner.step(5450) - expect(spy).toHaveBeenCalledWith(1) - spy.calls.reset() + // start of next loop + runner.step(100) + expect(spy).toHaveBeenCalledWith(0) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) + // move 400 into current loop which is 0.5 progress + // the progress value is 5400 / 9800 + runner.step(400) + expect(spy).toHaveBeenCalledWith(0.5) + expect(runner.progress()).toBe(5400 / 9800) + }) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true, 100) - runner.queue(null, spy) + it('sets the progress of a runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - runner.step(4350) - expect(spy).toHaveBeenCalledWith(0) - spy.calls.reset() + expect(runner.progress(0.5).progress()).toBe(0.5) + expect(spy).toHaveBeenCalledWith(0.5) + }) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.75) + it('sets the progress of a runner when looping', () => { + var spy = createSpy('stepper') + var runner = new Runner(800).queue(null, spy).loop(10, false, 200) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) - }) - }) + // progress 0.5 somewhere in the middle of wait time + expect(runner.progress(0.5).progress()).toBe(0.5) + expect(spy).toHaveBeenCalledWith(1) - describe('reversed', () => { - describe('nonswinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, false, 100).reverse() - runner.queue(null, spy) + // start of next loop + runner.step(100) + expect(spy).toHaveBeenCalledWith(0) - runner.step(5450) - expect(spy).toHaveBeenCalledWith(0) - spy.calls.reset() + // should move 0.5 into the next loop + expect(runner.progress(5400 / 9800).progress()).toBe(5400 / 9800) + expect(spy.calls.mostRecent().args[0]).toBeCloseTo(0.5) + }) + }) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) + describe('position()', () => { - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, false, 100).reverse() - runner.queue(null, spy) + it('gets the position of a runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - runner.step(4350) - expect(spy).toHaveBeenCalledWith(0) - spy.calls.reset() + runner.step(300) + expect(spy).toHaveBeenCalledWith(0.3) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) - }) + expect(runner.position()).toBe(0.3) + }) - describe('swinging', () => { - it('does behave correctly at the end of an even loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(6, true, 100).reverse() - runner.queue(null, spy) + it('gets the position of a runner when looping', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true, 100).queue(null, spy) - runner.step(5450) - expect(spy).toHaveBeenCalledWith(0) - spy.calls.reset() + runner.step(1200) + expect(spy).toHaveBeenCalledWith(0.9) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.75) - runner.step(250) - expect(spy).toHaveBeenCalledWith(1) - }) + expect(runner.position()).toBe(0.9) + }) - it('does behave correctly at the end of an uneven loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true, 100).reverse() - runner.queue(null, spy) + it('sets the position of a runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).queue(null, spy) - runner.step(4350) - expect(spy).toHaveBeenCalledWith(1) - spy.calls.reset() + expect(runner.position(0.5).position()).toBe(0.5) + expect(spy).toHaveBeenCalledWith(0.5) + }) - runner.step(800) - expect(spy).toHaveBeenCalledWith(0.25) - runner.step(250) - expect(spy).toHaveBeenCalledWith(0) - }) - }) - }) + it('sets the position of a runner in a loop', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).loop(5, true, 100).queue(null, spy) + + runner.step(1200) + expect(runner.position(0.4).position()).toBe(0.4) + expect(spy).toHaveBeenCalledWith(0.4) + + expect(runner.position(0).position()).toBe(0) + expect(spy).toHaveBeenCalledWith(0) + + expect(runner.position(1).position()).toBe(1) + expect(spy).toHaveBeenCalledWith(1) }) }) - }) + describe('element()', () => { + it('returns the element bound to this runner if any', () => { + var runner1 = new Runner() + expect(runner1.element()).toBe(null) + + var element = SVG('') + var runner2 = element.animate() + expect(runner2.element()).toBe(element) + }) - describe('active()', () => { - it('acts as a getter without parameters', () => { - var runner = new Runner() - expect(runner.active()).toBe(true) + it('sets an element to be bound to the runner', () => { + var runner = new Runner() + var element = SVG('') + expect(runner.element(element)).toBe(runner) + expect(runner.element()).toBe(element) + }) }) - it('disables the runner when false is passed', () => { - var runner = new Runner() - expect(runner.active(false)).toBe(runner) - expect(runner.active()).toBe(false) + describe('timeline()', () => { + it('returns the timeline bound to this runner if any', () => { + var runner1 = new Runner() + expect(runner1.element()).toBe(null) + + var element = SVG('') + var runner2 = element.animate() + expect(runner2.timeline()).toBe(element.timeline()) + }) + + it('sets a timeline to be bound to the runner', () => { + var runner = new Runner() + var timeline = new Timeline() + expect(runner.timeline(timeline)).toBe(runner) + expect(runner.timeline()).toBe(timeline) + }) }) - it('enables the runner when true is passed', () => { - var runner = new Runner() - expect(runner.active(false)).toBe(runner) - expect(runner.active(true)).toBe(runner) - expect(runner.active()).toBe(true) + describe('schedule()', () => { + it('schedules the runner on a timeline', () => { + var runner = new Runner() + var timeline = new Timeline() + var spy = spyOn(timeline, 'schedule').and.callThrough() + + expect(runner.schedule(timeline, 200, 'now')).toBe(runner) + expect(runner.timeline()).toBe(timeline) + expect(spy).toHaveBeenCalledWith(runner, 200, 'now') + }) + + it('schedules the runner on its own timeline', () => { + var runner = new Runner() + var timeline = new Timeline() + var spy = spyOn(timeline, 'schedule') + runner.timeline(timeline) + + expect(runner.schedule(200, 'now')).toBe(runner) + expect(runner.timeline()).toBe(timeline) + expect(spy).toHaveBeenCalledWith(runner, 200, 'now') + }) + + it('throws if no timeline is given', () => { + var runner = new Runner() + expect(() => runner.schedule(200, 'now')).toThrowError('Runner cannot be scheduled without timeline') + }) + }) - }) - describe('duration()', () => { - it('return the full duration of the runner including all loops and waits', () => { - var runner = new Runner(800).loop(10, true, 200) - expect(runner.duration()).toBe(9800) + describe('unschedule()', () => { + it('unschedules this runner from its timeline', () => { + var runner = new Runner() + var timeline = new Timeline() + var spy = spyOn(timeline, 'unschedule').and.callThrough() + + expect(runner.schedule(timeline, 200, 'now')).toBe(runner) + expect(runner.unschedule()).toBe(runner) + expect(spy).toHaveBeenCalledWith(runner) + expect(runner.timeline()).toBe(null) + }) }) - }) - describe('loop()', () => { - it('makes this runner looping', () => { - var runner = new Runner(1000).loop(5) - expect(runner.duration()).toBe(5000) + describe('animate()', () => { + it('creates a new runner scheduled after the first', () => { + var runner = new Runner(1000) + var timeline = new Timeline() + + runner.schedule(timeline) + + var runner2 = runner.animate(500, 1000) + + var t = timeline.time() + + expect(runner2.timeline()).toBe(timeline) + expect(runner2.time()).toBe(0) + + expect(timeline.schedule()).toEqual(arrayContaining([ + objectContaining({ start: t, duration: 1000, end: t + 1000, runner: runner }), + objectContaining({ start: t + 2000, duration: 500, end: t + 2500, runner: runner2 }) + ])) + }) + + it('reuses timeline and element of current runner', () => { + const element = new Rect() + const timeline = new Timeline() + const runner = new Runner().element(element).timeline(timeline) + const after = runner.animate() + expect(after.timeline()).toBe(timeline) + expect(after.element()).toBe(element) + }) + + it('doesnt reuse element if not set', () => { + const timeline = new Timeline() + const runner = new Runner().timeline(timeline) + const after = runner.animate() + expect(after.element()).toBe(null) + }) }) - }) - describe('time()', () => { - it('returns itself', () => { - var runner = new Runner() - expect(runner.time(0)).toBe(runner) + describe('delay()', () => { + it('calls animate with delay parameters', () => { + var runner = new Runner(1000) + spyOn(runner, 'animate') + + runner.delay(500) + expect(runner.animate).toHaveBeenCalledWith(0, 500) + }) }) - it('acts as a getter with no parameter passed', () => { - var runner = new Runner() - expect(runner.time()).toBe(0) + describe('during()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.during(runFn)).toBe(runner) + }) + + it('calls queue passing only a function to call on every step', () => { + var runner = new Runner() + spyOn(runner, 'queue') + runner.during(runFn) + + expect(runner.queue).toHaveBeenCalledWith(null, runFn) + }) }) - it('reschedules the runner to a new time', () => { - var runner = new Runner() - runner.time(10) + describe('after()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.after(runFn)).toBe(runner) + }) + + it('binds a function to the after event', () => { + var runner = new Runner() + spyOn(runner, 'on') + runner.after(runFn) - expect(runner.time()).toBe(10) + expect(runner.on).toHaveBeenCalledWith('finished', runFn) + }) }) - it('calls step to reschedule', () => { - var runner = new Runner() - spyOn(runner, 'step') - runner.time(10) + describe('finish()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.finish()).toBe(runner) + }) + + it('calls step with Infinity as argument', () => { + var runner = new Runner() + spyOn(runner, 'step') + runner.finish() - expect(runner.step).toHaveBeenCalledWith(10) + expect(runner.step).toHaveBeenCalledWith(Infinity) + }) }) - }) - describe('loops()', () => { - it('get the loops of a runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) + describe('reverse()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.reverse()).toBe(runner) + }) - runner.step(300) - expect(spy).toHaveBeenCalledWith(0.3) + it('reverses the runner', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).reverse().queue(null, spy) + runner.step(750) + expect(spy).toHaveBeenCalledWith(0.25) + }) + + it('reverses the runner when true is passed', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).reverse(true).queue(null, spy) + runner.step(750) + expect(spy).toHaveBeenCalledWith(0.25) + }) - expect(runner.loops()).toBe(0.3) + it('unreverses the runner when true is passed', () => { + var spy = createSpy('stepper') + var runner = new Runner(1000).reverse(false).queue(null, spy) + runner.step(750) + expect(spy).toHaveBeenCalledWith(0.75) + }) }) - it('sets the loops of the runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) - expect(runner.loops(0.5).loops()).toBe(0.5) - expect(spy).toHaveBeenCalledWith(0.5) + describe('ease()', () => { + it('returns itself', () => { + var runner = new Runner() + expect(runner.ease(() => {})).toBe(runner) + }) - expect(runner.loops(0.1).loops()).toBe(0.1) - expect(spy).toHaveBeenCalledWith(0.1) + it('creates an easing Controller from the easing function', () => { + var runner = new Runner() + runner.ease(() => {}) - expect(runner.loops(1.5).loops()).toBe(1) - expect(spy).toHaveBeenCalledWith(1) + expect(runner._stepper instanceof Ease).toBe(true) + }) }) - it('sets the loops of the runner in a loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true, 500).queue(null, spy) - expect(runner.loops(1.3).loops()).toBe(1.3) - expect(spy).toHaveBeenCalledWith(0.7) + describe('reset()', () => { + it('resets the runner by setting it to time 0', () => { + var runner = new Runner().step(16) + expect(runner.time()).toBe(16) + expect(runner.reset()).toBe(runner) + expect(runner.time()).toBe(0) + }) - expect(runner.loops(0.3).loops()).toBe(0.3) + it('doesnt reset if already reseted', () => { + var runner = Object.freeze(new Runner().reset()) + expect(runner.reset()).toBe(runner) + }) }) - }) - describe('progress()', () => { - it('gets the progress of a runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) + describe('private Methods', () => { + describe('_rememberMorpher()', () => { + it('adds a morper for a method to the runner', () => { + const runner = new Runner() + const morpher = new Morphable() + runner._rememberMorpher('move', morpher) + expect(runner._history.move).toEqual({ morpher, caller: undefined }) + }) - runner.step(300) - expect(spy).toHaveBeenCalledWith(0.3) + it('resumes the timeline in case this runner uses a controller', () => { + const timeline = new Timeline() + const spy = spyOn(timeline, 'play') + const runner = new Runner(new Controller(() => 0)).timeline(timeline) + const morpher = new Morphable() + runner._rememberMorpher('move', morpher) + expect(spy).toHaveBeenCalled() + }) + }) - expect(runner.progress()).toBe(0.3) - }) + describe('_tryRetarget()', () => { + it('tries to retarget a morpher for the animation and returns true', () => { + const rect = new Rect().move(0, 0) + const runner = rect.animate().move(10, 10) + jasmine.RequestAnimationFrame.tick(16) + expect(runner._tryRetarget('x', 20)).toBe(true) + expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) + }) - it('gets the progress of a runner when looping', () => { - var spy = createSpy('stepper') - var runner = new Runner(800).queue(null, spy).loop(10, false, 200) // duration should be 9800 + it('throws away the morpher if it wasnt initialized yet and returns false', () => { + const rect = new Rect().move(0, 0) + const runner = rect.animate().move(10, 10) + // In that case tryRetarget is not successfull + expect(runner._tryRetarget('x', 20)).toBe(false) + }) - // middle of animation, in the middle of wait time - runner.step(4900) - expect(runner.progress()).toBe(0.5) - expect(spy).toHaveBeenCalledWith(1) + it('does nothing if method wasnt found', () => { + const rect = new Rect().move(0, 0) + const runner = rect.animate().move(10, 10) + jasmine.RequestAnimationFrame.tick(16) + // In that case tryRetarget is not successfull + expect(runner._tryRetarget('foo', 20)).toBe(false) + }) - // start of next loop - runner.step(100) - expect(spy).toHaveBeenCalledWith(0) + it('does only work with controller for transformations and uses retarget function when retargeting transformations', () => { + const rect = new Rect() + const runner = rect.animate(new Controller(() => 0)).transform({ translate: [ 10, 10 ] }) + jasmine.RequestAnimationFrame.tick(16) + // In that case tryRetarget is not successfull + expect(runner._tryRetarget('transform', { translate: [ 20, 20 ] })).toBe(true) + }) - // move 400 into current loop which is 0.5 progress - // the progress value is 5400 / 9800 - runner.step(400) - expect(spy).toHaveBeenCalledWith(0.5) - expect(runner.progress()).toBe(5400 / 9800) - }) + it('starts the timeline if retarget was successfull', () => { + const timeline = new Timeline() + const rect = new Rect().move(0, 0).timeline(timeline) + const runner = rect.animate().move(10, 10) + jasmine.RequestAnimationFrame.tick(16) + const spy = spyOn(timeline, 'play') + expect(runner._tryRetarget('x', 20)).toBe(true) + expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('_initialise', () => { + it('does nothing if false is passed', () => { + const runner = Object.freeze(new Runner()) + expect(runner._initialise(false)).toBe(undefined) + }) - it('sets the progress of a runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) + it('does nothing if true is passed and runner is not declaritive', () => { + const runner = Object.freeze(new Runner()) + expect(runner._initialise(true)).toBe(undefined) + }) - expect(runner.progress(0.5).progress()).toBe(0.5) - expect(spy).toHaveBeenCalledWith(0.5) - }) + it('calls the initializer function on the queue when runner is declaritive', () => { + const runner = new Runner(() => 0).queue(initFn, runFn) + runner._initialise() + expect(initFn).toHaveBeenCalledTimes(1) + }) + + it('calls the initializer function on the queue when true is passed and runner is not declaritive', () => { + const runner = new Runner().queue(initFn, runFn) + runner._initialise(true) + expect(initFn).toHaveBeenCalledTimes(1) + }) + + it('does nothing if function is already initialized', () => { + const runner = new Runner().queue(initFn, runFn) + runner._initialise(true) + runner._initialise(true) + expect(initFn).toHaveBeenCalledTimes(1) + }) + }) - it('sets the progress of a runner when looping', () => { - var spy = createSpy('stepper') - var runner = new Runner(800).queue(null, spy).loop(10, false, 200) + describe('_run()', () => { + it('runs each queued function for the position or dt given', () => { + const runner = new Runner().queue(initFn, runFn) + runner._run(16) + expect(runFn).toHaveBeenCalledWith(16) + }) - // progress 0.5 somewhere in the middle of wait time - expect(runner.progress(0.5).progress()).toBe(0.5) - expect(spy).toHaveBeenCalledWith(1) + it('returns true if all runners converged', () => { + const spy = createSpy().and.returnValue(true) + const runner = new Runner().queue(initFn, spy) + expect(runner._run(16)).toBe(true) + }) + + it('returns true if all runners finished', () => { + const spy = createSpy().and.returnValue(true) + const runner = new Runner(100).queue(initFn, spy) + runner._run(200) + expect(runner._run(1)).toBe(true) + }) + }) + + describe('addTransform()', () => { + it('adds a transformation by multiplying', () => { + const runner = new Runner() + runner.addTransform({ translate: [ 10, 10 ] }) + expect(runner.transforms).toEqual(new Matrix(1, 0, 0, 1, 10, 10)) + }) + }) - // start of next loop - runner.step(100) - expect(spy).toHaveBeenCalledWith(0) + describe('clearTransform()', () => { + it('resets the transformations to identity', () => { + const runner = new Runner() + runner.addTransform({ translate: [ 10, 10 ] }) + runner.clearTransform() + expect(runner.transforms).toEqual(new Matrix()) + }) + }) - // should move 0.5 into the next loop - expect(runner.progress(5400 / 9800).progress()).toBe(5400 / 9800) - expect(spy.calls.mostRecent().args[0]).toBeCloseTo(0.5) + describe('clearTransformsFromQueue', () => { + it('deletes all functions from the queue which are transformations', () => { + const runner = new Runner().queue(initFn, runFn) + runner.transform({ translate: [ 10, 20 ] }) + runner.clearTransformsFromQueue() + expect(runner._queue.length).toBe(1) + }) + }) }) - }) - describe('position()', () => { + describe('Element', () => { + describe('animate()', () => { + it('creates a runner with the element set and schedules it on the timeline', () => { + var element = SVG('') + var runner = element.animate() + expect(runner instanceof Runner) + expect(runner.element()).toBe(element) + expect(runner.timeline()).toBe(element.timeline()) + expect(element.timeline().getLastRunnerInfo().runner).toBe(runner) + }) + }) - it('gets the position of a runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) + describe('delay()', () => { + it('calls animate with correct parameters', () => { + var element = SVG('') - runner.step(300) - expect(spy).toHaveBeenCalledWith(0.3) + spyOn(element, 'animate') + element.delay(100, 'now') + expect(element.animate).toHaveBeenCalledWith(0, 100, 'now') + }) + }) - expect(runner.position()).toBe(0.3) - }) + describe('_clearTransformRunnersBefore()', () => { + it('calls clearBefore on the runner array', () => { + const rect = new Rect() + rect._prepareRunner() + const spy = spyOn(rect._transformationRunners, 'clearBefore') + rect._clearTransformRunnersBefore({ id: 1 }) + expect(spy).toHaveBeenCalledWith(1) + }) + }) - it('gets the position of a runner when looping', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true, 100).queue(null, spy) + describe('_currentTransform()', () => { + it('calculates the current transformation of this element', () => { + const rect = new Rect() + rect._prepareRunner() + const runner1 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner2 = new Runner().addTransform({ rotate: 45 }) + const runner3 = new Runner().addTransform({ translate: [ 10, 20 ] }) + + rect._addRunner(runner1) + rect._addRunner(runner2) + rect._addRunner(runner3) + expect(rect._currentTransform(runner3)).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + .translate(10, 20) + ) + }) + }) - runner.step(1200) - expect(spy).toHaveBeenCalledWith(0.9) + describe('_addRunner()', () => { + it('adds a runenr to the runner array of this element', () => { + const rect = new Rect() + rect._prepareRunner() + const spy = spyOn(rect._transformationRunners, 'add') + const runner = new Runner() + rect._addRunner(runner) + expect(spy).toHaveBeenCalledWith(runner) + }) + }) - expect(runner.position()).toBe(0.9) + describe('_prepareRunner()', () => { + it('adds a runner array to the element', () => { + const rect = new Rect() + expect(rect._transformationRunners).toBe(undefined) + rect._prepareRunner() + expect(rect._transformationRunners).toEqual(any(RunnerArray)) + }) + + it('only adds it if no animation is in progress', () => { + const rect = new Rect() + expect(rect._transformationRunners).toBe(undefined) + rect._prepareRunner() + const arr = rect._transformationRunners + rect._frameId = 1 + rect._prepareRunner() + expect(rect._transformationRunners).toBe(arr) + }) + }) }) - it('sets the position of a runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).queue(null, spy) + describe('methods', () => { + describe('attr()', () => { + it('relays to styleAttr with "attr" as parameter', () => { + const runner = new Runner() + const spy = spyOn(runner, 'styleAttr') + runner.attr(1, 2) + expect(spy).toHaveBeenCalledWith('attr', 1, 2) + }) + }) + + describe('css()', () => { + it('relays to styleAttr with "css" as parameter', () => { + const runner = new Runner() + const spy = spyOn(runner, 'styleAttr') + runner.css(1, 2) + expect(spy).toHaveBeenCalledWith('css', 1, 2) + }) + }) - expect(runner.position(0.5).position()).toBe(0.5) - expect(spy).toHaveBeenCalledWith(0.5) - }) + describe('styleAttr()', () => { + it('adds a morpher for attr', () => { + const runner = new Runner() + runner.styleAttr('attr', 'x', 5) + expect(runner._history.attr.morpher).toEqual(any(Morphable)) + expect(runner._history.attr.morpher.to()).toEqual([ 'x', SVGNumber, 2, 5, '' ]) + }) - it('sets the position of a runner in a loop', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).loop(5, true, 100).queue(null, spy) + it('adds a morpher for css', () => { + const runner = new Runner() + runner.styleAttr('css', 'x', 5) + expect(runner._history.css.morpher).toEqual(any(Morphable)) + expect(runner._history.css.morpher.to()).toEqual([ 'x', SVGNumber, 2, 5, '' ]) + }) - runner.step(1200) - expect(runner.position(0.4).position()).toBe(0.4) - expect(spy).toHaveBeenCalledWith(0.4) + it('adds init and run fn for execution when runner runs', () => { + const element = new Rect().move(0, 0) + const runner = new Runner(100).ease('-').element(element) + runner.styleAttr('attr', 'x', 5) + runner.step(50) + expect(runner._history.attr.morpher.from()).toEqual([ 'x', SVGNumber, 2, 0, '' ]) + expect(runner._history.attr.morpher.to()).toEqual([ 'x', SVGNumber, 2, 5, '' ]) + expect(element.x()).toBe(2.5) + }) - expect(runner.position(0).position()).toBe(0) - expect(spy).toHaveBeenCalledWith(0) + it('it also works when the object contains other morphable values', () => { + const element = new Rect().fill('#fff').stroke('#000') + const runner = new Runner(100).ease('-').element(element) + runner.styleAttr('attr', { fill: '#000', stroke: new Color('#fff') }) + runner.step(50) + expect(runner._history.attr.morpher.from()).toEqual( + [ 'fill', Color, 5, 255, 255, 255, 0, 'rgb', 'stroke', Color, 5, 0, 0, 0, 0, 'rgb' ] + ) + + expect(runner._history.attr.morpher.to()).toEqual( + [ 'fill', Color, 5, 0, 0, 0, 0, 'rgb', 'stroke', Color, 5, 255, 255, 255, 0, 'rgb' ] + ) + const result = runner._history.attr.morpher.at(0.5).valueOf() + expect(result.fill).toEqual(any(Color)) + expect(result.stroke).toEqual(any(Color)) + expect(result.fill.toArray()).toEqual([ 127.5, 127.5, 127.5, 0, 'rgb' ]) + expect(result.stroke.toArray()).toEqual([ 127.5, 127.5, 127.5, 0, 'rgb' ]) + }) - expect(runner.position(1).position()).toBe(1) - expect(spy).toHaveBeenCalledWith(1) - }) - }) + it('it changes color space', () => { + const element = new Rect().fill('#fff') + const runner = new Runner(100).ease('-').element(element) + runner.styleAttr('attr', { fill: new Color(100, 12, 12, 'hsl') }) + runner.step(50) + expect(runner._history.attr.morpher.from()).toEqual( + [ 'fill', Color, 5, 0, 0, 100, 0, 'hsl' ] + ) + + expect(runner._history.attr.morpher.to()).toEqual( + [ 'fill', Color, 5, 100, 12, 12, 0, 'hsl' ] + ) + const result = runner._history.attr.morpher.at(0.5).valueOf() + expect(result.fill).toEqual(any(Color)) + expect(result.fill.toArray()).toEqual([ 50, 6, 56, 0, 'hsl' ]) + expect(element.fill()).toBe('#969388') + }) - describe('element()', () => { - it('returns the element bound to this runner if any', () => { - var runner1 = new Runner() - expect(runner1.element()).toBe(null) + it('retargets if called two times with new key', () => { + const element = new Rect().fill('#fff') + const runner = new Runner(100).ease('-').element(element) + runner.styleAttr('attr', { fill: new Color(100, 12, 12, 'hsl') }) + runner.step(50) + expect(element.fill()).toBe('#969388') + runner.styleAttr('attr', { fill: new Color(100, 50, 50, 'hsl'), x: 50 }) + runner.step(25) + expect(element.fill()).toBe('#b1c37c') + expect(element.x()).toBe(37.5) + }) - var element = SVG('') - var runner2 = element.animate() - expect(runner2.element()).toBe(element) - }) + it('retargets if called two times without new key', () => { + const element = new Rect().fill('#fff') + const runner = new Runner(100).ease('-').element(element) + runner.styleAttr('attr', { fill: new Color(100, 12, 12, 'hsl') }) + runner.step(50) + expect(element.fill()).toBe('#969388') + runner.styleAttr('attr', { fill: new Color(100, 50, 50, 'hsl') }) + runner.step(25) + expect(element.fill()).toBe('#b1c37c') + }) + }) - it('sets an element to be bound to the runner', () => { - var runner = new Runner() - var element = SVG('') - expect(runner.element(element)).toBe(runner) - expect(runner.element()).toBe(element) - }) - }) + function closeTo (number, precision = 2) { + return { + /* + * The asymmetricMatch function is required, and must return a boolean. + */ + asymmetricMatch: function (compareTo) { + const factor = 10 ** precision + return Math.round((~~(compareTo * factor)) / factor) === Math.round((~~(number * factor)) / factor) + }, + + /* + * The jasmineToString method is used in the Jasmine pretty printer, and will + * be seen by the user in the message when a test fails. + */ + jasmineToString: function () { + return '' + } + } + } + + function equal (obj) { + return { + /* + * The asymmetricMatch function is required, and must return a boolean. + */ + asymmetricMatch: function (compareTo) { + if (!(compareTo instanceof obj.constructor)) return false + + const keys = Object.keys(obj) + const difference = Object.keys(compareTo).filter((el) => !keys.includes(el)) + + if (difference.length) return false + + for (const key in obj) { + if (obj[key] !== compareTo[key]) return false + } + + return true + }, + + /* + * The jasmineToString method is used in the Jasmine pretty printer, and will + * be seen by the user in the message when a test fails. + */ + jasmineToString: function () { + return '' + } + } + } + + describe('zoom()', () => { + it('adds a zoom morpher to the queue', () => { + const element = SVG().size(100, 100).viewbox(0, 0, 100, 100) + const runner = new Runner(100).ease('-').element(element) + runner.zoom(2, { x: 0, y: 0 }) + runner.step(50) + expect(runner._history.zoom.morpher.from()).toEqual( + [ 1, '' ] + ) + expect(runner._history.zoom.morpher.to()).toEqual( + [ 2, '' ] + ) + + expect(element.zoom()).toBeCloseTo(1.5, 10) + expect(element.viewbox().toArray()).toEqual([ 0, 0, closeTo(66.666, 3), closeTo(66.666, 3) ]) + }) - describe('timeline()', () => { - it('returns the timeline bound to this runner if any', () => { - var runner1 = new Runner() - expect(runner1.element()).toBe(null) + it('retargets if called twice', () => { + const element = SVG().size(100, 100).viewbox(0, 0, 100, 100) + const runner = new Runner(100).ease('-').element(element) + runner.zoom(2, { x: 0, y: 0 }) + runner.step(50) + runner.zoom(4, { x: 0, y: 0 }) + expect(runner._history.zoom.morpher.from()).toEqual( + [ 1, '' ] + ) + expect(runner._history.zoom.morpher.to()).toEqual( + [ 4, '' ] + ) + + runner.step(25) + expect(element.zoom()).toBeCloseTo(3.25, 10) + expect(element.viewbox().toArray()).toEqual([ 0, 0, closeTo(30.769, 3), closeTo(30.769, 3) ]) + }) + }) - var element = SVG('') - var runner2 = element.animate() - expect(runner2.timeline()).toBe(element.timeline()) - }) + describe('transform()', () => { + it('does not retarget for non-declaritive transformations', () => { + const runner = new Runner() + const spy = spyOn(runner, '_tryRetarget') + runner.transform({ translate: [ 10, 20 ] }) + expect(spy).not.toHaveBeenCalled() + }) - it('sets a timeline to be bound to the runner', () => { - var runner = new Runner() - var timeline = new Timeline() - expect(runner.timeline(timeline)).toBe(runner) - expect(runner.timeline()).toBe(timeline) - }) - }) + it('does not retarget for relative transformations', () => { + const runner = new Runner(new Controller(() => 0)) + const spy = spyOn(runner, '_tryRetarget') + runner.transform({ translate: [ 10, 20 ] }, true) + expect(spy).not.toHaveBeenCalled() + }) - describe('schedule()', () => { - it('schedules the runner on a timeline', () => { - var runner = new Runner() - var timeline = new Timeline() - var spy = spyOn(timeline, 'schedule').and.callThrough() + it('does retarget for absolute declaritive transformations', () => { + const runner = new Runner(new Controller(() => 0)) + const spy = spyOn(runner, '_tryRetarget') + runner.transform({ translate: [ 10, 20 ] }) + expect(spy).toHaveBeenCalled() + }) - expect(runner.schedule(timeline, 200, 'now')).toBe(runner) - expect(runner.timeline()).toBe(timeline) - expect(spy).toHaveBeenCalledWith(runner, 200, 'now') - }) + it('calls queue with isTransform=true', () => { + const runner = new Runner() + const spy = spyOn(runner, 'queue') + runner.transform({ translate: [ 10, 20 ] }) + expect(spy).toHaveBeenCalledWith(any(Function), any(Function), any(Function), true) + }) - it('schedules the runner on its own timeline', () => { - var runner = new Runner() - var timeline = new Timeline() - var spy = spyOn(timeline, 'schedule') - runner.timeline(timeline) + it('steps an affine transformation correctly', () => { + const element = new Rect() + const runner = new Runner(100).ease('-').element(element) + runner.transform({ translate: [ 10, 20 ], scale: 2, rotate: 90 }) + runner.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + expect(element.matrix().decompose()).toEqual(objectContaining({ + translateX: 5, + translateY: 10, + scaleX: closeTo(1.5, 10), + scaleY: closeTo(1.5), + rotate: closeTo(45, 10) + })) + }) - expect(runner.schedule(200, 'now')).toBe(runner) - expect(runner.timeline()).toBe(timeline) - expect(spy).toHaveBeenCalledWith(runner, 200, 'now') - }) - }) + it('retargets an affine transformation correctly', () => { + const element = new Rect() + const runner = new Runner(() => 1).element(element) + runner.transform({ translate: [ 10, 20 ], scale: 2, rotate: 90 }) + runner.step(50) + runner.transform({ scale: 2 }) + + // transform sets its to-target to the morpher in the initialisation step + // because it depends on the from-target. Declaritive animation run the init-step + // on every frame. Thats why we step here to see the effect of our retargeting + runner.step(25) + + expect(runner._history.transform.morpher.to()).toEqual( + [ 2, 2, 0, 0, 0, 0, 0, 0 ] + ) + }) - describe('unschedule()', () => { - it('unschedules this runner from its timeline', () => { - var runner = new Runner() - var timeline = new Timeline() - var spy = spyOn(timeline, 'unschedule').and.callThrough() + it('retargets an affine transformation correctly and sets new origin', () => { + const element = new Rect() + const runner = new Runner(() => 1).element(element) + runner.transform({ translate: [ 10, 20 ], scale: 2, rotate: 90 }) + runner.step(50) + runner.transform({ scale: 2, origin: [ 10, 10 ] }) + + // transform sets its to-target to the morpher in the initialisation step + // because it depends on the from-target. Declaritive animation run the init-step + // on every frame. Thats why we step here to see the effect of our retargeting + runner.step(25) + + expect(runner._history.transform.morpher.to()).toEqual( + [ 2, 2, 0, 0, 0, 0, 10, 10 ] + ) + }) - expect(runner.schedule(timeline, 200, 'now')).toBe(runner) - expect(runner.unschedule()).toBe(runner) - expect(spy).toHaveBeenCalledWith(runner) - expect(runner.timeline()).toBe(null) - }) - }) + it('steps multiple relative animations correctly', () => { + const element = new Rect() + const runner = new Runner(100).ease('-').element(element) + runner.translate(10, 20).scale(2).rotate(45) + runner.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix().translate(5, 10).scale(1.5, 5, 10).rotate(22.5, 5, 10) + ) + }) + + it('steps multiple relative animations correctly from multiple runners', () => { + const element = new Rect() + const runner1 = new Runner(100).ease('-').element(element) + const runner2 = new Runner(100).ease('-').element(element) + const runner3 = new Runner(100).ease('-').element(element) + runner1.translate(10, 20) + runner2.scale(2) + runner3.rotate(45) + runner1.step(50) + runner2.step(50) + runner3.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix().translate(5, 10).scale(1.5, 5, 10).rotate(22.5, 5, 10) + ) + }) + + it('absolute transformations correctly overwrite relatives', () => { + const element = new Rect() + const runner1 = new Runner(100).ease('-').element(element) + const runner2 = new Runner(100).ease('-').element(element) + const runner3 = new Runner(100).ease('-').element(element) + runner1.translate(10, 20) + runner2.transform({ scale: 2 }) + runner3.rotate(45) + runner1.step(50) + runner2.step(50) + runner3.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + expect(runner1._queue.length).toBe(0) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix().scale(1.5).rotate(22.5) + ) + }) + + it('correctly animates matrices directly', () => { + const element = new Rect() + const runner = new Runner(100).ease('-').element(element) + runner.transform(new Matrix({ rotate: 90 })) + runner.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix(0.5, 0.5, -0.5, 0.5, 0, 0) + ) + }) + + it('correctly animates matrices affine', () => { + const element = new Rect() + const runner = new Runner(100).ease('-').element(element) + runner.transform(Object.assign({ affine: true }, new Matrix({ rotate: 90 }))) + runner.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix({ rotate: 45 }) + ) + }) - describe('animate()', () => { - it('creates a new runner scheduled after the first', () => { - var runner = new Runner(1000) - var timeline = new Timeline() + it('correctly animates matrices affine by passing third parameter', () => { + const element = new Rect() + const runner = new Runner(100).ease('-').element(element) + runner.transform(new Matrix({ rotate: 90 }), true, true) + runner.step(50) + // transform sets an immediate callback to apply all merged transforms + // when every runner had the chance to add its bit of tranforms + jasmine.RequestAnimationFrame.tick(1) + + // The origin is transformed with every + expect(element.matrix()).toEqual( + new Matrix({ rotate: 45 }) + ) + }) - runner.schedule(timeline) + it('correctly animates a declaritive relative rotation', () => { + const element = new Rect() + const runner = new Runner(() => 1).element(element) + runner.transform({ rotate: 90 }, true) + runner.step(16) + jasmine.RequestAnimationFrame.tick(1) + runner.step(16) + jasmine.RequestAnimationFrame.tick(1) + expect(element.matrix()).not.toEqual(new Matrix()) + }) + }) - var runner2 = runner.animate(500, 1000) + describe('x()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.x(10) + expect(spy).toHaveBeenCalledWith('x', 10) + }) + }) - var t = timeline.time() + describe('y()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.y(10) + expect(spy).toHaveBeenCalledWith('y', 10) + }) + }) - expect(runner2.timeline()).toBe(timeline) - expect(runner2.time()).toBe(0) + describe('dx()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumberDelta') + runner.dx(10) + expect(spy).toHaveBeenCalledWith('x', 10) + }) - expect(timeline.schedule()).toEqual(arrayContaining([ - objectContaining({ start: t, duration: 1000, end: t + 1000, runner: runner }), - objectContaining({ start: t + 2000, duration: 500, end: t + 2500, runner: runner2 }) - ])) + it('uses a delta of 0 by default', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumberDelta') + runner.dx() + expect(spy).toHaveBeenCalledWith('x', 0) + }) + }) + + describe('dy()', () => { + it('queues a number', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumberDelta') + runner.dy(10) + expect(spy).toHaveBeenCalledWith('y', 10) + }) + + it('uses a delta of 0 by default', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumberDelta') + runner.dy() + expect(spy).toHaveBeenCalledWith('y', 0) + }) + }) + + describe('dmove()', () => { + it('calls dx and dy', () => { + const runner = new Runner() + const spy1 = spyOn(runner, 'dx').and.returnValue(runner) + const spy2 = spyOn(runner, 'dy').and.returnValue(runner) + runner.dmove(10, 20) + expect(spy1).toHaveBeenCalledWith(10) + expect(spy2).toHaveBeenCalledWith(20) + }) + }) + + describe('_queueNumberDelta', () => { + it('queues a morpher of type SVGNumber', () => { + const element = new Rect().x(10) + const runner = new Runner(100).ease('-').element(element) + runner._queueNumberDelta('x', 10) + runner.step(50) + expect(runner._history.x.morpher.type()).toEqual(SVGNumber) + expect(runner._history.x.morpher.from()).toEqual([ 10, '' ]) + expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) + + expect(element.x()).toBe(15) + }) + + it('retargets corectly', () => { + const element = new Rect().x(10) + const runner = new Runner(100).ease('-').element(element) + runner._queueNumberDelta('x', 10) + runner.step(25) + runner._queueNumberDelta('x', 20) + + expect(runner._history.x.morpher.to()).toEqual([ 30, '' ]) + + runner.step(25) + expect(element.x()).toBe(20) + }) + }) + + describe('_queueObject', () => { + it('queues a morphable object', () => { + const element = new Rect().x(10) + const runner = new Runner(100).ease('-').element(element) + runner._queueObject('x', new SVGNumber(20)) + runner.step(50) + expect(runner._history.x.morpher.type()).toEqual(SVGNumber) + expect(runner._history.x.morpher.from()).toEqual([ 10, '' ]) + expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) + + expect(element.x()).toBe(15) + }) + + it('queues a morphable primitive', () => { + const element = new Rect().fill('#000') + const runner = new Runner(100).ease('-').element(element) + runner._queueObject('fill', '#fff') + runner.step(50) + expect(runner._history.fill.morpher.type()).toEqual(Color) + + expect(element.fill()).toBe('#808080') + }) + + it('retargets corectly', () => { + const element = new Rect().x(10) + const runner = new Runner(100).ease('-').element(element) + runner._queueObject('x', 20) + + runner.step(25) + + runner._queueObject('x', 30) + runner.step(25) + expect(element.x()).toBe(20) + }) + }) + + describe('_queueNumber', () => { + it('queues an SVGNumber with _queueObject', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueObject') + runner._queueNumber('x', 10) + expect(spy).toHaveBeenCalledWith('x', equal(new SVGNumber(10))) + }) + }) + + describe('cy()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.cy(10) + expect(spy).toHaveBeenCalledWith('cy', 10) + }) + }) + + describe('cx()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.cx(10) + expect(spy).toHaveBeenCalledWith('cx', 10) + }) + }) + + describe('move()', () => { + it('calls x and y', () => { + const runner = new Runner() + const spy1 = spyOn(runner, 'x').and.returnValue(runner) + const spy2 = spyOn(runner, 'y').and.returnValue(runner) + runner.move(10, 20) + expect(spy1).toHaveBeenCalledWith(10) + expect(spy2).toHaveBeenCalledWith(20) + }) + }) + + describe('center()', () => { + it('calls cx and cy', () => { + const runner = new Runner() + const spy1 = spyOn(runner, 'cx').and.returnValue(runner) + const spy2 = spyOn(runner, 'cy').and.returnValue(runner) + runner.center(10, 20) + expect(spy1).toHaveBeenCalledWith(10) + expect(spy2).toHaveBeenCalledWith(20) + }) + }) + + describe('size()', () => { + it('calls width and height', () => { + const runner = new Runner() + const spy1 = spyOn(runner, 'width').and.returnValue(runner) + const spy2 = spyOn(runner, 'height').and.returnValue(runner) + runner.size(10, 20) + expect(spy1).toHaveBeenCalledWith(10) + expect(spy2).toHaveBeenCalledWith(20) + }) + + it('figures out height if only width given', () => { + const element = new Rect().size(10, 10) + const runner = new Runner().element(element) + const spy1 = spyOn(runner, 'width').and.returnValue(runner) + const spy2 = spyOn(runner, 'height').and.returnValue(runner) + runner.size(20) + expect(spy1).toHaveBeenCalledWith(20) + expect(spy2).toHaveBeenCalledWith(20) + }) + + it('figures out width if only height given', () => { + const element = new Rect().size(10, 10) + const runner = new Runner().element(element) + const spy1 = spyOn(runner, 'width').and.returnValue(runner) + const spy2 = spyOn(runner, 'height').and.returnValue(runner) + runner.size(null, 20) + expect(spy1).toHaveBeenCalledWith(20) + expect(spy2).toHaveBeenCalledWith(20) + }) + }) + + describe('width()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.width(10) + expect(spy).toHaveBeenCalledWith('width', 10) + }) + }) + + describe('height()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.height(10) + expect(spy).toHaveBeenCalledWith('height', 10) + }) + }) + + describe('plot()', () => { + it('queues a morphable array', () => { + const element = new Polygon().plot([ 10, 10, 20, 20 ]) + const runner = new Runner(100).ease('-').element(element) + runner.plot(20, 20, 30, 30) + runner.step(50) + expect(runner._history.plot.morpher.from()).toEqual([ 10, 10, 20, 20 ]) + expect(runner._history.plot.morpher.to()).toEqual([ 20, 20, 30, 30 ]) + expect(element.array()).toEqual(new PointArray([ 15, 15, 25, 25 ])) + }) + + it('retargets correctly', () => { + const element = new Polygon().plot([ 10, 10, 20, 20 ]) + const runner = new Runner(100).ease('-').element(element) + runner.plot(20, 20, 30, 30) + runner.step(25) + runner.plot(30, 30, 40, 40) + runner.step(25) + expect(runner._history.plot.morpher.from()).toEqual([ 10, 10, 20, 20 ]) + expect(runner._history.plot.morpher.to()).toEqual([ 30, 30, 40, 40 ]) + expect(element.array()).toEqual(new PointArray([ 20, 20, 30, 30 ])) + }) + }) + + describe('leading()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueNumber') + runner.leading(10) + expect(spy).toHaveBeenCalledWith('leading', 10) + }) + }) + + describe('viewbox()', () => { + it('queues a numer', () => { + const runner = new Runner() + const spy = spyOn(runner, '_queueObject') + runner.viewbox(10, 10, 100, 100) + expect(spy).toHaveBeenCalledWith('viewbox', equal(new Box(10, 10, 100, 100))) + }) + }) + + describe('update()', () => { + it('relays to attr call', () => { + const runner = new Runner() + const spy = spyOn(runner, 'attr') + runner.update(0.5, '#fff', 1) + expect(spy).toHaveBeenCalledWith('offset', 0.5) + expect(spy).toHaveBeenCalledWith('stop-color', '#fff') + expect(spy).toHaveBeenCalledWith('stop-opacity', 1) + }) + }) }) }) - describe('delay()', () => { - it('calls animate with delay parameters', () => { - var runner = new Runner(1000) - spyOn(runner, 'animate') + describe('FakeRunner', () => { + describe('()', () => { + it('creates a new FakeRunner with a new matrix which is always done', () => { + const runner = new FakeRunner() + expect(runner.transforms).toEqual(new Matrix()) + expect(runner.id).toBe(-1) + expect(runner.done).toBe(true) + }) + }) - runner.delay(500) - expect(runner.animate).toHaveBeenCalledWith(0, 500) + describe('mergeWith()', () => { + it('merges the transformations of a runner with another and returns a FakeRunner', () => { + const fake = new FakeRunner() + const runner = new Runner().addTransform({ translate: [ 10, 20 ] }) + const newRunner = fake.mergeWith(runner) + expect(newRunner).toEqual(any(FakeRunner)) + expect(newRunner.transforms).toEqual(new Matrix({ translate: [ 10, 20 ] })) + }) }) }) - describe('during()', () => { - it('returns itself', () => { - var runner = new Runner() - expect(runner.during(runFn)).toBe(runner) + describe('RunnerArray', () => { + describe('add()', () => { + it('adds a runner to the runner array', () => { + const runner = new Runner() + const arr = new RunnerArray() + arr.add(runner) + expect(arr.length()).toBe(1) + }) + + it('does not add the same runner twice', () => { + const runner = new Runner() + const arr = new RunnerArray() + arr.add(runner) + arr.add(runner) + expect(arr.length()).toBe(1) + }) }) - it('calls queue passing only a function to call on every step', () => { - var runner = new Runner() - spyOn(runner, 'queue') - runner.during(runFn) + describe('getByID()', () => { + it('returns a runner by its id', () => { + const runner = new Runner() + const arr = new RunnerArray() + arr.add(runner) + expect(arr.getByID(runner.id)).toBe(runner) + }) + }) - expect(runner.queue).toHaveBeenCalledWith(null, runFn) + describe('remove()', () => { + it('removes a runner by its id', () => { + const runner = new Runner() + const arr = new RunnerArray() + arr.add(runner) + arr.remove(runner.id) + expect(arr.length()).toBe(0) + }) }) - }) - // describe('after()', () => { - // it('returns itself', () => { - // var runner = new Runner() - // expect(runner.after(runFn)).toBe(runner) - // }) - // - // it('binds a function to the after event', () => { - // var runner = new Runner() - // spyOn(runner, 'on') - // runner.after(runFn) - // - // expect(runner.on).toHaveBeenCalledWith('finish', runFn) - // }) - // }) - // - // describe('finish()', () => { - // it('returns itself', () => { - // var runner = new Runner() - // expect(runner.finish()).toBe(runner) - // }) - // - // it('calls step with Infinity as argument', () => { - // var runner = new Runner() - // spyOn(runner, 'step') - // runner.finish() - // - // expect(runner.step).toHaveBeenCalledWith(Infinity) - // }) - // }) - - describe('reverse()', () => { - it('returns itself', () => { - var runner = new Runner() - expect(runner.reverse()).toBe(runner) - }) - - it('reverses the runner', () => { - var spy = createSpy('stepper') - var runner = new Runner(1000).reverse().queue(null, spy) - runner.step(750) - expect(spy).toHaveBeenCalledWith(0.25) + describe('merge()', () => { + it('merges all runners which are done', () => { + const runner1 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner2 = new Runner().addTransform({ rotate: 45 }) + const runner3 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const arr = new RunnerArray() + arr.add(runner1).add(runner2).add(runner3) + runner1.done = true + runner2.done = true + runner3.done = true + arr.merge() + expect(arr.runners[0]).toEqual(any(FakeRunner)) + expect(arr.runners[0].transforms).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + .translate(10, 20) + ) + }) + + it('skips runners which are not done', () => { + const runner1 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner2 = new Runner().addTransform({ rotate: 45 }) + const runner3 = new Runner().addTransform({ rotate: 45 }) + const runner4 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner5 = new Runner().addTransform({ rotate: 45 }) + const arr = new RunnerArray() + arr.add(runner1).add(runner2).add(runner3).add(runner4).add(runner5) + runner1.done = true + runner2.done = true + runner3.done = false + runner4.done = true + runner5.done = true + arr.merge() + expect(arr.runners[0]).toEqual(any(FakeRunner)) + expect(arr.runners[0].transforms).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + ) + + expect(arr.runners[2]).toEqual(any(FakeRunner)) + expect(arr.runners[2].transforms).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + ) + + expect(arr.runners[1]).toBe(runner3) + }) + + it('skips runners which have a timeline and are scheduled on that timeline', () => { + const runner1 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner2 = new Runner().addTransform({ rotate: 45 }) + const runner3 = new Runner().addTransform({ rotate: 45 }) + const runner4 = new Runner().addTransform({ translate: [ 10, 20 ] }) + const runner5 = new Runner().addTransform({ rotate: 45 }) + const arr = new RunnerArray() + arr.add(runner1).add(runner2).add(runner3).add(runner4).add(runner5) + runner1.done = true + runner2.done = true + runner3.done = true + runner4.done = true + runner5.done = true + + runner3.schedule(new Timeline()) + arr.merge() + expect(arr.runners[0]).toEqual(any(FakeRunner)) + expect(arr.runners[0].transforms).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + ) + + expect(arr.runners[2]).toEqual(any(FakeRunner)) + expect(arr.runners[2].transforms).toEqual( + new Matrix({ translate: [ 10, 20 ] }) + .rotate(45) + ) + + expect(arr.runners[1]).toBe(runner3) + }) }) - }) - describe('ease()', () => { - it('returns itself', () => { - var runner = new Runner() - expect(runner.ease(() => {})).toBe(runner) + describe('edit()', () => { + it('replaces one runner with another', () => { + const arr = new RunnerArray() + const runner1 = new Runner() + const runner2 = new Runner() + arr.add(runner1) + arr.edit(runner1.id, runner2) + expect(arr.length()).toBe(1) + expect(arr.runners[0]).toBe(runner2) + }) }) - it('creates an easing Controller from the easing function', () => { - var runner = new Runner() - runner.ease(() => {}) + describe('length()', () => { + it('returns the number of runners in the array', () => { + const arr = new RunnerArray().add(new Runner()).add(new Runner()) + expect(arr.length()).toBe(2) + }) + }) - expect(runner._stepper instanceof Ease).toBe(true) + describe('clearBefore', () => { + it('removes all runners before a certain runner', () => { + const runner1 = new Runner() + const runner2 = new Runner() + const runner3 = new Runner() + const runner4 = new Runner() + const runner5 = new Runner() + const arr = new RunnerArray() + arr.add(runner1).add(runner2).add(runner3).add(runner4).add(runner5) + arr.clearBefore(runner3.id) + expect(arr.length()).toBe(4) + expect(arr.runners).toEqual([ any(FakeRunner), runner3, runner4, runner5 ]) + }) }) }) }) diff --git a/src/animation/Morphable.js b/src/animation/Morphable.js index 0a28e8e..2d48e10 100644 --- a/src/animation/Morphable.js +++ b/src/animation/Morphable.js @@ -10,6 +10,34 @@ import PathArray from '../types/PathArray.js' import SVGArray from '../types/SVGArray.js' import SVGNumber from '../types/SVGNumber.js' +const getClassForType = (value) => { + const type = typeof value + + if (type === 'number') { + return SVGNumber + } else if (type === 'string') { + if (Color.isColor(value)) { + return Color + } else if (delimiter.test(value)) { + return pathLetters.test(value) + ? PathArray + : SVGArray + } else if (numberAndUnit.test(value)) { + return SVGNumber + } else { + return NonMorphable + } + } else if (morphableTypes.indexOf(value.constructor) > -1) { + return value.constructor + } else if (Array.isArray(value)) { + return SVGArray + } else if (type === 'object') { + return ObjectBag + } else { + return NonMorphable + } +} + export default class Morphable { constructor (stepper) { this._stepper = stepper || new Ease('-') @@ -52,32 +80,7 @@ export default class Morphable { _set (value) { if (!this._type) { - var type = typeof value - - if (type === 'number') { - this.type(SVGNumber) - } else if (type === 'string') { - if (Color.isColor(value)) { - this.type(Color) - } else if (delimiter.test(value)) { - this.type(pathLetters.test(value) - ? PathArray - : SVGArray - ) - } else if (numberAndUnit.test(value)) { - this.type(SVGNumber) - } else { - this.type(NonMorphable) - } - } else if (morphableTypes.indexOf(value.constructor) > -1) { - this.type(value.constructor) - } else if (Array.isArray(value)) { - this.type(SVGArray) - } else if (type === 'object') { - this.type(ObjectBag) - } else { - this.type(NonMorphable) - } + this.type(getClassForType(value)) } var result = (new this._type(value)) @@ -86,6 +89,13 @@ export default class Morphable { : this._from ? result[this._from[4]]() : result } + + if (this._type === ObjectBag) { + result = this._to ? result.align(this._to) + : this._from ? result.align(this._from) + : result + } + result = result.toArray() this._morphObj = this._morphObj || new this._type() @@ -208,7 +218,7 @@ export class ObjectBag { this.values = [] if (Array.isArray(objOrArr)) { - this.values = objOrArr + this.values = objOrArr.slice() return } @@ -216,7 +226,9 @@ export class ObjectBag { var entries = [] for (const i in objOrArr) { - entries.push([ i, objOrArr[i] ]) + const Type = getClassForType(objOrArr[i]) + const val = new Type(objOrArr[i]).toArray() + entries.push([ i, Type, val.length, ...val ]) } entries.sort(sortByKey) @@ -229,8 +241,13 @@ export class ObjectBag { var obj = {} var arr = this.values - for (var i = 0, len = arr.length; i < len; i += 2) { - obj[arr[i]] = arr[i + 1] + // for (var i = 0, len = arr.length; i < len; i += 2) { + while (arr.length) { + const key = arr.shift() + const Type = arr.shift() + const num = arr.shift() + const values = arr.splice(0, num) + obj[key] = new Type(values).valueOf() } return obj @@ -239,6 +256,17 @@ export class ObjectBag { toArray () { return this.values } + + align (other) { + for (let i = 0, il = this.values.length; i < il; ++i) { + if (this.values[i] === Color) { + const space = other[i + 6] + const color = new Color(this.values.splice(i + 2, 5))[space]().toArray() + this.values.splice(i + 2, 0, ...color) + } + } + return this + } } const morphableTypes = [ diff --git a/src/animation/Runner.js b/src/animation/Runner.js index 8c56423..e2ba380 100644 --- a/src/animation/Runner.js +++ b/src/animation/Runner.js @@ -13,6 +13,7 @@ import Morphable, { TransformBag } from './Morphable.js' import Point from '../types/Point.js' import SVGNumber from '../types/SVGNumber.js' import Timeline from './Timeline.js' +import { ObjectBag } from '../main.js' export default class Runner extends EventTarget { constructor (options) { @@ -381,7 +382,7 @@ export default class Runner extends EventTarget { // for the case of transformations, we use the special retarget function // which has access to the outer scope if (this._history[method].caller.retarget) { - this._history[method].caller.retarget(target, extra) + this._history[method].caller.retarget.call(this, target, extra) // for everything else a simple morpher change is sufficient } else { this._history[method].morpher.to(target) @@ -487,7 +488,7 @@ export default class Runner extends EventTarget { Runner.id = 0 -class FakeRunner { +export class FakeRunner { constructor (transforms = new Matrix(), id = -1, done = true) { this.transforms = transforms this.id = id @@ -527,7 +528,7 @@ function mergeTransforms () { } } -class RunnerArray { +export class RunnerArray { constructor () { this.runners = [] this.ids = [] @@ -556,7 +557,8 @@ class RunnerArray { merge () { let lastRunner = null - this.runners.forEach((runner, i) => { + for (let i = 0; i < this.runners.length; ++i) { + const runner = this.runners[i] const condition = lastRunner && runner.done && lastRunner.done @@ -567,11 +569,14 @@ class RunnerArray { if (condition) { // the +1 happens in the function this.remove(runner.id) - this.edit(lastRunner.id, runner.mergeWith(lastRunner)) + const newRunner = runner.mergeWith(lastRunner) + this.edit(lastRunner.id, newRunner) + lastRunner = newRunner + --i + } else { + lastRunner = runner } - - lastRunner = runner - }) + } return this } @@ -649,6 +654,14 @@ registerMethods({ } }) +const intersect = (a, b) => { + var setB = new Set(b) + return [ ...new Set(a) ].filter(x => setB.has(x)) +} + +// Will output the elements from array A that are not in the array B +const difference = (a, b) => a.filter(x => !b.includes(x)) + extend(Runner, { attr (a, v) { return this.styleAttr('attr', a, v) @@ -659,24 +672,56 @@ extend(Runner, { return this.styleAttr('css', s, v) }, - styleAttr (type, name, val) { - // apply attributes individually - if (typeof name === 'object') { - for (var key in name) { - this.styleAttr(type, key, name[key]) - } - return this + styleAttr (type, nameOrAttrs, val) { + if (typeof nameOrAttrs === 'string') { + return this.styleAttr(type, { [nameOrAttrs]: val }) } - var morpher = new Morphable(this._stepper).to(val) + let attrs = nameOrAttrs + if (this._tryRetarget(type, attrs)) return this + + var morpher = new Morphable(this._stepper).to(attrs) + let keys = Object.keys(attrs) this.queue(function () { - morpher = morpher.from(this.element()[type](name)) + morpher = morpher.from(this.element()[type](keys)) }, function (pos) { - this.element()[type](name, morpher.at(pos)) + this.element()[type](morpher.at(pos).valueOf()) return morpher.done() + }, function (newToAttrs) { + + // Check if any new keys were added + const newKeys = Object.keys(newToAttrs) + const differences = difference(newKeys, keys) + + // If their are new keys, initialize them and add them to morpher + if (differences.length) { + // Get the values + const addedFromAttrs = this.element()[type](differences) + + // Get the already initialized values + const oldFromAttrs = new ObjectBag(morpher.from()).valueOf() + + // Merge old and new + Object.assign(oldFromAttrs, addedFromAttrs) + morpher.from(oldFromAttrs) + } + + // Get the object from the morpher + const oldToAttrs = new ObjectBag(morpher.to()).valueOf() + + // Merge in new attributes + Object.assign(oldToAttrs, newToAttrs) + + // Change morpher target + morpher.to(oldToAttrs) + + // Make sure that we save the work we did so we don't need it to do again + keys = newKeys + attrs = newToAttrs }) + this._rememberMorpher(type, morpher) return this }, @@ -812,7 +857,7 @@ extend(Runner, { (newTransforms.origin || 'center').toString() !== (transforms.origin || 'center').toString() ) { - origin = getOrigin(transforms, element) + origin = getOrigin(newTransforms, element) } // overwrite the old transformations with the new ones diff --git a/src/types/Color.js b/src/types/Color.js index eb6368e..eb6168f 100644 --- a/src/types/Color.js +++ b/src/types/Color.js @@ -28,7 +28,7 @@ function is (object, space) { } function getParameters (a, b) { - const params = is(a, 'rgb') ? { _a: a.r, _b: a.g, _c: a.b, space: 'rgb' } + const params = is(a, 'rgb') ? { _a: a.r, _b: a.g, _c: a.b, _d: 0, space: 'rgb' } : is(a, 'xyz') ? { _a: a.x, _b: a.y, _c: a.z, _d: 0, space: 'xyz' } : is(a, 'hsl') ? { _a: a.h, _b: a.s, _c: a.l, _d: 0, space: 'hsl' } : is(a, 'lab') ? { _a: a.l, _b: a.a, _c: a.b, _d: 0, space: 'lab' } -- 2.39.5