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