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(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 ( ) } function useStateAndRef( initial: S, ): [S, MutableRefObject, (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