playground now has a shared folder and a personal folder

This commit is contained in:
Aria Minaei 2021-10-04 20:25:38 +02:00
parent 1647d91dc5
commit 4d49a8bdd6
18 changed files with 39 additions and 383 deletions

View file

@ -0,0 +1,137 @@
import studio from '@theatre/studio'
import type {UseDragOpts} from './useDrag'
import useDrag from './useDrag'
import React, {useLayoutEffect, useMemo, useState} from 'react'
import type {IProject, ISheet} from '@theatre/core'
import {onChange} from '@theatre/core'
import type {IScrub, IStudio} from '@theatre/studio'
studio.initialize()
const boxObjectConfig = {
x: 0,
y: 0,
}
const Box: React.FC<{
id: string
sheet: ISheet
selection: IStudio['selection']
}> = ({id, sheet, selection}) => {
// This is cheap to call and always returns the same value, so no need for useMemo()
const obj = sheet.object(id, boxObjectConfig)
const isSelected = selection.includes(obj)
const [pos, setPos] = useState<{x: number; y: number}>({x: 0, y: 0})
useLayoutEffect(() => {
const unsubscribeFromChanges = onChange(obj.props, (newValues) => {
setPos(newValues)
})
return unsubscribeFromChanges
}, [id])
const [divRef, setDivRef] = useState<HTMLElement | null>(null)
const dragOpts = useMemo((): UseDragOpts => {
let scrub: IScrub | undefined
let initial: typeof obj.value
let firstOnDragCalled = false
return {
onDragStart() {
scrub = studio.scrub()
initial = obj.value
firstOnDragCalled = false
},
onDrag(x, y) {
if (!firstOnDragCalled) {
studio.setSelection([obj])
firstOnDragCalled = true
}
scrub!.capture(({set}) => {
set(obj.props, {x: x + initial.x, y: y + initial.y})
})
},
onDragEnd(dragHappened) {
if (dragHappened) {
scrub!.commit()
} else {
scrub!.discard()
}
},
lockCursorTo: 'move',
}
}, [])
useDrag(divRef, dragOpts)
return (
<div
onClick={() => {
studio.setSelection([obj])
}}
ref={setDivRef}
style={{
width: 100,
height: 100,
background: 'gray',
position: 'absolute',
left: pos.x + 'px',
top: pos.y + 'px',
boxSizing: 'border-box',
border: isSelected ? '1px solid #5a92fa' : '1px solid transparent',
}}
></div>
)
}
let lastBoxId = 1
export const Scene: React.FC<{project: IProject}> = ({project}) => {
const [boxes, setBoxes] = useState<Array<string>>(['0', '1'])
// This is cheap to call and always returns the same value, so no need for useMemo()
const sheet = project.sheet('Scene', 'default')
const [selection, setSelection] = useState<IStudio['selection']>()
useLayoutEffect(() => {
return studio.onSelectionChange((newState) => {
setSelection(newState)
})
})
return (
<div
style={{
position: 'absolute',
left: '0',
right: '0',
top: 0,
bottom: '0',
background: 'black',
}}
>
<button
style={{
top: '16px',
left: '60px',
position: 'absolute',
}}
onClick={() => {
setBoxes((boxes) => [...boxes, String(++lastBoxId)])
}}
>
Add
</button>
{boxes.map((id) => (
<Box
key={'box' + id}
id={id}
sheet={sheet}
selection={selection ?? []}
/>
))}
</div>
)
}

View file

@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom'
import studio from '@theatre/studio'
import {getProject} from '@theatre/core'
import {Scene} from './Scene'
/**
* This is a basic example of using Theatre for manipulating the DOM.
*
* It also uses {@link IStudio.selection | studio.selection} to customize
* the selection behavior.
*/
studio.initialize()
ReactDOM.render(
<Scene project={getProject('Sample project')} />,
document.getElementById('root'),
)

View file

@ -0,0 +1,150 @@
import {useLayoutEffect, useRef} from 'react'
const noop = () => {}
function createCursorLock(cursor: string) {
const el = document.createElement('div')
el.style.cssText = `
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999999;`
el.style.cursor = cursor
document.body.appendChild(el)
const relinquish = () => {
document.body.removeChild(el)
}
return relinquish
}
export type UseDragOpts = {
disabled?: boolean
dontBlockMouseDown?: boolean
lockCursorTo?: string
onDragStart?: (event: MouseEvent) => void | false
onDragEnd?: (dragHappened: boolean) => void
onDrag: (dx: number, dy: number, event: MouseEvent) => void
}
export default function useDrag(
target: HTMLElement | undefined | null,
opts: UseDragOpts,
) {
const optsRef = useRef<typeof opts>(opts)
optsRef.current = opts
const modeRef = useRef<'dragStartCalled' | 'dragging' | 'notDragging'>(
'notDragging',
)
const stateRef = useRef<{
dragHappened: boolean
startPos: {
x: number
y: number
}
}>({dragHappened: false, startPos: {x: 0, y: 0}})
useLayoutEffect(() => {
if (!target) return
const getDistances = (event: MouseEvent): [number, number] => {
const {startPos} = stateRef.current
return [event.screenX - startPos.x, event.screenY - startPos.y]
}
let relinquishCursorLock = noop
const dragHandler = (event: MouseEvent) => {
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
}
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
modeRef.current = 'dragging'
const deltas = getDistances(event)
optsRef.current.onDrag(deltas[0], deltas[1], event)
}
const dragEndHandler = () => {
removeDragListeners()
modeRef.current = 'notDragging'
optsRef.current.onDragEnd &&
optsRef.current.onDragEnd(stateRef.current.dragHappened)
relinquishCursorLock()
relinquishCursorLock = noop
}
const addDragListeners = () => {
document.addEventListener('mousemove', dragHandler)
document.addEventListener('mouseup', dragEndHandler)
}
const removeDragListeners = () => {
document.removeEventListener('mousemove', dragHandler)
document.removeEventListener('mouseup', dragEndHandler)
}
const preventUnwantedClick = (event: MouseEvent) => {
if (optsRef.current.disabled) return
if (stateRef.current.dragHappened) {
if (
!optsRef.current.dontBlockMouseDown &&
modeRef.current !== 'notDragging'
) {
event.stopPropagation()
event.preventDefault()
}
stateRef.current.dragHappened = false
}
}
const dragStartHandler = (event: MouseEvent) => {
const opts = optsRef.current
if (opts.disabled === true) return
if (event.button !== 0) return
const resultOfStart = opts.onDragStart && opts.onDragStart(event)
if (resultOfStart === false) return
if (!opts.dontBlockMouseDown) {
event.stopPropagation()
event.preventDefault()
}
modeRef.current = 'dragStartCalled'
const {screenX, screenY} = event
stateRef.current.startPos = {x: screenX, y: screenY}
stateRef.current.dragHappened = false
addDragListeners()
}
const onMouseDown = (e: MouseEvent) => {
dragStartHandler(e)
}
target.addEventListener('mousedown', onMouseDown)
target.addEventListener('click', preventUnwantedClick)
return () => {
removeDragListeners()
target.removeEventListener('mousedown', onMouseDown)
target.removeEventListener('click', preventUnwantedClick)
relinquishCursorLock()
if (modeRef.current !== 'notDragging') {
optsRef.current.onDragEnd &&
optsRef.current.onDragEnd(modeRef.current === 'dragging')
}
modeRef.current = 'notDragging'
}
}, [target])
}

View file

@ -0,0 +1,112 @@
import {
editable as e,
RefreshSnapshot,
SheetProvider,
extension,
} from '@theatre/r3f'
import {OrbitControls, Stars} from '@react-three/drei'
import {getProject} from '@theatre/core'
import React, {Suspense, useState} from 'react'
import {Canvas} from '@react-three/fiber'
import {useGLTF} from '@react-three/drei'
import sceneGLB from './scene.glb'
import studio from '@theatre/studio'
studio.extend(extension)
studio.initialize()
document.body.style.backgroundColor = '#171717'
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
author: overlaps (https://sketchfab.com/overlaps)
license: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
source: https://sketchfab.com/models/91964c1ce1a34c3985b6257441efa500
title: Space exploration [WLP series #8]
*/
function Model({url}: {url: string}) {
const {nodes} = useGLTF(url) as any
return (
<group rotation={[-Math.PI / 2, 0, 0]} position={[0, -7, 0]} scale={7}>
<group rotation={[Math.PI / 13.5, -Math.PI / 5.8, Math.PI / 5.6]}>
<e.mesh
uniqueName="Thingy"
receiveShadow
castShadow
geometry={nodes.planet001.geometry}
material={nodes.planet001.material}
/>
<e.mesh
uniqueName="Debris 2"
receiveShadow
castShadow
geometry={nodes.planet002.geometry}
material={nodes.planet002.material}
/>
<e.mesh
uniqueName="Debris 1"
geometry={nodes.planet003.geometry}
material={nodes.planet003.material}
/>
</group>
</group>
)
}
function App() {
const bgs = ['#272730', '#b7c5d1']
const [bgIndex, setBgIndex] = useState(0)
const bg = bgs[bgIndex]
return (
<div
onClick={() => {
// return setBgIndex((bgIndex) => (bgIndex + 1) % bgs.length)
}}
>
<Canvas dpr={[1.5, 2]} linear shadows frameloop="demand">
<SheetProvider getSheet={() => getProject('Space').sheet('Scene')}>
<fog attach="fog" args={[bg, 16, 30]} />
<color attach="background" args={[bg]} />
<ambientLight intensity={0.75} />
<e.perspectiveCamera
uniqueName="Camera"
// @ts-ignore
makeDefault
position={[0, 0, 16]}
fov={75}
>
<e.pointLight
uniqueName="Light 1"
intensity={1}
position={[-10, -25, -10]}
/>
<e.spotLight
uniqueName="Light 2"
castShadow
intensity={2.25}
angle={0.2}
penumbra={1}
position={[-25, 20, -15]}
shadow-mapSize={[1024, 1024]}
shadow-bias={-0.0001}
/>
</e.perspectiveCamera>
<Suspense fallback={null}>
<RefreshSnapshot />
<Model url={sceneGLB} />
</Suspense>
<OrbitControls
enablePan={false}
enableZoom={true}
maxPolarAngle={Math.PI / 2}
minPolarAngle={Math.PI / 2}
/>
<Stars radius={500} depth={50} count={1000} factor={10} />
</SheetProvider>
</Canvas>
</div>
)
}
export default App

View file

@ -0,0 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))

Binary file not shown.

View file

@ -0,0 +1,152 @@
import type {MutableRefObject} from 'react'
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import studio from '@theatre/studio'
import type {ISheet} from '@theatre/core'
import {types} from '@theatre/core'
import type {ITurtle} from './turtle'
import {drawTurtlePlan, makeTurtlePlan} from './turtle'
studio.initialize()
const objConfig = {
startingPoint: {
x: types.number(0.5, {range: [0, 1]}),
y: types.number(0.5, {range: [0, 1]}),
},
scale: types.number(1, {range: [0.1, 1000]}),
}
const TurtleRenderer: React.FC<{
sheet: ISheet
objKey: string
width: number
height: number
programFn: (t: ITurtle) => void
}> = (props) => {
const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null)
const context = useMemo(() => {
if (canvas) {
return canvas.getContext('2d')!
}
}, [canvas])
const dimsRef = useRef({width: props.width, height: props.height})
dimsRef.current = {width: props.width, height: props.height}
const obj = useMemo(() => {
return props.sheet.object(props.objKey, objConfig)
}, [props.sheet, props.objKey])
useEffect(() => {
obj.onValuesChange((v) => {
setTransforms(v)
})
}, [obj])
const [transforms, transformsRef, setTransforms] = useStateAndRef<
typeof obj.value
>({scale: 1, startingPoint: {x: 0.5, y: 0.5}})
const bounds = useMemo(() => canvas?.getBoundingClientRect(), [canvas])
useLayoutEffect(() => {
if (!canvas) return
const receiveWheelEvent = (event: WheelEvent) => {
event.preventDefault()
event.stopPropagation()
const oldTransform = transformsRef.current
const newTransform: typeof oldTransform = {
...oldTransform,
startingPoint: {...oldTransform.startingPoint},
}
if (event.ctrlKey) {
const scaleFactor = 1 - (event.deltaY / dimsRef.current.height) * 1.2
newTransform.scale *= scaleFactor
// const bounds = canvas.getBoundingClientRect()
const anchorPoint = {
x: (event.clientX - bounds!.left) / dimsRef.current.width,
y: (event.clientY - bounds!.top) / dimsRef.current.height,
}
newTransform.startingPoint.x =
anchorPoint.x -
(anchorPoint.x - newTransform.startingPoint.x) * scaleFactor
newTransform.startingPoint.y =
anchorPoint.y -
(anchorPoint.y - newTransform.startingPoint.y) * scaleFactor
} else {
newTransform.startingPoint.x =
oldTransform.startingPoint.x - event.deltaX / dimsRef.current.width
newTransform.startingPoint.y =
oldTransform.startingPoint.y - event.deltaY / dimsRef.current.height
}
studio.transaction((api) => {
api.set(obj.props, newTransform)
})
// setTransforms(newTransform)
}
const listenerOptions = {
capture: true,
passive: false,
}
canvas.addEventListener('wheel', receiveWheelEvent, listenerOptions)
return () => {
canvas.removeEventListener('wheel', receiveWheelEvent, listenerOptions)
}
}, [canvas])
const plan = useMemo(() => makeTurtlePlan(props.programFn), [props.programFn])
useEffect(() => {
if (!context) return
drawTurtlePlan(
plan,
context,
{
width: props.width,
height: props.height,
scale: transforms.scale,
startFrom: {
x: transforms.startingPoint.x * props.width,
y: transforms.startingPoint.y * props.height,
},
},
1,
)
}, [props.width, props.height, plan, context, transforms])
return (
<canvas width={props.width} height={props.height} ref={setCanvas}></canvas>
)
}
function useStateAndRef<S>(
initial: S,
): [S, MutableRefObject<S>, (s: S) => void] {
const [state, setState] = useState(initial)
const stateRef = useRef(state)
const set = useCallback((s: S) => {
stateRef.current = s
setState(s)
}, [])
return [state, stateRef, set]
}
export default TurtleRenderer

View file

@ -0,0 +1,55 @@
/**
* A super basic Turtle geometry renderer hooked up to Theatre, so the parameters
* can be tweaked and animated.
*/
import React, {useMemo, useState} from 'react'
import {render} from 'react-dom'
import {getProject} from '@theatre/core'
import type {ITurtle} from './turtle'
import TurtleRenderer from './TurtleRenderer'
import {useBoundingClientRect} from './utils'
const project = getProject('Turtle Playground')
const sheet = project.sheet('Turtle', 'The only one')
const TurtleExample: React.FC<{}> = (props) => {
const [container, setContainer] = useState<HTMLDivElement | null>(null)
const programFn = useMemo(() => {
return ({forward, backward, left, right, repeat}: ITurtle) => {
const steps = 10
repeat(steps, () => {
forward(steps * 2)
right(360 / steps)
})
}
}, [])
const bounds = useBoundingClientRect(container)
return (
<div
ref={setContainer}
style={{
position: 'fixed',
top: '0',
right: '0',
bottom: '0',
left: '0',
background: 'black',
}}
>
{bounds && (
<TurtleRenderer
sheet={sheet}
objKey="Renderer"
width={bounds.width}
height={bounds.height}
programFn={programFn}
/>
)}
</div>
)
}
render(<TurtleExample />, document.getElementById('root'))

View file

@ -0,0 +1,189 @@
import clamp from 'lodash-es/clamp'
type Op_Move = {
type: 'Move'
amount: number
angle: number
penDown: boolean
}
type Op_ModifyContext = {
type: 'ContextModifier'
fn: (ctx: CanvasRenderingContext2D) => void
}
type Op = Op_ModifyContext | Op_Move
type IPlan = {
totalTravel: number
ops: Op[]
}
export function makeTurtlePlan(fn: (turtle: Turtle) => void): IPlan {
const plan: IPlan = {
totalTravel: 0,
ops: [],
}
const turtle = new Turtle(plan)
fn(turtle)
return plan
}
export function drawTurtlePlan(
plan: IPlan,
ctx: CanvasRenderingContext2D,
{
width,
height,
scale,
startFrom,
}: {
width: number
height: number
scale: number
startFrom: {x: number; y: number}
},
tilProgression: number,
): void {
const {ops} = plan
if (ops.length === 0) return
const targetDistance = clamp(tilProgression, 0, 1) * plan.totalTravel
ctx.clearRect(0, 0, width, height)
let traveledSoFar = 0
let pos = {...startFrom}
ctx.beginPath()
ctx.lineWidth = 2
ctx.strokeStyle = 'white'
ctx.moveTo(pos.x, pos.y)
for (const op of ops) {
if (traveledSoFar >= targetDistance) return
if (op.type === 'ContextModifier') {
op.fn(ctx)
} else {
let amount = Math.abs(op.amount)
const sign = op.amount < 0 ? -1 : 1
const {angle} = op
const roomTilTarget = targetDistance - traveledSoFar
const distanceInThisStep = roomTilTarget < amount ? roomTilTarget : amount
traveledSoFar += distanceInThisStep
pos = move(pos, angle, distanceInThisStep * sign, scale, op.penDown, ctx)
ctx.stroke()
}
}
}
function move(
pointA: {x: number; y: number},
_angle: number,
amount: number,
scale: number,
penIsDown: boolean,
ctx: CanvasRenderingContext2D,
): {x: number; y: number} {
const angle = (_angle * Math.PI) / 180
const unrotatedTarget = {
x: pointA.x + amount * scale,
y: pointA.y,
}
const pointB = {
x:
pointA.x +
Math.cos(angle) * (unrotatedTarget.x - pointA.x) -
Math.sin(angle) * (unrotatedTarget.y - pointA.y),
y:
pointA.y +
Math.sin(angle) * (unrotatedTarget.x - pointA.x) -
Math.sin(angle) * (unrotatedTarget.y - pointA.y),
}
if (penIsDown) {
ctx.lineTo(pointB.x, pointB.y)
} else {
ctx.moveTo(pointB.x, pointB.y)
}
return pointB
}
class Turtle {
private _state = {
penIsDown: true,
angle: -90,
}
constructor(private _plan: IPlan) {}
fn = (innerFn: () => void) => {
return innerFn
}
private _pushContextModifier(fn: (ctx: CanvasRenderingContext2D) => void) {
this._plan.ops.push({type: 'ContextModifier', fn})
}
press = (n: number) => {
this._pushContextModifier((ctx) => {
ctx.lineWidth = n
})
}
forward = (amount: number) => {
this._plan.ops.push({
type: 'Move',
amount,
penDown: this._state.penIsDown,
angle: this._state.angle,
})
this._plan.totalTravel += Math.abs(amount)
return this
}
backward = (amount: number) => {
return this.forward(amount)
}
right = (deg: number) => {
this._rotate(deg)
return this
}
left = (deg: number) => {
this._rotate(-deg)
return this
}
private _rotate(deg: number) {
this._state.angle += deg
}
penup = () => {
this._state.penIsDown = false
return this
}
pendown = () => {
this._state.penIsDown = true
return this
}
repeat = (n: number, fn: (i: number) => void) => {
for (let i = 0; i < n; i++) {
fn(i)
}
return this
}
}
export type ITurtle = Turtle

View file

@ -0,0 +1,19 @@
import {useLayoutEffect, useState} from 'react'
export function useBoundingClientRect(
node: HTMLElement | null,
): null | DOMRect {
const [bounds, set] = useState<null | DOMRect>(null)
useLayoutEffect(() => {
if (node) {
set(node.getBoundingClientRect())
}
return () => {
set(null)
}
}, [node])
return bounds
}