diff --git a/theatre/core/src/coreExports.ts b/theatre/core/src/coreExports.ts index 1f3fb59..e957e1c 100644 --- a/theatre/core/src/coreExports.ts +++ b/theatre/core/src/coreExports.ts @@ -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

>( pointer: P, @@ -144,3 +158,28 @@ export function onChange

>( ) } } + +/** + * 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(pointer: PointerType): T { + if (isPointer(pointer)) { + return valueDerivation(pointer).getValue() as $IntentionalAny + } else { + throw new Error(`Called val(p) where p is not a pointer.`) + } +} diff --git a/theatre/core/src/sequences/Sequence.ts b/theatre/core/src/sequences/Sequence.ts index 1dc7613..cba59e6 100644 --- a/theatre/core/src/sequences/Sequence.ts +++ b/theatre/core/src/sequences/Sequence.ts @@ -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 _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): IDerivation { + 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 { diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index df5c5c1..f0e052c 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -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<{ diff --git a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts index 3558284..538e381 100644 --- a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts @@ -14,10 +14,12 @@ import type { export default class AudioPlaybackController implements IPlaybackController { _mainGain: GainNode - private _state: Atom = new Atom({position: 0}) + private _state: Atom = new Atom({ + position: 0, + playing: false, + }) readonly statePointer: Pointer _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 { - 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) diff --git a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts index f85b313..268f80e 100644 --- a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts @@ -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 + readonly statePointer: Pointer 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 = new Atom({position: 0}) + private _state: Atom = new Atom({ + position: 0, + playing: false, + }) readonly statePointer: Pointer + 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,