From: Ulrich-Matthias Schäfer Date: Fri, 1 May 2020 10:17:24 +0000 (+1000) Subject: added specs for animation files except runner, fixed a few things X-Git-Tag: 3.1.0~33 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=897bbfa075055097d64d42d7a32952bec9c75665;p=svg.js.git added specs for animation files except runner, fixed a few things --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 6292c29..9789900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 54d7cdc..744d712 100644 --- 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¤cy_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¤cy_code=EUR&bn=PP-DonationsBF%3Abtn_donate_74x21.png%3ANonHostedGuest) diff --git a/spec/RAFPlugin.js b/spec/RAFPlugin.js index 3e82c70..c644ee4 100644 --- a/spec/RAFPlugin.js +++ b/spec/RAFPlugin.js @@ -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 } /** diff --git a/spec/spec/animation/Animator.js b/spec/spec/animation/Animator.js index 80f2eab..043beaa 100644 --- a/spec/spec/animation/Animator.js +++ b/spec/spec/animation/Animator.js @@ -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 index 0000000..b1d4b8f --- /dev/null +++ b/spec/spec/animation/Controller.js @@ -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) + }) + }) + }) +}) diff --git a/spec/spec/animation/Morphable.js b/spec/spec/animation/Morphable.js index 4b0e2f1..c1f27e8 100644 --- a/spec/spec/animation/Morphable.js +++ b/spec/spec/animation/Morphable.js @@ -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 }) + }) + }) + }) }) diff --git a/spec/spec/animation/Queue.js b/spec/spec/animation/Queue.js index 4282bf9..8386405 100644 --- a/spec/spec/animation/Queue.js +++ b/spec/spec/animation/Queue.js @@ -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 () { diff --git a/spec/spec/animation/Timeline.js b/spec/spec/animation/Timeline.js index 1acc663..f23e620 100644 --- a/spec/spec/animation/Timeline.js +++ b/spec/spec/animation/Timeline.js @@ -1,14 +1,204 @@ -/* 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 index ab9f51c..0000000 --- a/spec/spec/animation/easing.js +++ /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]) - }) - }) - }) -}) diff --git a/src/animation/Controller.js b/src/animation/Controller.js index 35fa1ae..972679e 100644 --- a/src/animation/Controller.js +++ b/src/animation/Controller.js @@ -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') diff --git a/src/animation/Morphable.js b/src/animation/Morphable.js index 93debe7..0a28e8e 100644 --- a/src/animation/Morphable.js +++ b/src/animation/Morphable.js @@ -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 diff --git a/src/animation/Queue.js b/src/animation/Queue.js index 0d3cdcd..1858b99 100644 --- a/src/animation/Queue.js +++ b/src/animation/Queue.js @@ -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) { diff --git a/src/animation/Timeline.js b/src/animation/Timeline.js index 2137727..d175ae6 100644 --- a/src/animation/Timeline.js +++ b/src/animation/Timeline.js @@ -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) {