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 ` + + + + + + ${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 +} + +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(); +