theatre/packages/playground/src/turtle/TurtleRenderer.tsx
2021-09-06 11:26:00 +02:00

152 lines
4 KiB
TypeScript

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