diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index 0861b95..d7d98e6 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -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 + 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 + /** + * 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 } } diff --git a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts index cafea71..7fec78e 100644 --- a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts @@ -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) { - this._updatePositionInState(range.start) - } else if ( - prevTime === range.end && - (direction === 'normal' || direction === 'alternate') - ) { - this._updatePositionInState(range.start) - } else if ( - prevTime === range.start && - (direction === 'reverse' || direction === 'alternateReverse') - ) { - this._updatePositionInState(range.end) + { + const startPos = this.getCurrentPosition() + + if (startPos < range.start || startPos > range.end) { + this._updatePositionInState(range.start) + } else if ( + startPos === range.end && + (direction === 'normal' || direction === 'alternate') + ) { + this._updatePositionInState(range.start) + } else if ( + 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() + const initialTickerTime = ticker.time + const totalPlaybackLength = iterationLength * iterationCount + let initialElapsedPos = this.getCurrentPosition() - range.start + if (direction === 'reverse' || direction === 'alternateReverse') { + initialElapsedPos = range.end - initialElapsedPos + } - 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 newTime = lastTime + timeDiff + const tick = (currentTickerTime: number) => { + const elapsedTickerTime = Math.max( + currentTickerTime - initialTickerTime, + 0, + ) + const elapsedTickerTimeInSeconds = elapsedTickerTime / 1000 - 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 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 + } + } else { + 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 } + + this._updatePositionInState(currentIterationPos) 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 (direction === 'normal') { - this._updatePositionInState(range.start + diff) - } else { - goingForward = -1 - this._updatePositionInState(range.end - diff) - } - requestNextTick() - return - } } else { - this._updatePositionInState(newTime) - requestNextTick() - return + if (direction === 'normal') { + this._updatePositionInState(range.end) + } else if (direction === 'reverse') { + this._updatePositionInState(range.start) + } else { + const isLastIterationEven = (iterationCount - 1) % 2 === 0 + if (direction === 'alternate') { + if (isLastIterationEven) { + this._updatePositionInState(range.end) + } else { + this._updatePositionInState(range.start) + } + } else { + if (isLastIterationEven) { + this._updatePositionInState(range.start) + } else { + this._updatePositionInState(range.end) + } + } + } + this.playing = false + deferred.resolve(true) } } diff --git a/theatre/core/src/sheetObjects/SheetObject.test.ts b/theatre/core/src/sheetObjects/SheetObject.test.ts index b54bb6d..162a4d2 100644 --- a/theatre/core/src/sheetObjects/SheetObject.test.ts +++ b/theatre/core/src/sheetObjects/SheetObject.test.ts @@ -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}, }) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx index 61b65e3..b3a2e53 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/DopeSheet/Right/HorizontallyScrollableArea.tsx @@ -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