Initial OSS commit
This commit is contained in:
commit
4a7303f40a
391 changed files with 245738 additions and 0 deletions
152
packages/playground/src/turtle/TurtleRenderer.tsx
Normal file
152
packages/playground/src/turtle/TurtleRenderer.tsx
Normal 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'
|
||||
|
||||
const objConfig = {
|
||||
props: types.compound({
|
||||
startingPoint: types.compound({
|
||||
x: types.number(0.5, {min: 0, max: 1}),
|
||||
y: types.number(0.5, {min: 0, max: 1}),
|
||||
}),
|
||||
scale: types.number(1, {min: 0.1}),
|
||||
}),
|
||||
}
|
||||
|
||||
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, null, 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
|
52
packages/playground/src/turtle/index.tsx
Normal file
52
packages/playground/src/turtle/index.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
// sdf
|
||||
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: '20vw',
|
||||
bottom: '30vh',
|
||||
left: '20vw',
|
||||
background: 'black',
|
||||
}}
|
||||
>
|
||||
{bounds && (
|
||||
<TurtleRenderer
|
||||
sheet={sheet}
|
||||
objKey="Renderer"
|
||||
width={bounds.width}
|
||||
height={bounds.height}
|
||||
programFn={programFn}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(<TurtleExample />, document.getElementById('root'))
|
189
packages/playground/src/turtle/turtle.ts
Normal file
189
packages/playground/src/turtle/turtle.ts
Normal 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
|
19
packages/playground/src/turtle/utils.ts
Normal file
19
packages/playground/src/turtle/utils.ts
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue