theatre/packages/playground/src/turtle/turtle.ts
2021-06-18 13:05:06 +02:00

189 lines
3.5 KiB
TypeScript

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