- fixed `replace()` which works without a parent now, too | - fixed `replace()` which works without a parent now, too | ||||
- fixed `defs()` which correctly returns `null` when called on a detached node that is not a root node | - fixed `defs()` which correctly returns `null` when called on a detached node that is not a root node | ||||
- fixed `reference()` which correctly returns `null` instead of throwing when specifying an attribute which holds a number | - fixed `reference()` which correctly returns `null` instead of throwing when specifying an attribute which holds a number | ||||
- fixed `flatten()` which correctly flattens now but doesnt accept parameters anymore (makes no sense) | |||||
- fixed `flatten()` which correctly flattens now but doesn't accept parameters anymore (makes no sense) | |||||
- fixed `ungroup()` which now inserts the elements at the correct position in the correct order and has position as second argument now | - fixed `ungroup()` which now inserts the elements at the correct position in the correct order and has position as second argument now | ||||
- fixed `position` for `transform()` to also allow a position of 0 | - 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 `bbox()` of `PathArray` and `PointArray` which returns an instance of `Box` now | ||||
- fixed animate when=after to be really "now" when no runner is on the timeline | - fixed animate when=after to be really "now" when no runner is on the timeline | ||||
- fixed animate attr which is also retargetable now | - fixed animate attr which is also retargetable now | ||||
- fixed internals of ObjectBag which can hold other Morphable values now | - fixed internals of ObjectBag which can hold other Morphable values now | ||||
- fixed animate transform which didnt change its origin on retarget for declaritive animations | |||||
- fixed animate transform which didnt change its origin on retarget for declarative animations | |||||
- fixed path parsing (#1145) | - fixed path parsing (#1145) | ||||
- fixed `clone()` to return the correct instance (#1154) | - fixed `clone()` to return the correct instance (#1154) | ||||
div.id = 'fixtures' | div.id = 'fixtures' | ||||
try { | try { | ||||
// FIXME: doesnt work in svgdom | |||||
// FIXME: doesn't work in svgdom | |||||
div.style.position = 'absolute' | div.style.position = 'absolute' | ||||
div.style.top = 0 | div.style.top = 0 | ||||
div.style.left = 0 | div.style.left = 0 | ||||
div.id = 'canvas' | div.id = 'canvas' | ||||
try { | try { | ||||
// FIXME: doesnt work in svgdom | |||||
// FIXME: doesn't work in svgdom | |||||
div.style.position = 'absolute' | div.style.position = 'absolute' | ||||
div.style.top = 0 | div.style.top = 0 | ||||
div.style.left = 0 | div.style.left = 0 |
expect(pid.step(0, 100, 1000, {})).toBe(500) | expect(pid.step(0, 100, 1000, {})).toBe(500) | ||||
}) | }) | ||||
it('doesnt uses antiwindup if disabled', () => { | |||||
it('does not use antiwindup if disabled', () => { | |||||
const pid = new PID(0, 5, 0, false) | const pid = new PID(0, 5, 0, false) | ||||
expect(pid.step(0, 100, 1000, {})).toBe(500000) | expect(pid.step(0, 100, 1000, {})).toBe(500000) | ||||
}) | }) |
expect(runFn.calls.count()).toBe(2) | expect(runFn.calls.count()).toBe(2) | ||||
}) | }) | ||||
it('calls initFn on every step if its declaritive', () => { | |||||
it('calls initFn on every step if its declarative', () => { | |||||
var runner = new Runner(new Controller()) | var runner = new Runner(new Controller()) | ||||
runner.queue(initFn, runFn, true) | runner.queue(initFn, runFn, true) | ||||
expect(after.element()).toBe(element) | expect(after.element()).toBe(element) | ||||
}) | }) | ||||
it('doesnt reuse element if not set', () => { | |||||
it('does not reuse element if not set', () => { | |||||
const timeline = new Timeline() | const timeline = new Timeline() | ||||
const runner = new Runner().timeline(timeline) | const runner = new Runner().timeline(timeline) | ||||
const after = runner.animate() | const after = runner.animate() | ||||
expect(runner.time()).toBe(0) | expect(runner.time()).toBe(0) | ||||
}) | }) | ||||
it('doesnt reset if already reseted', () => { | |||||
it('does not reset if already reset', () => { | |||||
var runner = Object.freeze(new Runner().reset()) | var runner = Object.freeze(new Runner().reset()) | ||||
expect(runner.reset()).toBe(runner) | expect(runner.reset()).toBe(runner) | ||||
}) | }) | ||||
expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) | expect(runner._history.x.morpher.to()).toEqual([ 20, '' ]) | ||||
}) | }) | ||||
it('throws away the morpher if it wasnt initialized yet and returns false', () => { | |||||
it('throws away the morpher if it was not initialized yet and returns false', () => { | |||||
const rect = new Rect().move(0, 0) | const rect = new Rect().move(0, 0) | ||||
const runner = rect.animate().move(10, 10) | const runner = rect.animate().move(10, 10) | ||||
// In that case tryRetarget is not successful | // In that case tryRetarget is not successful | ||||
expect(runner._tryRetarget('x', 20)).toBe(false) | expect(runner._tryRetarget('x', 20)).toBe(false) | ||||
}) | }) | ||||
it('does nothing if method wasnt found', () => { | |||||
it('does nothing if method was not found', () => { | |||||
const rect = new Rect().move(0, 0) | const rect = new Rect().move(0, 0) | ||||
const runner = rect.animate().move(10, 10) | const runner = rect.animate().move(10, 10) | ||||
jasmine.RequestAnimationFrame.tick(16) | jasmine.RequestAnimationFrame.tick(16) | ||||
expect(runner._initialise(false)).toBe(undefined) | expect(runner._initialise(false)).toBe(undefined) | ||||
}) | }) | ||||
it('does nothing if true is passed and runner is not declaritive', () => { | |||||
it('does nothing if true is passed and runner is not declarative', () => { | |||||
const runner = Object.freeze(new Runner()) | const runner = Object.freeze(new Runner()) | ||||
expect(runner._initialise(true)).toBe(undefined) | expect(runner._initialise(true)).toBe(undefined) | ||||
}) | }) | ||||
it('calls the initializer function on the queue when runner is declaritive', () => { | |||||
it('calls the initializer function on the queue when runner is declarative', () => { | |||||
const runner = new Runner(() => 0).queue(initFn, runFn) | const runner = new Runner(() => 0).queue(initFn, runFn) | ||||
runner._initialise() | runner._initialise() | ||||
expect(initFn).toHaveBeenCalledTimes(1) | expect(initFn).toHaveBeenCalledTimes(1) | ||||
}) | }) | ||||
it('calls the initializer function on the queue when true is passed and runner is not declaritive', () => { | |||||
it('calls the initializer function on the queue when true is passed and runner is not declarative', () => { | |||||
const runner = new Runner().queue(initFn, runFn) | const runner = new Runner().queue(initFn, runFn) | ||||
runner._initialise(true) | runner._initialise(true) | ||||
expect(initFn).toHaveBeenCalledTimes(1) | expect(initFn).toHaveBeenCalledTimes(1) | ||||
}) | }) | ||||
describe('transform()', () => { | describe('transform()', () => { | ||||
it('does not retarget for non-declaritive transformations', () => { | |||||
it('does not retarget for non-declarative transformations', () => { | |||||
const runner = new Runner() | const runner = new Runner() | ||||
const spy = spyOn(runner, '_tryRetarget') | const spy = spyOn(runner, '_tryRetarget') | ||||
runner.transform({ translate: [ 10, 20 ] }) | runner.transform({ translate: [ 10, 20 ] }) | ||||
expect(spy).not.toHaveBeenCalled() | expect(spy).not.toHaveBeenCalled() | ||||
}) | }) | ||||
it('does retarget for absolute declaritive transformations', () => { | |||||
it('does retarget for absolute declarative transformations', () => { | |||||
const runner = new Runner(new Controller(() => 0)) | const runner = new Runner(new Controller(() => 0)) | ||||
const spy = spyOn(runner, '_tryRetarget') | const spy = spyOn(runner, '_tryRetarget') | ||||
runner.transform({ translate: [ 10, 20 ] }) | runner.transform({ translate: [ 10, 20 ] }) | ||||
// transform sets its to-target to the morpher in the initialisation step | // transform sets its to-target to the morpher in the initialisation step | ||||
// because it depends on the from-target. Declaritive animation run the init-step | // because it depends on the from-target. Declaritive animation run the init-step | ||||
// on every frame. Thats why we step here to see the effect of our retargeting | |||||
// on every frame. That is why we step here to see the effect of our retargeting | |||||
runner.step(25) | runner.step(25) | ||||
expect(runner._history.transform.morpher.to()).toEqual( | expect(runner._history.transform.morpher.to()).toEqual( | ||||
// transform sets its to-target to the morpher in the initialisation step | // transform sets its to-target to the morpher in the initialisation step | ||||
// because it depends on the from-target. Declaritive animation run the init-step | // because it depends on the from-target. Declaritive animation run the init-step | ||||
// on every frame. Thats why we step here to see the effect of our retargeting | |||||
// on every frame. That is why we step here to see the effect of our retargeting | |||||
runner.step(25) | runner.step(25) | ||||
expect(runner._history.transform.morpher.to()).toEqual( | expect(runner._history.transform.morpher.to()).toEqual( | ||||
) | ) | ||||
}) | }) | ||||
it('correctly animates a declaritive relative rotation', () => { | |||||
it('correctly animates a declarative relative rotation', () => { | |||||
const element = new Rect() | const element = new Rect() | ||||
const runner = new Runner(() => 1).element(element) | const runner = new Runner(() => 1).element(element) | ||||
runner.transform({ rotate: 90 }, true) | runner.transform({ rotate: 90 }, true) |
expect(timeline._runners[0].start).toBe(200) | expect(timeline._runners[0].start).toBe(200) | ||||
}) | }) | ||||
it('schedules the runner as absolute if this runner wasnt on the timeline', () => { | |||||
it('schedules the runner as absolute if this runner was not on the timeline', () => { | |||||
const timeline = new Timeline() | const timeline = new Timeline() | ||||
const runner = new Runner(1000) | const runner = new Runner(1000) | ||||
timeline.schedule(runner, 100, 'relative') | timeline.schedule(runner, 100, 'relative') | ||||
expect(spy).toHaveBeenCalled() | expect(spy).toHaveBeenCalled() | ||||
}) | }) | ||||
it('doesnt run runners if they are not active', () => { | |||||
it('does not run runners if they are not active', () => { | |||||
const timeline = new Timeline() | const timeline = new Timeline() | ||||
const runner = new Runner(1000).active(false) | const runner = new Runner(1000).active(false) | ||||
timeline.schedule(runner).play() // we have to play because its synchronous here | timeline.schedule(runner).play() // we have to play because its synchronous here |
expect(eventTarget.events).toEqual({}) | expect(eventTarget.events).toEqual({}) | ||||
}) | }) | ||||
it('doesnt do anything if no event object is found on the instance', () => { | |||||
it('does not do anything if no event object is found on the instance', () => { | |||||
const eventTarget = new EventTarget() | const eventTarget = new EventTarget() | ||||
delete eventTarget.events | delete eventTarget.events | ||||
clearEvents(eventTarget) | clearEvents(eventTarget) |
expect(match[3]).toBe('56') | expect(match[3]).toBe('56') | ||||
}) | }) | ||||
/* it('doesnt matches without #', () => { | |||||
/* it('does not matches without #', () => { | |||||
const match = '123456'.match(regex.hex) | const match = '123456'.match(regex.hex) | ||||
expect(match).toBe(null) | expect(match).toBe(null) | ||||
}) */ | }) */ | ||||
it('doesnt matches other then 0-f #', () => { | |||||
it('does not matches other then 0-f #', () => { | |||||
const match = '#09afhz'.match(regex.hex) | const match = '#09afhz'.match(regex.hex) | ||||
expect(match).toBe(null) | expect(match).toBe(null) | ||||
}) | }) | ||||
it('doesnt matches non full hex', () => { | |||||
it('does not matches non full hex', () => { | |||||
const match = '#aaa'.match(regex.hex) | const match = '#aaa'.match(regex.hex) | ||||
expect(match).toBe(null) | expect(match).toBe(null) | ||||
}) | }) | ||||
expect(match[3]).toBe('56') | expect(match[3]).toBe('56') | ||||
}) | }) | ||||
it('doesnt match in the wrong format', () => { | |||||
it('does not match in the wrong format', () => { | |||||
expect('rgb( 12 , 34 , 56)'.match(regex.rgb)).toBe(null) | expect('rgb( 12 , 34 , 56)'.match(regex.rgb)).toBe(null) | ||||
expect('12,34,56'.match(regex.rgb)).toBe(null) | expect('12,34,56'.match(regex.rgb)).toBe(null) | ||||
expect('(12,34,56)'.match(regex.rgb)).toBe(null) | expect('(12,34,56)'.match(regex.rgb)).toBe(null) |
it('restores the content from the dom with Tspan', () => { | it('restores the content from the dom with Tspan', () => { | ||||
// We create a new Tspan here because the one used before was part of text creation | // We create a new Tspan here because the one used before was part of text creation | ||||
// and therefore is marked as newline and thats not what we want to test | |||||
// and therefore is marked as newline and that is not what we want to test | |||||
const tspan = new Tspan().plain('Just plain text!') | const tspan = new Tspan().plain('Just plain text!') | ||||
expect(tspan.text()).toBe('Just plain text!') | expect(tspan.text()).toBe('Just plain text!') | ||||
}) | }) |
expect(rect.data('fill', 'string').attr('data-fill')).toBe('string') | expect(rect.data('fill', 'string').attr('data-fill')).toBe('string') | ||||
}) | }) | ||||
it('doesnt convert to json with third parameter true', () => { | |||||
it('does not convert to json with third parameter true', () => { | |||||
const rect = new Rect() | const rect = new Rect() | ||||
expect(rect.data('fill', { some: 'object' }, true).attr('data-fill')).toBe({}.toString()) | expect(rect.data('fill', { some: 'object' }, true).attr('data-fill')).toBe({}.toString()) | ||||
}) | }) |
expect(arr.toString()).toBe('M0 0L100 100Z ') | expect(arr.toString()).toBe('M0 0L100 100Z ') | ||||
}) | }) | ||||
// this test is designed to cover a certain line but it doesnt work because of #608 | |||||
// this test is designed to cover a certain line but it doesn't work because of #608 | |||||
it('returns the valueOf when PathArray is given', () => { | it('returns the valueOf when PathArray is given', () => { | ||||
const p = new PathArray('m10 10 h 80 v 80 h -80 l 300 400 z') | const p = new PathArray('m10 10 h 80 v 80 h -80 l 300 400 z') | ||||
this._time = 0 | this._time = 0 | ||||
this._lastTime = 0 | this._lastTime = 0 | ||||
// At creation, the runner is in reseted state | |||||
// At creation, the runner is in reset state | |||||
this._reseted = true | this._reseted = true | ||||
// Save transforms applied to this runner | // Save transforms applied to this runner | ||||
this._frameId = null | this._frameId = null | ||||
// Stores how long a runner is stored after beeing done | |||||
// Stores how long a runner is stored after being done | |||||
this._persist = this._isDeclarative ? true : null | this._persist = this._isDeclarative ? true : null | ||||
} | } | ||||
const declarative = this._isDeclarative | const declarative = this._isDeclarative | ||||
this.done = !declarative && !justFinished && this._time >= duration | this.done = !declarative && !justFinished && this._time >= duration | ||||
// Runner is running. So its not in reseted state anymore | |||||
// Runner is running. So its not in reset state anymore | |||||
this._reseted = false | this._reseted = false | ||||
let converged = false | let converged = false | ||||
// do nothing and return false | // do nothing and return false | ||||
_tryRetarget (method, target, extra) { | _tryRetarget (method, target, extra) { | ||||
if (this._history[method]) { | if (this._history[method]) { | ||||
// if the last method wasnt even initialised, throw it away | |||||
// if the last method wasn't even initialised, throw it away | |||||
if (!this._history[method].caller.initialised) { | if (!this._history[method].caller.initialised) { | ||||
const index = this._queue.indexOf(this._history[method].caller) | const index = this._queue.indexOf(this._history[method].caller) | ||||
this._queue.splice(index, 1) | this._queue.splice(index, 1) | ||||
this._transformationRunners.add(runner) | this._transformationRunners.add(runner) | ||||
// Make sure that the runner merge is executed at the very end of | // Make sure that the runner merge is executed at the very end of | ||||
// all Animator functions. Thats why we use immediate here to execute | |||||
// all Animator functions. That is why we use immediate here to execute | |||||
// the merge right after all frames are run | // the merge right after all frames are run | ||||
Animator.cancelImmediate(this._frameId) | Animator.cancelImmediate(this._frameId) | ||||
this._frameId = Animator.immediate(mergeTransforms.bind(this)) | this._frameId = Animator.immediate(mergeTransforms.bind(this)) | ||||
_queueNumberDelta (method, to) { | _queueNumberDelta (method, to) { | ||||
to = new SVGNumber(to) | to = new SVGNumber(to) | ||||
// Try to change the target if we have this method already registerd | |||||
// Try to change the target if we have this method already registered | |||||
if (this._tryRetarget(method, to)) return this | if (this._tryRetarget(method, to)) return this | ||||
// Make a morpher and queue the animation | // Make a morpher and queue the animation | ||||
}, | }, | ||||
_queueObject (method, to) { | _queueObject (method, to) { | ||||
// Try to change the target if we have this method already registerd | |||||
// Try to change the target if we have this method already registered | |||||
if (this._tryRetarget(method, to)) return this | if (this._tryRetarget(method, to)) return this | ||||
// Make a morpher and queue the animation | // Make a morpher and queue the animation |
this.fire('time', this._time) | this.fire('time', this._time) | ||||
// This is for the case that the timeline was seeked so that the time | // This is for the case that the timeline was seeked so that the time | ||||
// is now before the startTime of the runner. Thats why we need to set | |||||
// is now before the startTime of the runner. That is why we need to set | |||||
// the runner to position 0 | // the runner to position 0 | ||||
// FIXME: | // FIXME: |
if (!type) return parent | if (!type) return parent | ||||
// loop trough ancestors if type is given | |||||
// loop through ancestors if type is given | |||||
do { | do { | ||||
if (typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent | if (typeof type === 'string' ? parent.matches(type) : parent instanceof type) return parent | ||||
} while ((parent = adopt(parent.node.parentNode))) | } while ((parent = adopt(parent.node.parentNode))) |
// that means, their clientRect is always as big as the content. | // that means, their clientRect is always as big as the content. | ||||
// Furthermore this size is incorrect if the element is further transformed by its parents | // Furthermore this size is incorrect if the element is further transformed by its parents | ||||
// computedStyle: Only returns meaningful values if css was used with px. We dont go this route here! | // computedStyle: Only returns meaningful values if css was used with px. We dont go this route here! | ||||
// getBBox: returns the bounding box of its content - that doesnt help! | |||||
// getBBox: returns the bounding box of its content - that doesn't help! | |||||
let { width, height } = this.attr([ 'width', 'height' ]) | let { width, height } = this.attr([ 'width', 'height' ]) | ||||
// Width and height is a string when a number with a unit is present which we can't use | // Width and height is a string when a number with a unit is present which we can't use |
const origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY) | const origin = new Point(o.origin || o.around || o.ox || o.originX, o.oy || o.originY) | ||||
const ox = origin.x | const ox = origin.x | ||||
const oy = origin.y | const oy = origin.y | ||||
// We need Point to be invalid if nothing was passed because we cannot default to 0 here. Thats why NaN | |||||
// We need Point to be invalid if nothing was passed because we cannot default to 0 here. That is why NaN | |||||
const position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN) | const position = new Point(o.position || o.px || o.positionX || NaN, o.py || o.positionY || NaN) | ||||
const px = position.x | const px = position.x | ||||
const py = position.y | const py = position.y | ||||
if (isFinite(t.px) || isFinite(t.py)) { | if (isFinite(t.px) || isFinite(t.py)) { | ||||
const origin = new Point(ox, oy).transform(transformer) | const origin = new Point(ox, oy).transform(transformer) | ||||
// TODO: Replace t.px with isFinite(t.px) | // TODO: Replace t.px with isFinite(t.px) | ||||
// Doesnt work because t.px is also 0 if it wasnt passed | |||||
// Doesn't work because t.px is also 0 if it wasn't passed | |||||
const dx = isFinite(t.px) ? t.px - origin.x : 0 | const dx = isFinite(t.px) ? t.px - origin.x : 0 | ||||
const dy = isFinite(t.py) ? t.py - origin.y : 0 | const dy = isFinite(t.py) ? t.py - origin.y : 0 | ||||
transformer.translateO(dx, dy) | transformer.translateO(dx, dy) |
// that the first char is < and thus an element | // that the first char is < and thus an element | ||||
element = adopter(wrapper.firstChild) | element = adopter(wrapper.firstChild) | ||||
// make sure, that element doesnt have its wrapper attached | |||||
// make sure, that element doesn't have its wrapper attached | |||||
wrapper.removeChild(wrapper.firstChild) | wrapper.removeChild(wrapper.firstChild) | ||||
return element | return element | ||||
} | } |