Implemented sequence.pointer
This enables observing and reacting to changes to the position of the sequence, as well as its length and playback state. API docs: https://docs.theatrejs.com/api/core.isequence.pointer.html Fixes #20 Also provides a stop-gap solution to #32 until we have the API in place.
This commit is contained in:
parent
a2f0c1d341
commit
7c0765bff2
5 changed files with 145 additions and 14 deletions
|
@ -125,9 +125,23 @@ const validateProjectIdOrThrow = (value: string) => {
|
|||
/**
|
||||
* Calls `callback` every time the pointed value of `pointer` changes.
|
||||
*
|
||||
* @param pointer - A pointer (like `object.props.x`)
|
||||
* @param callback - The callback is called every time the value of pointerOrDerivation changes
|
||||
* @param pointer - A Pointer (like `object.props.x`)
|
||||
* @param callback - The callback is called every time the value of pointer changes
|
||||
* @returns An unsubscribe function
|
||||
*
|
||||
* @example
|
||||
* Usage:
|
||||
* ```ts
|
||||
* import {getProject, onChange} from '@theatre/core'
|
||||
*
|
||||
* const obj = getProject("A project").sheet("Scene").object("Box", {position: {x: 0}})
|
||||
*
|
||||
* const usubscribe = onChange(obj.props.position.x, (x) => {
|
||||
* console.log('position.x changed to:', x)
|
||||
* })
|
||||
*
|
||||
* setTimeout(usubscribe, 10000) // stop listening to changes after 10 seconds
|
||||
* ```
|
||||
*/
|
||||
export function onChange<P extends PointerType<$IntentionalAny>>(
|
||||
pointer: P,
|
||||
|
@ -144,3 +158,28 @@ export function onChange<P extends PointerType<$IntentionalAny>>(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Pointer and returns the value it points to.
|
||||
*
|
||||
* @param pointer - A pointer (like `object.props.x`)
|
||||
* @returns The value the pointer points to
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* Usage
|
||||
* ```ts
|
||||
* import {val, getProject} from '@theatre/core'
|
||||
*
|
||||
* const obj = getProject("A project").sheet("Scene").object("Box", {position: {x: 0}})
|
||||
*
|
||||
* console.log(val(obj.props.position.x)) // logs the value of obj.props.x
|
||||
* ```
|
||||
*/
|
||||
export function val<T>(pointer: PointerType<T>): T {
|
||||
if (isPointer(pointer)) {
|
||||
return valueDerivation(pointer).getValue() as $IntentionalAny
|
||||
} else {
|
||||
throw new Error(`Called val(p) where p is not a pointer.`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {SequenceAddress} from '@theatre/shared/utils/addresses'
|
|||
import didYouMean from '@theatre/shared/utils/didYouMean'
|
||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||
import type {IBox, IDerivation, Pointer} from '@theatre/dataverse'
|
||||
import {pointer} from '@theatre/dataverse'
|
||||
import {Box, prism, val, valueDerivation} from '@theatre/dataverse'
|
||||
import {padStart} from 'lodash-es'
|
||||
import type {
|
||||
|
@ -14,6 +15,7 @@ import type {
|
|||
import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController'
|
||||
import TheatreSequence from './TheatreSequence'
|
||||
import logger from '@theatre/shared/logger'
|
||||
import type {ISequence} from '..'
|
||||
|
||||
export type IPlaybackRange = [from: number, to: number]
|
||||
|
||||
|
@ -40,6 +42,9 @@ export default class Sequence {
|
|||
private _positionFormatterD: IDerivation<ISequencePositionFormatter>
|
||||
_playableRangeD: undefined | IDerivation<{start: number; end: number}>
|
||||
|
||||
readonly pointer: ISequence['pointer'] = pointer({root: this, path: []})
|
||||
readonly $$isIdentityDerivationProvider = true
|
||||
|
||||
constructor(
|
||||
readonly _project: Project,
|
||||
readonly _sheet: Sheet,
|
||||
|
@ -68,6 +73,31 @@ export default class Sequence {
|
|||
)
|
||||
}
|
||||
|
||||
getIdentityDerivation(path: Array<string | number>): IDerivation<unknown> {
|
||||
if (path.length === 0) {
|
||||
return prism((): ISequence['pointer']['$$__pointer_type'] => ({
|
||||
length: val(this.pointer.length),
|
||||
playing: val(this.pointer.playing),
|
||||
position: val(this.pointer.position),
|
||||
}))
|
||||
}
|
||||
if (path.length > 1) {
|
||||
return prism(() => undefined)
|
||||
}
|
||||
const [prop] = path
|
||||
if (prop === 'length') {
|
||||
return this._lengthD
|
||||
} else if (prop === 'position') {
|
||||
return this._positionD
|
||||
} else if (prop === 'playing') {
|
||||
return prism(() => {
|
||||
return val(this._statePointerDerivation.getValue().playing)
|
||||
})
|
||||
} else {
|
||||
return prism(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
get positionFormatter(): ISequencePositionFormatter {
|
||||
return this._positionFormatterD.getValue()
|
||||
}
|
||||
|
@ -136,7 +166,7 @@ export default class Sequence {
|
|||
}
|
||||
|
||||
get playing() {
|
||||
return this._playbackControllerBox.get().playing
|
||||
return val(this._playbackControllerBox.get().statePointer.playing)
|
||||
}
|
||||
|
||||
_makeRangeFromSequenceTemplate(): IDerivation<IPlaybackRange> {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type Sequence from './Sequence'
|
|||
import type {IPlaybackDirection, IPlaybackRange} from './Sequence'
|
||||
import AudioPlaybackController from './playbackControllers/AudioPlaybackController'
|
||||
import coreTicker from '@theatre/core/coreTicker'
|
||||
import type {Pointer} from '@theatre/dataverse'
|
||||
|
||||
interface IAttachAudioArgs {
|
||||
/**
|
||||
|
@ -88,6 +89,42 @@ export interface ISequence {
|
|||
*/
|
||||
position: number
|
||||
|
||||
/**
|
||||
* A Pointer to the sequence's inner state.
|
||||
*
|
||||
* @remarks
|
||||
* As with any Pointer, you can use this with {@link onChange | onChange()} to listen to its value changes
|
||||
* or with {@link val | val()} to read its current value.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import {onChange, val} from '@theatre/core'
|
||||
*
|
||||
* // let's assume `sheet` is a sheet
|
||||
* const sequence = sheet.sequence
|
||||
*
|
||||
* onChange(sequence.pointer.length, (len) => {
|
||||
* console.log("Length of the sequence changed to:", len)
|
||||
* })
|
||||
*
|
||||
* onChange(sequence.pointer.position, (position) => {
|
||||
* console.log("Position of the sequence changed to:", position)
|
||||
* })
|
||||
*
|
||||
* onChange(sequence.pointer.playing, (playing) => {
|
||||
* console.log(playing ? 'playing' : 'paused')
|
||||
* })
|
||||
*
|
||||
* // we can also read the current value of the pointer
|
||||
* console.log('current length is', val(sequence.pointer.length))
|
||||
* ```
|
||||
*/
|
||||
pointer: Pointer<{
|
||||
playing: boolean
|
||||
length: number
|
||||
position: number
|
||||
}>
|
||||
|
||||
/**
|
||||
* Attaches an audio source to the sequence. Playing the sequence automatically
|
||||
* plays the audio source and their times are kept in sync.
|
||||
|
@ -252,6 +289,10 @@ export default class TheatreSequence implements ISequence {
|
|||
|
||||
return {audioContext, destinationNode, decodedBuffer, gainNode}
|
||||
}
|
||||
|
||||
get pointer(): ISequence['pointer'] {
|
||||
return privateAPI(this).pointer
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAudioBuffer(args: IAttachAudioArgs): Promise<{
|
||||
|
|
|
@ -14,10 +14,12 @@ import type {
|
|||
|
||||
export default class AudioPlaybackController implements IPlaybackController {
|
||||
_mainGain: GainNode
|
||||
private _state: Atom<IPlaybackState> = new Atom({position: 0})
|
||||
private _state: Atom<IPlaybackState> = new Atom<IPlaybackState>({
|
||||
position: 0,
|
||||
playing: false,
|
||||
})
|
||||
readonly statePointer: Pointer<IPlaybackState>
|
||||
_stopPlayCallback: () => void = noop
|
||||
playing: boolean = false
|
||||
|
||||
constructor(
|
||||
private readonly _ticker: Ticker,
|
||||
|
@ -31,11 +33,19 @@ export default class AudioPlaybackController implements IPlaybackController {
|
|||
this._mainGain.connect(this._nodeDestination)
|
||||
}
|
||||
|
||||
private get _playing() {
|
||||
return this._state.getState().playing
|
||||
}
|
||||
|
||||
private set _playing(playing: boolean) {
|
||||
this._state.setIn(['playing'], playing)
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
||||
pause() {
|
||||
this._stopPlayCallback()
|
||||
this.playing = false
|
||||
this._playing = false
|
||||
this._stopPlayCallback = noop
|
||||
}
|
||||
|
||||
|
@ -57,11 +67,11 @@ export default class AudioPlaybackController implements IPlaybackController {
|
|||
rate: number,
|
||||
direction: IPlaybackDirection,
|
||||
): Promise<boolean> {
|
||||
if (this.playing) {
|
||||
if (this._playing) {
|
||||
this.pause()
|
||||
}
|
||||
|
||||
this.playing = true
|
||||
this._playing = true
|
||||
|
||||
const ticker = this._ticker
|
||||
let startPos = this.getCurrentPosition()
|
||||
|
@ -129,7 +139,7 @@ export default class AudioPlaybackController implements IPlaybackController {
|
|||
requestNextTick()
|
||||
} else {
|
||||
this._updatePositionInState(range[1])
|
||||
this.playing = false
|
||||
this._playing = false
|
||||
cleanup()
|
||||
deferred.resolve(true)
|
||||
}
|
||||
|
@ -145,7 +155,7 @@ export default class AudioPlaybackController implements IPlaybackController {
|
|||
ticker.offThisOrNextTick(tick)
|
||||
ticker.offNextTick(tick)
|
||||
|
||||
if (this.playing) deferred.resolve(false)
|
||||
if (this._playing) deferred.resolve(false)
|
||||
}
|
||||
const requestNextTick = () => ticker.onNextTick(tick)
|
||||
ticker.onThisOrNextTick(tick)
|
||||
|
|
|
@ -9,13 +9,13 @@ import {Atom} from '@theatre/dataverse'
|
|||
|
||||
export interface IPlaybackState {
|
||||
position: number
|
||||
playing: boolean
|
||||
}
|
||||
|
||||
export interface IPlaybackController {
|
||||
playing: boolean
|
||||
getCurrentPosition(): number
|
||||
gotoPosition(position: number): void
|
||||
statePointer: Pointer<IPlaybackState>
|
||||
readonly statePointer: Pointer<IPlaybackState>
|
||||
destroy(): void
|
||||
|
||||
play(
|
||||
|
@ -29,10 +29,13 @@ export interface IPlaybackController {
|
|||
}
|
||||
|
||||
export default class DefaultPlaybackController implements IPlaybackController {
|
||||
playing: boolean = false
|
||||
_stopPlayCallback: () => void = noop
|
||||
private _state: Atom<IPlaybackState> = new Atom({position: 0})
|
||||
private _state: Atom<IPlaybackState> = new Atom<IPlaybackState>({
|
||||
position: 0,
|
||||
playing: false,
|
||||
})
|
||||
readonly statePointer: Pointer<IPlaybackState>
|
||||
|
||||
constructor(private readonly _ticker: Ticker) {
|
||||
this.statePointer = this._state.pointer
|
||||
}
|
||||
|
@ -57,6 +60,14 @@ export default class DefaultPlaybackController implements IPlaybackController {
|
|||
return this._state.getState().position
|
||||
}
|
||||
|
||||
get playing() {
|
||||
return this._state.getState().playing
|
||||
}
|
||||
|
||||
set playing(playing: boolean) {
|
||||
this._state.setIn(['playing'], playing)
|
||||
}
|
||||
|
||||
play(
|
||||
iterationCount: number,
|
||||
range: IPlaybackRange,
|
||||
|
|
Loading…
Reference in a new issue