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,
* or resolves to false if playback gets interrupted (for example by calling sequence.pause())
*/
play(
conf?: Partial<{
iterationCount: number
range: IPlaybackRange
rate: number
direction: IPlaybackDirection
}>,
): Promise<boolean>
play(conf?: {
/**
* The number of times the animation must run. Must be an integer larger
* than 0. Defaults to 1. Pick Infinity to run forever
*/
iterationCount?: number
/**
* 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
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 {
@ -72,11 +91,11 @@ export default class TheatreSequence implements ISequence {
privateAPI(this).pause()
}
get time() {
get position() {
return privateAPI(this).position
}
set time(t: number) {
privateAPI(this).position = t
set position(position: number) {
privateAPI(this).position = position
}
}

View file

@ -70,98 +70,93 @@ export default class DefaultPlaybackController implements IPlaybackController {
this.playing = true
const ticker = this._ticker
let lastTickerTime = ticker.time
const dur = range.end - range.start
const prevTime = this.getCurrentPosition()
const iterationLength = range.end - range.start
if (prevTime < range.start || prevTime > range.end) {
{
const startPos = this.getCurrentPosition()
if (startPos < range.start || startPos > range.end) {
this._updatePositionInState(range.start)
} else if (
prevTime === range.end &&
startPos === range.end &&
(direction === 'normal' || direction === 'alternate')
) {
this._updatePositionInState(range.start)
} else if (
prevTime === range.start &&
startPos === range.start &&
(direction === 'reverse' || direction === 'alternateReverse')
) {
this._updatePositionInState(range.end)
}
let goingForward =
direction === 'alternateReverse' || direction === 'reverse' ? -1 : 1
let countSoFar = 1
}
const deferred = defer<boolean>()
const tick = (tickerTimeInMs: number) => {
const tickerTime = tickerTimeInMs / 1000
const lastTime = this.getCurrentPosition()
const timeDiff = (tickerTime - lastTickerTime) * (rate * goingForward)
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 initialTickerTime = ticker.time
const totalPlaybackLength = iterationLength * iterationCount
let initialElapsedPos = this.getCurrentPosition() - range.start
if (direction === 'reverse' || direction === 'alternateReverse') {
initialElapsedPos = range.end - initialElapsedPos
}
const newTime = lastTime + timeDiff
if (newTime < range.start) {
if (countSoFar === iterationCount) {
this._updatePositionInState(range.start)
this.playing = false
deferred.resolve(true)
return
} else {
countSoFar++
const diff = (range.start - newTime) % dur
const tick = (currentTickerTime: number) => {
const elapsedTickerTime = Math.max(
currentTickerTime - initialTickerTime,
0,
)
const elapsedTickerTimeInSeconds = elapsedTickerTime / 1000
const elapsedPos = Math.min(
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') {
this._updatePositionInState(range.end - diff)
currentIterationPos = iterationLength - currentIterationPos
} else {
goingForward = 1
this._updatePositionInState(range.start + diff)
const isCurrentIterationNumberEven = iterationNumber % 2 === 0
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 {
countSoFar++
const diff = (newTime - range.end) % dur
if (isCurrentIterationNumberEven) {
currentIterationPos = iterationLength - currentIterationPos
}
}
}
}
this._updatePositionInState(currentIterationPos)
requestNextTick()
} else {
if (direction === 'normal') {
this._updatePositionInState(range.start + diff)
this._updatePositionInState(range.end)
} else if (direction === 'reverse') {
this._updatePositionInState(range.start)
} else {
goingForward = -1
this._updatePositionInState(range.end - diff)
}
requestNextTick()
return
const isLastIterationEven = (iterationCount - 1) % 2 === 0
if (direction === 'alternate') {
if (isLastIterationEven) {
this._updatePositionInState(range.end)
} else {
this._updatePositionInState(range.start)
}
} else {
this._updatePositionInState(newTime)
requestNextTick()
return
if (isLastIterationEven) {
this._updatePositionInState(range.start)
} 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({
position: {x: 0, y: 3, z: 2},
})
seq.time = 5
expect(seq.time).toEqual(5)
seq.position = 5
expect(seq.position).toEqual(5)
expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 3, z: 2},
})
seq.time = 11
seq.position = 11
expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 3.29999747758308, z: 2},
})
seq.time = 15
seq.position = 15
expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 4.5, z: 2},
})
seq.time = 22
seq.position = 22
expect(objValues.next().value).toMatchObject({
position: {x: 0, y: 6, z: 2},
})

View file

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