Sequence bugfixes and documentation

* The alternate playback directions now work
* Clicking outside the playable area no longer raises errors
* Documented the playback API
This commit is contained in:
Aria Minaei 2021-09-03 13:15:04 +02:00
parent 31d6acefca
commit 3cd126186e
4 changed files with 118 additions and 101 deletions

View file

@ -12,18 +12,37 @@ export interface ISequence {
* Returns a promise that either resolves to true when the playback completes, * Returns a promise that either resolves to true when the playback completes,
* or resolves to false if playback gets interrupted (for example by calling sequence.pause()) * or resolves to false if playback gets interrupted (for example by calling sequence.pause())
*/ */
play( play(conf?: {
conf?: Partial<{ /**
iterationCount: number * The number of times the animation must run. Must be an integer larger
range: IPlaybackRange * than 0. Defaults to 1. Pick Infinity to run forever
rate: number */
direction: IPlaybackDirection iterationCount?: number
}>, /**
): Promise<boolean> * Limits the range to be played. Default is [0, sequence.length]
*/
range?: IPlaybackRange
/**
* The playback rate. Defaults to 1. Choosing 2 would play the animation
* at twice the speed.
*/
rate?: number
/**
* The direction of the playback. Similar to CSS's animation-direction
*/
direction?: IPlaybackDirection
}): Promise<boolean>
/**
* Pauses the currently playing animation
*/
pause(): void pause(): void
time: number /**
* The current position of the playhead.
* In a time-based sequence, this represents the current time.
*/
position: number
} }
export default class TheatreSequence implements ISequence { export default class TheatreSequence implements ISequence {
@ -72,11 +91,11 @@ export default class TheatreSequence implements ISequence {
privateAPI(this).pause() privateAPI(this).pause()
} }
get time() { get position() {
return privateAPI(this).position return privateAPI(this).position
} }
set time(t: number) { set position(position: number) {
privateAPI(this).position = t privateAPI(this).position = position
} }
} }

View file

@ -70,98 +70,93 @@ export default class DefaultPlaybackController implements IPlaybackController {
this.playing = true this.playing = true
const ticker = this._ticker const ticker = this._ticker
let lastTickerTime = ticker.time const iterationLength = range.end - range.start
const dur = range.end - range.start
const prevTime = this.getCurrentPosition()
if (prevTime < range.start || prevTime > range.end) { {
const startPos = this.getCurrentPosition()
if (startPos < range.start || startPos > range.end) {
this._updatePositionInState(range.start) this._updatePositionInState(range.start)
} else if ( } else if (
prevTime === range.end && startPos === range.end &&
(direction === 'normal' || direction === 'alternate') (direction === 'normal' || direction === 'alternate')
) { ) {
this._updatePositionInState(range.start) this._updatePositionInState(range.start)
} else if ( } else if (
prevTime === range.start && startPos === range.start &&
(direction === 'reverse' || direction === 'alternateReverse') (direction === 'reverse' || direction === 'alternateReverse')
) { ) {
this._updatePositionInState(range.end) this._updatePositionInState(range.end)
} }
}
let goingForward =
direction === 'alternateReverse' || direction === 'reverse' ? -1 : 1
let countSoFar = 1
const deferred = defer<boolean>() const deferred = defer<boolean>()
const initialTickerTime = ticker.time
const tick = (tickerTimeInMs: number) => { const totalPlaybackLength = iterationLength * iterationCount
const tickerTime = tickerTimeInMs / 1000 let initialElapsedPos = this.getCurrentPosition() - range.start
const lastTime = this.getCurrentPosition() if (direction === 'reverse' || direction === 'alternateReverse') {
const timeDiff = (tickerTime - lastTickerTime) * (rate * goingForward) initialElapsedPos = range.end - initialElapsedPos
lastTickerTime = tickerTime
/*
* I don't know why exactly this happens, but every 10 times or so, the first sequence.play({iterationCount: 1}),
* the first call of tick() will have a timeDiff < 0.
* This might be because of Spectre mitigation (they randomize performance.now() a bit), or it could be that
* I'm using performance.now() the wrong way.
* Anyway, this seems like a working fix for it:
*/
if (timeDiff < 0) {
requestNextTick()
return
} }
const newTime = lastTime + timeDiff
if (newTime < range.start) { const tick = (currentTickerTime: number) => {
if (countSoFar === iterationCount) { const elapsedTickerTime = Math.max(
this._updatePositionInState(range.start) currentTickerTime - initialTickerTime,
this.playing = false 0,
deferred.resolve(true) )
return const elapsedTickerTimeInSeconds = elapsedTickerTime / 1000
} else {
countSoFar++ const elapsedPos = Math.min(
const diff = (range.start - newTime) % dur elapsedTickerTimeInSeconds * rate + initialElapsedPos,
totalPlaybackLength,
)
if (elapsedPos !== totalPlaybackLength) {
const iterationNumber = Math.floor(elapsedPos / iterationLength)
let currentIterationPos =
((elapsedPos / iterationLength) % 1) * iterationLength
if (direction !== 'normal') {
if (direction === 'reverse') { if (direction === 'reverse') {
this._updatePositionInState(range.end - diff) currentIterationPos = iterationLength - currentIterationPos
} else { } else {
goingForward = 1 const isCurrentIterationNumberEven = iterationNumber % 2 === 0
this._updatePositionInState(range.start + diff) if (direction === 'alternate') {
if (!isCurrentIterationNumberEven) {
currentIterationPos = iterationLength - currentIterationPos
} }
requestNextTick()
return
}
} else if (newTime === range.end) {
this._updatePositionInState(range.end)
if (countSoFar === iterationCount) {
this.playing = false
deferred.resolve(true)
return
}
requestNextTick()
return
} else if (newTime > range.end) {
if (countSoFar === iterationCount) {
this._updatePositionInState(range.end)
this.playing = false
deferred.resolve(true)
return
} else { } else {
countSoFar++ if (isCurrentIterationNumberEven) {
const diff = (newTime - range.end) % dur currentIterationPos = iterationLength - currentIterationPos
}
}
}
}
this._updatePositionInState(currentIterationPos)
requestNextTick()
} else {
if (direction === 'normal') { if (direction === 'normal') {
this._updatePositionInState(range.start + diff) this._updatePositionInState(range.end)
} else if (direction === 'reverse') {
this._updatePositionInState(range.start)
} else { } else {
goingForward = -1 const isLastIterationEven = (iterationCount - 1) % 2 === 0
this._updatePositionInState(range.end - diff) if (direction === 'alternate') {
} if (isLastIterationEven) {
requestNextTick() this._updatePositionInState(range.end)
return } else {
this._updatePositionInState(range.start)
} }
} else { } else {
this._updatePositionInState(newTime) if (isLastIterationEven) {
requestNextTick() this._updatePositionInState(range.start)
return } else {
this._updatePositionInState(range.end)
}
}
}
this.playing = false
deferred.resolve(true)
} }
} }

View file

@ -99,29 +99,29 @@ describe(`SheetObject`, () => {
}), }),
) )
expect(seq.time).toEqual(0) expect(seq.position).toEqual(0)
expect(objValues.next().value).toMatchObject({ expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 3, z: 2}, position: {x: 0, y: 3, z: 2},
}) })
seq.time = 5 seq.position = 5
expect(seq.time).toEqual(5) expect(seq.position).toEqual(5)
expect(objValues.next().value).toMatchObject({ expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 3, z: 2}, position: {x: 0, y: 3, z: 2},
}) })
seq.time = 11 seq.position = 11
expect(objValues.next().value).toMatchObject({ expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 3.29999747758308, z: 2}, position: {x: 0, y: 3.29999747758308, z: 2},
}) })
seq.time = 15 seq.position = 15
expect(objValues.next().value).toMatchObject({ expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 4.5, z: 2}, position: {x: 0, y: 4.5, z: 2},
}) })
seq.time = 22 seq.position = 22
expect(objValues.next().value).toMatchObject({ expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 6, z: 2}, position: {x: 0, y: 6, z: 2},
}) })

View file

@ -128,9 +128,12 @@ function useDragHandlers(
const initialPositionInClippedSpace = const initialPositionInClippedSpace =
event.clientX - containerEl!.getBoundingClientRect().left event.clientX - containerEl!.getBoundingClientRect().left
const initialPositionInUnitSpace = val( const initialPositionInUnitSpace = clamp(
layoutP.clippedSpace.toUnitSpace, val(layoutP.clippedSpace.toUnitSpace)(initialPositionInClippedSpace),
)(initialPositionInClippedSpace) 0,
Infinity,
)
sequence = val(layoutP.sheet).getSequence() sequence = val(layoutP.sheet).getSequence()
sequence.position = initialPositionInUnitSpace sequence.position = initialPositionInUnitSpace