From d6498585295962c55a6d412e74ad7f65af86adb7 Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 14 Jan 2023 15:57:13 +0200 Subject: [PATCH] Feature: Custom RAFDrivers (#374) Co-authored-by: Pete Feltham Co-authored-by: Andrew Prifer --- packages/benchmarks/src/index.tsx | 14 +-- packages/dataverse/src/Ticker.ts | 95 +++++++++++++------ .../src/shared/custom-raf-driver/App.tsx | 36 +++++++ .../src/shared/custom-raf-driver/index.tsx | 18 ++++ .../hello-world-extension-dataverse/index.tsx | 67 +++++++------ .../src/tests/r3f-stress-test/index.tsx | 6 +- packages/r3f/src/extension/index.ts | 23 ++--- packages/r3f/src/index.ts | 4 + packages/r3f/src/main/RafDriverProvider.tsx | 26 +++++ packages/r3f/src/main/editable.tsx | 11 ++- theatre/core/src/CoreBundle.ts | 3 + theatre/core/src/coreExports.ts | 25 ++++- theatre/core/src/coreTicker.ts | 60 +++++++++--- theatre/core/src/privateAPIs.ts | 4 + theatre/core/src/rafDrivers.ts | 60 ++++++++++++ theatre/core/src/sequences/Sequence.ts | 21 ++-- theatre/core/src/sequences/TheatreSequence.ts | 14 ++- .../AudioPlaybackController.ts | 16 ++-- .../DefaultPlaybackController.ts | 17 ++-- .../src/sheetObjects/TheatreSheetObject.ts | 17 +++- theatre/studio/src/Studio.ts | 49 +++++++++- theatre/studio/src/TheatreStudio.ts | 9 +- .../studio/src/UIRoot/useKeyboardShortcuts.ts | 1 + .../FrameGrid/FrameGrid.tsx | 4 +- .../FrameGrid/StampsGrid.tsx | 4 +- theatre/studio/src/studioTicker.ts | 5 - 26 files changed, 464 insertions(+), 145 deletions(-) create mode 100644 packages/playground/src/shared/custom-raf-driver/App.tsx create mode 100644 packages/playground/src/shared/custom-raf-driver/index.tsx create mode 100644 packages/r3f/src/main/RafDriverProvider.tsx create mode 100644 theatre/core/src/rafDrivers.ts delete mode 100644 theatre/studio/src/studioTicker.ts diff --git a/packages/benchmarks/src/index.tsx b/packages/benchmarks/src/index.tsx index eb0879a..11361c5 100644 --- a/packages/benchmarks/src/index.tsx +++ b/packages/benchmarks/src/index.tsx @@ -1,5 +1,5 @@ // import studio from '@theatre/studio' -import {getProject} from '@theatre/core' +import {createRafDriver, getProject} from '@theatre/core' import type { UnknownShorthandCompoundProps, ISheet, @@ -7,11 +7,11 @@ import type { } from '@theatre/core' // @ts-ignore import benchProject1State from './Bench project 1.theatre-project-state.json' -import {Ticker} from '@theatre/dataverse' -import {setCoreTicker} from '@theatre/core/coreTicker' +import {setCoreRafDriver} from '@theatre/core/coreTicker' -const ticker = new Ticker() -setCoreTicker(ticker) +const driver = createRafDriver({name: 'BenchmarkRafDriver'}) + +setCoreRafDriver(driver) // studio.initialize({}) @@ -79,7 +79,7 @@ async function test1() { } function iterateOnSequence() { - ticker.tick() + driver.tick(performance.now()) const startTime = performance.now() for (let i = 1; i < CONFIG.numberOfIterations; i++) { onChangeEventsFired = 0 @@ -87,7 +87,7 @@ async function test1() { for (const sheet of sheets) { sheet.sequence.position = pos } - ticker.tick() + driver.tick(performance.now()) if (onChangeEventsFired !== objects.length) { console.info( `Expected ${objects.length} onChange events, got ${onChangeEventsFired}`, diff --git a/packages/dataverse/src/Ticker.ts b/packages/dataverse/src/Ticker.ts index c72c90b..f0032c5 100644 --- a/packages/dataverse/src/Ticker.ts +++ b/packages/dataverse/src/Ticker.ts @@ -1,44 +1,36 @@ type ICallback = (t: number) => void -function createRafTicker() { - const ticker = new Ticker() - - if (typeof window !== 'undefined') { - /** - * @remarks - * TODO users should also be able to define their own ticker. - */ - const onAnimationFrame = (t: number) => { - ticker.tick(t) - window.requestAnimationFrame(onAnimationFrame) - } - window.requestAnimationFrame(onAnimationFrame) - } else { - ticker.tick(0) - setTimeout(() => ticker.tick(1), 0) - } - - return ticker -} - -let rafTicker: undefined | Ticker +/** + * The number of ticks that can pass without any scheduled callbacks before the Ticker goes dormant. This is to prevent + * the Ticker from staying active forever, even if there are no scheduled callbacks. + * + * Perhaps counting ticks vs. time is not the best way to do this. But it's a start. + */ +const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant /** * The Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an * external scheduling strategy, e.g. a raf. */ export default class Ticker { - /** Get a shared `requestAnimationFrame` ticker. */ - static get raf(): Ticker { - if (!rafTicker) { - rafTicker = createRafTicker() - } - return rafTicker - } private _scheduledForThisOrNextTick: Set private _scheduledForNextTick: Set private _timeAtCurrentTick: number private _ticking: boolean = false + + /** + * Whether the Ticker is dormant + */ + private _dormant: boolean = true + + private _numberOfDormantTicks = 0 + + /** + * Whether the Ticker is dormant + */ + get dormant(): boolean { + return this._dormant + } /** * Counts up for every tick executed. * Internally, this is used to measure ticks per second. @@ -48,7 +40,18 @@ export default class Ticker { */ public __ticks = 0 - constructor() { + constructor( + private _conf?: { + /** + * This is called when the Ticker goes dormant. + */ + onDormant?: () => void + /** + * This is called when the Ticker goes active. + */ + onActive?: () => void + }, + ) { this._scheduledForThisOrNextTick = new Set() this._scheduledForNextTick = new Set() this._timeAtCurrentTick = 0 @@ -70,6 +73,9 @@ export default class Ticker { */ onThisOrNextTick(fn: ICallback) { this._scheduledForThisOrNextTick.add(fn) + if (this._dormant) { + this._goActive() + } } /** @@ -82,6 +88,9 @@ export default class Ticker { */ onNextTick(fn: ICallback) { this._scheduledForNextTick.add(fn) + if (this._dormant) { + this._goActive() + } } /** @@ -116,6 +125,19 @@ export default class Ticker { } else return performance.now() } + private _goActive() { + if (!this._dormant) return + this._dormant = false + this._conf?.onActive?.() + } + + private _goDormant() { + if (this._dormant) return + this._dormant = true + this._numberOfDormantTicks = 0 + this._conf?.onDormant?.() + } + /** * Triggers a tick which starts executing the callbacks scheduled for this tick. * @@ -135,6 +157,19 @@ export default class Ticker { this.__ticks++ + if (!this._dormant) { + if ( + this._scheduledForNextTick.size === 0 && + this._scheduledForThisOrNextTick.size === 0 + ) { + this._numberOfDormantTicks++ + if (this._numberOfDormantTicks >= EMPTY_TICKS_BEFORE_GOING_DORMANT) { + this._goDormant() + return + } + } + } + this._ticking = true this._timeAtCurrentTick = t for (const v of this._scheduledForNextTick) { diff --git a/packages/playground/src/shared/custom-raf-driver/App.tsx b/packages/playground/src/shared/custom-raf-driver/App.tsx new file mode 100644 index 0000000..b0a8e0c --- /dev/null +++ b/packages/playground/src/shared/custom-raf-driver/App.tsx @@ -0,0 +1,36 @@ +import {editable as e, RafDriverProvider, SheetProvider} from '@theatre/r3f' +import type {IRafDriver} from '@theatre/core' +import {getProject} from '@theatre/core' +import React from 'react' +import {Canvas} from '@react-three/fiber' + +const EditablePoints = e('points', 'mesh') + +function App(props: {rafDriver: IRafDriver}) { + return ( +
+ + + + + + + + + + + +
+ ) +} + +export default App diff --git a/packages/playground/src/shared/custom-raf-driver/index.tsx b/packages/playground/src/shared/custom-raf-driver/index.tsx new file mode 100644 index 0000000..00a0f46 --- /dev/null +++ b/packages/playground/src/shared/custom-raf-driver/index.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App' +import studio from '@theatre/studio' +import extension from '@theatre/r3f/dist/extension' +import {createRafDriver} from '@theatre/core' + +const rafDriver = createRafDriver({name: 'a custom 5fps raf driver'}) +setInterval(() => { + rafDriver.tick(performance.now()) +}, 200) + +studio.extend(extension) +studio.initialize({ + __experimental_rafDriver: rafDriver, +}) + +ReactDOM.render(, document.getElementById('root')) diff --git a/packages/playground/src/shared/hello-world-extension-dataverse/index.tsx b/packages/playground/src/shared/hello-world-extension-dataverse/index.tsx index 73e4934..2fd63be 100644 --- a/packages/playground/src/shared/hello-world-extension-dataverse/index.tsx +++ b/packages/playground/src/shared/hello-world-extension-dataverse/index.tsx @@ -4,7 +4,8 @@ import App from './App' import type {ToolsetConfig} from '@theatre/studio' import studio from '@theatre/studio' import extension from '@theatre/r3f/dist/extension' -import {Atom, prism, Ticker, val} from '@theatre/dataverse' +import {Atom, prism, val} from '@theatre/dataverse' +import {onChange} from '@theatre/core' /** * Let's take a look at how we can use `prism`, `Ticker`, and `val` from Theatre.js's Dataverse library @@ -28,41 +29,39 @@ studio.extend({ global(set, studio) { const exampleBox = new Atom('mobile') - const untapFn = prism(() => [ - { - type: 'Switch', - value: val(exampleBox.prism), - onChange: (value) => exampleBox.set(value), - options: [ - { - value: 'mobile', - label: 'view mobile version', - svgSource: '😀', - }, - { - value: 'desktop', - label: 'view desktop version', - svgSource: '🪢', - }, - ], - }, - { - type: 'Icon', - title: 'Example Icon', - svgSource: '👁‍🗨', - onClick: () => { - console.log('hello') + const untapFn = onChange( + prism(() => [ + { + type: 'Switch', + value: val(exampleBox.prism), + onChange: (value) => exampleBox.set(value), + options: [ + { + value: 'mobile', + label: 'view mobile version', + svgSource: '😀', + }, + { + value: 'desktop', + label: 'view desktop version', + svgSource: '🪢', + }, + ], }, - }, - ]) - // listen to changes to this prism using the requestAnimationFrame shared ticker - .onChange( - Ticker.raf, - (value) => { - set(value) + { + type: 'Icon', + title: 'Example Icon', + svgSource: '👁‍🗨', + onClick: () => { + console.log('hello') + }, }, - true, - ) + ]), + (value) => { + set(value) + }, + ) + // listen to changes to this prism using the requestAnimationFrame shared ticker return untapFn }, diff --git a/packages/playground/src/tests/r3f-stress-test/index.tsx b/packages/playground/src/tests/r3f-stress-test/index.tsx index a02b5fd..22b9a11 100644 --- a/packages/playground/src/tests/r3f-stress-test/index.tsx +++ b/packages/playground/src/tests/r3f-stress-test/index.tsx @@ -3,14 +3,16 @@ import ReactDOM from 'react-dom' import App from './App' import studio from '@theatre/studio' import extension from '@theatre/r3f/dist/extension' -import {Ticker} from '@theatre/dataverse' +import getStudio from '@theatre/studio/getStudio' + +const studioPrivate = getStudio() studio.extend(extension) studio.initialize() ReactDOM.render(, document.getElementById('root')) -const raf = Ticker.raf +const raf = studioPrivate.ticker // Show "ticks per second" information in performance measurements using the User Timing API // See https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API diff --git a/packages/r3f/src/extension/index.ts b/packages/r3f/src/extension/index.ts index ce33bae..7f9bb1d 100644 --- a/packages/r3f/src/extension/index.ts +++ b/packages/r3f/src/extension/index.ts @@ -1,11 +1,12 @@ import SnapshotEditor from './components/SnapshotEditor' import type {IExtension} from '@theatre/studio' -import {prism, Ticker, val} from '@theatre/dataverse' +import {prism, val} from '@theatre/dataverse' import {getEditorSheetObject} from './editorStuff' import ReactDOM from 'react-dom' import React from 'react' import type {ToolsetConfig} from '@theatre/studio' import useExtensionStore from './useExtensionStore' +import {onChange} from '@theatre/core' const io5CameraOutline = `Camera` const gameIconMove = `` @@ -35,13 +36,9 @@ const r3fExtension: IExtension = { }, ] }) - return calc.onChange( - Ticker.raf, - () => { - set(calc.getValue()) - }, - true, - ) + return onChange(calc, () => { + set(calc.getValue()) + }) }, 'snapshot-editor': (set, studio) => { const {createSnapshot} = useExtensionStore.getState() @@ -140,13 +137,9 @@ const r3fExtension: IExtension = { }, ] }) - return calc.onChange( - Ticker.raf, - () => { - set(calc.getValue()) - }, - true, - ) + return onChange(calc, () => { + set(calc.getValue()) + }) }, }, panes: [ diff --git a/packages/r3f/src/index.ts b/packages/r3f/src/index.ts index ff6e4de..644e009 100644 --- a/packages/r3f/src/index.ts +++ b/packages/r3f/src/index.ts @@ -21,6 +21,10 @@ export { export {makeStoreKey as __private_makeStoreKey} from './main/utils' export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider' +export { + default as RafDriverProvider, + useCurrentRafDriver, +} from './main/RafDriverProvider' export {refreshSnapshot} from './main/utils' export {default as RefreshSnapshot} from './main/RefreshSnapshot' export * from './drei' diff --git a/packages/r3f/src/main/RafDriverProvider.tsx b/packages/r3f/src/main/RafDriverProvider.tsx new file mode 100644 index 0000000..483b9f1 --- /dev/null +++ b/packages/r3f/src/main/RafDriverProvider.tsx @@ -0,0 +1,26 @@ +import type {ReactNode} from 'react' +import React, {createContext, useContext, useEffect} from 'react' +import type {IRafDriver} from '@theatre/core' + +const ctx = createContext<{rafDriver: IRafDriver}>(undefined!) + +export const useCurrentRafDriver = (): IRafDriver | undefined => { + return useContext(ctx)?.rafDriver +} + +const RafDriverProvider: React.FC<{ + driver: IRafDriver + children: ReactNode +}> = ({driver, children}) => { + useEffect(() => { + if (!driver || driver.type !== 'Theatre_RafDriver_PublicAPI') { + throw new Error( + `driver in has an invalid value`, + ) + } + }, [driver]) + + return {children} +} + +export default RafDriverProvider diff --git a/packages/r3f/src/main/editable.tsx b/packages/r3f/src/main/editable.tsx index 1581ed7..e93ab16 100644 --- a/packages/r3f/src/main/editable.tsx +++ b/packages/r3f/src/main/editable.tsx @@ -11,6 +11,7 @@ import {makeStoreKey} from './utils' import type {$FixMe, $IntentionalAny} from '../types' import type {ISheetObject} from '@theatre/core' import {notify} from '@theatre/core' +import {useCurrentRafDriver} from './RafDriverProvider' const createEditable = ( config: EditableFactoryConfig, @@ -74,6 +75,7 @@ const createEditable = ( const objectRef = useRef() const sheet = useCurrentSheet()! + const rafDriver = useCurrentRafDriver() const [sheetObject, setSheetObject] = useState< undefined | ISheetObject<$FixMe> @@ -211,15 +213,18 @@ Then you can use it in your JSX like any other editable component. Note the make setFromTheatre(sheetObject.value) - const untap = sheetObject.onValuesChange(setFromTheatre) + const unsubscribe = sheetObject.onValuesChange( + setFromTheatre, + rafDriver, + ) return () => { - untap() + unsubscribe() sheetObject.sheet.detachObject(theatreKey) allRegisteredObjects.delete(sheetObject) editorStore.getState().removeEditable(storeKey) } - }, [sheetObject]) + }, [sheetObject, rafDriver]) return ( // @ts-ignore diff --git a/theatre/core/src/CoreBundle.ts b/theatre/core/src/CoreBundle.ts index a12364c..6e08ff5 100644 --- a/theatre/core/src/CoreBundle.ts +++ b/theatre/core/src/CoreBundle.ts @@ -2,11 +2,13 @@ import type {Studio} from '@theatre/studio/Studio' import projectsSingleton from './projects/projectsSingleton' import {privateAPI} from './privateAPIs' import * as coreExports from './coreExports' +import {getCoreRafDriver} from './coreTicker' export type CoreBits = { projectsP: typeof projectsSingleton.atom.pointer.projects privateAPI: typeof privateAPI coreExports: typeof coreExports + getCoreRafDriver: typeof getCoreRafDriver } export default class CoreBundle { @@ -30,6 +32,7 @@ export default class CoreBundle { projectsP: projectsSingleton.atom.pointer.projects, privateAPI: privateAPI, coreExports, + getCoreRafDriver, } callback(bits) diff --git a/theatre/core/src/coreExports.ts b/theatre/core/src/coreExports.ts index fc2edad..e60b643 100644 --- a/theatre/core/src/coreExports.ts +++ b/theatre/core/src/coreExports.ts @@ -8,15 +8,19 @@ import {InvalidArgumentError} from '@theatre/shared/utils/errors' import {validateName} from '@theatre/shared/utils/sanitizers' import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' import deepEqual from 'fast-deep-equal' -import type {PointerType} from '@theatre/dataverse' +import type {PointerType, Prism} from '@theatre/dataverse' import {isPointer} from '@theatre/dataverse' import {isPrism, pointerToPrism} from '@theatre/dataverse' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {ProjectId} from '@theatre/shared/utils/ids' import {_coreLogger} from './_coreLogger' import {getCoreTicker} from './coreTicker' +import type {IRafDriver} from './rafDrivers' +import {privateAPI} from './privateAPIs' export {notify} from '@theatre/shared/notify' export {types} +export {createRafDriver} from './rafDrivers' +export type {IRafDriver} from './rafDrivers' /** * Returns a project of the given id, or creates one if it doesn't already exist. @@ -150,15 +154,26 @@ const validateProjectIdOrThrow = (value: string) => { * setTimeout(usubscribe, 10000) // stop listening to changes after 10 seconds * ``` */ -export function onChange

>( +export function onChange< + P extends PointerType<$IntentionalAny> | Prism<$IntentionalAny>, +>( pointer: P, - callback: (value: P extends PointerType ? T : unknown) => void, + callback: ( + value: P extends PointerType + ? T + : P extends Prism + ? T + : unknown, + ) => void, + driver?: IRafDriver, ): VoidFn { + const ticker = driver ? privateAPI(driver).ticker : getCoreTicker() + if (isPointer(pointer)) { const pr = pointerToPrism(pointer) - return pr.onChange(getCoreTicker(), callback as $IntentionalAny, true) + return pr.onChange(ticker, callback as $IntentionalAny, true) } else if (isPrism(pointer)) { - return pointer.onChange(getCoreTicker(), callback as $IntentionalAny, true) + return pointer.onChange(ticker, callback as $IntentionalAny, true) } else { throw new Error( `Called onChange(p) where p is neither a pointer nor a prism.`, diff --git a/theatre/core/src/coreTicker.ts b/theatre/core/src/coreTicker.ts index c3a103c..ad9ad4d 100644 --- a/theatre/core/src/coreTicker.ts +++ b/theatre/core/src/coreTicker.ts @@ -1,17 +1,55 @@ -import {Ticker} from '@theatre/dataverse' +import type {Ticker} from '@theatre/dataverse' +import {privateAPI} from './privateAPIs' +import type {IRafDriver, RafDriverPrivateAPI} from './rafDrivers' +import {createRafDriver} from './rafDrivers' -let coreTicker: Ticker - -export function setCoreTicker(ticker: Ticker) { - if (coreTicker) { - throw new Error(`coreTicker is already set`) +function createBasicRafDriver(): IRafDriver { + let rafId: number | null = null + const start = (): void => { + if (typeof window !== 'undefined') { + const onAnimationFrame = (t: number) => { + driver.tick(t) + rafId = window.requestAnimationFrame(onAnimationFrame) + } + rafId = window.requestAnimationFrame(onAnimationFrame) + } else { + driver.tick(0) + setTimeout(() => driver.tick(1), 0) + } } - coreTicker = ticker + + const stop = (): void => { + if (typeof window !== 'undefined') { + if (rafId !== null) { + window.cancelAnimationFrame(rafId) + } + } else { + // nothing to do in SSR + } + } + + const driver = createRafDriver({name: 'DefaultCoreRafDriver', start, stop}) + + return driver +} + +let coreRafDriver: RafDriverPrivateAPI | undefined + +export function getCoreRafDriver(): RafDriverPrivateAPI { + if (!coreRafDriver) { + setCoreRafDriver(createBasicRafDriver()) + } + return coreRafDriver! } export function getCoreTicker(): Ticker { - if (!coreTicker) { - coreTicker = Ticker.raf - } - return coreTicker + return getCoreRafDriver().ticker +} + +export function setCoreRafDriver(driver: IRafDriver) { + if (coreRafDriver) { + throw new Error(`\`setCoreRafDriver()\` is already called.`) + } + const driverPrivateApi = privateAPI(driver) + coreRafDriver = driverPrivateApi } diff --git a/theatre/core/src/privateAPIs.ts b/theatre/core/src/privateAPIs.ts index cadc326..c1c42f5 100644 --- a/theatre/core/src/privateAPIs.ts +++ b/theatre/core/src/privateAPIs.ts @@ -8,6 +8,7 @@ import type Sheet from '@theatre/core/sheets/Sheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {UnknownShorthandCompoundProps} from './propTypes/internals' import type {$IntentionalAny} from '@theatre/shared/utils/types' +import type {IRafDriver, RafDriverPrivateAPI} from './rafDrivers' const publicAPIToPrivateAPIMap = new WeakMap() @@ -24,6 +25,8 @@ export function privateAPI

( ? SheetObject : P extends ISequence ? Sequence + : P extends IRafDriver + ? RafDriverPrivateAPI : never { return publicAPIToPrivateAPIMap.get(pub) } @@ -35,6 +38,7 @@ export function privateAPI

( export function setPrivateAPI(pub: IProject, priv: Project): void export function setPrivateAPI(pub: ISheet, priv: Sheet): void export function setPrivateAPI(pub: ISequence, priv: Sequence): void +export function setPrivateAPI(pub: IRafDriver, priv: RafDriverPrivateAPI): void export function setPrivateAPI( pub: ISheetObject, priv: SheetObject, diff --git a/theatre/core/src/rafDrivers.ts b/theatre/core/src/rafDrivers.ts new file mode 100644 index 0000000..55634c5 --- /dev/null +++ b/theatre/core/src/rafDrivers.ts @@ -0,0 +1,60 @@ +import {Ticker} from '@theatre/dataverse' +import {setPrivateAPI} from './privateAPIs' + +export interface IRafDriver { + /** + * All raf derivers have have `driver.type === 'Theatre_RafDriver_PublicAPI'` + */ + readonly type: 'Theatre_RafDriver_PublicAPI' + name: string + id: number + tick: (time: number) => void +} + +export interface RafDriverPrivateAPI { + readonly type: 'Theatre_RafDriver_PrivateAPI' + publicApi: IRafDriver + ticker: Ticker + start?: () => void + stop?: () => void +} + +let lastDriverId = 0 + +export function createRafDriver(conf?: { + name?: string + start?: () => void + stop?: () => void +}): IRafDriver { + const tick = (time: number): void => { + ticker.tick(time) + } + + const ticker = new Ticker({ + onActive() { + conf?.start?.() + }, + onDormant() { + conf?.stop?.() + }, + }) + + const driverPublicApi: IRafDriver = { + tick, + id: lastDriverId++, + name: conf?.name ?? `CustomRafDriver-${lastDriverId}`, + type: 'Theatre_RafDriver_PublicAPI', + } + + const driverPrivateApi: RafDriverPrivateAPI = { + type: 'Theatre_RafDriver_PrivateAPI', + publicApi: driverPublicApi, + ticker, + start: conf?.start, + stop: conf?.stop, + } + + setPrivateAPI(driverPublicApi, driverPrivateApi) + + return driverPublicApi +} diff --git a/theatre/core/src/sequences/Sequence.ts b/theatre/core/src/sequences/Sequence.ts index 1973512..ee83e57 100644 --- a/theatre/core/src/sequences/Sequence.ts +++ b/theatre/core/src/sequences/Sequence.ts @@ -3,7 +3,7 @@ import type Sheet from '@theatre/core/sheets/Sheet' import type {SequenceAddress} from '@theatre/shared/utils/addresses' import didYouMean from '@theatre/shared/utils/didYouMean' import {InvalidArgumentError} from '@theatre/shared/utils/errors' -import type {Prism, Pointer} from '@theatre/dataverse' +import type {Prism, Pointer, Ticker} from '@theatre/dataverse' import {Atom} from '@theatre/dataverse' import {pointer} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse' @@ -17,7 +17,6 @@ import TheatreSequence from './TheatreSequence' import type {ILogger} from '@theatre/shared/logger' import type {ISequence} from '..' import {notify} from '@theatre/shared/notify' -import {getCoreTicker} from '@theatre/core/coreTicker' export type IPlaybackRange = [from: number, to: number] @@ -64,7 +63,7 @@ export default class Sequence { this.publicApi = new TheatreSequence(this) this._playbackControllerBox = new Atom( - playbackController ?? new DefaultPlaybackController(getCoreTicker()), + playbackController ?? new DefaultPlaybackController(), ) this._prismOfStatePointer = prism( @@ -195,17 +194,23 @@ export default class Sequence { * @returns a promise that gets rejected if the playback stopped for whatever reason * */ - playDynamicRange(rangeD: Prism): Promise { - return this._playbackControllerBox.getState().playDynamicRange(rangeD) + playDynamicRange( + rangeD: Prism, + ticker: Ticker, + ): Promise { + return this._playbackControllerBox + .getState() + .playDynamicRange(rangeD, ticker) } async play( - conf?: Partial<{ + conf: Partial<{ iterationCount: number range: IPlaybackRange rate: number direction: IPlaybackDirection }>, + ticker: Ticker, ): Promise { const sequenceDuration = this.length const range: IPlaybackRange = @@ -320,6 +325,7 @@ To fix this, either set \`conf.range[1]\` to be less the duration of the sequenc [range[0], range[1]], rate, direction, + ticker, ) } @@ -328,10 +334,11 @@ To fix this, either set \`conf.range[1]\` to be less the duration of the sequenc range: IPlaybackRange, rate: number, direction: IPlaybackDirection, + ticker: Ticker, ): Promise { return this._playbackControllerBox .getState() - .play(iterationCount, range, rate, direction) + .play(iterationCount, range, rate, direction, ticker) } pause() { diff --git a/theatre/core/src/sequences/TheatreSequence.ts b/theatre/core/src/sequences/TheatreSequence.ts index 899ec5a..f91c30b 100644 --- a/theatre/core/src/sequences/TheatreSequence.ts +++ b/theatre/core/src/sequences/TheatreSequence.ts @@ -6,6 +6,7 @@ import AudioPlaybackController from './playbackControllers/AudioPlaybackControll import {getCoreTicker} from '@theatre/core/coreTicker' import type {Pointer} from '@theatre/dataverse' import {notify} from '@theatre/shared/notify' +import type {IRafDriver} from '@theatre/core/rafDrivers' interface IAttachAudioArgs { /** @@ -76,6 +77,12 @@ export interface ISequence { * The direction of the playback. Similar to CSS's animation-direction */ direction?: IPlaybackDirection + + /** + * Optionally provide a RAF driver to use for the playback. It'll default to + * the core driver if not provided, which is a `requestAnimationFrame()` driver. + */ + rafDriver?: IRafDriver }): Promise /** @@ -233,11 +240,15 @@ export default class TheatreSequence implements ISequence { range: IPlaybackRange rate: number direction: IPlaybackDirection + rafDriver: IRafDriver }>, ): Promise { const priv = privateAPI(this) if (priv._project.isReady()) { - return priv.play(conf) + const ticker = conf?.rafDriver + ? privateAPI(conf.rafDriver).ticker + : getCoreTicker() + return priv.play(conf ?? {}, ticker) } else { if (process.env.NODE_ENV !== 'production') { notify.warning( @@ -289,7 +300,6 @@ export default class TheatreSequence implements ISequence { await resolveAudioBuffer(args) const playbackController = new AudioPlaybackController( - getCoreTicker(), decodedBuffer, audioContext, gainNode, diff --git a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts index 138a318..abe5526 100644 --- a/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/AudioPlaybackController.ts @@ -23,7 +23,6 @@ export default class AudioPlaybackController implements IPlaybackController { _stopPlayCallback: () => void = noop constructor( - private readonly _ticker: Ticker, private readonly _decodedBuffer: AudioBuffer, private readonly _audioContext: AudioContext, private readonly _nodeDestination: AudioNode, @@ -34,7 +33,10 @@ export default class AudioPlaybackController implements IPlaybackController { this._mainGain.connect(this._nodeDestination) } - playDynamicRange(rangeD: Prism): Promise { + playDynamicRange( + rangeD: Prism, + ticker: Ticker, + ): Promise { const deferred = defer() if (this._playing) this.pause() @@ -44,7 +46,7 @@ export default class AudioPlaybackController implements IPlaybackController { const play = () => { stop?.() - stop = this._loopInRange(rangeD.getValue()).stop + stop = this._loopInRange(rangeD.getValue(), ticker).stop } // We're keeping the rangeD hot, so we can read from it on every tick without @@ -61,9 +63,11 @@ export default class AudioPlaybackController implements IPlaybackController { return deferred.promise } - private _loopInRange(range: IPlaybackRange): {stop: () => void} { + private _loopInRange( + range: IPlaybackRange, + ticker: Ticker, + ): {stop: () => void} { const rate = 1 - const ticker = this._ticker let startPos = this.getCurrentPosition() const iterationLength = range[1] - range[0] @@ -152,6 +156,7 @@ export default class AudioPlaybackController implements IPlaybackController { range: IPlaybackRange, rate: number, direction: IPlaybackDirection, + ticker: Ticker, ): Promise { if (this._playing) { this.pause() @@ -159,7 +164,6 @@ export default class AudioPlaybackController implements IPlaybackController { this._playing = true - const ticker = this._ticker let startPos = this.getCurrentPosition() const iterationLength = range[1] - range[0] diff --git a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts index 51fdf1c..b099534 100644 --- a/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts +++ b/theatre/core/src/sequences/playbackControllers/DefaultPlaybackController.ts @@ -23,6 +23,7 @@ export interface IPlaybackController { range: IPlaybackRange, rate: number, direction: IPlaybackDirection, + ticker: Ticker, ): Promise /** @@ -36,7 +37,10 @@ export interface IPlaybackController { * @returns a promise that gets rejected if the playback stopped for whatever reason * */ - playDynamicRange(rangeD: Prism): Promise + playDynamicRange( + rangeD: Prism, + ticker: Ticker, + ): Promise pause(): void } @@ -49,7 +53,7 @@ export default class DefaultPlaybackController implements IPlaybackController { }) readonly statePointer: Pointer - constructor(private readonly _ticker: Ticker) { + constructor() { this.statePointer = this._state.pointer } @@ -86,6 +90,7 @@ export default class DefaultPlaybackController implements IPlaybackController { range: IPlaybackRange, rate: number, direction: IPlaybackDirection, + ticker: Ticker, ): Promise { if (this.playing) { this.pause() @@ -93,7 +98,6 @@ export default class DefaultPlaybackController implements IPlaybackController { this.playing = true - const ticker = this._ticker const iterationLength = range[1] - range[0] { @@ -203,15 +207,16 @@ export default class DefaultPlaybackController implements IPlaybackController { return deferred.promise } - playDynamicRange(rangeD: Prism): Promise { + playDynamicRange( + rangeD: Prism, + ticker: Ticker, + ): Promise { if (this.playing) { this.pause() } this.playing = true - const ticker = this._ticker - const deferred = defer() // We're keeping the rangeD hot, so we can read from it on every tick without diff --git a/theatre/core/src/sheetObjects/TheatreSheetObject.ts b/theatre/core/src/sheetObjects/TheatreSheetObject.ts index 478fe6d..d04dbe0 100644 --- a/theatre/core/src/sheetObjects/TheatreSheetObject.ts +++ b/theatre/core/src/sheetObjects/TheatreSheetObject.ts @@ -1,6 +1,5 @@ import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs' import type {IProject} from '@theatre/core/projects/TheatreProject' -import {getCoreTicker} from '@theatre/core/coreTicker' import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {SheetObjectAddress} from '@theatre/shared/utils/addresses' import SimpleCache from '@theatre/shared/utils/SimpleCache' @@ -18,6 +17,8 @@ import type { } from '@theatre/core/propTypes/internals' import {debounce} from 'lodash-es' import type {DebouncedFunc} from 'lodash-es' +import type {IRafDriver} from '@theatre/core/rafDrivers' +import {onChange} from '@theatre/core/coreExports' export interface ISheetObject< Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps, @@ -70,6 +71,8 @@ export interface ISheetObject< /** * Calls `fn` every time the value of the props change. * + * @param fn - The callback is called every time the value of the props change, plus once at the beginning. + * @param rafDriver - (Optional) The RAF driver to use. Defaults to the core RAF driver. * @returns an Unsubscribe function * * @example @@ -86,7 +89,10 @@ export interface ISheetObject< * // you can call unsubscribe() to stop listening to changes * ``` */ - onValuesChange(fn: (values: this['value']) => void): VoidFn + onValuesChange( + fn: (values: this['value']) => void, + rafDriver?: IRafDriver, + ): VoidFn /** * Sets the initial value of the object. This value overrides the default @@ -157,8 +163,11 @@ export default class TheatreSheetObject< }) } - onValuesChange(fn: (values: this['value']) => void): VoidFn { - return this._valuesPrism().onChange(getCoreTicker(), fn, true) + onValuesChange( + fn: (values: this['value']) => void, + rafDriver?: IRafDriver, + ): VoidFn { + return onChange(this._valuesPrism(), fn, rafDriver) } // internal: Make the deviration keepHot if directly read diff --git a/theatre/studio/src/Studio.ts b/theatre/studio/src/Studio.ts index d3aa260..62431b9 100644 --- a/theatre/studio/src/Studio.ts +++ b/theatre/studio/src/Studio.ts @@ -1,7 +1,7 @@ import Scrub from '@theatre/studio/Scrub' import type {StudioHistoricState} from '@theatre/studio/store/types/historic' import type UI from '@theatre/studio/UI' -import type {Pointer} from '@theatre/dataverse' +import type {Pointer, Ticker} from '@theatre/dataverse' import {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse' import type { CommitOrDiscard, @@ -26,6 +26,7 @@ import shallowEqual from 'shallowequal' import {createStore} from './IDBStorage' import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets' import {notify} from './notify' +import type {RafDriverPrivateAPI} from '@theatre/core/rafDrivers' export type CoreExports = typeof _coreExports @@ -98,6 +99,22 @@ export class Studio { */ private _didWarnAboutNotInitializing = false + /** + * This will be set as soon as `@theatre/core` registers itself on `@theatre/studio` + */ + private _coreBits: CoreBits | undefined + + get ticker(): Ticker { + if (!this._rafDriver) { + throw new Error( + '`studio.ticker` was read before studio.initialize() was called.', + ) + } + return this._rafDriver.ticker + } + + private _rafDriver: RafDriverPrivateAPI | undefined + get atomP() { return this._store.atomP } @@ -126,6 +143,12 @@ export class Studio { } async initialize(opts?: Parameters[0]) { + if (!this._coreBits) { + throw new Error( + `You seem to have imported \`@theatre/studio\` without importing \`@theatre/core\`. Make sure to include an import of \`@theatre/core\` before calling \`studio.initializer()\`.`, + ) + } + if (this._initializeFnCalled) { console.log( `\`studio.initialize()\` is already called. Ignoring subsequent calls.`, @@ -151,6 +174,29 @@ export class Studio { storeOpts.usePersistentStorage = false } + if (opts?.__experimental_rafDriver) { + if ( + opts.__experimental_rafDriver.type !== 'Theatre_RafDriver_PublicAPI' + ) { + throw new Error( + 'parameter `rafDriver` in `studio.initialize({__experimental_rafDriver})` must be either be undefined, or the return type of core.createRafDriver()', + ) + } + + const rafDriverPrivateApi = this._coreBits.privateAPI( + opts.__experimental_rafDriver, + ) + if (!rafDriverPrivateApi) { + // TODO - need to educate the user about this edge case + throw new Error( + 'parameter `rafDriver` in `studio.initialize({__experimental_rafDriver})` seems to come from a different version of `@theatre/core` than the version that is attached to `@theatre/studio`', + ) + } + this._rafDriver = rafDriverPrivateApi + } else { + this._rafDriver = this._coreBits.getCoreRafDriver() + } + try { await this._store.initialize(storeOpts) } catch (e) { @@ -187,6 +233,7 @@ export class Studio { } setCoreBits(coreBits: CoreBits) { + this._coreBits = coreBits this._corePrivateApi = coreBits.privateAPI this._coreAtom.setByPointer((p) => p.core, coreBits.coreExports) this._setProjectsP(coreBits.projectsP) diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts index 7dc56e5..7a2d741 100644 --- a/theatre/studio/src/TheatreStudio.ts +++ b/theatre/studio/src/TheatreStudio.ts @@ -1,5 +1,4 @@ -import type {IProject, ISheet, ISheetObject} from '@theatre/core' -import studioTicker from '@theatre/studio/studioTicker' +import type {IProject, IRafDriver, ISheet, ISheetObject} from '@theatre/core' import type {Prism, Pointer} from '@theatre/dataverse' import {prism} from '@theatre/dataverse' import SimpleCache from '@theatre/shared/utils/SimpleCache' @@ -173,6 +172,8 @@ export interface _StudioInitializeOpts { * Default: true */ usePersistentStorage?: boolean + + __experimental_rafDriver?: IRafDriver | undefined } /** @@ -440,7 +441,9 @@ export default class TheatreStudio implements IStudio { } onSelectionChange(fn: (s: (ISheetObject | ISheet)[]) => void): VoidFn { - return this._getSelectionPrism().onChange(studioTicker, fn, true) + const studio = getStudio() + + return this._getSelectionPrism().onChange(studio.ticker, fn, true) } get selection(): Array { diff --git a/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts b/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts index 022fb54..4dbfd71 100644 --- a/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts +++ b/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts @@ -110,6 +110,7 @@ export default function useKeyboardShortcuts() { const playbackPromise = seq.playDynamicRange( prism(() => val(controlledPlaybackStateD).range), + getStudio().ticker, ) const playbackStateBox = getPlaybackStateBox(seq) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/FrameGrid.tsx b/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/FrameGrid.tsx index 6ae424f..23a6d9c 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/FrameGrid.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/FrameGrid.tsx @@ -5,7 +5,7 @@ import {prism, val} from '@theatre/dataverse' import React, {useLayoutEffect, useMemo, useRef, useState} from 'react' import styled from 'styled-components' import createGrid from './createGrid' -import studioTicker from '@theatre/studio/studioTicker' +import getStudio from '@theatre/studio/getStudio' const Container = styled.div` position: absolute; @@ -76,7 +76,7 @@ const FrameGrid: React.FC<{ snapToGrid: (n: number) => sequence.closestGridPosition(n), } }).onChange( - studioTicker, + getStudio().ticker, (p) => { ctx.save() ctx.scale(ratio!, ratio!) diff --git a/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/StampsGrid.tsx b/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/StampsGrid.tsx index 0446484..9831543 100644 --- a/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/StampsGrid.tsx +++ b/theatre/studio/src/panels/SequenceEditorPanel/FrameGrid/StampsGrid.tsx @@ -6,7 +6,7 @@ import {darken} from 'polished' import React, {useLayoutEffect, useRef, useState} from 'react' import styled from 'styled-components' import createGrid from './createGrid' -import studioTicker from '@theatre/studio/studioTicker' +import getStudio from '@theatre/studio/getStudio' const Container = styled.div` position: absolute; @@ -86,7 +86,7 @@ const StampsGrid: React.FC<{ sequencePositionFormatter: sequence.positionFormatter, snapToGrid: (n: number) => sequence.closestGridPosition(n), } - }).onChange(studioTicker, drawStamps, true) + }).onChange(getStudio().ticker, drawStamps, true) }, [fullSecondStampsContainer, width, layoutP]) return ( diff --git a/theatre/studio/src/studioTicker.ts b/theatre/studio/src/studioTicker.ts deleted file mode 100644 index 45dffe0..0000000 --- a/theatre/studio/src/studioTicker.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {Ticker} from '@theatre/dataverse' - -const studioTicker = Ticker.raf - -export default studioTicker