352 lines
9.7 KiB
JavaScript
352 lines
9.7 KiB
JavaScript
import init from "./out.mjs"
|
|
|
|
const mod = await init()
|
|
|
|
let result = null
|
|
|
|
let code = null
|
|
|
|
export function run (source) {
|
|
code = source
|
|
const output = mod.ludus(source).value
|
|
result = JSON.parse(output)
|
|
return result
|
|
}
|
|
|
|
export function stdout () {
|
|
if (!result) return ""
|
|
return result.io.console.data
|
|
}
|
|
|
|
export function turtle_commands () {
|
|
if (!result) return []
|
|
return result.io.turtle.data
|
|
}
|
|
|
|
const turtle_init = {
|
|
position: [0, 0],
|
|
heading: 0,
|
|
pendown: true,
|
|
pencolor: "white",
|
|
penwidth: 1,
|
|
visible: true
|
|
}
|
|
|
|
const colors = {
|
|
black: [0, 0, 0, 255],
|
|
silver: [192, 192, 192, 255],
|
|
gray: [128, 128, 128, 255],
|
|
white: [255, 255, 255, 255],
|
|
maroon: [128, 0, 0, 255],
|
|
red: [255, 0, 0, 255],
|
|
purple: [128, 0, 128, 255],
|
|
fuchsia: [255, 0, 255, 255],
|
|
green: [0, 128, 0, 255],
|
|
lime: [0, 255, 0, 255],
|
|
olive: [128, 128, 0, 255],
|
|
yellow: [255, 255, 0, 255],
|
|
navy: [0, 0, 128, 255],
|
|
blue: [0, 0, 255, 255],
|
|
teal: [0, 128, 128, 255],
|
|
aqua: [0, 255, 25, 255],
|
|
}
|
|
|
|
function resolve_color (color) {
|
|
if (typeof color === 'string') return colors[color]
|
|
if (typeof color === 'number') return [color, color, color, 255]
|
|
if (Array.isArray(color)) return color
|
|
}
|
|
|
|
let background_color = "black"
|
|
|
|
function add (v1, v2) {
|
|
const [x1, y1] = v1
|
|
const [x2, y2] = v2
|
|
return [x1 + x2, y1 + y2]
|
|
}
|
|
|
|
function mult (vector, scalar) {
|
|
const [x, y] = vector
|
|
return [x * scalar, y * scalar]
|
|
}
|
|
|
|
function unit_of (heading) {
|
|
const turns = -heading + 0.25
|
|
const radians = turn_to_rad(turns)
|
|
return [Math.cos(radians), Math.sin(radians)]
|
|
}
|
|
|
|
function command_to_state (prev_state, curr_command) {
|
|
const verb = curr_command[0]
|
|
switch (verb) {
|
|
case "goto": {
|
|
const [_, x, y] = curr_command
|
|
return {...prev_state, position: [x, y]}
|
|
}
|
|
case "home": {
|
|
return {...prev_state, position: [0, 0], heading: 0}
|
|
}
|
|
case "right": {
|
|
const [_, angle] = curr_command
|
|
const {heading} = prev_state
|
|
return {...prev_state, heading: heading + angle}
|
|
}
|
|
case "left": {
|
|
const [_, angle] = curr_command
|
|
const {heading} = prev_state
|
|
return {...prev_state, heading: heading - angle}
|
|
}
|
|
case "forward": {
|
|
const [_, steps] = curr_command
|
|
const {heading, position} = prev_state
|
|
const unit = unit_of(heading)
|
|
const move = mult(unit, steps)
|
|
return {...prev_state, position: add(position, move)}
|
|
}
|
|
case "back": {
|
|
const [_, steps] = curr_command
|
|
const {heading, position} = prev_state
|
|
const unit = unit_of(heading)
|
|
const move = mult(unit, -steps)
|
|
return {...prev_state, position: add(position, move)}
|
|
}
|
|
case "penup": {
|
|
return {...prev_state, pendown: false}
|
|
}
|
|
case "pendown": {
|
|
return {...prev_state, pendown: true}
|
|
}
|
|
case "pendwith": {
|
|
const [_, width] = curr_command
|
|
return {...prev_state, penwidth: width}
|
|
}
|
|
case "pencolor": {
|
|
if (curr_command.length = 2) {
|
|
const [_, color] = curr_command
|
|
return {...prev_state, pencolor: color}
|
|
} else {
|
|
const [_, r, g, b, a] = curr_command
|
|
return {...prev_state, pencolor: [r, g, b, a]}
|
|
}
|
|
}
|
|
case "setheading": {
|
|
const [_, heading] = curr_command
|
|
return {...prev_state, heading: heading}
|
|
}
|
|
case "loadstate": {
|
|
const [_, x, y, heading, visible, pendown, penwidth] = curr_command
|
|
// there are 7 fixed-arity arguments
|
|
// color will either be the last one or four
|
|
let pencolor = curr_command.slice(7)
|
|
// if there's one, it's a string, unpack it
|
|
if (pencolor.length = 1) pencolor = pencolor[0]
|
|
return {position: [x, y], heading, visible, pendown, penwidth, pencolor}
|
|
}
|
|
case "show": {
|
|
return {...prev_state, visible: true}
|
|
}
|
|
case "hide": {
|
|
return {...prev_state, visible: false}
|
|
}
|
|
case "background": {
|
|
let color = curr_command.slice(1)
|
|
if (color.lengh = 1) color = color[0]
|
|
background_color = color
|
|
return prev_state
|
|
}
|
|
}
|
|
}
|
|
|
|
function are_eq (v1, v2) {
|
|
const [x1, y1] = v1
|
|
const [x2, y2] = v2
|
|
return (x1 === x2) && (y1 === y2)
|
|
}
|
|
|
|
function states_to_call (prev, curr) {
|
|
const calls = []
|
|
// whose state should we use?
|
|
// pen states will only differ on more than one property
|
|
// if we use `loadstate`
|
|
// my sense is `prev`, but that may change
|
|
if (prev.pendown && !are_eq(prev.position, curr.position)) {
|
|
calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]])
|
|
}
|
|
if (curr.pencolor !== prev.pencolor) {
|
|
if (Array.isArray(curr.pencolor)) {
|
|
calls.push(["stroke", ...curr.pencolor])
|
|
} else {
|
|
calls.push(["stroke", curr.pencolor])
|
|
}
|
|
}
|
|
if (curr.penwidth !== prev.penwidth) {
|
|
calls.push(["strokeWeight", curr.penwidth])
|
|
}
|
|
return calls
|
|
}
|
|
|
|
const turtle_radius = 20
|
|
|
|
const turtle_angle = 0.385
|
|
|
|
const turtle_color = [255, 255, 255, 150]
|
|
|
|
const p5_call_root = [
|
|
["background", ...resolve_color(background_color)],
|
|
["stroke", ...resolve_color(turtle_init.pencolor)]
|
|
]
|
|
|
|
function rotate (vector, heading) {
|
|
const radians = turn_to_rad(heading)
|
|
const [x, y] = vector
|
|
return [
|
|
(x * Math.cos (radians)) - (y * Math.sin (radians)),
|
|
(x * Math.sin (radians)) + (y * Math.cos (radians))
|
|
]
|
|
}
|
|
|
|
function turn_to_rad (heading) {
|
|
return heading * 2 * Math.PI
|
|
}
|
|
|
|
function turn_to_deg (heading) {
|
|
return (heading * 360)
|
|
}
|
|
|
|
function svg_render_line (prev, curr) {
|
|
if (!prev.pendown) return ""
|
|
if (are_eq(prev.position, curr.position)) return ""
|
|
const {position: [x1, y1], pencolor, penwidth} = prev
|
|
const {position: [x2, y2]} = curr
|
|
const [r, g, b, a] = resolve_color(pencolor)
|
|
return `
|
|
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="rgb(${r} ${g} ${b})" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
|
|
`
|
|
}
|
|
|
|
function escape_svg (svg) {
|
|
return svg
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'")
|
|
}
|
|
|
|
function svg_render_path (states) {
|
|
const path = []
|
|
for (let i = 1; i < states.length; ++i) {
|
|
const prev = states[i - 1]
|
|
const curr = states[i]
|
|
path.push(svg_render_line(prev, curr))
|
|
}
|
|
return path.join("")
|
|
}
|
|
|
|
function svg_render_turtle (state) {
|
|
if (!state.visible) return ""
|
|
const [fr, fg, fb, fa] = turtle_color
|
|
const fill_alpha = fa/255
|
|
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
|
|
const origin = [0, turtle_radius]
|
|
const [x1, y1] = origin
|
|
const [x2, y2] = rotate(origin, turtle_angle)
|
|
const [x3, y3] = rotate(origin, -turtle_angle)
|
|
const [pr, pg, pb, pa] = resolve_color(pencolor)
|
|
const pen_alpha = pa/255
|
|
const ink = pendown ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="rgb(${pr} ${pg} ${pb})" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
|
|
return `
|
|
<g transform="translate(${x}, ${y})rotate(${turn_to_deg(heading)})">
|
|
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="rgb(${fr} ${fg} ${fb})" fill-opacity="${fill_alpha}"/>
|
|
${ink}
|
|
</g>
|
|
`
|
|
}
|
|
|
|
export function svg (commands) {
|
|
const states = [turtle_init]
|
|
commands.reduce((prev_state, command) => {
|
|
const new_state = command_to_state(prev_state, command)
|
|
states.push(new_state)
|
|
return new_state
|
|
}, turtle_init)
|
|
const {maxX, maxY, minX, minY} = states.reduce((accum, {position: [x, y]}) => {
|
|
accum.maxX = Math.max(accum.maxX, x)
|
|
accum.maxY = Math.max(accum.maxY, y)
|
|
accum.minX = Math.min(accum.minX, x)
|
|
accum.minY = Math.min(accum.minY, y)
|
|
return accum
|
|
|
|
}, {maxX: 0, maxY: 0, minX: 0, minY: 0})
|
|
const [r, g, b, a] = resolve_color(background_color)
|
|
const view_width = maxX - minX
|
|
const view_height = maxY - minY
|
|
const margin = Math.max(view_width, view_height) * 0.1
|
|
const x1 = Math.min(-300, (minX - margin))
|
|
const y1 = Math.min(-300, (minY - margin))
|
|
const x2 = Math.max(600, (maxX + margin))
|
|
const y2 = Math.max(600, (maxY + margin))
|
|
const path = svg_render_path(states)
|
|
const turtle = svg_render_turtle(states[states.length - 1])
|
|
return `<?xml version="1.0" standalone="no"?>
|
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="background-color:rgb(${r} ${g} ${b}); background-opacity: ${a/255}" viewBox="${x1} ${y1} ${x2} ${y2}">
|
|
|
|
<g transform="scale(-1, 1) rotate(180)">
|
|
${path}
|
|
${turtle}
|
|
</g>
|
|
|
|
<ludus>
|
|
${escape_svg(code)}
|
|
</ludus>
|
|
</svg>
|
|
`
|
|
}
|
|
|
|
function p5_render_turtle (state, calls) {
|
|
if (!state.visible) return
|
|
calls.push(["push"])
|
|
const [r, g, b, a] = turtle_color
|
|
calls.push(["fill", r, g, b, a])
|
|
const {heading, pencolor, position: [x, y], pendown, penwidth} = state
|
|
const origin = [0, turtle_radius]
|
|
const [x1, y1] = origin
|
|
const [x2, y2] = rotate(origin, turtle_angle)
|
|
const [x3, y3] = rotate(origin, -turtle_angle)
|
|
calls.push(["translate", x, y])
|
|
calls.push(["rotate", turn_to_rad(heading)])
|
|
calls.push(["noStroke"])
|
|
calls.push(["beginShape"])
|
|
calls.push(["vertex", x1, y1])
|
|
calls.push(["vertex", x2, y2])
|
|
calls.push(["vertex", x3, y3])
|
|
calls.push(["endShape"])
|
|
calls.push(["strokeWeight", penwidth])
|
|
calls.push(["stroke", ...resolve_color(pencolor)])
|
|
if (pendown) calls.push(["line", 0, 0, x1, y1])
|
|
calls.push(["pop"])
|
|
return calls
|
|
}
|
|
|
|
export function p5_calls (commands) {
|
|
const states = [turtle_init]
|
|
commands.reduce((prev_state, command) => {
|
|
const new_state = command_to_state(prev_state, command)
|
|
states.push(new_state)
|
|
return new_state
|
|
}, turtle_init)
|
|
const p5_calls = [...p5_call_root]
|
|
for (let i = 1; i < states.length; ++i) {
|
|
const prev = states[i - 1]
|
|
const curr = states[i]
|
|
const calls = states_to_call(prev, curr)
|
|
for (const call of calls) {
|
|
p5_calls.push(call)
|
|
}
|
|
}
|
|
p5_calls[0] = ["background", ...resolve_color(background_color)]
|
|
p5_render_turtle(states[states.length - 1], p5_calls)
|
|
return p5_calls
|
|
}
|
|
|