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:
parent
80e79499df
commit
d649858529
26 changed files with 464 additions and 145 deletions
|
@ -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}`,
|
||||||
|
|
|
@ -1,44 +1,36 @@
|
||||||
type ICallback = (t: number) => void
|
type ICallback = (t: number) => void
|
||||||
|
|
||||||
function createRafTicker() {
|
/**
|
||||||
const ticker = new 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.
|
||||||
if (typeof window !== 'undefined') {
|
*
|
||||||
/**
|
* Perhaps counting ticks vs. time is not the best way to do this. But it's a start.
|
||||||
* @remarks
|
*/
|
||||||
* TODO users should also be able to define their own ticker.
|
const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant
|
||||||
*/
|
|
||||||
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 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) {
|
||||||
|
|
36
packages/playground/src/shared/custom-raf-driver/App.tsx
Normal file
36
packages/playground/src/shared/custom-raf-driver/App.tsx
Normal 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
|
18
packages/playground/src/shared/custom-raf-driver/index.tsx
Normal file
18
packages/playground/src/shared/custom-raf-driver/index.tsx
Normal 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'))
|
|
@ -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,41 +29,39 @@ 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',
|
{
|
||||||
value: val(exampleBox.prism),
|
type: 'Switch',
|
||||||
onChange: (value) => exampleBox.set(value),
|
value: val(exampleBox.prism),
|
||||||
options: [
|
onChange: (value) => exampleBox.set(value),
|
||||||
{
|
options: [
|
||||||
value: 'mobile',
|
{
|
||||||
label: 'view mobile version',
|
value: 'mobile',
|
||||||
svgSource: '😀',
|
label: 'view mobile version',
|
||||||
},
|
svgSource: '😀',
|
||||||
{
|
},
|
||||||
value: 'desktop',
|
{
|
||||||
label: 'view desktop version',
|
value: 'desktop',
|
||||||
svgSource: '🪢',
|
label: 'view desktop version',
|
||||||
},
|
svgSource: '🪢',
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
{
|
|
||||||
type: 'Icon',
|
|
||||||
title: 'Example Icon',
|
|
||||||
svgSource: '👁🗨',
|
|
||||||
onClick: () => {
|
|
||||||
console.log('hello')
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
])
|
type: 'Icon',
|
||||||
// listen to changes to this prism using the requestAnimationFrame shared ticker
|
title: 'Example Icon',
|
||||||
.onChange(
|
svgSource: '👁🗨',
|
||||||
Ticker.raf,
|
onClick: () => {
|
||||||
(value) => {
|
console.log('hello')
|
||||||
set(value)
|
},
|
||||||
},
|
},
|
||||||
true,
|
]),
|
||||||
)
|
(value) => {
|
||||||
|
set(value)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// listen to changes to this prism using the requestAnimationFrame shared ticker
|
||||||
|
|
||||||
return untapFn
|
return untapFn
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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'
|
||||||
|
|
26
packages/r3f/src/main/RafDriverProvider.tsx
Normal file
26
packages/r3f/src/main/RafDriverProvider.tsx
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.`,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
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 {
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
60
theatre/core/src/rafDrivers.ts
Normal file
60
theatre/core/src/rafDrivers.ts
Normal 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
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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!)
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import {Ticker} from '@theatre/dataverse'
|
|
||||||
|
|
||||||
const studioTicker = Ticker.raf
|
|
||||||
|
|
||||||
export default studioTicker
|
|
Loading…
Reference in a new issue