Skip to content

Commit

Permalink
feat(timeline): add more flexibility to queue
Browse files Browse the repository at this point in the history
BREAKING CHANGE: timeline now takes queue options as an object, not an array.

The queue object can take three props:

- `after` - The shape name to queue after (in sequence)
- `at` - The shape name to queue at (in parallel)
- `offset` - The millisecond queue offset

The queue prop can still take a number or a string, which will be
treated as offset or after values respectively.
  • Loading branch information
colinmeinke committed Jul 22, 2017
1 parent 7d3c28b commit 29c4032
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 31 deletions.
71 changes: 47 additions & 24 deletions src/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ import { input } from './middleware'
*
* @typedef {Object} ShapeWithOptions
*
* @property {Shape} shape
* @property {(string|number)} [after] - The name of the Shape to queue after (in sequence).
* @property {(string|number)} [at] - The name of the Shape to queue at (in parallel).
* @property {(string|number)} name - A unique reference.
* @property {(string|number)} follow - The name of the Shape to queue after.
* @property {number} offset - Millisecond offset from end of the follow Shape to start of this Shape.
* @property {number} offset - The offset in milliseconds to adjust the queuing of this shape.
* @property {Shape} shape
*/

/**
Expand Down Expand Up @@ -257,7 +258,8 @@ const sameDirection = (alternate, iterations) => {
* Calculate the start position of a Shape on the Timeline.
*
* @param {Object} props
* @param {(string|number)} [props.follow]
* @param {(string|number)} [props.after]
* @param {(string|number)} [props.at]
* @param {MsTimelineShape[]} props.msTimelineShapes
* @param {number} props.offset
* @param {number} props.timelineEnd - The current end of the timeline.
Expand All @@ -267,13 +269,17 @@ const sameDirection = (alternate, iterations) => {
* @example
* shapeStart({ 'foo', msTimelineShapes, 200, 2000 })
*/
const shapeStart = ({ follow, msTimelineShapes, offset, timelineEnd }) => {
if (typeof follow !== 'undefined') {
const shapeStart = ({ after, at, msTimelineShapes, offset, timelineEnd }) => {
if (typeof after !== 'undefined' || typeof at !== 'undefined') {
const reference = typeof after !== 'undefined' ? after : at

for (let i = 0; i < msTimelineShapes.length; i++) {
const s = msTimelineShapes[ i ]

if (follow === s.shape.name) {
return s.timelinePosition.end + offset
if (reference === s.shape.name) {
return (typeof at !== 'undefined'
? s.timelinePosition.start
: s.timelinePosition.end) + offset
}
}

Expand All @@ -283,17 +289,15 @@ const shapeStart = ({ follow, msTimelineShapes, offset, timelineEnd }) => {
for (let j = 0; j < s.shape.keyframes.length; j++) {
const keyframe = s.shape.keyframes[ j ]

if (follow === keyframe.name) {
if (follow === keyframe.name) {
return s.timelinePosition.start +
s.shape.duration * keyframe.position + offset
}
if (reference === keyframe.name) {
return s.timelinePosition.start +
s.shape.duration * keyframe.position + offset
}
}
}

if (__DEV__) {
throw new Error(`No Shape or Keyframe matching name '${follow}'`)
throw new Error(`No Shape or Keyframe matching name '${reference}'`)
}
}

Expand Down Expand Up @@ -328,24 +332,42 @@ const shapeWithOptionsFromArray = ([ shape, options ], i) => {
}

if (typeof queue !== 'undefined') {
if (Array.isArray(queue)) {
if (__DEV__ && (typeof queue[ 0 ] !== 'string' && typeof queue[ 0 ] !== 'number')) {
throw new TypeError(`The queue prop first array item must be of type string or number`)
if (typeof queue === 'object' && (!Array.isArray(queue) && queue !== null)) {
const { after, at, offset = 0 } = queue

if (__DEV__ && (typeof offset !== 'undefined' && typeof offset !== 'number')) {
throw new TypeError(`The queue.offset prop must be of type number`)
}

if (__DEV__ && (typeof at !== 'undefined' && typeof after !== 'undefined')) {
throw new TypeError(`You cannot pass both queue.at and queue.after props`)
}

if (__DEV__ && (typeof at !== 'undefined' && typeof at !== 'string' && typeof at !== 'number')) {
throw new TypeError(`The queue.at prop must be of type string or number`)
}

if (__DEV__ && (typeof after !== 'undefined' && typeof after !== 'string' && typeof after !== 'number')) {
throw new TypeError(`The queue.after prop must be of type string or number`)
}

if (typeof at !== 'undefined') {
return { at, name, offset, shape }
}

if (__DEV__ && (typeof queue[ 1 ] !== 'number')) {
throw new TypeError(`The queue prop second array item must be of type number`)
if (typeof after !== 'undefined') {
return { after, name, offset, shape }
}

return { follow: queue[ 0 ], name, offset: queue[ 1 ], shape }
return { name, offset, shape }
} else if (typeof queue === 'number') {
return { name, offset: queue, shape }
} else if (typeof queue === 'string') {
return { follow: queue, name, offset: 0, shape }
return { after: queue, name, offset: 0, shape }
}

if (__DEV__) {
throw new TypeError(`The queue prop must be of type number, string or array`)
throw new TypeError(`The queue prop must be of type number, string or object`)
}

return
Expand Down Expand Up @@ -487,7 +509,7 @@ const timelineShapesAndDuration = (shapesWithOptions, middleware) => {

const msTimelineShapes = []

shapesWithOptions.map(({ follow, name, offset, shape }, i) => {
shapesWithOptions.map(({ after, at, name, offset, shape }, i) => {
if (__DEV__ && typeof shape.timeline !== 'undefined') {
throw new Error(`A Shape can only be added to one timeline`)
}
Expand All @@ -501,7 +523,8 @@ const timelineShapesAndDuration = (shapesWithOptions, middleware) => {
}

const start = shapeStart({
follow,
after,
at,
msTimelineShapes,
offset,
timelineEnd
Expand Down
129 changes: 122 additions & 7 deletions tests/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,28 @@ describe('timeline', () => {
.toThrow('The name prop must be of type string or number')
})

it('should throw if passed an invalid shape queue option', () => {
it('should throw if passed an array shape queue option', () => {
const validShape = shape({ type: 'rect', width: 50, height: 50, x: 100, y: 100 })
expect(() => timeline([ validShape, { queue: [ 'valid', 'invalid' ] } ]))
.toThrow('The queue prop second array item must be of type number')
expect(() => timeline([ validShape, { queue: [] } ]))
.toThrow('The queue prop must be of type number, string or object')
})

it('should throw if passed an invalid shape queue.at option', () => {
const validShape = shape({ type: 'rect', width: 50, height: 50, x: 100, y: 100 })
expect(() => timeline([ validShape, { queue: { at: [] } } ]))
.toThrow('The queue.at prop must be of type string or number')
})

it('should throw if passed an invalid shape queue.after option', () => {
const validShape = shape({ type: 'rect', width: 50, height: 50, x: 100, y: 100 })
expect(() => timeline([ validShape, { queue: { after: [] } } ]))
.toThrow('The queue.after prop must be of type string or number')
})

it('should throw if passed an invalid shape queue.offset option', () => {
const validShape = shape({ type: 'rect', width: 50, height: 50, x: 100, y: 100 })
expect(() => timeline([ validShape, { queue: { offset: 'invalid' } } ]))
.toThrow('The queue.offset prop must be of type number')
})

it('should not throw if passed a valid Shape and options', () => {
Expand Down Expand Up @@ -195,7 +213,26 @@ describe('timeline', () => {
expect(() => {
timeline(
shape1,
[ shape2, { queue: [ 'foo', -200 ] } ]
[ shape2, { queue: { after: 'foo' } } ]
)
}).toThrow(`No Shape or Keyframe matching name 'foo'`)
})

it('should throw when Shape queued at unknown Shape or Keyframe', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 500 }
)

const shape2 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

expect(() => {
timeline(
shape1,
[ shape2, { queue: { at: 'foo' } } ]
)
}).toThrow(`No Shape or Keyframe matching name 'foo'`)
})
Expand All @@ -219,13 +256,39 @@ describe('timeline', () => {
const { timelineShapes } = timeline(
[ shape1, { name: 'foo' } ],
shape2,
[ shape3, { queue: [ 'foo', -350 ] } ]
[ shape3, { queue: { after: 'foo', offset: -350 } } ]
)

expect(timelineShapes[ 2 ].timelinePosition.start).toBe(150 / 850)
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(500 / 850)
})

it('should correctly queue Shape at named (string) Shape', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 500 }
)

const shape2 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const shape3 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const { timelineShapes } = timeline(
[ shape1, { name: 'foo' } ],
shape2,
[ shape3, { queue: { at: 'foo', offset: -350 } } ]
)

expect(timelineShapes[ 2 ].timelinePosition.start).toBe(0 / 1200)
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(350 / 1200)
})

it('should correctly queue Shape after named (index) Shape', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
Expand All @@ -245,13 +308,39 @@ describe('timeline', () => {
const { timelineShapes } = timeline(
shape1,
shape2,
[ shape3, { queue: [ 0, -350 ] } ]
[ shape3, { queue: { after: 0, offset: -350 } } ]
)

expect(timelineShapes[ 2 ].timelinePosition.start).toBe(150 / 850)
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(500 / 850)
})

it('should correctly queue Shape at named (index) Shape', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 500 }
)

const shape2 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const shape3 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const { timelineShapes } = timeline(
shape1,
shape2,
[ shape3, { queue: { at: 0, offset: -350 } } ]
)

expect(timelineShapes[ 2 ].timelinePosition.start).toBe(0 / 1200)
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(350 / 1200)
})

it('should correctly queue Shape after named Keyframe', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
Expand Down Expand Up @@ -293,7 +382,7 @@ describe('timeline', () => {
const { timelineShapes } = timeline(
[ shape1, { name: 'foo' } ],
shape2,
[ shape3, { queue: [ 'foo', -350 ] } ]
[ shape3, { queue: { after: 'foo', offset: -350 } } ]
)

expect(timelineShapes[ 0 ].timelinePosition.start).toBe(0 / 1100)
Expand All @@ -304,6 +393,32 @@ describe('timeline', () => {
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(750 / 1100)
})

it('should correctly queue Shape if passed queue object with only offset prop', () => {
const shape1 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 500 }
)

const shape2 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const shape3 = shape(
{ type: 'rect', width: 50, height: 50, x: 100, y: 100 },
{ type: 'rect', width: 50, height: 50, x: 100, y: 100, duration: 350 }
)

const { timelineShapes } = timeline(
shape1,
shape2,
[ shape3, { queue: { offset: -350 } } ]
)

expect(timelineShapes[ 2 ].timelinePosition.start).toBe(500 / 850)
expect(timelineShapes[ 2 ].timelinePosition.end).toBe(850 / 850)
})

it('should throw if a Shape is already associated with a timeline', () => {
const validShape = shape({ type: 'rect', width: 50, height: 50, x: 100, y: 100 })

Expand Down

0 comments on commit 29c4032

Please sign in to comment.