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 {getProject} from '@theatre/core'
|
||||
import {createRafDriver, getProject} from '@theatre/core'
|
||||
import type {
|
||||
UnknownShorthandCompoundProps,
|
||||
ISheet,
|
||||
|
@ -7,11 +7,11 @@ import type {
|
|||
} from '@theatre/core'
|
||||
// @ts-ignore
|
||||
import benchProject1State from './Bench project 1.theatre-project-state.json'
|
||||
import {Ticker} from '@theatre/dataverse'
|
||||
import {setCoreTicker} from '@theatre/core/coreTicker'
|
||||
import {setCoreRafDriver} from '@theatre/core/coreTicker'
|
||||
|
||||
const ticker = new Ticker()
|
||||
setCoreTicker(ticker)
|
||||
const driver = createRafDriver({name: 'BenchmarkRafDriver'})
|
||||
|
||||
setCoreRafDriver(driver)
|
||||
|
||||
// studio.initialize({})
|
||||
|
||||
|
@ -79,7 +79,7 @@ async function test1() {
|
|||
}
|
||||
|
||||
function iterateOnSequence() {
|
||||
ticker.tick()
|
||||
driver.tick(performance.now())
|
||||
const startTime = performance.now()
|
||||
for (let i = 1; i < CONFIG.numberOfIterations; i++) {
|
||||
onChangeEventsFired = 0
|
||||
|
@ -87,7 +87,7 @@ async function test1() {
|
|||
for (const sheet of sheets) {
|
||||
sheet.sequence.position = pos
|
||||
}
|
||||
ticker.tick()
|
||||
driver.tick(performance.now())
|
||||
if (onChangeEventsFired !== objects.length) {
|
||||
console.info(
|
||||
`Expected ${objects.length} onChange events, got ${onChangeEventsFired}`,
|
||||
|
|
|
@ -1,44 +1,36 @@
|
|||
type ICallback = (t: number) => void
|
||||
|
||||
function createRafTicker() {
|
||||
const ticker = new Ticker()
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
/**
|
||||
* @remarks
|
||||
* TODO users should also be able to define their own ticker.
|
||||
*/
|
||||
const onAnimationFrame = (t: number) => {
|
||||
ticker.tick(t)
|
||||
window.requestAnimationFrame(onAnimationFrame)
|
||||
}
|
||||
window.requestAnimationFrame(onAnimationFrame)
|
||||
} else {
|
||||
ticker.tick(0)
|
||||
setTimeout(() => ticker.tick(1), 0)
|
||||
}
|
||||
|
||||
return ticker
|
||||
}
|
||||
|
||||
let rafTicker: undefined | Ticker
|
||||
/**
|
||||
* The number of ticks that can pass without any scheduled callbacks before the Ticker goes dormant. This is to prevent
|
||||
* the Ticker from staying active forever, even if there are no scheduled callbacks.
|
||||
*
|
||||
* Perhaps counting ticks vs. time is not the best way to do this. But it's a start.
|
||||
*/
|
||||
const EMPTY_TICKS_BEFORE_GOING_DORMANT = 60 /*fps*/ * 3 /*seconds*/ // on a 60fps screen, 3 seconds should pass before the ticker goes dormant
|
||||
|
||||
/**
|
||||
* The Ticker class helps schedule callbacks. Scheduled callbacks are executed per tick. Ticks can be triggered by an
|
||||
* external scheduling strategy, e.g. a raf.
|
||||
*/
|
||||
export default class Ticker {
|
||||
/** Get a shared `requestAnimationFrame` ticker. */
|
||||
static get raf(): Ticker {
|
||||
if (!rafTicker) {
|
||||
rafTicker = createRafTicker()
|
||||
}
|
||||
return rafTicker
|
||||
}
|
||||
private _scheduledForThisOrNextTick: Set<ICallback>
|
||||
private _scheduledForNextTick: Set<ICallback>
|
||||
private _timeAtCurrentTick: number
|
||||
private _ticking: boolean = false
|
||||
|
||||
/**
|
||||
* Whether the Ticker is dormant
|
||||
*/
|
||||
private _dormant: boolean = true
|
||||
|
||||
private _numberOfDormantTicks = 0
|
||||
|
||||
/**
|
||||
* Whether the Ticker is dormant
|
||||
*/
|
||||
get dormant(): boolean {
|
||||
return this._dormant
|
||||
}
|
||||
/**
|
||||
* Counts up for every tick executed.
|
||||
* Internally, this is used to measure ticks per second.
|
||||
|
@ -48,7 +40,18 @@ export default class Ticker {
|
|||
*/
|
||||
public __ticks = 0
|
||||
|
||||
constructor() {
|
||||
constructor(
|
||||
private _conf?: {
|
||||
/**
|
||||
* This is called when the Ticker goes dormant.
|
||||
*/
|
||||
onDormant?: () => void
|
||||
/**
|
||||
* This is called when the Ticker goes active.
|
||||
*/
|
||||
onActive?: () => void
|
||||
},
|
||||
) {
|
||||
this._scheduledForThisOrNextTick = new Set()
|
||||
this._scheduledForNextTick = new Set()
|
||||
this._timeAtCurrentTick = 0
|
||||
|
@ -70,6 +73,9 @@ export default class Ticker {
|
|||
*/
|
||||
onThisOrNextTick(fn: ICallback) {
|
||||
this._scheduledForThisOrNextTick.add(fn)
|
||||
if (this._dormant) {
|
||||
this._goActive()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,6 +88,9 @@ export default class Ticker {
|
|||
*/
|
||||
onNextTick(fn: ICallback) {
|
||||
this._scheduledForNextTick.add(fn)
|
||||
if (this._dormant) {
|
||||
this._goActive()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,6 +125,19 @@ export default class Ticker {
|
|||
} else return performance.now()
|
||||
}
|
||||
|
||||
private _goActive() {
|
||||
if (!this._dormant) return
|
||||
this._dormant = false
|
||||
this._conf?.onActive?.()
|
||||
}
|
||||
|
||||
private _goDormant() {
|
||||
if (this._dormant) return
|
||||
this._dormant = true
|
||||
this._numberOfDormantTicks = 0
|
||||
this._conf?.onDormant?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a tick which starts executing the callbacks scheduled for this tick.
|
||||
*
|
||||
|
@ -135,6 +157,19 @@ export default class Ticker {
|
|||
|
||||
this.__ticks++
|
||||
|
||||
if (!this._dormant) {
|
||||
if (
|
||||
this._scheduledForNextTick.size === 0 &&
|
||||
this._scheduledForThisOrNextTick.size === 0
|
||||
) {
|
||||
this._numberOfDormantTicks++
|
||||
if (this._numberOfDormantTicks >= EMPTY_TICKS_BEFORE_GOING_DORMANT) {
|
||||
this._goDormant()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._ticking = true
|
||||
this._timeAtCurrentTick = t
|
||||
for (const v of this._scheduledForNextTick) {
|
||||
|
|
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 studio from '@theatre/studio'
|
||||
import extension from '@theatre/r3f/dist/extension'
|
||||
import {Atom, prism, Ticker, val} from '@theatre/dataverse'
|
||||
import {Atom, prism, val} from '@theatre/dataverse'
|
||||
import {onChange} from '@theatre/core'
|
||||
|
||||
/**
|
||||
* Let's take a look at how we can use `prism`, `Ticker`, and `val` from Theatre.js's Dataverse library
|
||||
|
@ -28,41 +29,39 @@ studio.extend({
|
|||
global(set, studio) {
|
||||
const exampleBox = new Atom('mobile')
|
||||
|
||||
const untapFn = prism<ToolsetConfig>(() => [
|
||||
{
|
||||
type: 'Switch',
|
||||
value: val(exampleBox.prism),
|
||||
onChange: (value) => exampleBox.set(value),
|
||||
options: [
|
||||
{
|
||||
value: 'mobile',
|
||||
label: 'view mobile version',
|
||||
svgSource: '😀',
|
||||
},
|
||||
{
|
||||
value: 'desktop',
|
||||
label: 'view desktop version',
|
||||
svgSource: '🪢',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'Icon',
|
||||
title: 'Example Icon',
|
||||
svgSource: '👁🗨',
|
||||
onClick: () => {
|
||||
console.log('hello')
|
||||
const untapFn = onChange(
|
||||
prism<ToolsetConfig>(() => [
|
||||
{
|
||||
type: 'Switch',
|
||||
value: val(exampleBox.prism),
|
||||
onChange: (value) => exampleBox.set(value),
|
||||
options: [
|
||||
{
|
||||
value: 'mobile',
|
||||
label: 'view mobile version',
|
||||
svgSource: '😀',
|
||||
},
|
||||
{
|
||||
value: 'desktop',
|
||||
label: 'view desktop version',
|
||||
svgSource: '🪢',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
// listen to changes to this prism using the requestAnimationFrame shared ticker
|
||||
.onChange(
|
||||
Ticker.raf,
|
||||
(value) => {
|
||||
set(value)
|
||||
{
|
||||
type: 'Icon',
|
||||
title: 'Example Icon',
|
||||
svgSource: '👁🗨',
|
||||
onClick: () => {
|
||||
console.log('hello')
|
||||
},
|
||||
},
|
||||
true,
|
||||
)
|
||||
]),
|
||||
(value) => {
|
||||
set(value)
|
||||
},
|
||||
)
|
||||
// listen to changes to this prism using the requestAnimationFrame shared ticker
|
||||
|
||||
return untapFn
|
||||
},
|
||||
|
|
|
@ -3,14 +3,16 @@ import ReactDOM from 'react-dom'
|
|||
import App from './App'
|
||||
import studio from '@theatre/studio'
|
||||
import extension from '@theatre/r3f/dist/extension'
|
||||
import {Ticker} from '@theatre/dataverse'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
|
||||
const studioPrivate = getStudio()
|
||||
|
||||
studio.extend(extension)
|
||||
studio.initialize()
|
||||
|
||||
ReactDOM.render(<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
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import SnapshotEditor from './components/SnapshotEditor'
|
||||
import type {IExtension} from '@theatre/studio'
|
||||
import {prism, Ticker, val} from '@theatre/dataverse'
|
||||
import {prism, val} from '@theatre/dataverse'
|
||||
import {getEditorSheetObject} from './editorStuff'
|
||||
import ReactDOM from 'react-dom'
|
||||
import React from 'react'
|
||||
import type {ToolsetConfig} from '@theatre/studio'
|
||||
import useExtensionStore from './useExtensionStore'
|
||||
import {onChange} from '@theatre/core'
|
||||
|
||||
const io5CameraOutline = `<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>`
|
||||
|
@ -35,13 +36,9 @@ const r3fExtension: IExtension = {
|
|||
},
|
||||
]
|
||||
})
|
||||
return calc.onChange(
|
||||
Ticker.raf,
|
||||
() => {
|
||||
set(calc.getValue())
|
||||
},
|
||||
true,
|
||||
)
|
||||
return onChange(calc, () => {
|
||||
set(calc.getValue())
|
||||
})
|
||||
},
|
||||
'snapshot-editor': (set, studio) => {
|
||||
const {createSnapshot} = useExtensionStore.getState()
|
||||
|
@ -140,13 +137,9 @@ const r3fExtension: IExtension = {
|
|||
},
|
||||
]
|
||||
})
|
||||
return calc.onChange(
|
||||
Ticker.raf,
|
||||
() => {
|
||||
set(calc.getValue())
|
||||
},
|
||||
true,
|
||||
)
|
||||
return onChange(calc, () => {
|
||||
set(calc.getValue())
|
||||
})
|
||||
},
|
||||
},
|
||||
panes: [
|
||||
|
|
|
@ -21,6 +21,10 @@ export {
|
|||
export {makeStoreKey as __private_makeStoreKey} from './main/utils'
|
||||
|
||||
export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider'
|
||||
export {
|
||||
default as RafDriverProvider,
|
||||
useCurrentRafDriver,
|
||||
} from './main/RafDriverProvider'
|
||||
export {refreshSnapshot} from './main/utils'
|
||||
export {default as RefreshSnapshot} from './main/RefreshSnapshot'
|
||||
export * from './drei'
|
||||
|
|
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 {ISheetObject} from '@theatre/core'
|
||||
import {notify} from '@theatre/core'
|
||||
import {useCurrentRafDriver} from './RafDriverProvider'
|
||||
|
||||
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||
config: EditableFactoryConfig,
|
||||
|
@ -74,6 +75,7 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
|||
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||
|
||||
const sheet = useCurrentSheet()!
|
||||
const rafDriver = useCurrentRafDriver()
|
||||
|
||||
const [sheetObject, setSheetObject] = useState<
|
||||
undefined | ISheetObject<$FixMe>
|
||||
|
@ -211,15 +213,18 @@ Then you can use it in your JSX like any other editable component. Note the make
|
|||
|
||||
setFromTheatre(sheetObject.value)
|
||||
|
||||
const untap = sheetObject.onValuesChange(setFromTheatre)
|
||||
const unsubscribe = sheetObject.onValuesChange(
|
||||
setFromTheatre,
|
||||
rafDriver,
|
||||
)
|
||||
|
||||
return () => {
|
||||
untap()
|
||||
unsubscribe()
|
||||
sheetObject.sheet.detachObject(theatreKey)
|
||||
allRegisteredObjects.delete(sheetObject)
|
||||
editorStore.getState().removeEditable(storeKey)
|
||||
}
|
||||
}, [sheetObject])
|
||||
}, [sheetObject, rafDriver])
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue