diff --git a/pkg/ludus.js b/pkg/ludus.js
index 1781b3f..5dbdd46 100644
--- a/pkg/ludus.js
+++ b/pkg/ludus.js
@@ -1,4 +1,8 @@
-if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, svg, flush_commands, commands, result, flush_result, input, is_running, key_down, key_up, is_starting_up}
+import {p5} from "./p5.js"
+
+import {svg as svg_2} from "./svg.js"
+
+if (window) window.ludus = {run, kill, flush_stdout, stdout, p5, flush_commands, commands, result, flush_result, input, is_running, key_down, key_up, is_starting_up, p5, svg}
const worker_url = new URL("worker.js", import.meta.url)
const worker = new Worker(worker_url, {type: "module"})
@@ -104,7 +108,7 @@ export function run (source) {
ludus_console = ""
ludus_commands = []
ludus_result = null
- code = null
+ code = source
running = true
ready = false
keys_down = new Set();
@@ -180,394 +184,10 @@ export function key_up (str) {
if (is_running()) keys_down.delete(str)
}
-//////////// turtle plumbing below
-// TODO: refactor this out into modules
-const turtle_init = {
- position: [0, 0],
- heading: 0,
- pendown: true,
- pencolor: "white",
- penwidth: 1,
- visible: true
-}
+export {p5} from "./p5.js"
-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
- 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, "'")
-}
-
-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}
-
- `
-}
-
-// TODO: update this to match the new `p5` function
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] = 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 `
-
- `
+ console.log("generating svg for ${code}")
+ return svg_2(commands, 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 [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
-// }
-
-function last (arr) {
- return arr[arr.length - 1]
-}
-
-export function p5 (commands) {
- const all_states = {}
- for (const command of commands) {
- const [turtle_id, _, this_command] = command
- let stack = all_states[turtle_id]
- if (!stack) {
- const new_stack = [turtle_init]
- all_states[turtle_id] = new_stack
- stack = new_stack
- }
- let prev_state = last(all_states[turtle_id])
- const new_state = command_to_state(prev_state, this_command)
- all_states[turtle_id].push(new_state)
- }
- 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 (const states of Object.values(all_states)) {
- console.log(states)
- 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
-}
-
-
diff --git a/pkg/p5.js b/pkg/p5.js
new file mode 100644
index 0000000..0f803ab
--- /dev/null
+++ b/pkg/p5.js
@@ -0,0 +1,91 @@
+import {eq_vect, eq_color, resolve_color, turtle_color, turtle_radius, turtle_angle, turn_to_rad, turtle_init, command_to_state, background_color, rotate, last} from "./turtle_geometry.js"
+
+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
+}
+
+function p5_call_root () {
+ return [
+ ["background", ...resolve_color(background_color)],
+ ["push"],
+ ["rotate", Math.PI],
+ ["scale", -1, 1],
+ ["stroke", ...resolve_color(turtle_init.pencolor)],
+ ]
+}
+
+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 all_states = {}
+ for (const command of commands) {
+ const [turtle_id, _, this_command] = command
+ let stack = all_states[turtle_id]
+ if (!stack) {
+ const new_stack = [turtle_init]
+ all_states[turtle_id] = new_stack
+ stack = new_stack
+ }
+ let prev_state = last(all_states[turtle_id])
+ const new_state = command_to_state(prev_state, this_command)
+ all_states[turtle_id].push(new_state)
+ }
+ 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 (const states of Object.values(all_states)) {
+ console.log(states)
+ 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
+}
+
+
diff --git a/pkg/svg.js b/pkg/svg.js
new file mode 100644
index 0000000..e751c8b
--- /dev/null
+++ b/pkg/svg.js
@@ -0,0 +1,120 @@
+import {eq_vect, resolve_color, turtle_color, turtle_radius, rotate, turn_to_deg, command_to_state, turtle_init, background_color, turtle_angle, last} from "./turtle_geometry.js"
+
+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, "'")
+}
+
+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}
+
+ `
+}
+
+// TODO: update this to match the new `p5` function
+export function svg (commands, code) {
+ const all_states = {}
+ for (const command of commands) {
+ const [turtle_id, _, this_command] = command
+ let stack = all_states[turtle_id]
+ if (!stack) {
+ const new_stack = [turtle_init]
+ all_states[turtle_id] = new_stack
+ stack = new_stack
+ }
+ const prev_state = last(all_states[turtle_id])
+ const new_state = command_to_state(prev_state, this_command)
+ all_states[turtle_id].push(new_state)
+ }
+ let maxX = -Infinity, maxY = -Infinity, minX = Infinity, minY = Infinity
+ for (const states of Object.values(all_states)) {
+ for (const {position: [x, y]} of states) {
+ maxX = Math.max(maxX, x)
+ maxY = Math.max(maxY, y)
+ minX = Math.min(minX, x)
+ minY = Math.min(minY, y)
+ }
+ }
+ const [r, g, b] = 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
+ let path = ""
+ let turtle = ""
+ for (const states of Object.values(all_states)) {
+ path = path + svg_render_path(states)
+ turtle = svg_render_turtle(last(states))
+ }
+ return `
+
+ `
+}
+
diff --git a/pkg/turtle_geometry.js b/pkg/turtle_geometry.js
new file mode 100644
index 0000000..f8de0b3
--- /dev/null
+++ b/pkg/turtle_geometry.js
@@ -0,0 +1,169 @@
+export const turtle_init = {
+ position: [0, 0],
+ heading: 0,
+ pendown: true,
+ pencolor: "white",
+ penwidth: 1,
+ visible: true
+}
+
+export const turtle_radius = 20
+
+export const turtle_angle = 0.385
+
+export let turtle_color = [255, 255, 255, 150]
+
+export const colors = {
+ black: [0, 0, 0, 255],
+ silver: [192, 192, 192, 255],
+ gray: [128, 128, 128, 255],
+ grey: [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],
+}
+
+export 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?
+}
+
+export let background_color = "black"
+
+export function add (v1, v2) {
+ const [x1, y1] = v1
+ const [x2, y2] = v2
+ return [x1 + x2, y1 + y2]
+}
+
+export function mult (vector, scalar) {
+ const [x, y] = vector
+ return [x * scalar, y * scalar]
+}
+
+export function unit_of (heading) {
+ const turns = -heading + 0.25
+ const radians = turn_to_rad(turns)
+ return [Math.cos(radians), Math.sin(radians)]
+}
+
+export function command_to_state (prev_state, curr_command) {
+ const [verb] = curr_command
+ 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
+ }
+ }
+}
+
+export function eq_vect (v1, v2) {
+ const [x1, y1] = v1
+ const [x2, y2] = v2
+ return (x1 === x2) && (y1 === y2)
+}
+
+export 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
+}
+
+export 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))
+ ]
+}
+
+export function turn_to_rad (heading) {
+ return (heading % 1) * 2 * Math.PI
+}
+
+export function turn_to_deg (heading) {
+ return (heading % 1) * 360
+}
+
+export function last (arr) {
+ return arr[arr.length - 1]
+}
+
+