import init from "./out.mjs" const mod = await init() let res = null let code = null export function run (source) { code = source const output = mod.ludus(source).value res = JSON.parse(output) return res } export function stdout () { if (!res) return "" return res.io.stdout.data } export function turtle_commands () { if (!res) return [] return res.io.turtle.data } export function result () { return res } 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 return [0, 0, 0, 255] // default to black? } 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 "penwidth": { const [_, width] = curr_command return {...prev_state, penwidth: width} } case "pencolor": { const [_, color] = curr_command return {...prev_state, pencolor: color} } case "setheading": { const [_, heading] = curr_command return {...prev_state, heading: heading} } case "loadstate": { // console.log("LOADSTATE: ", curr_command) const [_, [x, y], heading, visible, pendown, penwidth, pencolor] = curr_command 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": { background_color = curr_command[1] return prev_state } } } function eq_vect (v1, v2) { const [x1, y1] = v1 const [x2, y2] = v2 return (x1 === x2) && (y1 === y2) } function eq_color (c1, c2) { if (c1 === c2) return true const res1 = resolve_color(c1) const res2 = resolve_color(c2) for (let i = 0; i < res1.length; ++i) { if (res1[i] !== res2[i]) return false } return true } 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 && !eq_vect(prev.position, curr.position)) { calls.push(["line", prev.position[0], prev.position[1], curr.position[0], curr.position[1]]) } if (!eq_color(curr.pencolor, prev.pencolor)) { calls.push(["stroke", ...resolve_color(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)], ["push"], ["rotate", Math.PI], ["scale", -1, 1], ["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 % 1) * 2 * Math.PI } function turn_to_deg (heading) { return (heading % 1) * 360 } function svg_render_line (prev, curr) { if (!prev.pendown) return "" if (eq_vect(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, "'") } export function extract_ludus (svg) { const code = svg.split("")[1]?.split("")[0] ?? "" return code .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 ? `` : "" return ` ${ink} ` } export function svg (commands) { // console.log(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) // console.log(states) 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) * 1.2 const view_height = (maxY - minY) * 1.2 const margin = Math.max(view_width, view_height) * 0.1 const x1 = minX - margin // don't actually need these: // 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]) // need negative turtle rotation with the other p5 translations 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 (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) // console.log(states) 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) p5_calls.push(["pop"]) return p5_calls }