diff --git a/pkg/.gitignore b/pkg/.gitignore
new file mode 100644
index 0000000..074963b
--- /dev/null
+++ b/pkg/.gitignore
@@ -0,0 +1,2 @@
+*.wasm
+*.wasm*
diff --git a/pkg/ludus.js b/pkg/ludus.js
new file mode 100644
index 0000000..cbb2699
--- /dev/null
+++ b/pkg/ludus.js
@@ -0,0 +1,385 @@
+const mod = require("./rudus.js");
+
+let res = null
+
+let code = null
+
+function run (source) {
+ code = source
+ const output = mod.run(source)
+ res = JSON.parse(output)
+ return res
+}
+
+function stdout () {
+ if (!res) return ""
+ return res.io.stdout.data
+}
+
+function turtle_commands () {
+ if (!res) return []
+ return res.io.turtle.data
+}
+
+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, 255, 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
+
+let turtle_color = [255, 255, 255, 150]
+
+function p5_call_root () {
+ return [
+ ["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 hex (n) {
+ return n.toString(16).padStart(2, "0")
+}
+
+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, "'")
+}
+
+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}
+
+ `
+}
+
+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)
+ if ((r+g+b)/3 > 128) turtle_color = [0, 0, 0, 150]
+ 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 x_origin = minX - margin
+ const y_origin = -maxY - margin
+ const path = svg_render_path(states)
+ const turtle = svg_render_turtle(states[states.length - 1])
+ return `
+
+ `
+}
+
+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
+}
+
+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 [r, g, b, _] = resolve_color(background_color)
+ if ((r + g + b)/3 > 128) turtle_color = [0, 0, 0, 150]
+ 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
+}
+
+module.exports = {
+ run,
+ stdout,
+ turtle_commands,
+ result,
+ extract_ludus,
+ p5,
+ svg,
+}
diff --git a/pkg/rudus.d.ts b/pkg/rudus.d.ts
new file mode 100644
index 0000000..45b68f4
--- /dev/null
+++ b/pkg/rudus.d.ts
@@ -0,0 +1,3 @@
+/* tslint:disable */
+/* eslint-disable */
+export function run(src: string): string;
diff --git a/pkg/rudus.js b/pkg/rudus.js
new file mode 100644
index 0000000..1f40af7
--- /dev/null
+++ b/pkg/rudus.js
@@ -0,0 +1,119 @@
+
+let imports = {};
+imports['__wbindgen_placeholder__'] = module.exports;
+let wasm;
+const { TextEncoder, TextDecoder } = require(`util`);
+
+let WASM_VECTOR_LEN = 0;
+
+let cachedUint8ArrayMemory0 = null;
+
+function getUint8ArrayMemory0() {
+ if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
+ cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
+ }
+ return cachedUint8ArrayMemory0;
+}
+
+let cachedTextEncoder = new TextEncoder('utf-8');
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+ ? function (arg, view) {
+ return cachedTextEncoder.encodeInto(arg, view);
+}
+ : function (arg, view) {
+ const buf = cachedTextEncoder.encode(arg);
+ view.set(buf);
+ return {
+ read: arg.length,
+ written: buf.length
+ };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+ if (realloc === undefined) {
+ const buf = cachedTextEncoder.encode(arg);
+ const ptr = malloc(buf.length, 1) >>> 0;
+ getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
+ WASM_VECTOR_LEN = buf.length;
+ return ptr;
+ }
+
+ let len = arg.length;
+ let ptr = malloc(len, 1) >>> 0;
+
+ const mem = getUint8ArrayMemory0();
+
+ let offset = 0;
+
+ for (; offset < len; offset++) {
+ const code = arg.charCodeAt(offset);
+ if (code > 0x7F) break;
+ mem[ptr + offset] = code;
+ }
+
+ if (offset !== len) {
+ if (offset !== 0) {
+ arg = arg.slice(offset);
+ }
+ ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
+ const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
+ const ret = encodeString(arg, view);
+
+ offset += ret.written;
+ ptr = realloc(ptr, len, offset, 1) >>> 0;
+ }
+
+ WASM_VECTOR_LEN = offset;
+ return ptr;
+}
+
+let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+function getStringFromWasm0(ptr, len) {
+ ptr = ptr >>> 0;
+ return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
+}
+/**
+ * @param {string} src
+ * @returns {string}
+ */
+module.exports.run = function(src) {
+ let deferred2_0;
+ let deferred2_1;
+ try {
+ const ptr0 = passStringToWasm0(src, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+ const len0 = WASM_VECTOR_LEN;
+ const ret = wasm.run(ptr0, len0);
+ deferred2_0 = ret[0];
+ deferred2_1 = ret[1];
+ return getStringFromWasm0(ret[0], ret[1]);
+ } finally {
+ wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
+ }
+};
+
+module.exports.__wbindgen_init_externref_table = function() {
+ const table = wasm.__wbindgen_export_0;
+ const offset = table.grow(4);
+ table.set(0, undefined);
+ table.set(offset + 0, undefined);
+ table.set(offset + 1, null);
+ table.set(offset + 2, true);
+ table.set(offset + 3, false);
+ ;
+};
+
+const path = require('path').join(__dirname, 'rudus_bg.wasm');
+const bytes = require('fs').readFileSync(path);
+
+const wasmModule = new WebAssembly.Module(bytes);
+const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
+wasm = wasmInstance.exports;
+module.exports.__wasm = wasm;
+
+wasm.__wbindgen_start();
+