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.
|
* Calls `callback` every time the pointed value of `pointer` changes.
|
||||||
*
|
*
|
||||||
* @param pointer - A pointer (like `object.props.x`)
|
* @param pointer - A Pointer (like `object.props.x`)
|
||||||
* @param callback - The callback is called every time the value of pointerOrDerivation changes
|
* @param callback - The callback is called every time the value of pointer changes
|
||||||
* @returns An unsubscribe function
|
* @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>>(
|
export function onChange<P extends PointerType<$IntentionalAny>>(
|
||||||
pointer: P,
|
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 didYouMean from '@theatre/shared/utils/didYouMean'
|
||||||
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
import {InvalidArgumentError} from '@theatre/shared/utils/errors'
|
||||||
import type {IBox, IDerivation, Pointer} from '@theatre/dataverse'
|
import type {IBox, IDerivation, Pointer} from '@theatre/dataverse'
|
||||||
|
import {pointer} from '@theatre/dataverse'
|
||||||
import {Box, prism, val, valueDerivation} from '@theatre/dataverse'
|
import {Box, prism, val, valueDerivation} from '@theatre/dataverse'
|
||||||
import {padStart} from 'lodash-es'
|
import {padStart} from 'lodash-es'
|
||||||
import type {
|
import type {
|
||||||
|
@ -14,6 +15,7 @@ import type {
|
||||||
import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController'
|
import DefaultPlaybackController from './playbackControllers/DefaultPlaybackController'
|
||||||
import TheatreSequence from './TheatreSequence'
|
import TheatreSequence from './TheatreSequence'
|
||||||
import logger from '@theatre/shared/logger'
|
import logger from '@theatre/shared/logger'
|
||||||
|
import type {ISequence} from '..'
|
||||||
|
|
||||||
export type IPlaybackRange = [from: number, to: number]
|
export type IPlaybackRange = [from: number, to: number]
|
||||||
|
|
||||||
|
@ -40,6 +42,9 @@ export default class Sequence {
|
||||||
private _positionFormatterD: IDerivation<ISequencePositionFormatter>
|
private _positionFormatterD: IDerivation<ISequencePositionFormatter>
|
||||||
_playableRangeD: undefined | IDerivation<{start: number; end: number}>
|
_playableRangeD: undefined | IDerivation<{start: number; end: number}>
|
||||||
|
|
||||||
|
readonly pointer: ISequence['pointer'] = pointer({root: this, path: []})
|
||||||
|
readonly $$isIdentityDerivationProvider = true
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly _project: Project,
|
readonly _project: Project,
|
||||||
readonly _sheet: Sheet,
|
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 {
|
get positionFormatter(): ISequencePositionFormatter {
|
||||||
return this._positionFormatterD.getValue()
|
return this._positionFormatterD.getValue()
|
||||||
}
|
}
|
||||||
|
@ -136,7 +166,7 @@ export default class Sequence {
|
||||||
}
|
}
|
||||||
|
|
||||||
get playing() {
|
get playing() {
|
||||||
return this._playbackControllerBox.get().playing
|
return val(this._playbackControllerBox.get().statePointer.playing)
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeRangeFromSequenceTemplate(): IDerivation<IPlaybackRange> {
|
_makeRangeFromSequenceTemplate(): IDerivation<IPlaybackRange> {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type Sequence from './Sequence'
|
||||||
import type {IPlaybackDirection, IPlaybackRange} from './Sequence'
|
import type {IPlaybackDirection, IPlaybackRange} from './Sequence'
|
||||||
import AudioPlaybackController from './playbackControllers/AudioPlaybackController'
|
import AudioPlaybackController from './playbackControllers/AudioPlaybackController'
|
||||||
import coreTicker from '@theatre/core/coreTicker'
|
import coreTicker from '@theatre/core/coreTicker'
|
||||||
|
import type {Pointer} from '@theatre/dataverse'
|
||||||
|
|
||||||
interface IAttachAudioArgs {
|
interface IAttachAudioArgs {
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +89,42 @@ export interface ISequence {
|
||||||
*/
|
*/
|
||||||
position: number
|
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
|
* Attaches an audio source to the sequence. Playing the sequence automatically
|
||||||
* plays the audio source and their times are kept in sync.
|
* 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}
|
return {audioContext, destinationNode, decodedBuffer, gainNode}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get pointer(): ISequence['pointer'] {
|
||||||
|
return privateAPI(this).pointer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveAudioBuffer(args: IAttachAudioArgs): Promise<{
|
async function resolveAudioBuffer(args: IAttachAudioArgs): Promise<{
|
||||||
|
|
|
@ -14,10 +14,12 @@ import type {
|
||||||
|
|
||||||
export default class AudioPlaybackController implements IPlaybackController {
|
export default class AudioPlaybackController implements IPlaybackController {
|
||||||
_mainGain: GainNode
|
_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>
|
readonly statePointer: Pointer<IPlaybackState>
|
||||||
_stopPlayCallback: () => void = noop
|
_stopPlayCallback: () => void = noop
|
||||||
playing: boolean = false
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _ticker: Ticker,
|
private readonly _ticker: Ticker,
|
||||||
|
@ -31,11 +33,19 @@ export default class AudioPlaybackController implements IPlaybackController {
|
||||||
this._mainGain.connect(this._nodeDestination)
|
this._mainGain.connect(this._nodeDestination)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _playing() {
|
||||||
|
return this._state.getState().playing
|
||||||
|
}
|
||||||
|
|
||||||
|
private set _playing(playing: boolean) {
|
||||||
|
this._state.setIn(['playing'], playing)
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {}
|
destroy() {}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
this._stopPlayCallback()
|
this._stopPlayCallback()
|
||||||
this.playing = false
|
this._playing = false
|
||||||
this._stopPlayCallback = noop
|
this._stopPlayCallback = noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +67,11 @@ export default class AudioPlaybackController implements IPlaybackController {
|
||||||
rate: number,
|
rate: number,
|
||||||
direction: IPlaybackDirection,
|
direction: IPlaybackDirection,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (this.playing) {
|
if (this._playing) {
|
||||||
this.pause()
|
this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playing = true
|
this._playing = true
|
||||||
|
|
||||||
const ticker = this._ticker
|
const ticker = this._ticker
|
||||||
let startPos = this.getCurrentPosition()
|
let startPos = this.getCurrentPosition()
|
||||||
|
@ -129,7 +139,7 @@ export default class AudioPlaybackController implements IPlaybackController {
|
||||||
requestNextTick()
|
requestNextTick()
|
||||||
} else {
|
} else {
|
||||||
this._updatePositionInState(range[1])
|
this._updatePositionInState(range[1])
|
||||||
this.playing = false
|
this._playing = false
|
||||||
cleanup()
|
cleanup()
|
||||||
deferred.resolve(true)
|
deferred.resolve(true)
|
||||||
}
|
}
|
||||||
|
@ -145,7 +155,7 @@ export default class AudioPlaybackController implements IPlaybackController {
|
||||||
ticker.offThisOrNextTick(tick)
|
ticker.offThisOrNextTick(tick)
|
||||||
ticker.offNextTick(tick)
|
ticker.offNextTick(tick)
|
||||||
|
|
||||||
if (this.playing) deferred.resolve(false)
|
if (this._playing) deferred.resolve(false)
|
||||||
}
|
}
|
||||||
const requestNextTick = () => ticker.onNextTick(tick)
|
const requestNextTick = () => ticker.onNextTick(tick)
|
||||||
ticker.onThisOrNextTick(tick)
|
ticker.onThisOrNextTick(tick)
|
||||||
|
|
|
@ -9,13 +9,13 @@ import {Atom} from '@theatre/dataverse'
|
||||||
|
|
||||||
export interface IPlaybackState {
|
export interface IPlaybackState {
|
||||||
position: number
|
position: number
|
||||||
|
playing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPlaybackController {
|
export interface IPlaybackController {
|
||||||
playing: boolean
|
|
||||||
getCurrentPosition(): number
|
getCurrentPosition(): number
|
||||||
gotoPosition(position: number): void
|
gotoPosition(position: number): void
|
||||||
statePointer: Pointer<IPlaybackState>
|
readonly statePointer: Pointer<IPlaybackState>
|
||||||
destroy(): void
|
destroy(): void
|
||||||
|
|
||||||
play(
|
play(
|
||||||
|
@ -29,10 +29,13 @@ export interface IPlaybackController {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DefaultPlaybackController implements IPlaybackController {
|
export default class DefaultPlaybackController implements IPlaybackController {
|
||||||
playing: boolean = false
|
|
||||||
_stopPlayCallback: () => void = noop
|
_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>
|
readonly statePointer: Pointer<IPlaybackState>
|
||||||
|
|
||||||
constructor(private readonly _ticker: Ticker) {
|
constructor(private readonly _ticker: Ticker) {
|
||||||
this.statePointer = this._state.pointer
|
this.statePointer = this._state.pointer
|
||||||
}
|
}
|
||||||
|
@ -57,6 +60,14 @@ export default class DefaultPlaybackController implements IPlaybackController {
|
||||||
return this._state.getState().position
|
return this._state.getState().position
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get playing() {
|
||||||
|
return this._state.getState().playing
|
||||||
|
}
|
||||||
|
|
||||||
|
set playing(playing: boolean) {
|
||||||
|
this._state.setIn(['playing'], playing)
|
||||||
|
}
|
||||||
|
|
||||||
play(
|
play(
|
||||||
iterationCount: number,
|
iterationCount: number,
|
||||||
range: IPlaybackRange,
|
range: IPlaybackRange,
|
||||||
|
|
Loading…
Reference in a new issue