Initial OSS commit

This commit is contained in:
Aria Minaei 2021-06-18 13:05:06 +02:00
commit 4a7303f40a
391 changed files with 245738 additions and 0 deletions

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'
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

View 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'))

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
}