factor svg/p5 into modules; fix svg rendering
Former-commit-id: f635e878c9
This commit is contained in:
parent
795a50a69c
commit
16be2bce3d
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 = 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 `
|
||||
<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) {
|
||||
// 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 `<?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>
|
||||
`
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
|
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