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,