Compare commits

..

23 Commits

Author SHA1 Message Date
Scott Richmond
2686d70a49 change test case 2024-10-18 15:18:56 -04:00
Scott Richmond
e6b2ffe4c9 add ludus-extraction function from svg 2024-09-09 18:23:13 -04:00
Scott Richmond
7285e599c5 0.1.27 2024-08-02 14:56:05 -04:00
Scott Richmond
39984ac537 build 2024-08-02 14:55:46 -04:00
Scott Richmond
73b2343963 add p5 test file 2024-08-02 13:56:37 -04:00
Scott Richmond
1733de7dff build 2024-08-01 18:54:18 -04:00
Scott Richmond
be9a86a973 build 2024-08-01 18:50:09 -04:00
Scott Richmond
479e304357 fix viewBox math so we see just what is there 2024-08-01 18:35:28 -04:00
Scott Richmond
db5622bccd new random seed for every run 2024-08-01 18:00:11 -04:00
Scott Richmond
7f64164078 build 2024-08-01 17:45:09 -04:00
Scott Richmond
d9e0fd23ec build 2024-08-01 17:43:29 -04:00
Scott Richmond
ffed651b6e build 2024-08-01 17:40:54 -04:00
Scott Richmond
ea80f81c33 build 2024-08-01 17:31:36 -04:00
Scott Richmond
d477782ff6 build 2024-08-01 17:05:41 -04:00
Scott Richmond
4baabc0a20 build 2024-08-01 16:43:01 -04:00
Scott Richmond
1085c7ae44 build out svg adapter, viewBox isn't working yet 2024-07-31 19:25:22 -04:00
Scott Richmond
e72b9f91ca build 2024-07-31 12:06:42 -04:00
Scott Richmond
d8f152998e build out p5 adapter 2024-07-30 18:36:13 -04:00
Scott Richmond
af125ffbbb fix background! bugs; delete a bunch of p5 adapter code, now moved to JS 2024-07-30 18:35:55 -04:00
Scott Richmond
657ff3dedb remove print! 2024-07-30 18:32:54 -04:00
Scott Richmond
2c9f6f8279 start building out p5 adapter 2024-07-22 11:39:57 -04:00
Scott Richmond
4ca1da1240 build 2024-07-21 20:00:34 -04:00
Scott Richmond
121446c5c4 first step: Ludus speaks turtle graphics, not p5 calls 2024-07-21 19:22:42 -04:00
18 changed files with 623 additions and 178 deletions

Binary file not shown.

View File

@ -2,8 +2,360 @@ import init from "./out.mjs"
const mod = await init()
let res = null
let code = null
export function run (source) {
const result = mod.ludus(source).value
console.log(result)
return JSON.parse(result)
code = source
const output = mod.ludus(source).value
res = JSON.parse(output)
return res
}
export function stdout () {
if (!res) return ""
return res.io.console.data
}
export function turtle_commands () {
if (!res) return []
return res.io.turtle.data
}
export 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, 25, 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 "pendwith": {
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": {
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
const turtle_color = [255, 255, 255, 150]
const p5_call_root = [
["background", ...resolve_color(background_color)],
["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 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="rgb(${r} ${g} ${b})" stroke-opacity="${a/255}" stroke-width="${penwidth}"/>
`
}
function escape_svg (svg) {
return svg
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
export function extract_ludus (svg) {
const code = svg.split("<ludus>")[1]?.split("</ludus>")[0] ?? ""
return code
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, `"`)
.replace(/&apos;/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="rgb(${pr} ${pg} ${pb})" 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="rgb(${fr} ${fg} ${fb})" fill-opacity="${fill_alpha}"/>
${ink}
</g>
`
}
export function svg (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)
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)
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 x1 = minX - margin
// don't actually need these:
// const y1 = minY - margin
// const x2 = maxX + margin
const y2 = 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" style="background-color:rgb(${r} ${g} ${b}); background-opacity: ${a/255}" viewBox="${x1} ${-y2} ${view_width} ${view_height}">
<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])
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)
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)
return p5_calls
}

View File

@ -6489,7 +6489,7 @@ var __emscripten_stack_alloc = (a0) => (__emscripten_stack_alloc = wasmExports['
var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'])();
var ___cxa_is_pointer_type = createExportWrapper('__cxa_is_pointer_type', 1);
var dynCall_jiji = Module['dynCall_jiji'] = createExportWrapper('dynCall_jiji', 5);
var ___emscripten_embedded_file_data = Module['___emscripten_embedded_file_data'] = 1840592;
var ___emscripten_embedded_file_data = Module['___emscripten_embedded_file_data'] = 1816104;
function invoke_i(index) {
var sp = stackSave();
try {

Binary file not shown.

17
build/p5_test.mjs Normal file
View File

@ -0,0 +1,17 @@
import {run, p5} from "./ludus.mjs"
const code = `
print! ("Hello, world!")
pencolor! (colors :white)
repeat 10 {
repeat 4 {
fd! (500)
rt! (0.25)
}
rt! (0.1)
}`
const result = run(code)
console.log(result.io.stdout.data)
console.log(p5(result.io.turtle.data))

15
build/svg_test.mjs Normal file
View File

@ -0,0 +1,15 @@
import {run, svg} from "./ludus.mjs"
const code = `
pencolor! (colors :white)
repeat 10 {
repeat 4 {
fd! (500)
rt! (0.25)
}
rt! (0.1)
}`
const result = run(code)
console.log(svg(result.io.turtle.data))

106
build/svg_test.svg Normal file
View File

@ -0,0 +1,106 @@
<?xml version="1.0" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="background-color:rgb(0 0 0); background-opacity: 1" viewBox="-866.0173929337993 -866.0173929338016 1676.162696000903 1676.1626960009057">
<g transform="scale(-1, 1) rotate(180)">
<line x1="0" y1="0" x2="3.061616997868383e-14" y2="500" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="3.061616997868383e-14" y1="500" x2="500.00000000000006" y2="500" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="500.00000000000006" y1="500" x2="500.0000000000001" y2="0" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="500.0000000000001" y1="0" x2="1.1368683772161603e-13" y2="-6.123233995736766e-14" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.1368683772161603e-13" y1="-6.123233995736766e-14" x2="293.892626146237" y2="404.50849718747344" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="293.892626146237" y1="404.50849718747344" x2="698.4011233337105" y2="110.61587104123663" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="698.4011233337105" y1="110.61587104123663" x2="404.50849718747384" y2="-293.892626146237" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="404.50849718747384" y1="-293.892626146237" x2="2.2737367544323206e-13" y2="-2.8421709430404007e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="2.2737367544323206e-13" y1="-2.8421709430404007e-13" x2="475.5282581475771" y2="154.5084971874731" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="475.5282581475771" y1="154.5084971874731" x2="630.0367553350503" y2="-321.01976096010384" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="630.0367553350503" y1="-321.01976096010384" x2="154.50849718747332" y2="-475.528258147577" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="154.50849718747332" y1="-475.528258147577" x2="-2.842170943040401e-14" y2="-1.1368683772161603e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-2.842170943040401e-14" y1="-1.1368683772161603e-13" x2="475.5282581475765" y2="-154.5084971874746" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="475.5282581475765" y1="-154.5084971874746" x2="321.01976096010196" y2="-630.0367553350511" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="321.01976096010196" y1="-630.0367553350511" x2="-154.50849718747457" y2="-475.5282581475766" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-154.50849718747457" y1="-475.5282581475766" x2="1.1368683772161603e-12" y2="-4.547473508864641e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.1368683772161603e-12" y1="-4.547473508864641e-13" x2="293.8926261462368" y2="-404.5084971874748" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="293.8926261462368" y1="-404.5084971874748" x2="-110.61587104123754" y2="-698.4011233337105" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-110.61587104123754" y1="-698.4011233337105" x2="-404.5084971874731" y2="-293.89262614623607" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-404.5084971874731" y1="-293.89262614623607" x2="1.3642420526593924e-12" y2="-5.115907697472721e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.3642420526593924e-12" y1="-5.115907697472721e-13" x2="1.3948582226380762e-12" y2="-500.0000000000005" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.3948582226380762e-12" y1="-500.0000000000005" x2="-499.9999999999986" y2="-500.00000000000057" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-499.9999999999986" y1="-500.00000000000057" x2="-499.9999999999987" y2="-5.684341886080801e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-499.9999999999987" y1="-5.684341886080801e-13" x2="1.3073986337985843e-12" y2="-5.684341886080801e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.3073986337985843e-12" y1="-5.684341886080801e-13" x2="-293.8926261462343" y2="-404.508497187475" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-293.8926261462343" y1="-404.508497187475" x2="-698.4011233337087" y2="-110.61587104123936" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-698.4011233337087" y1="-110.61587104123936" x2="-404.5084971874733" y2="293.8926261462352" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-404.5084971874733" y1="293.8926261462352" x2="1.0800249583553523e-12" y2="-4.547473508864641e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="1.0800249583553523e-12" y1="-4.547473508864641e-13" x2="-475.528258147575" y2="-154.5084971874763" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-475.528258147575" y1="-154.5084971874763" x2="-630.0367553350509" y2="321.0197609600998" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-630.0367553350509" y1="321.0197609600998" x2="-154.50849718747486" y2="475.52825814757574" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-154.50849718747486" y1="475.52825814757574" x2="9.663381206337363e-13" y2="-3.410605131648481e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="9.663381206337363e-13" y1="-3.410605131648481e-13" x2="-475.5282581475769" y2="154.50849718747014" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-475.5282581475769" y1="154.50849718747014" x2="-321.0197609601066" y2="630.0367553350482" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-321.0197609601066" y1="630.0367553350482" x2="154.50849718747122" y2="475.5282581475776" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="154.50849718747122" y1="475.5282581475776" x2="7.105427357601002e-13" y2="-2.2737367544323206e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="7.105427357601002e-13" y1="-2.2737367544323206e-13" x2="-293.89262614623954" y2="404.5084971874708" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="-293.89262614623954" y1="404.5084971874708" x2="110.61587104123151" y2="698.401123333711" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="110.61587104123151" y1="698.401123333711" x2="404.5084971874717" y2="293.8926261462399" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<line x1="404.5084971874717" y1="293.8926261462399" x2="6.252776074688882e-13" y2="-2.8421709430404007e-13" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1"/>
<g transform="translate(6.252776074688882e-13, -2.8421709430404007e-13)rotate(359.9999999999994)">
<polygon points="0 20 -13.226237306473037 -15.00222139260919 13.226237306473037 -15.00222139260919" stroke="none" fill="rgb(255 255 255)" fill-opacity="0.5882352941176471"/>
<line x1="0" y1="20" x2="0" y2="0" stroke="rgb(255 255 255)" stroke-opacity="1" stroke-width="1" />
</g>
</g>
<ludus>
pencolor! (colors :white)
repeat 10 {
repeat 4 {
fd! (500)
rt! (0.25)
}
rt! (0.1)
}
</ludus>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1,3 +1,9 @@
import {run} from "./ludus.mjs"
console.log(run(`let foo = 42; "{foo} bar"`))
console.log(run(`
forward! (100)
right! (0.25)
print! ("foobar")
`))

18
build/test.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="background-color:navy" viewBox="-100 -100 500 500">
<rect x="-10.5" y="6.123233995736766e-15" width="300" height="300.2" stroke="white" fill="transparent" stroke-width="1"/>
<g transform="translate(100, 100) rotate(180) rotate(180)">
<polygon points="0 20 -13.2262 -15.0022 13.2262 -15.0022" stroke="none" fill="rgb(150 150 150)" fill-opacity="0.6"/>
<line x1="0" y1="0" x2="0" y2="20" stroke="white" stroke-width="1"/>
</g>
<ludus>
&amp; this is some code
forward! (100)
right! (0.25)
</ludus>
</svg>

After

Width:  |  Height:  |  Size: 620 B

View File

@ -23,9 +23,9 @@ publish:
# build the ludus jimage
build:
rm build/out.mjs
rm build/out.wasm
rm build/ludus.jimage
rm -f build/out.mjs
rm -f build/out.wasm
rm -f build/ludus.jimage
janet -c src/ludus.janet build/ludus.jimage
cd build && just build
git commit -am "build"

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.27",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.27",
"license": "GPL-3.0",
"devDependencies": {
"shadow-cljs": "^2.26.0",

View File

@ -1,6 +1,6 @@
{
"name": "@ludus/ludus-js-pure",
"version": "0.1.26",
"version": "0.1.27",
"description": "A Ludus interpreter in a pure JS function.",
"type": "module",
"main": "build/ludus.mjs",

View File

@ -3,23 +3,5 @@
& the goal is to output any global state held in Ludus
& this does not have base loaded into it, only prelude: must be pure Ludus
if turtle_state () :visible? then render_turtle! () else nil
reset_turtle! ()
& let console_msgs = flush! ()
let (r, g, b, a) = unbox (bgcolor)
store! (bgcolor, colors :black)
let draw_calls = unbox (p5_calls)
store! (p5_calls, [])
#{
& :result result is provided elsewhere
& :errors [] & if we get here there are no errors
& :console console_msgs
:draw concat (
[(:background, r, g, b, a), (:stroke, 255, 255, 255, 255)]
draw_calls)
}
store! (turtle_state, turtle_init)
store! (turtle_commands, [])

View File

@ -17,7 +17,6 @@ fn mod
fn neg?
fn print!
fn some?
fn state/call
fn store!
fn string
fn turn/rad
@ -968,6 +967,15 @@ fn dist {
((x, y)) -> dist (x, y)
}
fn heading/vector {
"Takes a turtle heading, and returns a unit vector of that heading."
(heading) -> {
& 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T
let a = add (neg (heading), 0.25)
(cos (a), sin (a))
}
}
&&& more number functions
fn random {
"Returns a random something. With zero arguments, returns a random number between 0 (inclusive) and 1 (exclusive). With one argument, returns a random number between 0 and n. With two arguments, returns a random number between m and n. Alternately, given a collection (list, dict, set), it returns a random member of that collection."
@ -1088,7 +1096,7 @@ let turtle_init = #{
:position (0, 0) & let's call this the origin for now
:heading 0 & this is straight up
:pendown? true
:pencolor colors :white
:pencolor :white
:penwidth 1
:visible? true
}
@ -1096,102 +1104,15 @@ let turtle_init = #{
& turtle states: refs that get modified by calls
& turtle_commands is a list of commands, expressed as tuples
box turtle_commands = []
& and a list of turtle states
box turtle_states = [turtle_init]
fn reset_turtle! {
"Resets the turtle to its original state."
() -> store! (turtle_states, [turtle_init])
}
& and a list of calls to p5--at least for now
box p5_calls = []
& ...and finally, a background color
& we need to store this separately because, while it can be updated later,
& it must be the first call to p5.
box bgcolor = colors :black
fn add_call! (call) -> update! (p5_calls, append! (_, call))
box turtle_state = turtle_init
fn add_command! (command) -> {
update! (turtle_commands, append! (_, command))
let prev = do turtle_states > unbox > last
let prev = unbox (turtle_state)
let curr = apply_command (prev, command)
update! (turtle_states, append! (_, curr))
let call = state/call ()
if call then { add_call! (call); :ok } else :ok
}
fn make_line ((x1, y1), (x2, y2)) -> (:line, x1, y1, x2, y2)
let turtle_radius = 20
let turtle_angle = 0.385
let turtle_color = (255, 255, 255, 150)
fn render_turtle! () -> {
let state = do turtle_states > unbox > last
if state :visible?
then {
let (r, g, b, a) = turtle_color
add_call! ((:fill, r, g, b, a))
let #{heading
:pencolor (pen_r, pen_g, pen_b, pen_a)
:position (x, y)
pendown?
...} = state
let origin = mult ((0, 1), turtle_radius)
let (x1, y1) = origin
let (x2, y2) = rotate (origin, turtle_angle)
let (x3, y3) = rotate (origin, neg (turtle_angle))
add_call! ((:push))
add_call! ((:translate, x, y))
add_call! ((:rotate, turn/rad (heading)))
add_call! ((:noStroke))
add_call! ((:beginShape))
add_call! ((:vertex, x1, y1))
add_call! ((:vertex, x2, y2))
add_call! ((:vertex, x3, y3))
add_call! ((:endShape))
& there's a happy bug here: the stroke will be the same width as the pen width. Keep this for now. Consider also showing the pen colour here?
add_call! ((:stroke, pen_r, pen_g, pen_b, pen_a))
if pendown? then add_call! ((:line, 0, 0, x1, y1)) else nil
add_call! ((:pop))
store! (turtle_state, curr)
:ok
}
else :ok
}
fn state/call () -> {
let cmd = do turtle_commands > unbox > last > first
let states = unbox (turtle_states)
let curr = last (states)
let prev = at (states, sub (count (states), 2))
match cmd with {
:forward -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:back -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:home -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:goto -> if curr :pendown?
then make_line (prev :position, curr :position)
else nil
:penwidth -> (:strokeWeight, curr :penwidth)
:pencolor -> {
let (r, g, b, a) = curr :pencolor
(:stroke, r, g, b, a)
}
:clear -> (:background, 0, 0, 0, 255)
_ -> nil
}
}
fn forward! {
"Moves the turtle forward by a number of steps. Alias: fd!"
@ -1237,6 +1158,7 @@ let pd! = pendown!
fn pencolor! {
"Changes the turtle's pen color. Takes a single grayscale value, an rgb tuple, or an rgba tuple. Alias: pc!"
(color as :keyword) -> add_command! ((:pencolor, color))
(gray as :number) -> add_command! ((:pencolor, (gray, gray, gray, 255)))
((r as :number, g as :number, b as :number)) -> add_command! ((:pencolor, (r, g, b, 255)))
((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:pencolor, (r, g, b, a)))
@ -1253,9 +1175,10 @@ let pw! = penwidth!
fn background! {
"Sets the background color behind the turtle and path. Alias: bg!"
(gray as :number) -> store! (bgcolor, (gray, gray, gray, 255))
((r as :number, g as :number, b as :number)) -> store! (bgcolor, (r, g, b, 255))
((r as :number, g as :number, b as :number, a as :number)) -> store! (bgcolor, (r, g, b, a))
(color as :keyword) -> add_command! ((:background, color))
(gray as :number) -> add_command! ((:background, (gray, gray, gray, 255)))
((r as :number, g as :number, b as :number)) -> add_command! ((:background, (r, g, b, 255)))
((r as :number, g as :number, b as :number, a as :number)) -> add_command! ((:background, (r, g, b, a)))
}
let bg! = background!
@ -1291,12 +1214,11 @@ fn hideturtle! {
() -> add_command! ((:hide))
}
fn heading/vector {
"Takes a turtle heading, and returns a unit vector of that heading."
(heading) -> {
& 0 is 90º/0.25T, 0.25 is 180º/0.5T, 0.5 is 270º/0.75T, 0.75 is 0º/0T
let a = add (heading, 0.25)
(cos (a), sin (a))
fn loadstate! {
"Sets the turtle state to a previously saved state."
(state) -> {
let #{position, heading, pendown?, pencolor, penwidth, visible?} = state
add_command! ((:loadstate, position, heading, visible?, pendown?, penwidth, pencolor))
}
}
@ -1329,49 +1251,37 @@ fn apply_command {
(:penwidth, pixels) -> assoc (state, :penwidth, pixels)
(:pencolor, color) -> assoc (state, :pencolor, color)
(:setheading, heading) -> assoc (state, :heading, heading)
(:loadstate, position, heading, visible?, pendown?, penwidth, pencolor) -> #{position, heading, visible?, pendown?, penwidth, pencolor}
(:show) -> assoc (state, :visible?, true)
(:hide) -> assoc (state, :visible?, false)
}
}
fn turtle_state {
"Returns the turtle's current state."
() -> do turtle_states > unbox > last
}
fn load_turtle_state! {
"Sets the turtle state to a previously saved state. Returns the state."
(state) -> {
update! (turtle_states, append! (_, state))
let call = state/call ()
if call then { add_call! (call); :ok } else :ok
(:background, _) -> state
}
}
& position () -> (x, y)
fn position {
"Returns the turtle's current position."
() -> turtle_state () :position
() -> do turtle_state > unbox > :position
}
fn heading {
"Returns the turtle's current heading."
() -> turtle_state () :heading
() -> do turtle_state > unbox > :heading
}
fn pendown? {
"Returns the turtle's pen state: true if the pen is down."
() -> turtle_state () :pendown?
() -> do turtle_state > unbox > :pendown?
}
fn pencolor {
"Returns the turtle's pen color as an (r, g, b, a) tuple."
() -> turtle_state () :pencolor
"Returns the turtle's pen color as an (r, g, b, a) tuple or keyword."
() -> do turtle_state > unbox > :pencolor
}
fn penwidth {
"Returns the turtle's pen width in pixels."
() -> turtle_state () :penwidth
() -> do turtle_state > unbox > :penwidth
}
box state = nil
@ -1392,7 +1302,6 @@ pkg Prelude {
background! & turtles
between? & math
bg! & turtles
bgcolor & turtles
bk! & turtles
bool & bool
bool? & bool
@ -1454,7 +1363,7 @@ pkg Prelude {
left! & turtles
list & lists
list? & lists
load_turtle_state! & turtles
loadstate! & turtles
lt! & turtles
lt? & math
lte? & math
@ -1476,7 +1385,6 @@ pkg Prelude {
omit & set
or & bool
ordered? & lists tuples strings
p5_calls & turtles
pc! & turtles
pd! & turtles
pencolor & turtles
@ -1497,9 +1405,7 @@ pkg Prelude {
random & math dicts lists tuples sets
random_int & math
range & math lists
render_turtle! & turtles
report! & environment
reset_turtle! & turtles
rest & lists tuples
right! & turtles
round & math
@ -1533,8 +1439,8 @@ pkg Prelude {
turn/deg & math
turn/rad & math
turtle_commands & turtles
turtle_init & turtles
turtle_state & turtles
turtle_states & turtles
type & values
unbox & boxes
unwrap! & results

View File

@ -12,52 +12,83 @@
(import /src/json :as j)
(defn ludus [source]
# if we can't load prelude, bail
(when (= :error prelude/pkg) (error "could not load prelude"))
# get us a clean working slate
(def ctx @{:^parent prelude/ctx})
(def errors @[])
(def draw @[])
(var result @"")
(def console @"")
# capture all `print`s
(setdyn :out console)
(def out @{:errors errors :draw draw :result result :console console})
# an output table
# this will change: the shape of our output
# at the moment, there's only one stack of turtle graphics
# we will be getting more
(def out @{:errors errors :result result
:io @{
:stdout @{:proto [:text-stream "0.1.0"] :data console}
:turtle @{:proto [:turtle-graphics "0.1.0"] :data @[]}}})
### start the program
# first, scanning
(def scanned (s/scan source))
(when (any? (scanned :errors))
(each err (scanned :errors)
(e/scan-error err))
(break (-> out j/encode string)))
# then, parsing
(def parsed (p/parse scanned))
(when (any? (parsed :errors))
(each err (parsed :errors)
(e/parse-error err))
(break (-> out j/encode string)))
# then, validation
(def validated (v/valid parsed ctx))
(when (any? (validated :errors))
(each err (validated :errors)
(e/validation-error err))
(break (-> out j/encode string)))
(try
(set result (i/interpret (parsed :ast) ctx))
# and, finally, try interpreting the program
(try (do
# we need to do this every run or we get the very same sequence of "random" numbers every time we run a program
(math/seedrandom (os/cryptorand 8))
(set result (i/interpret (parsed :ast) ctx)))
([err]
(e/runtime-error err)
(break (-> out j/encode string))))
(setdyn :out stdout)
(set (out :result) (b/show result))
(var post @{})
(try
(set post (i/interpret prelude/post/ast ctx))
([err] (e/runtime-error err)))
(set (out :draw) (post :draw))
# out
(-> out j/encode string)
)
# stop capturing output
(setdyn :out stdout)
# update our output table with our output
(set (out :result) (b/show result))
(set (((out :io) :turtle) :data) (get-in prelude/pkg [:turtle_commands :^value]))
# run the "postlude": any Ludus code that needs to run after each program
# right now this is just resetting the boxes that hold turtle commands and state
(try
(i/interpret prelude/post/ast ctx)
([err] (e/runtime-error err)))
# json-encode our output table, and convert it from a buffer to a string (which we require for playing nice with WASM/C)
(-> out j/encode string))
#### REPL
(comment
# (do
# (def start (os/clock))
(def source `
box foo = :bar
store! (foo, :baz)
unbox (foo)
fd! (100)
rt! (0.25)
fd! (100)
lt! (0.25)
fd! (100)
setheading! (0.75)
unbox (turtle_state)
`)
(def out (-> source
ludus

View File

@ -39,3 +39,4 @@
(def validation-errors (post-validated :errors))
(when (any? validation-errors) (each err validation-errors (e/validation-error err)) (break :error))
(post-parsed :ast)))

View File

@ -765,7 +765,7 @@ Deferred until a later iteration of Ludus:
(defn- cleanup [validator]
(def declared (get-in validator [:status :declared] {}))
(when (any? declared)
(each declaration declared
(each declaration (keys declared)
(array/push (validator :errors) {:node declaration :msg "declared fn, but not defined"})))
validator)

View File

@ -41,7 +41,7 @@ Turtle graphics describe the movements and drawing behaviours of screen, robot,
- Shows the turtle.
* `hide`, no arguments
- Hides the turtle.
* `loadstate`, x: number, y: number, heading: number, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number
* `loadstate`, x: number, y: number, heading: number, visible: boolean, pendown: boolean, width: number, color: string OR r: number, g: number, b: number, a: number
- Loads a turtle state.
* `clear`, no arguments
- Erases any paths drawn and sets the background color to the default.
@ -71,11 +71,22 @@ Colors should also be specifiable with strings corresponding to CSS basic colors
Ludus should have access to turtle states.
This is important for push/pop situations that we use for L-systems.
There are two ways to do this: Ludus does its own bookkeeping for turtle states, or it has a way to get the state from a turtle.
The latter has the value of being instantaneous, and gives us an _expected_ state of the turtle after the commands are all processed.
In particular, this will be necessary for the recursive L-systems that require pushing and popping turtle state.
The latter has the drawback of potentially allowing the turtle state and expected turtle state to fall out of synch.
The former has the value of always giving us the correct, actual state of the turtle.
It has the drawback of requiring such state reporting to be asynchronous, and perhaps wildly asynchronous, as things like moving robots and plotters will take quite some time to actually draw what Ludus tells it to.
(Being able to wait until `eq? (expected, actual)` to do anything else may well be extremely useful.)
That suggests, then, that both forms of turtle state are desirable and necessary.
Thus: turtles should communicate states (and thus there ought to be a protocol for communicating state back to Ludus) and Ludus should always do the bookkeeping of calculating the expected state.
**Turtles use Cartesian, rather than screen, coordinates.**
The starting position of the turtle is `(0, 0)`, which is the origin, and _centred_ in the field of view.
Increasing the x-coordinate moves the turtle to the right; increasing the y-coordinate moves the turtle _up_.
**Turtles use compass headings, not mathematical angles.**
Turtles start pointing vertially, at heading `0`.
Turning right _increases_ the heading; pointing due "east" is `0.25`; south `0.5`, and west, `0.75`.