Monorepo for Aesthetic.Computer
aesthetic.computer
1# Plan: L5 (Processing-style Lua) Support on Aesthetic Computer
2
3## Context
4
5AC supports two piece languages: JavaScript (`.mjs`) and KidLisp (`.lisp`). We want to add **Lua (`.lua`)** as a third, enabling L5/Processing-style sketches to run natively in the browser. This uses **Wasmoon** — a Lua 5.4 VM compiled to WebAssembly (~130KB gzipped) — following the exact same integration pattern as KidLisp.
6
7L5 is a Processing library for Lua (runs on Love2D desktop). We're not porting L5 itself — we're implementing the **Processing API surface** in Lua, bridged to AC's drawing primitives via Wasmoon's JS↔Lua interop.
8
9---
10
11## Files to Create/Modify
12
13| File | Action |
14|---|---|
15| `system/public/aesthetic.computer/dep/wasmoon/` | **Create** — vendor Wasmoon ESM + WASM binary |
16| `system/public/aesthetic.computer/lib/l5.mjs` | **Create** — Lua runtime adapter (~400 lines) |
17| `system/public/aesthetic.computer/lib/disk.mjs` | **Modify** — import l5, add `.lua` detection + fallback |
18| `system/public/aesthetic.computer/disks/l5-hello.lua` | **Create** — test piece |
19
20---
21
22## Step 1: Vendor Wasmoon
23
24Install `wasmoon` via npm and copy the ESM bundle + WASM binary into `dep/wasmoon/`. The `dep/` directory already holds vendored packages (three, gl-matrix, wasmboy, etc.).
25
26```
27dep/wasmoon/
28 index.mjs # ESM entry (LuaFactory, LuaEngine exports)
29 glue.wasm # Lua 5.4 VM compiled to WebAssembly
30```
31
32The WASM URL is constructed relative to the module:
33```javascript
34const WASM_URL = new URL("../dep/wasmoon/glue.wasm", import.meta.url).href;
35```
36
37---
38
39## Step 2: Create `lib/l5.mjs` — The Lua Runtime Adapter
40
41Follows the KidLisp singleton pattern. Exports one function: `module(source)` → returns `{ boot, paint, sim, act, leave }`.
42
43### 2.1 Architecture
44
45```
46l5.mjs
47 ├── ensureFactory() — lazy-init Wasmoon LuaFactory singleton
48 ├── module(source) — main export: compile Lua → lifecycle object
49 ├── createDrawingState()— fill/stroke/transform state manager
50 ├── injectConstants() — PI, TWO_PI, CENTER, CORNER, etc.
51 ├── injectMathGlobals() — sin, cos, random, lerp, map, dist, noise, etc.
52 ├── injectDrawingAPI() — background, fill, stroke, rect, circle, line, text, etc.
53 ├── updateInputGlobals()— mouseX, mouseY, width, height, frameCount per frame
54 └── dispatchEvent() — AC events → Lua callbacks (keyPressed, mousePressed, etc.)
55```
56
57### 2.2 Drawing State
58
59Processing has stateful fill/stroke that AC doesn't — the adapter tracks:
60
61```javascript
62{
63 fillColor: [255,255,255,255], // current fill RGBA
64 strokeColor: [0,0,0,255], // current stroke RGBA
65 fillEnabled: true, // noFill() toggles
66 strokeEnabled: true, // noStroke() toggles
67 strokeWeight: 1,
68 colorMode: "RGB", // RGB | HSB | HSL
69 colorMax: [255,255,255,255], // for colorMode scaling
70 rectMode: "CORNER", // CORNER | CENTER | CORNERS | RADIUS
71 ellipseMode: "CENTER",
72 textSizeVal: 8,
73 textAlignH: "LEFT",
74 // Transform stack for push()/pop()
75 currentTransform: { tx: 0, ty: 0 },
76 transformStack: [],
77 styleStack: [],
78}
79```
80
81Shape drawing applies fill then stroke:
82```javascript
83function drawShape($, state, fillFn, strokeFn) {
84 if (state.fillEnabled) {
85 $.ink(...state.fillColor);
86 fillFn($);
87 }
88 if (state.strokeEnabled) {
89 $.ink(...state.strokeColor);
90 strokeFn($);
91 }
92}
93```
94
95### 2.3 Lifecycle Bridge
96
97```
98L5 setup() → AC boot($) — called once
99L5 draw() → AC paint($) — called every frame
100L5 keyPressed() etc → AC act($) — dispatched per event
101L5 sim() (custom) → AC sim($) — if defined in Lua
102```
103
104Before each `paint()` call, input globals are updated:
105```javascript
106engine.global.set("mouseX", $.pen?.x ?? 0);
107engine.global.set("mouseY", $.pen?.y ?? 0);
108engine.global.set("width", $.screen?.width ?? 128);
109engine.global.set("height", $.screen?.height ?? 128);
110engine.global.set("frameCount", frameCount++);
111```
112
113### 2.4 Async Note
114
115Unlike `lisp.module()` which is synchronous, `l5.module()` is **async** (Wasmoon engine creation requires `await`). The loading path in disk.mjs already uses `await` for dynamic imports, so this fits naturally.
116
117---
118
119## Step 3: Full L5 → AC API Mapping
120
121### Lifecycle
122
123| L5 Function | AC Equivalent | Notes |
124|---|---|---|
125| `setup()` | `boot($)` | Runs once on load |
126| `draw()` | `paint($)` | Every frame |
127| `keyPressed()` | `act($)` on `keyboard:down:*` | |
128| `keyReleased()` | `act($)` on `keyboard:up:*` | |
129| `mousePressed()` | `act($)` on `touch` | |
130| `mouseReleased()` | `act($)` on `lift` | |
131| `mouseMoved()` | `act($)` on `move` | |
132| `mouseDragged()` | `act($)` on `draw` | |
133| `mouseClicked()` | Not mapped v1 | Need tap detection |
134| `mouseWheel()` | Not mapped v1 | |
135
136### Screen & Background
137
138| L5 | AC | Notes |
139|---|---|---|
140| `background(r,g,b)` | `$.wipe(r,g,b)` | Also supports single gray value |
141| `clear()` | `$.wipe(0,0,0,0)` | Transparent clear |
142| `size(w,h)` | `$.resolution(w,h)` | Set canvas resolution |
143
144### Color State
145
146| L5 | AC | Notes |
147|---|---|---|
148| `fill(r,g,b,a)` | Sets `state.fillColor`, applied via `$.ink()` before each shape | |
149| `noFill()` | `state.fillEnabled = false` | |
150| `stroke(r,g,b,a)` | Sets `state.strokeColor` | |
151| `noStroke()` | `state.strokeEnabled = false` | |
152| `strokeWeight(w)` | `state.strokeWeight = w` | Used for line thickness |
153| `colorMode(mode, max...)` | Internal state | Converts HSB/HSL → RGB for `$.ink()` |
154| `lerpColor(c1,c2,t)` | `num.blend(c1,c2,t)` or manual lerp | |
155| `color(r,g,b,a)` | Returns `[r,g,b,a]` table | Used as color value |
156| `red(c)` / `green(c)` / `blue(c)` / `alpha(c)` | Extract from color array | |
157
158### Shape Drawing
159
160| L5 | AC | Notes |
161|---|---|---|
162| `point(x,y)` | `$.plot(x,y)` | Uses stroke color |
163| `line(x1,y1,x2,y2)` | `$.line(x1,y1,x2,y2)` | Uses stroke color |
164| `rect(x,y,w,h)` | `$.box(x,y,w,h)` filled + `$.box(x,y,w,h,"outline")` stroked | Respects `rectMode` |
165| `square(x,y,s)` | Delegates to `rect(x,y,s,s)` | |
166| `circle(x,y,d)` | `$.circle(x,y,d/2)` | L5 uses diameter, AC uses radius |
167| `ellipse(x,y,w,h)` | `$.oval(x,y,w/2,h/2)` | Respects `ellipseMode` |
168| `triangle(x1,y1,...)` | `$.tri(x1,y1,x2,y2,x3,y3)` | |
169| `quad(x1,y1,...x4,y4)` | `$.poly([x1,y1,...])` | |
170| `arc(x,y,w,h,start,stop)` | `$.pie(x,y,w/2,start,stop)` | Approximate |
171| `beginShape()` / `vertex(x,y)` / `endShape(mode)` | Collect vertices → `$.shape(verts)` or `$.poly(verts)` | |
172
173### Text
174
175| L5 | AC | Notes |
176|---|---|---|
177| `text(str,x,y)` | `$.write(str, x, y)` | Uses fill color |
178| `textSize(s)` | `state.textSizeVal = s` | AC bitmap font is fixed; pass as scale option |
179| `textAlign(h,v)` | `state.textAlignH/V` | Internal tracking |
180| `textWidth(str)` | `$.text.width(str)` | |
181
182### Transforms
183
184| L5 | AC | Notes |
185|---|---|---|
186| `translate(x,y)` | `state.currentTransform.tx += x` | Coordinate offset applied to all shapes |
187| `push()` | `state.push()` | Saves transform + style state |
188| `pop()` | `state.pop()` | Restores transform + style state |
189| `resetMatrix()` | Reset transform to `{tx:0, ty:0}` | |
190| `rotate(angle)` | **Not supported v1** | Would need software matrix transform |
191| `scale(sx,sy)` | **Not supported v1** | Same limitation |
192
193### Math
194
195| L5 | AC / JS | Notes |
196|---|---|---|
197| `abs`, `ceil`, `floor`, `round`, `sqrt`, `pow`, `exp`, `log` | `Math.*` | Direct delegation |
198| `sq(x)` | `x * x` | |
199| `min`, `max` | `Math.min/max` | |
200| `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` | `Math.*` | |
201| `radians(deg)` | `deg * PI / 180` | Also in `num.radians` |
202| `degrees(rad)` | `rad * 180 / PI` | Also in `num.degrees` |
203| `random(min?, max?)` | `Math.random()` scaled | |
204| `constrain(v,lo,hi)` | `num.clamp(v,lo,hi)` | |
205| `dist(x1,y1,x2,y2)` | `num.dist(x1,y1,x2,y2)` | |
206| `lerp(a,b,t)` | `num.lerp(a,b,t)` | |
207| `map(v,iL,iH,oL,oH)` | `num.map(v,iL,iH,oL,oH)` | |
208| `noise(x,y?,z?)` | `num.perlin(x,y)` | z dimension ignored v1 |
209| `norm(v,lo,hi)` | `(v-lo)/(hi-lo)` | |
210| `fract(x)` | `x - Math.floor(x)` | |
211| `randomSeed(s)` | No-op v1 | |
212| `noiseSeed(s)` | No-op v1 | |
213| `randomGaussian()` | Box-Muller transform | |
214
215### Input Globals (updated each frame)
216
217| L5 | Source | Notes |
218|---|---|---|
219| `mouseX`, `mouseY` | `$.pen.x`, `$.pen.y` | |
220| `pmouseX`, `pmouseY` | Previous frame's pen position | Tracked internally |
221| `movedX`, `movedY` | `mouseX - pmouseX` | Computed |
222| `mouseIsPressed` | `$.pen.drawing` | |
223| `mouseButton` | From event data | LEFT/RIGHT/CENTER |
224| `key` | From keyboard event | Last key pressed |
225| `keyCode` | Char code of key | |
226| `keyIsPressed` | Any key currently held | Tracked in `keyState` map |
227| `width`, `height` | `$.screen.width/height` | |
228| `frameCount` | Internal counter | |
229| `deltaTime` | Computed from frame timing | |
230| `focused` | `true` | Always focused in AC |
231
232### Constants
233
234| L5 | Value |
235|---|---|
236| `PI` | `Math.PI` |
237| `HALF_PI` | `Math.PI / 2` |
238| `QUARTER_PI` | `Math.PI / 4` |
239| `TWO_PI` / `TAU` | `Math.PI * 2` |
240| `DEGREES` / `RADIANS` | Mode strings |
241| `RGB` / `HSB` / `HSL` | Color mode strings |
242| `LEFT` / `CENTER` / `RIGHT` | Alignment strings |
243| `TOP` / `BOTTOM` / `BASELINE` | Vertical alignment |
244| `CORNER` / `CORNERS` / `RADIUS` | Shape mode strings |
245| `CLOSE` | For `endShape(CLOSE)` |
246
247### Environment
248
249| L5 | AC | Notes |
250|---|---|---|
251| `frameRate(fps)` | `$.fps(fps)` | |
252| `cursor()` / `noCursor()` | `$.cursor()` | |
253| `loop()` / `noLoop()` / `isLooping()` | Internal flag; skip `draw()` calls when noLoop | |
254| `redraw()` | `$.needsPaint()` | |
255| `print(...)` / `println(...)` | `console.log(...)` | |
256| `millis()` | `performance.now()` | |
257| `day()`, `month()`, `year()`, `hour()`, `minute()`, `second()` | `new Date()` methods | |
258
259### Image (basic support)
260
261| L5 | AC | Notes |
262|---|---|---|
263| `loadImage(url)` | `$.get.picture(url)` | Async — returns promise |
264| `image(img,x,y)` | `$.paste(img,x,y)` | |
265| `get(x,y)` | Read from `$.screen.pixels` | |
266| `set(x,y,c)` | `$.ink(c); $.plot(x,y)` | |
267
268### Not Mapped (v1 scope exclusions)
269
270- `rotate()` / `scale()` — needs software matrix (significant effort)
271- `bezier()` / `curve()` — not in AC
272- `loadFont()` — AC has fixed bitmap fonts
273- `loadVideo()` / video playback — not in AC
274- `filter()` effects — partial (blur/invert exist)
275- `createGraphics()` — could map to `$.painting()` later
276- `smooth()` / `noSmooth()` — AC is pixel-native
277- `save()` / `loadStrings()` / file I/O — different paradigm
278- `blend()` modes — partial support
279- `tint()` / `noTint()` — not directly available
280- `applyMatrix()` — needs full matrix support
281
282---
283
284## Step 4: Modify `disk.mjs`
285
286### 4.1 Add import (near line 68)
287
288```javascript
289import * as l5 from "./l5.mjs";
290```
291
292### 4.2 Add `.lua` detection (after KidLisp block ~line 7563, before the `else` at ~7639)
293
294```javascript
295} else if (
296 (path && path.endsWith(".lua")) ||
297 (sourceToRun.trim().startsWith("--") && /function\s+(setup|draw)\s*\(/.test(sourceToRun))
298) {
299 sourceCode = sourceToRun;
300 originalCode = sourceCode;
301 pieceMetadata = { code: slug || "l5", trustLevel: "l5", anonymous: true };
302 send({ type: "boot-log", content: `compiling lua ...` });
303 loadedModule = await l5.module(sourceToRun);
304 send({ type: "boot-file", content: { filename: path, source: sourceCode.slice(0, 8000) } });
305 if (devReload) store["publishable-piece"] = { slug, source: sourceToRun, ext: "lua" };
306}
307```
308
309### 4.3 Extend fallback chain (~line 7736)
310
311After `.mjs` 404, try `.lua` before `.lisp`:
312```
313.mjs (404) → .lua (404) → .lisp (404) → 404 piece
314```
315
316---
317
318## Step 5: Error Handling
319
320- **Compile errors**: `engine.doString()` throws → return a module with `paint()` that displays the error on screen in red text
321- **Runtime errors**: Each lifecycle call wrapped in `try/catch`, logged with `"L5"` prefix
322- **Infinite loops**: v1 documents the limitation; v2 could use Lua debug hooks for instruction-count limits
323- **Cleanup**: `leave()` calls `engine.global.close()` to free WASM memory
324
325---
326
327## Step 6: Verification
328
3291. `npm run site` — start dev server
3302. Create `disks/l5-hello.lua` with basic `setup()`/`draw()` using `background`, `fill`, `circle`, `text`
3313. Navigate to `localhost:8888/l5-hello` — verify it renders
3324. Test input: click to add circles, verify `mouseX`/`mouseY`/`mousePressed()` work
3335. Test `push()`/`pop()`/`translate()` with nested transforms
3346. Test error display: intentionally break Lua syntax, verify error shows on screen
3357. Test fallback: delete `.mjs`, verify `.lua` loads via the fallback chain
3368. Test hot reload: edit `.lua` file, verify changes appear
337
338---
339
340## Step 7: L5 Documentation + Showcase Rollout
341
342### Goal
343
344Ship a **graspable proof page** that shows what L5 API surface AC supports, what is partial, and what is not yet implemented.
345
346### 7.1 Extend Existing `/docs` System First (lowest-risk)
347
348Use the current docs pipeline (`docs` command → `/docs` → `docs.js`) and add an L5 section there first:
349
350- Add `docs.api.l5` in `system/netlify/functions/docs.js`
351- Add index links under a new `<h2>L5 (Lua)</h2>` section
352- Add entries like:
353 - `overview`
354 - `lifecycle`
355 - `graphics`
356 - `color`
357 - `text`
358 - `math`
359 - `input`
360 - `environment`
361 - `compatibility` (full/partial/unsupported table)
362 - `unsupported` (explicit v1 exclusions)
363
364Resulting URLs:
365
366- `/docs/l5:overview`
367- `/docs/l5:compatibility`
368- etc.
369
370### 7.2 Add a Dedicated `/l5` Landing Page (show-off page)
371
372Create a static HTML page following AC frontend conventions (single-file page, no framework):
373
374- `system/public/l5.aesthetic.computer/index.html`
375
376Page sections:
377
3781. Hero: “L5 on Aesthetic Computer”
3792. “Run now” links (sample pieces)
3803. Compatibility matrix (L5 API → AC API mapping)
3814. “What’s different from Processing/Love2D”
3825. Live code snippets (Lua examples)
3836. CTA: “open docs”, “open prompt”, “try example”
384
385Style direction:
386
387- Reuse AC fonts (`Berkeley Mono`, `YWFT Processing`) and color token pattern from frontend style guide
388- Responsive layout for desktop/mobile
389- Keep everything in one HTML file unless it exceeds practical size
390
391### 7.3 Routing
392
393Add redirects in `system/netlify.toml`:
394
395- `from = "/l5"` → `/l5.aesthetic.computer/index.html`
396- `from = "https://l5.aesthetic.computer"` → `/l5.aesthetic.computer/index.html`
397- `from = "https://l5.aesthetic.computer/*"` → `/l5.aesthetic.computer/index.html`
398
399Optional aliases:
400
401- `/L5` → `/l5` (case convenience)
402
403### 7.4 Prompt/Command Integration
404
405Add quick entry commands in `system/public/aesthetic.computer/disks/prompt.mjs`:
406
407- `l5docs` → `out:/docs/l5:overview`
408- `l5` (or `l5learn`) → `out:/l5`
409
410Keep existing `docs` command unchanged.
411
412### 7.5 Single Source of Truth for Compatibility
413
414Avoid hand-maintaining the matrix in two places.
415
416Create one source object/file (for example `system/public/aesthetic.computer/lib/l5-reference.mjs`) with:
417
418- `name`
419- `l5Sig`
420- `acEquivalent`
421- `status` (`full`, `partial`, `unsupported`)
422- `notes`
423
424Use it to render:
425
426- `/docs` L5 entries
427- `/l5` compatibility table
428
429This prevents docs drift as runtime support changes.
430
431### 7.6 Accuracy Guardrails (important)
432
433Do **not** claim “full L5 parity” in v1.
434
435Label unsupported/partial APIs clearly (e.g. `rotate`, `scale`, `bezier`, file/video APIs). The page should be a trustable status board, not marketing-only copy.
436
437---
438
439## Step 8: Verification for Docs/Showcase
440
4411. Run `npm start`
4422. Verify:
443 - `/docs` shows new “L5 (Lua)” section
444 - `/docs/l5:overview` and `/docs/l5:compatibility` render correctly
445 - `/docs.json` includes `api.l5`
4463. Verify `/l5` loads and works on desktop + mobile breakpoints
4474. Verify prompt commands:
448 - `l5docs`
449 - `l5` (or chosen alias)
4505. Confirm every compatibility claim matches real runtime behavior
4516. Run `npm test` before merge