feat(remote): multiple-window support
This commit is contained in:
parent
8b8d2bf431
commit
bcfb91fbb7
5 changed files with 423 additions and 0 deletions
85
packages/playground/src/shared/remote/Box3D.tsx
Normal file
85
packages/playground/src/shared/remote/Box3D.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React, {useEffect, useRef} from 'react'
|
||||
import type {CSSProperties} from 'react'
|
||||
import {types} from '@theatre/core'
|
||||
import type {ISheet} from '@theatre/core'
|
||||
import {remote} from './Remote'
|
||||
|
||||
// Box element
|
||||
export const BoxSize = 100
|
||||
|
||||
const Box3DCSS: CSSProperties = {
|
||||
border: '1px solid #999',
|
||||
position: 'absolute',
|
||||
width: `${BoxSize}px`,
|
||||
height: `${BoxSize}px`,
|
||||
}
|
||||
|
||||
const Box3DTextCSS: CSSProperties = {
|
||||
margin: '0',
|
||||
padding: '0',
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
}
|
||||
|
||||
export const Box3D: React.FC<{
|
||||
sheet: ISheet
|
||||
name: string
|
||||
x: number
|
||||
y: number
|
||||
}> = ({sheet, name, x, y}) => {
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Animation
|
||||
useEffect(() => {
|
||||
const element = elementRef.current!
|
||||
const id = `Box - ${name}`
|
||||
const sheetObj = remote.sheetObject(
|
||||
sheet.address.sheetId,
|
||||
id,
|
||||
{
|
||||
background: types.rgba({r: 16 / 255, g: 16 / 255, b: 16 / 255, a: 1}),
|
||||
opacity: types.number(1, {range: [0, 1]}),
|
||||
position: {
|
||||
x: x,
|
||||
y: y,
|
||||
z: 0,
|
||||
},
|
||||
rotation: {
|
||||
x: types.number(0, {range: [-360, 360]}),
|
||||
y: types.number(0, {range: [-360, 360]}),
|
||||
z: types.number(0, {range: [-360, 360]}),
|
||||
},
|
||||
scale: {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
},
|
||||
},
|
||||
(values: any) => {
|
||||
const {background, opacity, position, rotation, scale} = values
|
||||
element.style.backgroundColor = `rgba(${background.r * 255}, ${
|
||||
background.g * 255
|
||||
}, ${background.b * 255}, 1)`
|
||||
element.style.opacity = opacity
|
||||
const translate3D = `translate3d(${position.x}px, ${position.y}px, ${position.z}px)`
|
||||
const rotate3D = `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`
|
||||
const scale3D = `scaleX(${scale.x}) scaleY(${scale.y}) scaleZ(${scale.z})`
|
||||
const transform = `${scale3D} ${translate3D} ${rotate3D}`
|
||||
element.style.transform = transform
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
if (sheetObj !== undefined) remote.unsubscribe(sheetObj)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={elementRef} style={Box3DCSS}>
|
||||
<span style={Box3DTextCSS}>{name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
152
packages/playground/src/shared/remote/Remote.ts
Normal file
152
packages/playground/src/shared/remote/Remote.ts
Normal file
|
@ -0,0 +1,152 @@
|
|||
import type {IProject, ISheet, ISheetObject} from '@theatre/core'
|
||||
import type {VoidFn} from '@theatre/dataverse/src/types'
|
||||
|
||||
export type TheatreUpdateCallback = (data: any) => void
|
||||
|
||||
export type BroadcastDataEvent =
|
||||
| 'setSheet'
|
||||
| 'setSheetObject'
|
||||
| 'updateSheetObject'
|
||||
| 'updateTimeline'
|
||||
|
||||
export interface BroadcastData {
|
||||
event: BroadcastDataEvent
|
||||
data: any
|
||||
}
|
||||
|
||||
export type BroadcastCallback = (data: BroadcastData) => void
|
||||
|
||||
// Default SheetObject.onValuesChange callback
|
||||
const noop: TheatreUpdateCallback = (values: any) => {}
|
||||
|
||||
function isColor(obj: any) {
|
||||
return (
|
||||
obj.r !== undefined &&
|
||||
obj.g !== undefined &&
|
||||
obj.b !== undefined &&
|
||||
obj.a !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
// Which hashtag to add to the URL
|
||||
const hashtag = 'editor'
|
||||
|
||||
class RemoteSingleton {
|
||||
// Remote
|
||||
mode = 'listener'
|
||||
channel: BroadcastChannel
|
||||
|
||||
// Theatre
|
||||
project!: IProject
|
||||
sheets: Map<string, ISheet> = new Map()
|
||||
sheetObjects: Map<string, ISheetObject> = new Map()
|
||||
sheetObjectCBs: Map<string, TheatreUpdateCallback> = new Map()
|
||||
sheetObjectUnsubscribe: Map<string, VoidFn> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.channel = new BroadcastChannel('theatre')
|
||||
this.showTheatre = document.location.hash.search(hashtag) > -1
|
||||
}
|
||||
|
||||
// Remote
|
||||
|
||||
send(data: BroadcastData) {
|
||||
if (this.mode === 'theatre') {
|
||||
this.channel.postMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
listen(callback: BroadcastCallback) {
|
||||
if (this.mode === 'listener') {
|
||||
this.channel.onmessage = (event: MessageEvent<any>) => {
|
||||
callback(event.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Theatre
|
||||
|
||||
sheet(name: string): ISheet {
|
||||
let sheet: any = this.sheets.get(name)
|
||||
if (sheet !== undefined) return sheet
|
||||
|
||||
sheet = this.project.sheet(name)
|
||||
this.sheets.set(name, sheet)
|
||||
return sheet
|
||||
}
|
||||
|
||||
sheetObject(
|
||||
sheetName: string,
|
||||
key: string,
|
||||
props: any,
|
||||
onUpdate?: TheatreUpdateCallback,
|
||||
): ISheetObject | undefined {
|
||||
const sheet = this.sheets.get(sheetName)
|
||||
if (sheet === undefined) return undefined
|
||||
|
||||
const objName = `${sheetName}_${key}`
|
||||
let obj = this.sheetObjects.get(objName)
|
||||
if (obj !== undefined) {
|
||||
obj = sheet.object(key, {...props, ...obj.value}, {reconfigure: true})
|
||||
return obj
|
||||
}
|
||||
|
||||
obj = sheet.object(key, props)
|
||||
this.sheetObjects.set(objName, obj)
|
||||
this.sheetObjectCBs.set(objName, onUpdate !== undefined ? onUpdate : noop)
|
||||
|
||||
const unsubscribe = obj.onValuesChange((values: any) => {
|
||||
if (this.showTheatre) {
|
||||
for (let i in values) {
|
||||
const value = values[i]
|
||||
if (typeof value === 'object') {
|
||||
if (isColor(value)) {
|
||||
values[i] = {
|
||||
r: value.r,
|
||||
g: value.g,
|
||||
b: value.b,
|
||||
a: value.a,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.send({
|
||||
event: 'updateSheetObject',
|
||||
data: {
|
||||
sheetObject: objName,
|
||||
values: values,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const callback = this.sheetObjectCBs.get(objName)
|
||||
if (callback !== undefined) callback(values)
|
||||
}
|
||||
})
|
||||
this.sheetObjectUnsubscribe.set(objName, unsubscribe)
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
unsubscribe(sheet: ISheetObject) {
|
||||
const id = `${sheet.address.sheetId}_${sheet.address.objectKey}`
|
||||
const unsubscribe = this.sheetObjectUnsubscribe.get(id)
|
||||
if (unsubscribe !== undefined) {
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
// Getters / Setters
|
||||
|
||||
get showTheatre(): boolean {
|
||||
return this.mode === 'theatre'
|
||||
}
|
||||
|
||||
set showTheatre(value: boolean) {
|
||||
if (value) {
|
||||
this.mode = 'theatre'
|
||||
document.title += ' - Editor'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const remote = new RemoteSingleton()
|
119
packages/playground/src/shared/remote/RemoteController.ts
Normal file
119
packages/playground/src/shared/remote/RemoteController.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import type {IProject, ISheet} from '@theatre/core'
|
||||
import studio from '@theatre/studio'
|
||||
import {remote} from './Remote'
|
||||
import type {BroadcastData, BroadcastDataEvent} from './Remote'
|
||||
|
||||
/**
|
||||
* Handles the communication between windows
|
||||
*/
|
||||
export default function RemoteController(project: IProject) {
|
||||
let activeSheet: ISheet | undefined = undefined
|
||||
remote.project = project
|
||||
|
||||
/**
|
||||
* Editor is hidden, this window receives updates
|
||||
*/
|
||||
const receiveRemote = () => {
|
||||
studio.ui.hide()
|
||||
|
||||
remote.listen((msg: BroadcastData) => {
|
||||
switch (msg.event) {
|
||||
case 'setSheet':
|
||||
const sheet = remote.sheets.get(msg.data.sheet)
|
||||
if (sheet !== undefined) {
|
||||
activeSheet = sheet
|
||||
studio.setSelection([sheet])
|
||||
}
|
||||
break
|
||||
|
||||
case 'setSheetObject':
|
||||
const sheetObj = remote.sheetObjects.get(
|
||||
`${msg.data.sheet}_${msg.data.key}`,
|
||||
)
|
||||
if (sheetObj !== undefined) {
|
||||
studio.setSelection([sheetObj])
|
||||
}
|
||||
break
|
||||
|
||||
case 'updateSheetObject':
|
||||
const sheetObjCB = remote.sheetObjectCBs.get(msg.data.sheetObject)
|
||||
if (sheetObjCB !== undefined) sheetObjCB(msg.data.values)
|
||||
break
|
||||
|
||||
case 'updateTimeline':
|
||||
activeSheet = remote.sheets.get(msg.data.sheet)
|
||||
if (activeSheet !== undefined) {
|
||||
activeSheet.sequence.position = msg.data.position
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor is visible, this window sends updates
|
||||
*/
|
||||
const sendRemote = () => {
|
||||
studio.ui.restore()
|
||||
|
||||
studio.onSelectionChange((value: any[]) => {
|
||||
if (value.length < 1) return
|
||||
|
||||
value.forEach((obj: any) => {
|
||||
let id = obj.address.sheetId
|
||||
let type: BroadcastDataEvent = 'setSheet'
|
||||
let data = {}
|
||||
switch (obj.type) {
|
||||
case 'Theatre_Sheet_PublicAPI':
|
||||
type = 'setSheet'
|
||||
data = {
|
||||
sheet: obj.address.sheetId,
|
||||
}
|
||||
activeSheet = remote.sheets.get(obj.address.sheetId)
|
||||
break
|
||||
|
||||
case 'Theatre_SheetObject_PublicAPI':
|
||||
type = 'setSheetObject'
|
||||
id += `_${obj.address.objectKey}`
|
||||
data = {
|
||||
sheet: obj.address.sheetId,
|
||||
key: obj.address.objectKey,
|
||||
}
|
||||
break
|
||||
}
|
||||
remote.send({event: type, data: data})
|
||||
})
|
||||
})
|
||||
|
||||
// Timeline
|
||||
let position = 0
|
||||
const onRafUpdate = () => {
|
||||
if (
|
||||
activeSheet !== undefined &&
|
||||
position !== activeSheet.sequence.position
|
||||
) {
|
||||
position = activeSheet.sequence.position
|
||||
const t = activeSheet as ISheet
|
||||
remote.send({
|
||||
event: 'updateTimeline',
|
||||
data: {
|
||||
position: position,
|
||||
sheet: t.address.sheetId,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const onRaf = () => {
|
||||
onRafUpdate()
|
||||
requestAnimationFrame(onRaf)
|
||||
}
|
||||
onRafUpdate() // Initial position
|
||||
onRaf()
|
||||
}
|
||||
|
||||
if (remote.showTheatre) {
|
||||
sendRemote()
|
||||
} else {
|
||||
receiveRemote()
|
||||
}
|
||||
}
|
55
packages/playground/src/shared/remote/Scene.tsx
Normal file
55
packages/playground/src/shared/remote/Scene.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React, {useEffect, useRef} from 'react'
|
||||
import type {CSSProperties} from 'react'
|
||||
import {types} from '@theatre/core'
|
||||
import {Box3D} from './Box3D'
|
||||
import {remote} from './Remote'
|
||||
|
||||
// Scene
|
||||
|
||||
const SceneCSS: CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
}
|
||||
|
||||
export const Scene: React.FC<{}> = ({}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null!)
|
||||
const sheet = remote.sheet('DOM')
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current!
|
||||
const sheetObj = remote.sheetObject(
|
||||
'DOM',
|
||||
'Container',
|
||||
{
|
||||
perspective: types.number(
|
||||
Math.max(window.innerWidth, window.innerHeight),
|
||||
{range: [0, 2000]},
|
||||
),
|
||||
originX: types.number(50, {range: [0, 100]}),
|
||||
originY: types.number(50, {range: [0, 100]}),
|
||||
},
|
||||
(values: any) => {
|
||||
container.style.perspective = `${values.perspective}px`
|
||||
container.style.perspectiveOrigin = `${values.originX}% ${values.originY}%`
|
||||
},
|
||||
)
|
||||
return () => {
|
||||
if (sheetObj !== undefined) remote.unsubscribe(sheetObj)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (remote.showTheatre) {
|
||||
SceneCSS.display = 'none'
|
||||
SceneCSS.visibility = 'hidden'
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} style={SceneCSS}>
|
||||
<Box3D sheet={sheet} name="Box" x={100} y={100} />
|
||||
</div>
|
||||
)
|
||||
}
|
12
packages/playground/src/shared/remote/index.tsx
Normal file
12
packages/playground/src/shared/remote/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import studio from '@theatre/studio'
|
||||
import {getProject} from '@theatre/core'
|
||||
import {Scene} from './Scene'
|
||||
import RemoteController from './RemoteController'
|
||||
|
||||
const project = getProject('Sample project')
|
||||
studio.initialize()
|
||||
RemoteController(project)
|
||||
|
||||
ReactDOM.render(<Scene />, document.getElementById('root'))
|
Loading…
Reference in a new issue