]> source.dussan.org Git - svg.js.git/commitdiff
added specs for animation files except runner, fixed a few things
authorUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Fri, 1 May 2020 10:17:24 +0000 (20:17 +1000)
committerUlrich-Matthias Schäfer <ulima.ums@googlemail.com>
Fri, 1 May 2020 10:17:24 +0000 (20:17 +1000)
13 files changed:
CHANGELOG.md
README.md
spec/RAFPlugin.js
spec/spec/animation/Animator.js
spec/spec/animation/Controller.js [new file with mode: 0644]
spec/spec/animation/Morphable.js
spec/spec/animation/Queue.js
spec/spec/animation/Timeline.js
spec/spec/animation/easing.js [deleted file]
src/animation/Controller.js
src/animation/Morphable.js
src/animation/Queue.js
src/animation/Timeline.js

index 6292c29cc002195552f76081b2359e93e5883350..9789900cd78d1e3e2791badd5d908efd3b6619f6 100644 (file)
@@ -30,6 +30,11 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http:
  - fixed `position` for `transform()` to also allow a position of 0
  - fixed `bbox()` of `PathArray` and `PointArray` which returns an instance of `Box` now
  - fixed bug in creation of PointArray which had still references to source arrays in it
+ - fixed `PID` controller and makeSetterGetter function
+ - fixed `Queue.push` which didnt let you push queue items
+ - 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
 
 ### Added
  - added second Parameter to `SVG(el, isHTML)` which allows to explicitely create elements in the HTML namespace (#1058)
@@ -43,7 +48,8 @@ The document follows the conventions described in [“Keep a CHANGELOG”](http:
  - added position argument for `toRoot()`
  - added attr syntax for `data()` method
  - added index and array parameter when passing a function to `List.each()` so that it mostly behaves like map
- - added possibility to pass transform object to `PointArray.transform()` ----- to Point
+ - added possibility to pass a transform object to `PointArray.transform()` similar to Point
+ - added `with-last` as `when` to `animate` and `schedule` to let an animation start with the start of the last one in the timeline
  - added lots of tests in es6 format
 
 ### Deleted
index 54d7cdcfed849944deff82ac0801b973de1f31da..744d7128fe8c9d4244cebaffb7632f3f59563647 100644 (file)
--- a/README.md
+++ b/README.md
@@ -26,4 +26,4 @@ SVG.js is licensed under the terms of the MIT License.
 ## Documentation
 Check [svgjs.com](https://svgjs.com/docs/3.0/) to learn more.
 
-[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=pay%40woutfierens.com&lc=US&item_name=SVG.JS&currency_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest)
+[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=ulima.ums%40googlemail.com&lc=US&item_name=SVG.JS&currency_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest)
index 3e82c707070c6b7d43173ddbc9389830024da118..c644ee4db99865b4c95c053a6c6710373d4c6fe5 100644 (file)
@@ -20,9 +20,10 @@ function RAFPlugin (jasmine) {
         throw new Error('You should pass a function to requestAnimationFrame')
       }
 
-      callbacks[index++] = fn
+      const i = index++
+      callbacks[i] = fn
 
-      return index
+      return i
     }
 
     /**
index 80f2eab19ece365fc6accc9dc2d696ff99537518..043beaab95c7bdd865c8b4cb47f228f670b4450c 100644 (file)
@@ -3,21 +3,22 @@
 import { Animator, Queue } from '../../../src/main.js'
 import { getWindow } from '../../../src/utils/window.js'
 
-describe('Animator.js', function () {
+describe('Animator.js', () => {
 
-  beforeEach(function () {
+  beforeEach(() => {
     jasmine.RequestAnimationFrame.install(getWindow())
     Animator.timeouts = new Queue()
     Animator.frames = new Queue()
+    Animator.immediates = new Queue()
     Animator.nextDraw = null
   })
 
-  afterEach(function () {
+  afterEach(() => {
     jasmine.RequestAnimationFrame.uninstall(getWindow())
   })
 
-  describe('timeout()', function () {
-    it('calls a function after a specific time', function () {
+  describe('timeout()', () => {
+    it('calls a function after a specific time', () => {
 
       var spy = jasmine.createSpy('tester')
       Animator.timeout(spy, 100)
@@ -29,8 +30,8 @@ describe('Animator.js', function () {
     })
   })
 
-  describe('cancelTimeout()', function () {
-    it('cancels a timeout which was created with timeout()', function () {
+  describe('cancelTimeout()', () => {
+    it('cancels a timeout which was created with timeout()', () => {
       var spy = jasmine.createSpy('tester')
       var id = Animator.timeout(spy, 100)
       Animator.clearTimeout(id)
@@ -41,8 +42,8 @@ describe('Animator.js', function () {
     })
   })
 
-  describe('frame()', function () {
-    it('calls a function at the next animationFrame', function () {
+  describe('frame()', () => {
+    it('calls a function at the next animationFrame', () => {
       var spy = jasmine.createSpy('tester')
 
       Animator.frame(spy)
@@ -52,4 +53,41 @@ describe('Animator.js', function () {
     })
   })
 
+  describe('cancelFrame()', () => {
+    it('cancels a single frame which was created with frame()', () => {
+      var spy = jasmine.createSpy('tester')
+
+      const id = Animator.frame(spy)
+      Animator.cancelFrame(id)
+
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick()
+      expect(spy).not.toHaveBeenCalled()
+    })
+  })
+
+  describe('immediate()', () => {
+    it('calls a function at the next animationFrame but after all frames are processed', () => {
+      var spy = jasmine.createSpy('tester')
+
+      Animator.immediate(spy)
+
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick()
+      expect(spy).toHaveBeenCalled()
+    })
+  })
+
+  describe('cancelImmediate()', () => {
+    it('cancels an immediate cakk which was created with immediate()', () => {
+      var spy = jasmine.createSpy('tester')
+
+      const id = Animator.immediate(spy)
+      Animator.cancelImmediate(id)
+
+      expect(spy).not.toHaveBeenCalled()
+      jasmine.RequestAnimationFrame.tick()
+      expect(spy).not.toHaveBeenCalled()
+    })
+  })
 })
diff --git a/spec/spec/animation/Controller.js b/spec/spec/animation/Controller.js
new file mode 100644 (file)
index 0000000..b1d4b8f
--- /dev/null
@@ -0,0 +1,403 @@
+/* globals describe, expect, it, jasmine */
+
+import { easing, defaults } from '../../../src/main.js'
+import { Stepper, Ease, Controller, Spring, PID } from '../../../src/animation/Controller.js'
+
+const { any, createSpy } = jasmine
+
+describe('Controller.js', () => {
+
+  describe('easing', () => {
+    var easedValues = {
+      '-': 0.5,
+      '<>': 0.5,
+      '>': 0.7071,
+      '<': 0.2929
+    }
+
+    ;[ '-', '<>', '<', '>' ].forEach((el) => {
+      describe(el, () => {
+        it('is 0 at 0', () => {
+          expect(easing[el](0)).toBe(0)
+        })
+        it('is 1 at 1', () => {
+          expect(Math.round(easing[el](1) * 1000) / 1000).toBe(1) // we need to round cause for some reason at some point 1==0.999999999
+        })
+        it('is eased at 0.5', () => {
+          expect(easing[el](0.5)).toBeCloseTo(easedValues[el])
+        })
+      })
+    })
+
+    describe('beziere()', () => {
+      const b1 = easing.bezier(0.25, 0.25, 0.75, 0.75)
+      const b2 = easing.bezier(-0.25, -0.25, 0.75, 0.75)
+      const b3 = easing.bezier(0.5, 0.5, 2, 2)
+      const b4 = easing.bezier(1, 1, 2, 2)
+      const b5 = easing.bezier(-1, -1, -2, -2)
+
+      it('is 0 at 0', () => {
+        expect(b1(0)).toBe(0)
+      })
+
+      it('is 1 at 1', () => {
+        expect(b1(1)).toBe(1)
+      })
+
+      it('is eased at 0.5', () => {
+        expect(b1(0.5)).toBe(0.5)
+        expect(b2(0.5)).toBe(0.3125)
+        expect(b3(0.5)).toBe(1.0625)
+        expect(b4(0.5)).toBe(1.25)
+        expect(b5(0.5)).toBe(-1)
+      })
+
+      it('handles values bigger 1', () => {
+        expect(b1(1.5)).toBe(1.5)
+        expect(b2(1.5)).toBe(1.5)
+        expect(b3(1.5)).toBe(1.5)
+        expect(b4(1.5)).toBe(1)
+        expect(b5(1.5)).toBe(1.5)
+      })
+
+      it('handles values lower 0', () => {
+        expect(b1(-0.5)).toBe(-0.5)
+        expect(b2(-0.5)).toBe(-0.5)
+        expect(b3(-0.5)).toBe(-0.5)
+        expect(b4(-0.5)).toBe(-0.5)
+        expect(b5(-0.5)).toBe(0)
+      })
+    })
+
+    describe('steps()', () => {
+      const s1 = easing.steps(5)
+      const s2 = easing.steps(5, 'start')
+      const s3 = easing.steps(5, 'end')
+      const s4 = easing.steps(5, 'none')
+      const s5 = easing.steps(5, 'both')
+
+      it('is 0 at 0', () => {
+        expect(s1(0)).toBe(0)
+        expect(s1(0, true)).toBe(0)
+        expect(s2(0)).toBe(0.2)
+        expect(s2(0, true)).toBe(0)
+        expect(s3(0)).toBe(0)
+        expect(s3(0, true)).toBe(0)
+        expect(s4(0)).toBe(0)
+        expect(s4(0, true)).toBe(0)
+        expect(s5(0)).toBe(1 / 6)
+        expect(s5(0, true)).toBe(0)
+      })
+
+      it('also works at values < 0', () => {
+        expect(s1(-0.1)).toBe(-0.2)
+        expect(s1(-0.1, true)).toBe(-0.2)
+        expect(s2(-0.1)).toBe(0)
+        expect(s2(-0.1, true)).toBe(0)
+        expect(s3(-0.1)).toBe(-0.2)
+        expect(s3(-0.1, true)).toBe(-0.2)
+        expect(s4(-0.1)).toBe(-0.25)
+        expect(s4(-0.1, true)).toBe(-0.25)
+        expect(s5(-0.1)).toBe(0)
+        expect(s5(-0.1, true)).toBe(0)
+      })
+
+      it('is 1 at 1', () => {
+        expect(s1(1)).toBe(1)
+        expect(s1(1, true)).toBe(0.8)
+        expect(s2(1)).toBe(1)
+        expect(s2(1, true)).toBe(1)
+        expect(s3(1)).toBe(1)
+        expect(s3(1, true)).toBe(0.8)
+        expect(s4(1)).toBe(1)
+        expect(s4(1, true)).toBe(1)
+        expect(s5(1)).toBe(1)
+        expect(s5(1, true)).toBe(5 / 6)
+      })
+
+      it('also works at values > 1', () => {
+        expect(s1(1.1)).toBe(1)
+        expect(s1(1.1, true)).toBe(1)
+        expect(s2(1.1)).toBe(1.2)
+        expect(s2(1.1, true)).toBe(1.2)
+        expect(s3(1.1)).toBe(1)
+        expect(s3(1.1, true)).toBe(1)
+        expect(s4(1.1)).toBe(1.25)
+        expect(s4(1.1, true)).toBe(1.25)
+        expect(s5(1.1)).toBe(1)
+        expect(s5(1.1, true)).toBe(1)
+      })
+
+      it('is eased at 0.1', () => {
+        expect(s1(0.1)).toBe(0)
+        expect(s1(0.1, true)).toBe(0)
+        expect(s2(0.1)).toBe(0.2)
+        expect(s2(0.1, true)).toBe(0)
+        expect(s3(0.1)).toBe(0)
+        expect(s3(0.1, true)).toBe(0)
+        expect(s4(0.1)).toBe(0)
+        expect(s4(0.1, true)).toBe(0)
+        expect(s5(0.1)).toBe(1 / 6)
+        expect(s5(0.1, true)).toBe(0)
+      })
+
+      it('is eased at 0.15', () => {
+        expect(s1(0.15)).toBe(0)
+        expect(s1(0.15, true)).toBe(0)
+        expect(s2(0.15)).toBe(0.2)
+        expect(s2(0.15, true)).toBe(0)
+        expect(s3(0.15)).toBe(0)
+        expect(s3(0.15, true)).toBe(0)
+        expect(s4(0.15)).toBe(0)
+        expect(s4(0.15, true)).toBe(0)
+        expect(s5(0.15)).toBe(1 / 6)
+        expect(s5(0.15, true)).toBe(0)
+      })
+
+      it('is eased at 0.2', () => {
+        expect(s1(0.2)).toBe(0.2)
+        expect(s1(0.2, true)).toBe(0.2)
+        expect(s2(0.2)).toBe(0.4)
+        expect(s2(0.2, true)).toBe(0.4)
+        expect(s3(0.2)).toBe(0.2)
+        expect(s3(0.2, true)).toBe(0.2)
+        expect(s4(0.2)).toBe(0.25)
+        expect(s4(0.2, true)).toBe(0.25)
+        expect(s5(0.2)).toBe(1 / 3)
+        expect(s5(0.2, true)).toBe(1 / 3)
+      })
+
+      it('is eased at 0.25', () => {
+        expect(s1(0.25)).toBe(0.2)
+        expect(s1(0.25, true)).toBe(0.2)
+        expect(s2(0.25)).toBe(0.4)
+        expect(s2(0.25, true)).toBe(0.4)
+        expect(s3(0.25)).toBe(0.2)
+        expect(s3(0.25, true)).toBe(0.2)
+        expect(s4(0.25)).toBe(0.25)
+        expect(s4(0.25, true)).toBe(0.25)
+        expect(s5(0.25)).toBe(1 / 3)
+        expect(s5(0.25, true)).toBe(1 / 3)
+      })
+    })
+  })
+
+  describe('Stepper', () => {
+    it('has a done() method', () => {
+      const stepper = new Stepper()
+      expect(stepper).toEqual(any(Stepper))
+      expect(stepper.done()).toBe(false)
+    })
+  })
+
+  describe('Ease', () => {
+    describe('()', () => {
+      it('wraps the default easing function by default', () => {
+        const ease = new Ease()
+        expect(ease.ease).toBe(easing[defaults.timeline.ease])
+      })
+
+      it('wraps an easing function found in "easing"', () => {
+        const ease = new Ease('-')
+        expect(ease.ease).toBe(easing['-'])
+      })
+
+      it('wraps a a custom easing function', () => {
+        const ease = new Ease(easing['-'])
+        expect(ease.ease).toBe(easing['-'])
+      })
+    })
+
+    describe('step()', () => {
+      it('provides an eased value to a position', () => {
+        let ease = new Ease(easing['-'])
+        expect(ease.step(2, 4, 0.5)).toBe(3)
+
+        ease = new Ease(() => 3)
+        expect(ease.step(2, 4, 0.5)).toBe(8)
+
+        ease = new Ease()
+        expect(ease.step(2, 4, 0.5)).toBeCloseTo(3.414, 3)
+      })
+
+      it('jumps to "to" value at pos 1 if from is not a number', () => {
+        const ease = new Ease('-')
+        expect(ease.step('Hallo', 'Welt', 0.999)).toBe('Hallo')
+        expect(ease.step('Hallo', 'Welt', 1)).toBe('Welt')
+      })
+    })
+  })
+
+  describe('Controller', () => {
+    describe('()', () => {
+      it('constructs a controller with the given stepper function set', () => {
+        const spy = createSpy()
+        const controller = new Controller(spy)
+        expect(controller).toEqual(any(Controller))
+        expect(controller.stepper).toBe(spy)
+      })
+    })
+
+    describe('step()', () => {
+      it('runs the stepper with current value, target value, dt and context', () => {
+        const spy = createSpy().and.returnValue('foo')
+        const controller = new Controller(spy)
+        expect(controller.step(10, 20, 30, 'bar')).toBe('foo')
+        expect(spy).toHaveBeenCalledWith(10, 20, 30, 'bar')
+      })
+    })
+
+    describe('done()', () => {
+      it('returns given values "done" property', () => {
+        const spy = createSpy()
+        const controller = new Controller(spy)
+        expect(controller.done({ done: 'yes' })).toBe('yes')
+      })
+    })
+  })
+
+  describe('Spring', () => {
+    describe('()', () => {
+      it('creates a spring with default duration and overshoot', () => {
+        const spring = new Spring()
+        expect(spring).toEqual(any(Spring))
+        expect(spring.duration()).toBe(500)
+        expect(spring.overshoot()).toBe(0)
+      })
+
+      it('creates a spring with given duration and overshoot', () => {
+        const spring = new Spring(100, 10)
+        expect(spring).toEqual(any(Spring))
+        expect(spring.duration()).toBe(100)
+        expect(spring.overshoot()).toBe(10)
+      })
+    })
+
+    describe('duration()', () => {
+      it('gets and sets a new duration for the spring controller', () => {
+        const spring = new Spring().duration(100)
+        expect(spring.duration()).toBe(100)
+      })
+    })
+
+    describe('overshoot()', () => {
+      it('gets and sets a new overshoot for the spring controller', () => {
+        const spring = new Spring().overshoot(10)
+        expect(spring.overshoot()).toBe(10)
+      })
+    })
+
+    describe('step()', () => {
+      it('calculates the new spring position', () => {
+        const spring = new Spring()
+        expect(spring.step(0, 100, 16, {})).toBeCloseTo(0.793, 3)
+      })
+
+      it('returns current if current is a string', () => {
+        const spring = new Spring()
+        expect(spring.step('Hallo', 'Welt', 16, {})).toBe('Hallo')
+      })
+
+      it('returns current if dt is 0', () => {
+        const spring = new Spring()
+        expect(spring.step(0, 100, 0, {})).toBe(0)
+      })
+
+      it('is done if dt is infinity and returns target', () => {
+        const spring = new Spring()
+        const context = {}
+        expect(spring.step(0, 100, Infinity, context)).toBe(100)
+        expect(spring.done(context)).toBe(true)
+      })
+
+      it('uses dt of 16 if it is over 100', () => {
+        const spring = new Spring()
+        expect(spring.step(0, 100, 101, {})).toBe(spring.step(0, 100, 16, {}))
+      })
+    })
+  })
+
+  describe('PID', () => {
+    describe('()', () => {
+      it('creates a PID controller with default values', () => {
+        const pid = new PID()
+        expect(pid).toEqual(any(PID))
+        expect(pid.p()).toBe(0.1)
+        expect(pid.i()).toBe(0.01)
+        expect(pid.d()).toBe(0)
+        expect(pid.windup()).toBe(1000)
+      })
+
+      it('creates a PID controller with given values', () => {
+        const pid = new PID(1, 2, 3, 4)
+        expect(pid).toEqual(any(PID))
+        expect(pid.p()).toBe(1)
+        expect(pid.i()).toBe(2)
+        expect(pid.d()).toBe(3)
+        expect(pid.windup()).toBe(4)
+      })
+    })
+
+    describe('p()', () => {
+      it('gets and sets the p parameter of the controller', () => {
+        const pid = new PID().p(100)
+        expect(pid.p()).toBe(100)
+      })
+    })
+
+    describe('i()', () => {
+      it('gets and sets the i parameter of the controller', () => {
+        const pid = new PID().i(100)
+        expect(pid.i()).toBe(100)
+      })
+    })
+
+    describe('d()', () => {
+      it('gets and sets the d parameter of the controller', () => {
+        const pid = new PID().d(100)
+        expect(pid.d()).toBe(100)
+      })
+    })
+
+    describe('windup()', () => {
+      it('gets and sets the windup parameter of the controller', () => {
+        const pid = new PID().windup(100)
+        expect(pid.windup()).toBe(100)
+      })
+    })
+
+    describe('step()', () => {
+      it('returns current if current is a string', () => {
+        const pid = new PID()
+        expect(pid.step('Hallo', 'Welt', 16, {})).toBe('Hallo')
+      })
+
+      it('returns current if dt is 0', () => {
+        const pid = new PID()
+        expect(pid.step(0, 100, 0, {})).toBe(0)
+      })
+
+      it('is done if dt is infinity and returns target', () => {
+        const pid = new PID()
+        const context = {}
+        expect(pid.step(0, 100, Infinity, context)).toBe(100)
+        expect(pid.done(context)).toBe(true)
+      })
+
+      it('caculates a new value', () => {
+        const pid = new PID()
+        expect(pid.step(0, 100, 16, {})).toBe(20)
+      })
+
+      it('uses antiwindup to restrict i power', () => {
+        const pid = new PID(0, 5, 0, 100)
+        expect(pid.step(0, 100, 1000, {})).toBe(500)
+      })
+
+      it('doesnt uses antiwindup if disabled', () => {
+        const pid = new PID(0, 5, 0, false)
+        expect(pid.step(0, 100, 1000, {})).toBe(500000)
+      })
+    })
+  })
+})
index 4b0e2f197cf9131cf42b3f68781b8547ab2d30a4..c1f27e811600e279d7c050d66a840052b0eeb3db 100644 (file)
@@ -1,13 +1,27 @@
 /* globals describe, expect, it, jasmine */
 
 import { Morphable, NonMorphable, ObjectBag, Color, Box, Matrix, PointArray, PathArray, TransformBag, Number as SVGNumber, Array as SVGArray } from '../../../src/main.js'
+import { Stepper, easing, Ease } from '../../../src/animation/Controller.js'
 
 const { objectContaining, arrayContaining, any } = jasmine
 
-describe('Morphable.js', function () {
-  describe('constructors', function () {
+describe('Morphable.js', () => {
+  describe('()', () => {
+    it('sets a default stepper', () => {
+      const morpher = new Morphable()
+      expect(morpher.stepper().ease).toBe(easing['-'])
+    })
+
+    it('sets the passed stepper', () => {
+      const ease = new Ease()
+      const morpher = new Morphable(ease)
+      expect(morpher.stepper()).toBe(ease)
+    })
+  })
+
+  describe('constructors', () => {
 
-    it('Morphable with SVGNumber', function () {
+    it('Morphable with SVGNumber', () => {
       var morpher = new Morphable().from(10).to(5)
 
       expect(morpher).toEqual(any(Morphable))
@@ -16,7 +30,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).valueOf()).toBe(7.5)
     })
 
-    it('Morphable with String', function () {
+    it('Morphable with String', () => {
       var morpher = new Morphable().from('foo').to('bar')
 
       expect(morpher).toEqual(any(Morphable))
@@ -26,7 +40,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(1).valueOf()).toBe('bar')
     })
 
-    it('Morphable with Object', function () {
+    it('Morphable with Object', () => {
       var morpher = new Morphable().from({ a: 5, b: 10 }).to({ a: 10, b: 20 })
 
       expect(morpher).toEqual(any(Morphable))
@@ -35,7 +49,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).valueOf()).toEqual(objectContaining({ a: 7.5, b: 15 }))
     })
 
-    it('Creates a morphable out of an SVGNumber', function () {
+    it('Creates a morphable out of an SVGNumber', () => {
       var morpher = new SVGNumber(5).to(10)
 
       expect(morpher).toEqual(any(Morphable))
@@ -44,7 +58,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).valueOf()).toBe(7.5)
     })
 
-    it('Creates a morphable out of an Color', function () {
+    it('Creates a morphable out of an Color', () => {
       var morpher = new Color('#fff').to('#000')
 
       expect(morpher).toEqual(any(Morphable))
@@ -53,7 +67,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).toHex()).toBe('#808080')
     })
 
-    it('Creates a morphable out of an Box', function () {
+    it('Creates a morphable out of an Box', () => {
       var morpher = new Box(1, 2, 3, 4).to([ 5, 6, 7, 8 ])
 
       expect(morpher).toEqual(any(Morphable))
@@ -62,7 +76,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5)).toEqual(objectContaining({ x: 3, y: 4, width: 5, height: 6 }))
     })
 
-    it('Creates a morphable out of an Matrix', function () {
+    it('Creates a morphable out of an Matrix', () => {
       var morpher = new Matrix(1, 2, 3, 4, 5, 6).to([ 3, 4, 5, 6, 7, 8 ])
 
       expect(morpher).toEqual(any(Morphable))
@@ -71,7 +85,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5)).toEqual(objectContaining(new Matrix(2, 3, 4, 5, 6, 7)))
     })
 
-    it('Creates a morphable out of an Array', function () {
+    it('Creates a morphable out of an SVGArray', () => {
       var morpher = new SVGArray([ 1, 2, 3, 4, 5, 6 ]).to([ 3, 4, 5, 6, 7, 8 ])
 
       expect(morpher).toEqual(any(Morphable))
@@ -80,7 +94,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 2, 3, 4, 5, 6, 7 ]))
     })
 
-    it('Creates a morphable out of an PointArray', function () {
+    it('Creates a morphable out of an PointArray', () => {
       var morpher = new PointArray([ 1, 2, 3, 4, 5, 6 ]).to([ 3, 4, 5, 6, 7, 8 ])
 
       expect(morpher).toEqual(any(Morphable))
@@ -89,7 +103,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 2, 3, 4, 5, 6, 7 ]))
     })
 
-    it('Creates a morphable out of an PathArray', function () {
+    it('Creates a morphable out of an PathArray', () => {
       var morpher = new PathArray([ 'M', 1, 2, 'L', 3, 4, 'L', 5, 6 ]).to([ 'M', 3, 4, 'L', 5, 6, 'L', 7, 8 ])
 
       expect(morpher).toEqual(any(Morphable))
@@ -98,17 +112,17 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5).toArray()).toEqual(arrayContaining([ 'M', 2, 3, 'L', 4, 5, 'L', 6, 7 ]))
     })
 
-    it('Creates a morphable out of an NonMorphable', function () {
-      var morpher = new NonMorphable('foo').to('bar')
+    it('creates a morphable from unmorphable types', () => {
+      var morpher = new Morphable().from('Hallo').to('Welt')
 
       expect(morpher).toEqual(any(Morphable))
       expect(morpher.type()).toBe(NonMorphable)
       expect(morpher.at(0.5)).toEqual(any(NonMorphable))
-      expect(morpher.at(0.5).valueOf()).toBe('foo')
-      expect(morpher.at(1).valueOf()).toBe('bar')
+      expect(morpher.at(0.5).valueOf()).toBe('Hallo')
+      expect(morpher.at(1).valueOf()).toBe('Welt')
     })
 
-    it('Creates a morphable out of an TransformBag', function () {
+    it('Creates a morphable out of an TransformBag', () => {
       var morpher = new TransformBag({ rotate: 0, translateX: 0 })
         .to({ rotate: 50, translateX: 20 })
 
@@ -119,7 +133,7 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5)).toEqual(objectContaining({ rotate: 25, translateX: 10 }))
     })
 
-    it('Creates a morphable out of an ObjectBag', function () {
+    it('Creates a morphable out of an ObjectBag', () => {
       var morpher = new ObjectBag({ a: 5, b: 10 }).to({ a: 10, b: 20 })
 
       expect(morpher).toEqual(any(Morphable))
@@ -127,41 +141,200 @@ describe('Morphable.js', function () {
       expect(morpher.at(0.5)).toEqual(any(Object))
       expect(morpher.at(0.5).valueOf()).toEqual(objectContaining({ a: 7.5, b: 15 }))
     })
+
+    it('creates a morphable from a color string', () => {
+      var morpher = new Morphable().from('#fff').to('#000')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(Color)
+      expect(morpher.at(0.5)).toEqual(any(Color))
+      expect(morpher.at(0.5).toHex()).toBe('#808080')
+
+      morpher = new Morphable().from('rgb(255,255,255)').to('rgb(0,0,0)')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(Color)
+      expect(morpher.at(0.5)).toEqual(any(Color))
+      expect(morpher.at(0.5).toHex()).toBe('#808080')
+    })
+
+    it('creates a morphable from path string', () => {
+      var morpher = new Morphable().from('M 0 0 L 10 10').to('M 0 0 L 20 20')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(PathArray)
+      expect(morpher.at(0.5)).toEqual(any(PathArray))
+      expect(morpher.at(0.5).toString()).toBe('M0 0L15 15 ')
+    })
+
+    it('creates a morphable from number string', () => {
+      var morpher = new Morphable().from('10').to('20')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGNumber)
+      expect(morpher.at(0.5)).toEqual(any(SVGNumber))
+      expect(morpher.at(0.5).toString()).toBe('15')
+
+      morpher = new Morphable().from('10px').to('20px')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGNumber)
+      expect(morpher.at(0.5)).toEqual(any(SVGNumber))
+      expect(morpher.at(0.5).toString()).toBe('15px')
+    })
+
+    it('creates a morphable from delimited string', () => {
+      var morpher = new Morphable().from(' 0 1,  2  , 3  ').to('4,5,6,7')
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGArray)
+      expect(morpher.at(0.5)).toEqual(any(SVGArray))
+      expect(morpher.at(0.5)).toEqual([ 2, 3, 4, 5 ])
+    })
+
+    it('creates a morphable from an array', () => {
+      var morpher = new Morphable().from([ 0, 1, 2, 3 ]).to([ 4, 5, 6, 7 ])
+
+      expect(morpher).toEqual(any(Morphable))
+      expect(morpher.type()).toBe(SVGArray)
+      expect(morpher.at(0.5)).toEqual(any(SVGArray))
+      expect(morpher.at(0.5)).toEqual([ 2, 3, 4, 5 ])
+    })
+
+    it('converts the to-color to the from-type', () => {
+      var morpher = new Color('#fff').to(new Color(1, 2, 3, 'hsl'))
+      expect(new Color(morpher.from()).space).toBe('rgb')
+      expect(morpher.at(0.5).space).toBe('rgb')
+    })
+
+    it('converts the from-color to the to-type', () => {
+      const morpher = new Morphable().to(new Color(1, 2, 3, 'hsl')).from('#fff')
+      expect(new Color(morpher.from()).space).toBe('hsl')
+      expect(morpher.at(0.5).space).toBe('hsl')
+    })
   })
 
-  describe('from()', function () {
-    it('sets the type of the runner', function () {
+  describe('from()', () => {
+    it('sets the type of the runner', () => {
       var morpher = new Morphable().from(5)
       expect(morpher.type()).toBe(SVGNumber)
     })
 
-    it('sets the from attribute to an array representation of the morphable type', function () {
+    it('sets the from attribute to an array representation of the morphable type', () => {
       var morpher = new Morphable().from(5)
       expect(morpher.from()).toEqual(arrayContaining([ 5 ]))
     })
   })
 
-  describe('type()', function () {
-    it('sets the type of the runner', function () {
+  describe('type()', () => {
+    it('sets the type of the runner', () => {
       var morpher = new Morphable().type(SVGNumber)
       expect(morpher._type).toBe(SVGNumber)
     })
 
-    it('gets the type of the runner', function () {
+    it('gets the type of the runner', () => {
       var morpher = new Morphable().type(SVGNumber)
       expect(morpher.type()).toBe(SVGNumber)
     })
   })
 
-  describe('to()', function () {
-    it('sets the type of the runner', function () {
+  describe('to()', () => {
+    it('sets the type of the runner', () => {
       var morpher = new Morphable().to(5)
       expect(morpher.type()).toBe(SVGNumber)
     })
 
-    it('sets the from attribute to an array representation of the morphable type', function () {
+    it('sets the from attribute to an array representation of the morphable type', () => {
       var morpher = new Morphable().to(5)
       expect(morpher.to()).toEqual(arrayContaining([ 5 ]))
     })
   })
+
+  describe('stepper()', () => {
+    it('sets and gets the stepper of the Morphable', () => {
+      const stepper = new Stepper()
+      const morpher = new Morphable().stepper(stepper)
+      expect(morpher.stepper()).toBe(stepper)
+    })
+  })
+
+  describe('NonMorphable', () => {
+    describe('()', () => {
+      it('wraps any type into a NonMorphable from an array', () => {
+        const non = new NonMorphable([ 5 ])
+        expect(non.valueOf()).toBe(5)
+      })
+
+      it('wraps any type into a NonMorphable from any type', () => {
+        expect(new NonMorphable(5).valueOf()).toBe(5)
+        expect(new NonMorphable('Hello').valueOf()).toBe('Hello')
+      })
+    })
+
+    describe('toArray()', () => {
+      it('returns array representation of NonMorphable', () => {
+        expect(new NonMorphable(5).toArray()).toEqual([ 5 ])
+        expect(new NonMorphable('Hello').toArray()).toEqual([ 'Hello' ])
+      })
+    })
+  })
+
+  describe('TransformBag', () => {
+    describe('()', () => {
+      it('creates an object which holds transformations for morphing by passing array', () => {
+        const bag = new TransformBag([ 0, 1, 2, 3, 4, 5, 6, 7 ])
+        expect(bag.toArray()).toEqual([ 0, 1, 2, 3, 4, 5, 6, 7 ])
+      })
+
+      it('creates an object which holds transformations for morphing by passing object', () => {
+        const bag = new TransformBag({
+          scaleX: 0,
+          scaleY: 1,
+          shear: 2,
+          rotate: 3,
+          translateX: 4,
+          translateY: 5,
+          originX: 6,
+          originY: 7
+        })
+
+        expect(bag.toArray()).toEqual([ 0, 1, 2, 3, 4, 5, 6, 7 ])
+      })
+    })
+
+    describe('toArray()', () => {
+      it('creates an array out of the transform values', () => {
+        const bag = new TransformBag([ 0, 1, 2, 3, 4, 5, 6, 7 ])
+        expect(bag.toArray()).toEqual([ 0, 1, 2, 3, 4, 5, 6, 7 ])
+      })
+    })
+  })
+
+  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 ])
+      })
+
+      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 ])
+      })
+    })
+
+    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 ])
+      })
+    })
+
+    describe('valueOf()', () => {
+      it('create an object from the stored values', () => {
+        const bag = new ObjectBag({ foo: 1, bar: 2, baz: 3 })
+        expect(bag.valueOf()).toEqual({ foo: 1, bar: 2, baz: 3 })
+      })
+    })
+  })
 })
index 4282bf993200c7d88db9e8ec43ef5e28ee69547f..83864055d382d55aeb96bb52dbb43b57f092b8d4 100644 (file)
@@ -47,6 +47,18 @@ describe('Queue.js', function () {
       expect(queue.first()).toBe(1)
       expect(queue.last()).toBe(3)
     })
+
+    it('adds an item to the end of the queue', function () {
+      var queue = new Queue()
+      queue.push(1)
+      const item = queue.push(2)
+      queue.push(3)
+      queue.remove(item)
+      queue.push(item)
+
+      expect(queue.first()).toBe(1)
+      expect(queue.last()).toBe(2)
+    })
   })
 
   describe('remove ()', function () {
@@ -61,6 +73,18 @@ describe('Queue.js', function () {
       expect(queue.last()).toBe(2)
       expect(queue.first()).toBe(1)
     })
+
+    it('removes the given item from the queue', function () {
+      var queue = new Queue()
+      var item = queue.push(1)
+      queue.push(2)
+      queue.push(3)
+
+      queue.remove(item)
+
+      expect(queue.last()).toBe(3)
+      expect(queue.first()).toBe(2)
+    })
   })
 
   describe('shift ()', function () {
index 1acc663995c4151d17ce028e8f368da713479e75..f23e620994477c5185f6fc37d4b669edcc042a6c 100644 (file)
-/* globals describe, expect, it, beforeEach, container */
+/* globals describe, expect, it, beforeEach, afterEach, spyOn, container, jasmine */
 
-import { Timeline, SVG } from '../../../src/main.js'
+import { Timeline, SVG, Runner, Animator, Queue, Rect } from '../../../src/main.js'
+import { getWindow } from '../../../src/utils/window.js'
+
+const { createSpy, any } = jasmine
 
 describe('Timeline.js', () => {
+  beforeEach(() => {
+    jasmine.RequestAnimationFrame.install(getWindow())
+    Animator.timeouts = new Queue()
+    Animator.frames = new Queue()
+    Animator.immediates = new Queue()
+    Animator.nextDraw = null
+  })
+
+  afterEach(() => {
+    getWindow().cancelAnimationFrame(Animator.nextDraw)
+    jasmine.RequestAnimationFrame.uninstall(getWindow())
+  })
+
+  describe('()', () => {
+    it('creates a new Timeline with a default timesource', () => {
+      const timeline = new Timeline()
+      expect(timeline.source()).toEqual(any(Function))
+    })
+
+    it('creates a new Timeline with the passed timesource', () => {
+      const source = createSpy()
+      const timeline = new Timeline(source)
+      expect(timeline.source()).toBe(source)
+    })
+  })
+
+  describe('schedule()', () => {
+    it('schedules a runner at the start of the queue with a default delay of 0', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner)
+      expect(timeline._runners[0].start).toEqual(0)
+    })
+
+    it('sets a reference of the timeline to the runner', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner)
+      expect(runner.timeline()).toBe(timeline)
+    })
+
+    it('schedules after when no when is past', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      timeline.schedule(runner)
+      expect(timeline._runners[1].start).toBe(1000)
+    })
+
+    it('schedules after when when is last', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'last')
+      expect(timeline._runners[1].start).toBe(1000)
+    })
+
+    it('schedules after when when is after', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'after')
+      expect(timeline._runners[1].start).toBe(1000)
+    })
+
+    it('starts the animation right away when there is no runner to schedule after and when is after', () => {
+      const timeline = new Timeline().time(100)
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'after')
+      expect(timeline._runners[0].start).toBe(100)
+    })
+
+    it('schedules with start of the last runner when when is with-last', () => {
+      const timeline = new Timeline().schedule(new Runner(1000), 200)
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'with-last')
+      expect(timeline._runners[1].start).toBe(200)
+    })
+
+    it('starts the animation right away when there is no runner to schedule after and when is after', () => {
+      const timeline = new Timeline().time(100)
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'with-last')
+      expect(timeline._runners[0].start).toBe(100)
+    })
+
+    it('respects passed delay', () => {
+      const timeline = new Timeline().schedule(new Runner(1000), 1000)
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'after')
+      expect(timeline._runners[1].start).toBe(2000)
+    })
+
+    it('schedules the runner absolutely with absolute', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'absolute')
+      expect(timeline._runners[1].start).toBe(0)
+    })
+
+    it('schedules the runner absolutely with start', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 0, 'start')
+      expect(timeline._runners[1].start).toBe(0)
+    })
+
+    it('schedules the runner relatively to old start with relative', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 100).schedule(runner, 100, 'relative')
+      expect(timeline._runners[0].start).toBe(200)
+    })
+
+    it('schedules the runner as absolute if this runner wasnt on the timeline', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 100, 'relative')
+      expect(timeline._runners[0].start).toBe(100)
+    })
+
+    it('throws if when is not supported', () => {
+      const timeline = new Timeline().schedule(new Runner(1000), 1000)
+      const runner = new Runner(1000)
+      expect(() => timeline.schedule(runner, 0, 'not supported')).toThrowError('Invalid value for the "when" parameter')
+    })
+
+    it('uses persist value of the runner of present', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000).persist(100)
+      timeline.schedule(runner)
+      expect(timeline._runners[0].persist).toBe(100)
+    })
+  })
+
+  describe('unschedule()', () => {
+    it('removes a runner from the timeline', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner).unschedule(runner)
+      expect(runner.timeline()).toBe(null)
+      expect(timeline._runners).toEqual([])
+    })
+  })
+
+  describe('getRunnerInfoById()', () => {
+    it('gets a runner by its id from the timeline', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      expect(timeline.schedule(runner).getRunnerInfoById(runner.id).runner).toBe(runner)
+    })
+
+    it('returns null of runner not found', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      expect(timeline.getRunnerInfoById(runner.id)).toBe(null)
+    })
+  })
+
+  describe('getLastRunnerInfo()', () => {
+    it('gets a runner by its id from the timeline', () => {
+      const timeline = new Timeline().schedule(new Runner(1000))
+      const runner = new Runner(1000)
+      expect(timeline.schedule(runner).getLastRunnerInfo().runner).toBe(runner)
+    })
+  })
+
+  describe('getEndTime()', () => {
+    it('returns the end time of the runner which started last', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      const runner2 = new Runner(100)
+      timeline.schedule(runner).schedule(runner2, 500, 'start')
+      expect(timeline.getEndTime()).toBe(600)
+    })
+
+    it('returns the timeline time if no runner is scheduled', () => {
+      const timeline = new Timeline().time(100)
+      expect(timeline.getEndTime()).toBe(100)
+    })
+  })
+
   describe('getEndTimeOfTimeline', () => {
     it('returns 0 if no runners are scheduled', () => {
       const timeline = new Timeline()
       const endTime = timeline.getEndTimeOfTimeline()
       expect(endTime).toEqual(0)
     })
+
+    it('returns the time all runners are finished', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      const runner2 = new Runner(100)
+      timeline.schedule(runner).schedule(runner2, 500, 'start')
+      expect(timeline.getEndTimeOfTimeline()).toBe(1000)
+    })
   })
 
   describe('finish - issue #964', () => {
@@ -114,4 +304,192 @@ describe('Timeline.js', () => {
       expect(rect3.y()).toEqual(200)
     })
   })
+
+  describe('updateTime()', () => {
+    it('sets the time to the current time', () => {
+      const timeline = new Timeline(() => 200).play()
+      expect(timeline._lastSourceTime).toBe(200)
+    })
+  })
+
+  describe('stop()', () => {
+    it('sets the time to 0 and pauses the timeline', () => {
+      const timeline = new Timeline().time(100)
+      expect(timeline.stop().time()).toBe(0)
+      expect(timeline._paused).toBe(true)
+    })
+  })
+
+  describe('speed()', () => {
+    it('gets or sets the speed of the timeline', () => {
+      const timeline = new Timeline().speed(2)
+      expect(timeline.speed()).toBe(2)
+    })
+  })
+
+  describe('reverse()', () => {
+    it('reverses the timeline with no parameter given', () => {
+      const timeline = new Timeline().speed(2)
+      const spy = spyOn(timeline, 'speed').and.callThrough()
+      timeline.reverse()
+      expect(spy).toHaveBeenCalledWith(-2)
+      timeline.reverse()
+      expect(spy).toHaveBeenCalledWith(2)
+    })
+
+    it('reverses the timeline when true was passed', () => {
+      const timeline = new Timeline().speed(2)
+      const spy = spyOn(timeline, 'speed').and.callThrough()
+      timeline.reverse(true)
+      expect(spy).toHaveBeenCalledWith(-2)
+    })
+
+    it('plays normal direction when false was passed', () => {
+      const timeline = new Timeline().speed(-2)
+      const spy = spyOn(timeline, 'speed').and.callThrough()
+      timeline.reverse(false)
+      expect(spy).toHaveBeenCalledWith(2)
+    })
+  })
+
+  describe('seek()', () => {
+    it('seeks the time by a given delta', () => {
+      const timeline = new Timeline().time(100).seek(200)
+      expect(timeline.time()).toBe(300)
+    })
+  })
+
+  describe('time()', () => {
+    it('gets and sets the current time of the timeline', () => {
+      const timeline = new Timeline().time(100)
+      expect(timeline.time()).toBe(100)
+    })
+  })
+
+  describe('persist()', () => {
+    it('gets and sets the persist property of the timeline', () => {
+      const timeline = new Timeline().persist(true)
+      expect(timeline.persist()).toBe(true)
+    })
+  })
+
+  describe('source()', () => {
+    it('gets or sets the time source of the timeline', () => {
+      const source = () => 200
+      const timeline = new Timeline().source(source)
+      expect(timeline.source()).toBe(source)
+    })
+  })
+
+  describe('_stepFn', () => {
+    it('does a step in the timeline and runs all runners', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(16)
+      expect(runner.time()).toBe(16)
+    })
+
+    it('doenst run runners which start later', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner, 100).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(16)
+      expect(runner.time()).toBe(0)
+    })
+
+    it('reset runner if timeline was seeked backwards', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner)
+      const spy = spyOn(runner, 'reset').and.callThrough()
+      jasmine.RequestAnimationFrame.tick(1000)
+      timeline.seek(-1000)
+      expect(runner.time()).toBe(0)
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('doesnt run runners if they are not active', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000).active(false)
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(16)
+      expect(runner.time()).toBe(0)
+    })
+
+    it('unschedules runner if its finished', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000)
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1000)
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(runner.time()).toBe(1001)
+      expect(timeline.getRunnerInfoById(runner.id)).toBe(null)
+    })
+
+    it('does not unschedule if runner is persistent forever', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000).persist(true)
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1000)
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(runner.time()).toBe(1001)
+      expect(timeline.getRunnerInfoById(runner.id)).not.toBe(null)
+    })
+
+    it('does not unschedule if runner is persistent for a certain time', () => {
+      const timeline = new Timeline()
+      const runner = new Runner(1000).persist(100)
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1000)
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(runner.time()).toBe(1001)
+      expect(timeline.getRunnerInfoById(runner.id)).not.toBe(null)
+    })
+
+    it('fires finish if no runners left', () => {
+      const spy = createSpy()
+      const timeline = new Timeline().on('finished', spy)
+      const runner = new Runner(1000)
+      spy.calls.reset()
+      timeline.schedule(runner).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1000)
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(spy).toHaveBeenCalled()
+    })
+
+    it('continues if there are still runners left from us when going back in time', () => {
+      const spy = createSpy()
+      const timeline = new Timeline().on('finished', spy).time(1200).reverse(true)
+      const runner = new Runner(1000)
+      spy.calls.reset()
+      timeline.schedule(runner, 0).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(spy).not.toHaveBeenCalled()
+    })
+
+    it('finishes if time is backwards and 0', () => {
+      const spy = createSpy()
+      const timeline = new Timeline().on('finished', spy).reverse(true)
+      const runner = new Runner(1000)
+      spy.calls.reset()
+      timeline.schedule(runner, 0).play() // we have to play because its syncronous here
+      jasmine.RequestAnimationFrame.tick(1)
+      expect(spy).toHaveBeenCalled()
+    })
+  })
+
+  describe('Element', () => {
+    describe('timeline()', () => {
+      it('sets and gets the timeline of the element', () => {
+        const timeline = new Timeline()
+        const rect = new Rect().timeline(timeline)
+        expect(rect.timeline()).toBe(timeline)
+      })
+
+      it('creates a timeline on the fly when getting it', () => {
+        expect(new Rect().timeline()).toEqual(any(Timeline))
+      })
+    })
+  })
 })
diff --git a/spec/spec/animation/easing.js b/spec/spec/animation/easing.js
deleted file mode 100644 (file)
index ab9f51c..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-/* globals describe, expect, it */
-
-import { easing } from '../../../src/main.js'
-
-describe('easing', () => {
-  var easedValues = {
-    '-': 0.5,
-    '<>': 0.5,
-    '>': 0.7071,
-    '<': 0.2929
-  }
-
-  ;[ '-', '<>', '<', '>' ].forEach((el) => {
-    describe(el, () => {
-      it('is 0 at 0', () => {
-        expect(easing[el](0)).toBe(0)
-      })
-      it('is 1 at 1', () => {
-        expect(Math.round(easing[el](1) * 1000) / 1000).toBe(1) // we need to round cause for some reason at some point 1==0.999999999
-      })
-      it('is eased at 0.5', () => {
-        expect(easing[el](0.5)).toBeCloseTo(easedValues[el])
-      })
-    })
-  })
-})
index 35fa1ae0523fb6e5393e770bb27ef7abf011f764..972679e6778217e9d504cc21ad126e7f7031eef4 100644 (file)
@@ -9,7 +9,7 @@ The base stepper class that will be
 
 function makeSetterGetter (k, f) {
   return function (v) {
-    if (v == null) return this[v]
+    if (v == null) return this[k]
     this[k] = v
     if (f) f.call(this)
     return this
@@ -104,9 +104,9 @@ Easing Functions
 ***/
 
 export class Ease extends Stepper {
-  constructor (fn) {
+  constructor (fn = timeline.ease) {
     super()
-    this.ease = easing[fn || timeline.ease] || fn
+    this.ease = easing[fn] || fn
   }
 
   step (from, to, pos) {
@@ -155,10 +155,10 @@ function recalculate () {
 }
 
 export class Spring extends Controller {
-  constructor (duration, overshoot) {
+  constructor (duration = 500, overshoot = 0) {
     super()
-    this.duration(duration || 500)
-      .overshoot(overshoot || 0)
+    this.duration(duration)
+      .overshoot(overshoot)
   }
 
   step (current, target, dt, c) {
@@ -195,13 +195,8 @@ extend(Spring, {
 })
 
 export class PID extends Controller {
-  constructor (p, i, d, windup) {
+  constructor (p = 0.1, i = 0.01, d = 0, windup = 1000) {
     super()
-
-    p = p == null ? 0.1 : p
-    i = i == null ? 0.01 : i
-    d = d == null ? 0 : d
-    windup = windup == null ? 1000 : windup
     this.p(p).i(i).d(d).windup(windup)
   }
 
@@ -215,7 +210,7 @@ export class PID extends Controller {
     var p = target - current
     var i = (c.integral || 0) + p * dt
     var d = (p - (c.error || 0)) / dt
-    var windup = this.windup
+    var windup = this._windup
 
     // antiwindup
     if (windup !== false) {
@@ -232,7 +227,7 @@ export class PID extends Controller {
 }
 
 extend(PID, {
-  windup: makeSetterGetter('windup'),
+  windup: makeSetterGetter('_windup'),
   p: makeSetterGetter('P'),
   i: makeSetterGetter('I'),
   d: makeSetterGetter('D')
index 93debe77dda5136cc832211fa17f82d1e4a89604..0a28e8e9a9013ff3109fe90a31464aa3c0196b85 100644 (file)
@@ -195,6 +195,10 @@ TransformBag.defaults = {
   originY: 0
 }
 
+const sortByKey = (a, b) => {
+  return (a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0))
+}
+
 export class ObjectBag {
   constructor (...args) {
     this.init(...args)
@@ -215,9 +219,7 @@ export class ObjectBag {
       entries.push([ i, objOrArr[i] ])
     }
 
-    entries.sort((a, b) => {
-      return a[0] - b[0]
-    })
+    entries.sort(sortByKey)
 
     this.values = entries.reduce((last, curr) => last.concat(curr), [])
     return this
index 0d3cdcd3b3c09f8de50344f6d16c7271e95dfa58..1858b9991785e616fae29c01d41ee7f5b93910eb 100644 (file)
@@ -6,7 +6,7 @@ export default class Queue {
 
   push (value) {
     // An item stores an id and the provided value
-    var item = value.next ? value : { value: value, next: null, prev: null }
+    var item = typeof value.next !== 'undefined' ? value : { value: value, next: null, prev: null }
 
     // Deal with the queue being empty or populated
     if (this._last) {
index 2137727392f26cdb7db6fa1ada1b81ebb6428349..d175ae6d60597e8deff8ed14ea37a0c760d19cbf 100644 (file)
@@ -68,11 +68,15 @@ export default class Timeline extends EventTarget {
     } else if (when === 'now') {
       absoluteStartTime = this._time
     } else if (when === 'relative') {
-      const runnerInfo = this._runners[runner.id]
+      const runnerInfo = this.getRunnerInfoById(runner.id)
       if (runnerInfo) {
         absoluteStartTime = runnerInfo.start + delay
         delay = 0
       }
+    } else if (when === 'with-last') {
+      const lastRunnerInfo = this.getLastRunnerInfo()
+      const lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
+      absoluteStartTime = lastStartTime
     } else {
       throw new Error('Invalid value for the "when" parameter')
     }
@@ -110,26 +114,25 @@ export default class Timeline extends EventTarget {
     return this
   }
 
+  getRunnerInfoById (id) {
+    return this._runners[this._runnerIds.indexOf(id)] || null
+  }
+
+  getLastRunnerInfo () {
+    return this.getRunnerInfoById(this._lastRunnerId)
+  }
+
   // Calculates the end of the timeline
   getEndTime () {
-    var lastRunnerInfo = this._runners[this._runnerIds.indexOf(this._lastRunnerId)]
+    var lastRunnerInfo = this.getLastRunnerInfo()
     var lastDuration = lastRunnerInfo ? lastRunnerInfo.runner.duration() : 0
-    var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : 0
+    var lastStartTime = lastRunnerInfo ? lastRunnerInfo.start : this._time
     return lastStartTime + lastDuration
   }
 
   getEndTimeOfTimeline () {
-    let lastEndTime = 0
-    for (var i = 0; i < this._runners.length; i++) {
-      const runnerInfo = this._runners[i]
-      var duration = runnerInfo ? runnerInfo.runner.duration() : 0
-      var startTime = runnerInfo ? runnerInfo.start : 0
-      const endTime = startTime + duration
-      if (endTime > lastEndTime) {
-        lastEndTime = endTime
-      }
-    }
-    return lastEndTime
+    const endTimes = this._runners.map((i) => i.start + i.runner.duration())
+    return Math.max(0, ...endTimes)
   }
 
   // Makes sure, that after pausing the time doesn't jump
@@ -174,7 +177,7 @@ export default class Timeline extends EventTarget {
     if (yes == null) return this.speed(-currentSpeed)
 
     var positive = Math.abs(currentSpeed)
-    return this.speed(yes ? positive : -positive)
+    return this.speed(yes ? -positive : positive)
   }
 
   seek (dt) {