Feature: Custom RAFDrivers (#374)

Co-authored-by: Pete Feltham <dev@felthy.com>
Co-authored-by: Andrew Prifer <andrew.prifer@gmail.com>
This commit is contained in:
Aria 2023-01-14 15:57:13 +02:00 committed by Aria Minaei
parent 80e79499df
commit d649858529
26 changed files with 464 additions and 145 deletions

View file

@ -1,5 +1,5 @@
// import studio from '@theatre/studio' // import studio from '@theatre/studio'
import {getProject} from '@theatre/core' import {createRafDriver, getProject} from '@theatre/core'
import type { import type {
UnknownShorthandCompoundProps, UnknownShorthandCompoundProps,
ISheet, ISheet,
@ -7,11 +7,11 @@ import type {
} from '@theatre/core' } from '@theatre/core'
// @ts-ignore // @ts-ignore
import benchProject1State from './Bench project 1.theatre-project-state.json' import benchProject1State from './Bench project 1.theatre-project-state.json'
import {Ticker} from '@theatre/dataverse' import {setCoreRafDriver} from '@theatre/core/coreTicker'
import {setCoreTicker} from '@theatre/core/coreTicker'
const ticker = new Ticker() const driver = createRafDriver({name: 'BenchmarkRafDriver'})
setCoreTicker(ticker)
setCoreRafDriver(driver)
// studio.initialize({}) // studio.initialize({})
@ -79,7 +79,7 @@ async function test1() {
} }
function iterateOnSequence() { function iterateOnSequence() {
ticker.tick() driver.tick(performance.now())
const startTime = performance.now() const startTime = performance.now()
for (let i = 1; i < CONFIG.numberOfIterations; i++) { for (let i = 1; i < CONFIG.numberOfIterations; i++) {
onChangeEventsFired = 0 onChangeEventsFired = 0
@ -87,7 +87,7 @@ async function test1() {
for (const sheet of sheets) { for (const sheet of sheets) {
sheet.sequence.position = pos sheet.sequence.position = pos
} }
ticker.tick() driver.tick(performance.now())
if (onChangeEventsFired !== objects.length) { if (onChangeEventsFired !== objects.length) {
console.info( console.info(
`Expected ${objects.length} onChange events, got ${onChangeEventsFired}`, `Expected ${objects.length} onChange events, got ${onChangeEventsFired}`,

View file

@ -1,44 +1,36 @@
type ICallback = (t: number) => void type ICallback = (t: number) => void
function createRafTicker() {
const ticker = new Ticker()
if (typeof window !== 'undefined') {
/** /**
* @remarks * The number of ticks that can pass without any scheduled callbacks before the Ticker goes dormant. This is to prevent
* TODO users should also be able to define their own ticker. * 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 onAnimationFrame = (t: number) => { const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant
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 Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an * 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. * external scheduling strategy, e.g. a raf.
*/ */
export default class Ticker { export default class Ticker {
/** Get a shared `requestAnimationFrame` ticker. */
static get raf(): Ticker {
if (!rafTicker) {
rafTicker = createRafTicker()
}
return rafTicker
}
private _scheduledForThisOrNextTick: Set<ICallback> private _scheduledForThisOrNextTick: Set<ICallback>
private _scheduledForNextTick: Set<ICallback> private _scheduledForNextTick: Set<ICallback>
private _timeAtCurrentTick: number private _timeAtCurrentTick: number
private _ticking: boolean = false 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. * Counts up for every tick executed.
* Internally, this is used to measure ticks per second. * Internally, this is used to measure ticks per second.
@ -48,7 +40,18 @@ export default class Ticker {
*/ */
public __ticks = 0 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._scheduledForThisOrNextTick = new Set()
this._scheduledForNextTick = new Set() this._scheduledForNextTick = new Set()
this._timeAtCurrentTick = 0 this._timeAtCurrentTick = 0
@ -70,6 +73,9 @@ export default class Ticker {
*/ */
onThisOrNextTick(fn: ICallback) { onThisOrNextTick(fn: ICallback) {
this._scheduledForThisOrNextTick.add(fn) this._scheduledForThisOrNextTick.add(fn)
if (this._dormant) {
this._goActive()
}
} }
/** /**
@ -82,6 +88,9 @@ export default class Ticker {
*/ */
onNextTick(fn: ICallback) { onNextTick(fn: ICallback) {
this._scheduledForNextTick.add(fn) this._scheduledForNextTick.add(fn)
if (this._dormant) {
this._goActive()
}
} }
/** /**
@ -116,6 +125,19 @@ export default class Ticker {
} else return performance.now() } 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. * Triggers a tick which starts executing the callbacks scheduled for this tick.
* *
@ -135,6 +157,19 @@ export default class Ticker {
this.__ticks++ 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._ticking = true
this._timeAtCurrentTick = t this._timeAtCurrentTick = t
for (const v of this._scheduledForNextTick) { for (const v of this._scheduledForNextTick) {

View file

@ -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 (
<div
style={{
height: '100vh',
}}
>
<Canvas
dpr={[1.5, 2]}
linear
gl={{preserveDrawingBuffer: true}}
frameloop="demand"
>
<SheetProvider sheet={getProject('Space').sheet('Scene')}>
<RafDriverProvider driver={props.rafDriver}>
<ambientLight intensity={0.75} />
<EditablePoints theatreKey="points">
<torusKnotGeometry args={[1, 0.3, 128, 64]} />
<meshNormalMaterial />
</EditablePoints>
</RafDriverProvider>
</SheetProvider>
</Canvas>
</div>
)
}
export default App

View file

@ -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(<App rafDriver={rafDriver} />, document.getElementById('root'))

View file

@ -4,7 +4,8 @@ import App from './App'
import type {ToolsetConfig} from '@theatre/studio' import type {ToolsetConfig} from '@theatre/studio'
import studio from '@theatre/studio' import studio from '@theatre/studio'
import extension from '@theatre/r3f/dist/extension' 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 * Let's take a look at how we can use `prism`, `Ticker`, and `val` from Theatre.js's Dataverse library
@ -28,7 +29,8 @@ studio.extend({
global(set, studio) { global(set, studio) {
const exampleBox = new Atom('mobile') const exampleBox = new Atom('mobile')
const untapFn = prism<ToolsetConfig>(() => [ const untapFn = onChange(
prism<ToolsetConfig>(() => [
{ {
type: 'Switch', type: 'Switch',
value: val(exampleBox.prism), value: val(exampleBox.prism),
@ -54,15 +56,12 @@ studio.extend({
console.log('hello') console.log('hello')
}, },
}, },
]) ]),
// listen to changes to this prism using the requestAnimationFrame shared ticker
.onChange(
Ticker.raf,
(value) => { (value) => {
set(value) set(value)
}, },
true,
) )
// listen to changes to this prism using the requestAnimationFrame shared ticker
return untapFn return untapFn
}, },

View file

@ -3,14 +3,16 @@ import ReactDOM from 'react-dom'
import App from './App' import App from './App'
import studio from '@theatre/studio' import studio from '@theatre/studio'
import extension from '@theatre/r3f/dist/extension' 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.extend(extension)
studio.initialize() studio.initialize()
ReactDOM.render(<App />, document.getElementById('root')) ReactDOM.render(<App />, document.getElementById('root'))
const raf = Ticker.raf const raf = studioPrivate.ticker
// Show "ticks per second" information in performance measurements using the User Timing API // 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 // See https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API

View file

@ -1,11 +1,12 @@
import SnapshotEditor from './components/SnapshotEditor' import SnapshotEditor from './components/SnapshotEditor'
import type {IExtension} from '@theatre/studio' import type {IExtension} from '@theatre/studio'
import {prism, Ticker, val} from '@theatre/dataverse' import {prism, val} from '@theatre/dataverse'
import {getEditorSheetObject} from './editorStuff' import {getEditorSheetObject} from './editorStuff'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import React from 'react' import React from 'react'
import type {ToolsetConfig} from '@theatre/studio' import type {ToolsetConfig} from '@theatre/studio'
import useExtensionStore from './useExtensionStore' import useExtensionStore from './useExtensionStore'
import {onChange} from '@theatre/core'
const io5CameraOutline = `<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Camera</title><path d="M350.54 148.68l-26.62-42.06C318.31 100.08 310.62 96 302 96h-92c-8.62 0-16.31 4.08-21.92 10.62l-26.62 42.06C155.85 155.23 148.62 160 140 160H80a32 32 0 00-32 32v192a32 32 0 0032 32h352a32 32 0 0032-32V192a32 32 0 00-32-32h-59c-8.65 0-16.85-4.77-22.46-11.32z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="272" r="80" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M124 158v-22h-24v22"/></svg>` const io5CameraOutline = `<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Camera</title><path d="M350.54 148.68l-26.62-42.06C318.31 100.08 310.62 96 302 96h-92c-8.62 0-16.31 4.08-21.92 10.62l-26.62 42.06C155.85 155.23 148.62 160 140 160H80a32 32 0 00-32 32v192a32 32 0 0032 32h352a32 32 0 0032-32V192a32 32 0 00-32-32h-59c-8.65 0-16.85-4.77-22.46-11.32z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="272" r="80" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M124 158v-22h-24v22"/></svg>`
const gameIconMove = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 34.47l-90.51 90.51h67.883v108.393H124.98V165.49L34.47 256l90.51 90.51v-67.883h108.393V387.02H165.49L256 477.53l90.51-90.51h-67.883V278.627H387.02v67.883L477.53 256l-90.51-90.51v67.883H278.627V124.98h67.883L256 34.47z"/></svg>` const gameIconMove = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 34.47l-90.51 90.51h67.883v108.393H124.98V165.49L34.47 256l90.51 90.51v-67.883h108.393V387.02H165.49L256 477.53l90.51-90.51h-67.883V278.627H387.02v67.883L477.53 256l-90.51-90.51v67.883H278.627V124.98h67.883L256 34.47z"/></svg>`
@ -35,13 +36,9 @@ const r3fExtension: IExtension = {
}, },
] ]
}) })
return calc.onChange( return onChange(calc, () => {
Ticker.raf,
() => {
set(calc.getValue()) set(calc.getValue())
}, })
true,
)
}, },
'snapshot-editor': (set, studio) => { 'snapshot-editor': (set, studio) => {
const {createSnapshot} = useExtensionStore.getState() const {createSnapshot} = useExtensionStore.getState()
@ -140,13 +137,9 @@ const r3fExtension: IExtension = {
}, },
] ]
}) })
return calc.onChange( return onChange(calc, () => {
Ticker.raf,
() => {
set(calc.getValue()) set(calc.getValue())
}, })
true,
)
}, },
}, },
panes: [ panes: [

View file

@ -21,6 +21,10 @@ export {
export {makeStoreKey as __private_makeStoreKey} from './main/utils' export {makeStoreKey as __private_makeStoreKey} from './main/utils'
export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider' export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider'
export {
default as RafDriverProvider,
useCurrentRafDriver,
} from './main/RafDriverProvider'
export {refreshSnapshot} from './main/utils' export {refreshSnapshot} from './main/utils'
export {default as RefreshSnapshot} from './main/RefreshSnapshot' export {default as RefreshSnapshot} from './main/RefreshSnapshot'
export * from './drei' export * from './drei'

View file

@ -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 <RafDriverProvider deriver={driver}> has an invalid value`,
)
}
}, [driver])
return <ctx.Provider value={{rafDriver: driver}}>{children}</ctx.Provider>
}
export default RafDriverProvider

View file

@ -11,6 +11,7 @@ import {makeStoreKey} from './utils'
import type {$FixMe, $IntentionalAny} from '../types' import type {$FixMe, $IntentionalAny} from '../types'
import type {ISheetObject} from '@theatre/core' import type {ISheetObject} from '@theatre/core'
import {notify} from '@theatre/core' import {notify} from '@theatre/core'
import {useCurrentRafDriver} from './RafDriverProvider'
const createEditable = <Keys extends keyof JSX.IntrinsicElements>( const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
config: EditableFactoryConfig, config: EditableFactoryConfig,
@ -74,6 +75,7 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
const objectRef = useRef<JSX.IntrinsicElements[U]>() const objectRef = useRef<JSX.IntrinsicElements[U]>()
const sheet = useCurrentSheet()! const sheet = useCurrentSheet()!
const rafDriver = useCurrentRafDriver()
const [sheetObject, setSheetObject] = useState< const [sheetObject, setSheetObject] = useState<
undefined | ISheetObject<$FixMe> 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) setFromTheatre(sheetObject.value)
const untap = sheetObject.onValuesChange(setFromTheatre) const unsubscribe = sheetObject.onValuesChange(
setFromTheatre,
rafDriver,
)
return () => { return () => {
untap() unsubscribe()
sheetObject.sheet.detachObject(theatreKey) sheetObject.sheet.detachObject(theatreKey)
allRegisteredObjects.delete(sheetObject) allRegisteredObjects.delete(sheetObject)
editorStore.getState().removeEditable(storeKey) editorStore.getState().removeEditable(storeKey)
} }
}, [sheetObject]) }, [sheetObject, rafDriver])
return ( return (
// @ts-ignore // @ts-ignore

View file

@ -2,11 +2,13 @@ import type {Studio} from '@theatre/studio/Studio'
import projectsSingleton from './projects/projectsSingleton' import projectsSingleton from './projects/projectsSingleton'
import {privateAPI} from './privateAPIs' import {privateAPI} from './privateAPIs'
import * as coreExports from './coreExports' import * as coreExports from './coreExports'
import {getCoreRafDriver} from './coreTicker'
export type CoreBits = { export type CoreBits = {
projectsP: typeof projectsSingleton.atom.pointer.projects projectsP: typeof projectsSingleton.atom.pointer.projects
privateAPI: typeof privateAPI privateAPI: typeof privateAPI
coreExports: typeof coreExports coreExports: typeof coreExports
getCoreRafDriver: typeof getCoreRafDriver
} }
export default class CoreBundle { export default class CoreBundle {
@ -30,6 +32,7 @@ export default class CoreBundle {
projectsP: projectsSingleton.atom.pointer.projects, projectsP: projectsSingleton.atom.pointer.projects,
privateAPI: privateAPI, privateAPI: privateAPI,
coreExports, coreExports,
getCoreRafDriver,
} }
callback(bits) callback(bits)

View file

@ -8,15 +8,19 @@ import {InvalidArgumentError} from '@theatre/shared/utils/errors'
import {validateName} from '@theatre/shared/utils/sanitizers' import {validateName} from '@theatre/shared/utils/sanitizers'
import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue' import userReadableTypeOfValue from '@theatre/shared/utils/userReadableTypeOfValue'
import deepEqual from 'fast-deep-equal' 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 {isPointer} from '@theatre/dataverse'
import {isPrism, pointerToPrism} from '@theatre/dataverse' import {isPrism, pointerToPrism} from '@theatre/dataverse'
import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import type {ProjectId} from '@theatre/shared/utils/ids' import type {ProjectId} from '@theatre/shared/utils/ids'
import {_coreLogger} from './_coreLogger' import {_coreLogger} from './_coreLogger'
import {getCoreTicker} from './coreTicker' import {getCoreTicker} from './coreTicker'
import type {IRafDriver} from './rafDrivers'
import {privateAPI} from './privateAPIs'
export {notify} from '@theatre/shared/notify' export {notify} from '@theatre/shared/notify'
export {types} 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. * 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 * setTimeout(usubscribe, 10000) // stop listening to changes after 10 seconds
* ``` * ```
*/ */
export function onChange<P extends PointerType<$IntentionalAny>>( export function onChange<
P extends PointerType<$IntentionalAny> | Prism<$IntentionalAny>,
>(
pointer: P, pointer: P,
callback: (value: P extends PointerType<infer T> ? T : unknown) => void, callback: (
value: P extends PointerType<infer T>
? T
: P extends Prism<infer T>
? T
: unknown,
) => void,
driver?: IRafDriver,
): VoidFn { ): VoidFn {
const ticker = driver ? privateAPI(driver).ticker : getCoreTicker()
if (isPointer(pointer)) { if (isPointer(pointer)) {
const pr = pointerToPrism(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)) { } else if (isPrism(pointer)) {
return pointer.onChange(getCoreTicker(), callback as $IntentionalAny, true) return pointer.onChange(ticker, callback as $IntentionalAny, true)
} else { } else {
throw new Error( throw new Error(
`Called onChange(p) where p is neither a pointer nor a prism.`, `Called onChange(p) where p is neither a pointer nor a prism.`,

View file

@ -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 function createBasicRafDriver(): IRafDriver {
let rafId: number | null = null
export function setCoreTicker(ticker: Ticker) { const start = (): void => {
if (coreTicker) { if (typeof window !== 'undefined') {
throw new Error(`coreTicker is already set`) const onAnimationFrame = (t: number) => {
driver.tick(t)
rafId = window.requestAnimationFrame(onAnimationFrame)
} }
coreTicker = ticker rafId = window.requestAnimationFrame(onAnimationFrame)
} else {
driver.tick(0)
setTimeout(() => driver.tick(1), 0)
}
}
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 { export function getCoreTicker(): Ticker {
if (!coreTicker) { return getCoreRafDriver().ticker
coreTicker = Ticker.raf
} }
return coreTicker
export function setCoreRafDriver(driver: IRafDriver) {
if (coreRafDriver) {
throw new Error(`\`setCoreRafDriver()\` is already called.`)
}
const driverPrivateApi = privateAPI(driver)
coreRafDriver = driverPrivateApi
} }

View file

@ -8,6 +8,7 @@ import type Sheet from '@theatre/core/sheets/Sheet'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {UnknownShorthandCompoundProps} from './propTypes/internals' import type {UnknownShorthandCompoundProps} from './propTypes/internals'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import type {IRafDriver, RafDriverPrivateAPI} from './rafDrivers'
const publicAPIToPrivateAPIMap = new WeakMap() const publicAPIToPrivateAPIMap = new WeakMap()
@ -24,6 +25,8 @@ export function privateAPI<P extends {type: string}>(
? SheetObject ? SheetObject
: P extends ISequence : P extends ISequence
? Sequence ? Sequence
: P extends IRafDriver
? RafDriverPrivateAPI
: never { : never {
return publicAPIToPrivateAPIMap.get(pub) return publicAPIToPrivateAPIMap.get(pub)
} }
@ -35,6 +38,7 @@ export function privateAPI<P extends {type: string}>(
export function setPrivateAPI(pub: IProject, priv: Project): void export function setPrivateAPI(pub: IProject, priv: Project): void
export function setPrivateAPI(pub: ISheet, priv: Sheet): void export function setPrivateAPI(pub: ISheet, priv: Sheet): void
export function setPrivateAPI(pub: ISequence, priv: Sequence): void export function setPrivateAPI(pub: ISequence, priv: Sequence): void
export function setPrivateAPI(pub: IRafDriver, priv: RafDriverPrivateAPI): void
export function setPrivateAPI<Props extends UnknownShorthandCompoundProps>( export function setPrivateAPI<Props extends UnknownShorthandCompoundProps>(
pub: ISheetObject<Props>, pub: ISheetObject<Props>,
priv: SheetObject, priv: SheetObject,

View file

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

View file

@ -3,7 +3,7 @@ import type Sheet from '@theatre/core/sheets/Sheet'
import type {SequenceAddress} from '@theatre/shared/utils/addresses' 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 {Prism, Pointer} from '@theatre/dataverse' import type {Prism, Pointer, Ticker} from '@theatre/dataverse'
import {Atom} from '@theatre/dataverse' import {Atom} from '@theatre/dataverse'
import {pointer} from '@theatre/dataverse' import {pointer} from '@theatre/dataverse'
import {prism, val} 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 {ILogger} from '@theatre/shared/logger'
import type {ISequence} from '..' import type {ISequence} from '..'
import {notify} from '@theatre/shared/notify' import {notify} from '@theatre/shared/notify'
import {getCoreTicker} from '@theatre/core/coreTicker'
export type IPlaybackRange = [from: number, to: number] export type IPlaybackRange = [from: number, to: number]
@ -64,7 +63,7 @@ export default class Sequence {
this.publicApi = new TheatreSequence(this) this.publicApi = new TheatreSequence(this)
this._playbackControllerBox = new Atom( this._playbackControllerBox = new Atom(
playbackController ?? new DefaultPlaybackController(getCoreTicker()), playbackController ?? new DefaultPlaybackController(),
) )
this._prismOfStatePointer = prism( this._prismOfStatePointer = prism(
@ -195,17 +194,23 @@ export default class Sequence {
* @returns a promise that gets rejected if the playback stopped for whatever reason * @returns a promise that gets rejected if the playback stopped for whatever reason
* *
*/ */
playDynamicRange(rangeD: Prism<IPlaybackRange>): Promise<unknown> { playDynamicRange(
return this._playbackControllerBox.getState().playDynamicRange(rangeD) rangeD: Prism<IPlaybackRange>,
ticker: Ticker,
): Promise<unknown> {
return this._playbackControllerBox
.getState()
.playDynamicRange(rangeD, ticker)
} }
async play( async play(
conf?: Partial<{ conf: Partial<{
iterationCount: number iterationCount: number
range: IPlaybackRange range: IPlaybackRange
rate: number rate: number
direction: IPlaybackDirection direction: IPlaybackDirection
}>, }>,
ticker: Ticker,
): Promise<boolean> { ): Promise<boolean> {
const sequenceDuration = this.length const sequenceDuration = this.length
const range: IPlaybackRange = 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]], [range[0], range[1]],
rate, rate,
direction, direction,
ticker,
) )
} }
@ -328,10 +334,11 @@ To fix this, either set \`conf.range[1]\` to be less the duration of the sequenc
range: IPlaybackRange, range: IPlaybackRange,
rate: number, rate: number,
direction: IPlaybackDirection, direction: IPlaybackDirection,
ticker: Ticker,
): Promise<boolean> { ): Promise<boolean> {
return this._playbackControllerBox return this._playbackControllerBox
.getState() .getState()
.play(iterationCount, range, rate, direction) .play(iterationCount, range, rate, direction, ticker)
} }
pause() { pause() {

View file

@ -6,6 +6,7 @@ import AudioPlaybackController from './playbackControllers/AudioPlaybackControll
import {getCoreTicker} from '@theatre/core/coreTicker' import {getCoreTicker} from '@theatre/core/coreTicker'
import type {Pointer} from '@theatre/dataverse' import type {Pointer} from '@theatre/dataverse'
import {notify} from '@theatre/shared/notify' import {notify} from '@theatre/shared/notify'
import type {IRafDriver} from '@theatre/core/rafDrivers'
interface IAttachAudioArgs { interface IAttachAudioArgs {
/** /**
@ -76,6 +77,12 @@ export interface ISequence {
* The direction of the playback. Similar to CSS's animation-direction * The direction of the playback. Similar to CSS's animation-direction
*/ */
direction?: IPlaybackDirection 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<boolean> }): Promise<boolean>
/** /**
@ -233,11 +240,15 @@ export default class TheatreSequence implements ISequence {
range: IPlaybackRange range: IPlaybackRange
rate: number rate: number
direction: IPlaybackDirection direction: IPlaybackDirection
rafDriver: IRafDriver
}>, }>,
): Promise<boolean> { ): Promise<boolean> {
const priv = privateAPI(this) const priv = privateAPI(this)
if (priv._project.isReady()) { if (priv._project.isReady()) {
return priv.play(conf) const ticker = conf?.rafDriver
? privateAPI(conf.rafDriver).ticker
: getCoreTicker()
return priv.play(conf ?? {}, ticker)
} else { } else {
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
notify.warning( notify.warning(
@ -289,7 +300,6 @@ export default class TheatreSequence implements ISequence {
await resolveAudioBuffer(args) await resolveAudioBuffer(args)
const playbackController = new AudioPlaybackController( const playbackController = new AudioPlaybackController(
getCoreTicker(),
decodedBuffer, decodedBuffer,
audioContext, audioContext,
gainNode, gainNode,

View file

@ -23,7 +23,6 @@ export default class AudioPlaybackController implements IPlaybackController {
_stopPlayCallback: () => void = noop _stopPlayCallback: () => void = noop
constructor( constructor(
private readonly _ticker: Ticker,
private readonly _decodedBuffer: AudioBuffer, private readonly _decodedBuffer: AudioBuffer,
private readonly _audioContext: AudioContext, private readonly _audioContext: AudioContext,
private readonly _nodeDestination: AudioNode, private readonly _nodeDestination: AudioNode,
@ -34,7 +33,10 @@ export default class AudioPlaybackController implements IPlaybackController {
this._mainGain.connect(this._nodeDestination) this._mainGain.connect(this._nodeDestination)
} }
playDynamicRange(rangeD: Prism<IPlaybackRange>): Promise<unknown> { playDynamicRange(
rangeD: Prism<IPlaybackRange>,
ticker: Ticker,
): Promise<unknown> {
const deferred = defer<boolean>() const deferred = defer<boolean>()
if (this._playing) this.pause() if (this._playing) this.pause()
@ -44,7 +46,7 @@ export default class AudioPlaybackController implements IPlaybackController {
const play = () => { const play = () => {
stop?.() 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 // 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 return deferred.promise
} }
private _loopInRange(range: IPlaybackRange): {stop: () => void} { private _loopInRange(
range: IPlaybackRange,
ticker: Ticker,
): {stop: () => void} {
const rate = 1 const rate = 1
const ticker = this._ticker
let startPos = this.getCurrentPosition() let startPos = this.getCurrentPosition()
const iterationLength = range[1] - range[0] const iterationLength = range[1] - range[0]
@ -152,6 +156,7 @@ export default class AudioPlaybackController implements IPlaybackController {
range: IPlaybackRange, range: IPlaybackRange,
rate: number, rate: number,
direction: IPlaybackDirection, direction: IPlaybackDirection,
ticker: Ticker,
): Promise<boolean> { ): Promise<boolean> {
if (this._playing) { if (this._playing) {
this.pause() this.pause()
@ -159,7 +164,6 @@ export default class AudioPlaybackController implements IPlaybackController {
this._playing = true this._playing = true
const ticker = this._ticker
let startPos = this.getCurrentPosition() let startPos = this.getCurrentPosition()
const iterationLength = range[1] - range[0] const iterationLength = range[1] - range[0]

View file

@ -23,6 +23,7 @@ export interface IPlaybackController {
range: IPlaybackRange, range: IPlaybackRange,
rate: number, rate: number,
direction: IPlaybackDirection, direction: IPlaybackDirection,
ticker: Ticker,
): Promise<boolean> ): Promise<boolean>
/** /**
@ -36,7 +37,10 @@ export interface IPlaybackController {
* @returns a promise that gets rejected if the playback stopped for whatever reason * @returns a promise that gets rejected if the playback stopped for whatever reason
* *
*/ */
playDynamicRange(rangeD: Prism<IPlaybackRange>): Promise<unknown> playDynamicRange(
rangeD: Prism<IPlaybackRange>,
ticker: Ticker,
): Promise<unknown>
pause(): void pause(): void
} }
@ -49,7 +53,7 @@ export default class DefaultPlaybackController implements IPlaybackController {
}) })
readonly statePointer: Pointer<IPlaybackState> readonly statePointer: Pointer<IPlaybackState>
constructor(private readonly _ticker: Ticker) { constructor() {
this.statePointer = this._state.pointer this.statePointer = this._state.pointer
} }
@ -86,6 +90,7 @@ export default class DefaultPlaybackController implements IPlaybackController {
range: IPlaybackRange, range: IPlaybackRange,
rate: number, rate: number,
direction: IPlaybackDirection, direction: IPlaybackDirection,
ticker: Ticker,
): Promise<boolean> { ): Promise<boolean> {
if (this.playing) { if (this.playing) {
this.pause() this.pause()
@ -93,7 +98,6 @@ export default class DefaultPlaybackController implements IPlaybackController {
this.playing = true this.playing = true
const ticker = this._ticker
const iterationLength = range[1] - range[0] const iterationLength = range[1] - range[0]
{ {
@ -203,15 +207,16 @@ export default class DefaultPlaybackController implements IPlaybackController {
return deferred.promise return deferred.promise
} }
playDynamicRange(rangeD: Prism<IPlaybackRange>): Promise<unknown> { playDynamicRange(
rangeD: Prism<IPlaybackRange>,
ticker: Ticker,
): Promise<unknown> {
if (this.playing) { if (this.playing) {
this.pause() this.pause()
} }
this.playing = true this.playing = true
const ticker = this._ticker
const deferred = defer<boolean>() const deferred = defer<boolean>()
// We're keeping the rangeD hot, so we can read from it on every tick without // We're keeping the rangeD hot, so we can read from it on every tick without

View file

@ -1,6 +1,5 @@
import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs' import {privateAPI, setPrivateAPI} from '@theatre/core/privateAPIs'
import type {IProject} from '@theatre/core/projects/TheatreProject' import type {IProject} from '@theatre/core/projects/TheatreProject'
import {getCoreTicker} from '@theatre/core/coreTicker'
import type {ISheet} from '@theatre/core/sheets/TheatreSheet' import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
import type {SheetObjectAddress} from '@theatre/shared/utils/addresses' import type {SheetObjectAddress} from '@theatre/shared/utils/addresses'
import SimpleCache from '@theatre/shared/utils/SimpleCache' import SimpleCache from '@theatre/shared/utils/SimpleCache'
@ -18,6 +17,8 @@ import type {
} from '@theatre/core/propTypes/internals' } from '@theatre/core/propTypes/internals'
import {debounce} from 'lodash-es' import {debounce} from 'lodash-es'
import type {DebouncedFunc} 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< export interface ISheetObject<
Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps, Props extends UnknownShorthandCompoundProps = UnknownShorthandCompoundProps,
@ -70,6 +71,8 @@ export interface ISheetObject<
/** /**
* Calls `fn` every time the value of the props change. * 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 * @returns an Unsubscribe function
* *
* @example * @example
@ -86,7 +89,10 @@ export interface ISheetObject<
* // you can call unsubscribe() to stop listening to changes * // 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 * 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 { onValuesChange(
return this._valuesPrism().onChange(getCoreTicker(), fn, true) fn: (values: this['value']) => void,
rafDriver?: IRafDriver,
): VoidFn {
return onChange(this._valuesPrism(), fn, rafDriver)
} }
// internal: Make the deviration keepHot if directly read // internal: Make the deviration keepHot if directly read

View file

@ -1,7 +1,7 @@
import Scrub from '@theatre/studio/Scrub' import Scrub from '@theatre/studio/Scrub'
import type {StudioHistoricState} from '@theatre/studio/store/types/historic' import type {StudioHistoricState} from '@theatre/studio/store/types/historic'
import type UI from '@theatre/studio/UI' 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 {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse'
import type { import type {
CommitOrDiscard, CommitOrDiscard,
@ -26,6 +26,7 @@ import shallowEqual from 'shallowequal'
import {createStore} from './IDBStorage' import {createStore} from './IDBStorage'
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets' import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets'
import {notify} from './notify' import {notify} from './notify'
import type {RafDriverPrivateAPI} from '@theatre/core/rafDrivers'
export type CoreExports = typeof _coreExports export type CoreExports = typeof _coreExports
@ -98,6 +99,22 @@ export class Studio {
*/ */
private _didWarnAboutNotInitializing = false 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() { get atomP() {
return this._store.atomP return this._store.atomP
} }
@ -126,6 +143,12 @@ export class Studio {
} }
async initialize(opts?: Parameters<IStudio['initialize']>[0]) { async initialize(opts?: Parameters<IStudio['initialize']>[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) { if (this._initializeFnCalled) {
console.log( console.log(
`\`studio.initialize()\` is already called. Ignoring subsequent calls.`, `\`studio.initialize()\` is already called. Ignoring subsequent calls.`,
@ -151,6 +174,29 @@ export class Studio {
storeOpts.usePersistentStorage = false 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 { try {
await this._store.initialize(storeOpts) await this._store.initialize(storeOpts)
} catch (e) { } catch (e) {
@ -187,6 +233,7 @@ export class Studio {
} }
setCoreBits(coreBits: CoreBits) { setCoreBits(coreBits: CoreBits) {
this._coreBits = coreBits
this._corePrivateApi = coreBits.privateAPI this._corePrivateApi = coreBits.privateAPI
this._coreAtom.setByPointer((p) => p.core, coreBits.coreExports) this._coreAtom.setByPointer((p) => p.core, coreBits.coreExports)
this._setProjectsP(coreBits.projectsP) this._setProjectsP(coreBits.projectsP)

View file

@ -1,5 +1,4 @@
import type {IProject, ISheet, ISheetObject} from '@theatre/core' import type {IProject, IRafDriver, ISheet, ISheetObject} from '@theatre/core'
import studioTicker from '@theatre/studio/studioTicker'
import type {Prism, Pointer} from '@theatre/dataverse' import type {Prism, Pointer} from '@theatre/dataverse'
import {prism} from '@theatre/dataverse' import {prism} from '@theatre/dataverse'
import SimpleCache from '@theatre/shared/utils/SimpleCache' import SimpleCache from '@theatre/shared/utils/SimpleCache'
@ -173,6 +172,8 @@ export interface _StudioInitializeOpts {
* Default: true * Default: true
*/ */
usePersistentStorage?: boolean usePersistentStorage?: boolean
__experimental_rafDriver?: IRafDriver | undefined
} }
/** /**
@ -440,7 +441,9 @@ export default class TheatreStudio implements IStudio {
} }
onSelectionChange(fn: (s: (ISheetObject | ISheet)[]) => void): VoidFn { 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<ISheetObject | ISheet> { get selection(): Array<ISheetObject | ISheet> {

View file

@ -110,6 +110,7 @@ export default function useKeyboardShortcuts() {
const playbackPromise = seq.playDynamicRange( const playbackPromise = seq.playDynamicRange(
prism(() => val(controlledPlaybackStateD).range), prism(() => val(controlledPlaybackStateD).range),
getStudio().ticker,
) )
const playbackStateBox = getPlaybackStateBox(seq) const playbackStateBox = getPlaybackStateBox(seq)

View file

@ -5,7 +5,7 @@ import {prism, val} from '@theatre/dataverse'
import React, {useLayoutEffect, useMemo, useRef, useState} from 'react' import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import createGrid from './createGrid' import createGrid from './createGrid'
import studioTicker from '@theatre/studio/studioTicker' import getStudio from '@theatre/studio/getStudio'
const Container = styled.div` const Container = styled.div`
position: absolute; position: absolute;
@ -76,7 +76,7 @@ const FrameGrid: React.FC<{
snapToGrid: (n: number) => sequence.closestGridPosition(n), snapToGrid: (n: number) => sequence.closestGridPosition(n),
} }
}).onChange( }).onChange(
studioTicker, getStudio().ticker,
(p) => { (p) => {
ctx.save() ctx.save()
ctx.scale(ratio!, ratio!) ctx.scale(ratio!, ratio!)

View file

@ -6,7 +6,7 @@ import {darken} from 'polished'
import React, {useLayoutEffect, useRef, useState} from 'react' import React, {useLayoutEffect, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import createGrid from './createGrid' import createGrid from './createGrid'
import studioTicker from '@theatre/studio/studioTicker' import getStudio from '@theatre/studio/getStudio'
const Container = styled.div` const Container = styled.div`
position: absolute; position: absolute;
@ -86,7 +86,7 @@ const StampsGrid: React.FC<{
sequencePositionFormatter: sequence.positionFormatter, sequencePositionFormatter: sequence.positionFormatter,
snapToGrid: (n: number) => sequence.closestGridPosition(n), snapToGrid: (n: number) => sequence.closestGridPosition(n),
} }
}).onChange(studioTicker, drawStamps, true) }).onChange(getStudio().ticker, drawStamps, true)
}, [fullSecondStampsContainer, width, layoutP]) }, [fullSecondStampsContainer, width, layoutP])
return ( return (

View file

@ -1,5 +0,0 @@
import {Ticker} from '@theatre/dataverse'
const studioTicker = Ticker.raf
export default studioTicker