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": { console.log(curr_command) 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) + 180 } 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 ` ` } function escape_svg (svg) { return svg .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 ? `` : "" return ` ${ink} ` } 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 = minX - margin const y1 = minY - margin const x2 = maxX + margin const y2 = maxY + margin const path = svg_render_path(states) const turtle = svg_render_turtle(states[states.length - 1]) return ` ${path} ${turtle} ${escape_svg(code)} ` } 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 }