factor svg/p5 into modules; fix svg rendering
This commit is contained in:
parent
8dfb8c88fe
commit
d39113d755
398
pkg/ludus.js
398
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_url = new URL("worker.js", import.meta.url)
|
||||||
const worker = new Worker(worker_url, {type: "module"})
|
const worker = new Worker(worker_url, {type: "module"})
|
||||||
|
@ -104,7 +108,7 @@ export function run (source) {
|
||||||
ludus_console = ""
|
ludus_console = ""
|
||||||
ludus_commands = []
|
ludus_commands = []
|
||||||
ludus_result = null
|
ludus_result = null
|
||||||
code = null
|
code = source
|
||||||
running = true
|
running = true
|
||||||
ready = false
|
ready = false
|
||||||
keys_down = new Set();
|
keys_down = new Set();
|
||||||
|
@ -180,394 +184,10 @@ export function key_up (str) {
|
||||||
if (is_running()) keys_down.delete(str)
|
if (is_running()) keys_down.delete(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////// turtle plumbing below
|
export {p5} from "./p5.js"
|
||||||
// TODO: refactor this out into modules
|
|
||||||
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
|
|
||||||
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 `
|
|
||||||
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="square" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function escape_svg (svg) {
|
|
||||||
return svg
|
|
||||||
.replace(/&/g, "&")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/"/g, """)
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extract_ludus (svg) {
|
|
||||||
const code = svg.split("<ludus>")[1]?.split("</ludus>")[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 ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
|
|
||||||
return `
|
|
||||||
<g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
|
|
||||||
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
|
|
||||||
${ink}
|
|
||||||
</g>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: update this to match the new `p5` function
|
|
||||||
export function svg (commands) {
|
export function svg (commands) {
|
||||||
// console.log(commands)
|
console.log("generating svg for ${code}")
|
||||||
const states = [turtle_init]
|
return svg_2(commands, code)
|
||||||
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 `<?xml version="1.0" standalone="no"?>
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">
|
|
||||||
|
|
||||||
<rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />
|
|
||||||
|
|
||||||
<g transform="scale(-1, 1) rotate(180)">
|
|
||||||
${path}
|
|
||||||
${turtle}
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<ludus>
|
|
||||||
${escape_svg(code)}
|
|
||||||
</ludus>
|
|
||||||
</svg>
|
|
||||||
`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
91
pkg/p5.js
Normal file
91
pkg/p5.js
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
120
pkg/svg.js
Normal file
120
pkg/svg.js
Normal file
|
@ -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 `
|
||||||
|
<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#${hex(r)}${hex(g)}${hex(b)}" stroke-linecap="round" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function escape_svg (svg) {
|
||||||
|
return svg
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extract_ludus (svg) {
|
||||||
|
const code = svg.split("<ludus>")[1]?.split("</ludus>")[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 ? `<line x1="${x1}" y1="${y1}" x2="0" y2="0" stroke="#${hex(pr)}${hex(pg)}${hex(pb)}" stroke-linecap="round" stroke-opacity="${pen_alpha}" stroke-width="${penwidth}" />` : ""
|
||||||
|
return `
|
||||||
|
<g transform="translate(${x}, ${y})rotate(${-turn_to_deg(heading)})">
|
||||||
|
<polygon points="${x1} ${y1} ${x2} ${y2} ${x3} ${y3}" stroke="none" fill="#${hex(fr)}${hex(fg)}${hex(fb)})" fill-opacity="${fill_alpha}"/>
|
||||||
|
${ink}
|
||||||
|
</g>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 `<?xml version="1.0" standalone="no"?>
|
||||||
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="${x_origin} ${y_origin} ${view_width} ${view_height}" width="10in" height="8in">
|
||||||
|
|
||||||
|
<rect x="${x_origin - 5}" y="${y_origin - 5}" width="${view_width + 10}" height="${view_height + 10}" fill="#${hex(r)}${hex(g)}${hex(b)}" stroke-width="0" paint-order="fill" />
|
||||||
|
|
||||||
|
<g transform="scale(-1, 1) rotate(180)">
|
||||||
|
${path}
|
||||||
|
${turtle}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<ludus>
|
||||||
|
${escape_svg(code)}
|
||||||
|
</ludus>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
169
pkg/turtle_geometry.js
Normal file
169
pkg/turtle_geometry.js
Normal file
|
@ -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]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user