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:
Aria Minaei 2021-10-08 11:32:24 +02:00
parent a2f0c1d341
commit 7c0765bff2
5 changed files with 145 additions and 14 deletions

View file

@ -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.`)
}
}

View file

@ -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> {

View file

@ -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<{

View file

@ -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)

View file

@ -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,