Monorepo for Aesthetic.Computer aesthetic.computer
at main 451 lines 16 kB view raw view rendered
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