git » bitbop.git » commit 81aae70

run

author Alan Dipert
2025-10-06 00:31:03 UTC
committer Alan Dipert
2025-10-06 00:31:03 UTC
parent 9151c8b1496d4fa339a63e78f0e1ad016de53745

run

AGENTS.md +19 -0
DESIGN.md +329 -0
app.js +99 -5

diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..868bd95
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,19 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+Bitbop is a static client delivered from the repository root. `index.html` wires the layout and registers scripts. `app.js` houses custom editor behavior; create siblings (e.g., `editor-controls.js`) when splitting features and load them from `index.html`. `style.css` contains shared styling. `ace-builds/` and `JS-Interpreter/` are vendored upstream bundles; only update them by syncing from their sources and document the version bump in the commit message.
+
+## Build, Test, and Development Commands
+No bundler or package manifest is required today. Serve the site locally with `python3 -m http.server 8000` (rooted at the repository) or `npx http-server .` when Node tooling is preferred. When editing the Ace editor configuration, open `index.html` in a browser and use DevTools to verify resource loading. If you add a Node-based workflow, capture the exact command in this section for the next contributor.
+
+## Coding Style & Naming Conventions
+Use 2-space indentation for JavaScript, HTML, and CSS. Favor ES6 syntax (`const`/`let`, arrow functions) and single quotes for strings, matching the current files. Keep module names hyphenated (`editor-pane`, `run-button`) to align with DOM ids. Run `npx prettier --write app.js style.css` before committing if you introduce formatting-sensitive changes.
+
+## Testing Guidelines
+Automated tests are not bundled yet; validate changes by running the local server and exercising the core editor flow (load page, type code, confirm Ace highlights). When you introduce automated coverage, place specs beneath a new `tests/` directory and use descriptive filenames like `editor-controls.test.js`. Target smoke coverage of any new feature before merging.
+
+## Commit & Pull Request Guidelines
+Commits follow short, lowercase, imperative lines (`add readme`, `init`). Group related edits and avoid batching vendor upgrades with feature work. Pull requests should summarize the behavior change, list manual test steps, and link any tracking issue. Include screenshots or GIFs when UI changes affect the editor panel or theming.
+
+## Third-Party Assets
+Treat `ace-builds/` and `JS-Interpreter/` as read-only. When updating, replace their contents wholesale from upstream releases and note the source tag in the PR description. Avoid editing generated files by hand; extend functionality through wrapper scripts in the repository root instead.
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..eb65855
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,329 @@
+# DESIGN.md — bitbop.studio
+
+## 0. One-liner
+Text-first ES5 lab for ages 10–18 with **JS-Interpreter**, preemptable execution, **Turtle + Paint** graphics, and a playable/programmatic **music keyboard**.
+
+---
+
+## 1. Problem
+Block tools don’t build text fluency. The DOM/CSS stack is noisy. Native JS can freeze UI. Kids need real code, instant feedback, and safe stepping.
+
+---
+
+## 2. Core Concept
+- **Editor (left):** Ace (ES5), compact font, real syntax highlighting.
+- **Top-right:** Inputs grid (sliders, toggles, knobs) in VB6-style groups.
+- **Bottom-right:** Graphics: **Turtle** (vector) over **Paint** (bitmap).
+- **Top bar:** Play, Pause, Step, Stop, Reset, Speed.
+- **Music:** On-screen keyboard (mouse/touch) + programmatic API (WebAudio).
+- **Interpreter:** **Neil Fraser’s JS-Interpreter** in a Worker; cooperative stepping.
+
+---
+
+## 3. Non-Goals
+- No DOM/CSS teaching in learner path.
+- No network by default.
+- No ES6+ surface to learners (engine can be ES6 internally).
+
+---
+
+## 4. Runtime & Preemption
+
+### 4.1 Engine
+- **JS-Interpreter** (ES5) runs user code.
+- Run inside a **Web Worker**.
+- Stepping via `interpreter.step()` loop with **time budget**.
+- Yield back to UI every `budgetMs` (~1–2ms) or `maxSteps` per tick.
+- Watchdog kills runaway loops (no progress for N ms).
+
+```js
+// worker pseudo
+while (state === 'running') {
+  const t0 = performance.now();
+  let steps = 0;
+  while (performance.now() - t0 < budgetMs && steps++ < maxSteps && interpreter.step()) {}
+  if (!interpreter.paused_) postMessage({type:'TICK'});
+  await nextAnimationTickOrMessage();
+}
+```
+
+### 4.2 Process Model
+```
+Main/UI thread <—postMessage—> Worker (JS-Interpreter)
+Ace, Inputs, Canvas compositor     User code, API shims, Stepper
+```
+
+---
+
+## 5. Rendering Model (Turtle + Paint)
+
+### 5.1 Canvases
+- **paintCanvas** (bitmap, back buffer): freehand brush, fill, erase, stamps.
+- **turtleCanvas** (vector-ish via ops replay each frame): pen moves/turns.
+- **compositeCanvas**: on screen; compositor draws paint first, then turtle.
+
+### 5.2 Draw Ops (from Worker → Main)
+Batched per frame to limit overhead.
+
+```ts
+type PaintOp =
+  | {kind:'stroke', points:[{x:number,y:number}] , color:string, width:number, alpha:number}
+  | {kind:'fill', x:number,y:number, color:string}
+  | {kind:'clear'};
+
+type TurtleOp =
+  | {kind:'reset'}
+  | {kind:'move', x:number, y:number}
+  | {kind:'line', x:number, y:number, width:number, color:string}
+  | {kind:'heading', deg:number}
+  | {kind:'pen', down:boolean, color?:string, width?:number};
+```
+
+### 5.3 Compositor (Main)
+- Replays **PaintOp** into `paintCanvas` (persistent bitmap).
+- Replays **TurtleOp** into `turtleCanvas` (non-persistent path, redrawn each frame from turtle state log or incremental ops).
+- `requestAnimationFrame` merges layers to `compositeCanvas`.
+
+---
+
+## 6. Inputs Grid
+- Declarative groups; each control has `id`, `type`, `min/max/step/value`, `rateLimitMs`.
+- UI emits debounced patches to Worker.
+- Learner API: `Inputs.get(id)` returns scalar/boolean/string.
+
+---
+
+## 7. Music System
+
+### 7.1 UI Keyboard
+- 2–3 octaves clickable/touchable.
+- Velocity via press duration or fixed (v=0.8).
+- Sustain toggle (hold pedal).
+
+### 7.2 Engine
+- **WebAudio** on Main (Worker can’t access AudioContext).
+- Simple synths: sine/square/saw, ADSR envelope, optional low-pass.
+- **Scheduler:** Main thread schedules note on/off with `currentTime + δ`.
+
+### 7.3 API (Learner)
+Programmatic:
+
+```js
+Music.play('C4', 0.5);          // note, seconds
+Music.playAt('E4', 0.25, 0.1);  // note, dur, startOffset
+Music.setInstrument('saw');     // 'sine'|'square'|'saw'|'triangle'
+Music.setTempo(120);            // affects beat helpers
+Music.rest(0.25);               // quarter-beat rest helper (optional)
+```
+
+UI events (keyboard) are mirrored into API calls so code and UI share the same synth.
+
+---
+
+## 8. Public APIs (Learner-facing)
+
+### 8.1 Lifecycle
+```js
+setup(function init(){ /* once */ });
+loop(function tick(dt){ /* ~60 fps or mode rate */ });
+```
+
+### 8.2 Inputs
+```js
+const x = Inputs.get('slider1'); // number|boolean|string
+```
+
+### 8.3 Turtle
+```js
+Turtle.reset();
+Turtle.home();
+Turtle.penDown();
+Turtle.penUp();
+Turtle.color('#37a');
+Turtle.width(2);
+Turtle.turn(90);
+Turtle.move(20);
+```
+
+### 8.4 Paint
+```js
+Paint.clear();
+Paint.stroke([ {x:10,y:10}, {x:40,y:40} ], {color:'#000', width:3, alpha:1});
+Paint.fill(100, 120, '#ff0');
+```
+
+### 8.5 Music (above)
+
+### 8.6 Debug
+```js
+Debug.watch('x', () => x);
+Debug.breakIf(() => x > 100);
+```
+
+### 8.7 Modes
+```js
+Mode.set('turtle-basic');   // swaps input layout, tempo, defaults
+Mode.set('paint-keyboard'); // exposes paint tools + keyboard
+```
+
+**Rule:** APIs appear synchronous; engine handles async/messaging.
+
+---
+
+## 9. Worker Protocol
+
+### 9.1 UI → Worker
+```ts
+{type:'RUN', code:string, seed:number, mode:string}
+{type:'PAUSE'|'RESUME'|'STEP'|'STOP'}
+{type:'INPUTS', patch: Record<string,number|boolean|string>}
+```
+
+### 9.2 Worker → UI
+```ts
+{type:'STATUS', status:'running'|'paused'|'stopped'}
+{type:'RENDER', paint?:PaintOp[], turtle?:TurtleOp[]}
+{type:'WATCH', vars: Record<string, any>}
+{type:'ERROR', message:string, line?:number, col?:number}
+{type:'MUSIC', events: Array<{at:number,note:string|number,dur:number,vel?:number,cmd:'on'|'off'}>}
+```
+
+UI translates `{type:'MUSIC'}` events into WebAudio calls.
+
+---
+
+## 10. Implementation Details
+
+### 10.1 Editor (Ace)
+- Local `ace/src-min`.
+- JS mode, monokai theme, fontSize 12px, soft tabs 2, print margin off.
+
+### 10.2 Interpreter (JS-Interpreter)
+- Create interpreter with init function that injects **safe API shims**:
+  - `Inputs.get` (pulls from Worker state).
+  - `Turtle.*` push **TurtleOp**.
+  - `Paint.*` push **PaintOp**.
+  - `Music.*` push **MUSIC** events (UI schedules).
+  - `setup`, `loop` registration (store callbacks in interpreter’s global).
+- Stepping loop:
+  - If `loop` registered, engine calls it with `dt` on each tick (simulated via shared clock).
+  - Instruction/time budget enforced per tick.
+- No direct `window`, `document`, `fetch`, `eval`.
+
+### 10.3 Graphics
+- **Paint** uses flood-fill (scanline) and polyline rasterization. Implement manually if libs aren’t allowed.
+- **Turtle** maintains heading (deg), position, pen state, color, width. Emits minimal ops; UI maintains current path.
+
+### 10.4 Music
+- Note mapping: `A4=440`. Parse `C#4`, `Db4`, MIDI ints (60 = C4).
+- Envelope default: `A=5ms, D=50ms, S=0.8, R=80ms`.
+- Polyphony limit (e.g., 16 voices). Voice stealing oldest.
+
+### 10.5 Persistence
+- IndexedDB autosave (code + mode + input layout).
+- Export/Import `.bop` JSON (code, mode, inputs, paint bitmap optional as PNG data URL).
+
+### 10.6 Accessibility
+- Keyboard-operable inputs; ARIA labels.
+- High-contrast theme variant.
+- Optional text-to-speech for console output.
+
+---
+
+## 11. Performance Targets
+- Cold load ≤ 2s (offline).
+- UI never blocks.
+- 60fps target; degrade gracefully.
+- Input→effect latency ≤ 50ms.
+- Flood-fill worst-case bounded via chunked processing (yield between scanlines).
+
+---
+
+## 12. Constraints & Policies
+- CSP: `default-src 'self'`; disable `eval`.
+- Worker memory guard (~64MB heap).
+- Kill on >N ms without yield.
+- Offline by default; optional SW cache later.
+
+---
+
+## 13. File Layout
+```
+/public
+  index.html
+  /css
+    app.css
+  /ace/src-min/...
+  /js
+    app.js            // UI glue, compositor, music engine
+    editor.js         // Ace boot, global promise export
+    inputs.js         // grid, debounced patches
+    compositor.js     // paint/turtle layers, raf loop
+    music.js          // WebAudio synth + scheduler
+    runtime/
+      worker.js       // message loop, stepper, bridging
+      inject.js       // define APIs into JS-Interpreter
+      time.js         // dt clock, seedable RNG
+      turtle_api.js   // Turtle shim (collect ops)
+      paint_api.js    // Paint shim (collect ops)
+      music_api.js    // Music shim (queue events)
+```
+
+---
+
+## 14. Minimal Boot Snippets
+
+### 14.1 Editor Promise (Main)
+```js
+export const editor = new Promise(res => {
+  document.addEventListener('DOMContentLoaded', () => {
+    const ed = ace.edit('editor', {
+      mode: 'ace/mode/javascript',
+      theme: 'ace/theme/monokai',
+      fontSize: '12px',
+      tabSize: 2, useSoftTabs: true, showPrintMargin: false
+    });
+    res(ed);
+  });
+});
+```
+
+### 14.2 Worker Wiring (Main)
+```js
+const worker = new Worker('./js/runtime/worker.js', { type:'module' });
+
+function run(code, mode){ worker.postMessage({type:'RUN', code, seed:Date.now(), mode}); }
+['PAUSE','RESUME','STEP','STOP'].forEach(t => window[t.toLowerCase()] = () => worker.postMessage({type:t}));
+
+worker.onmessage = (e) => {
+  const m = e.data;
+  if (m.type === 'RENDER') enqueuePaint(m.paint), enqueueTurtle(m.turtle);
+  if (m.type === 'MUSIC')  scheduleMusic(m.events);
+  if (m.type === 'STATUS') setStatus(m.status);
+  if (m.type === 'ERROR')  showError(m.message, m.line);
+};
+```
+
+---
+
+## 15. Testing
+- Unit: interpreter API shims, note parser, flood-fill boundaries, RNG determinism.
+- Integration: Worker ↔ Main messaging, frame budget adherence, audio scheduling jitter.
+- Property: same seed → same Turtle lines; idempotent replays.
+
+---
+
+## 16. Modes (examples)
+- `turtle-basic`: 6 sliders (speed, turn, step, pen width, r,g,b), 4 toggles.
+- `paint-keyboard`: Paint tools palette + 2-octave keyboard + sustain.
+- `music-lab`: 3 octaves, waveform selector, ADSR sliders, tempo.
+
+---
+
+## 17. Success Criteria
+- New user draws with mouse **and** with Turtle code in ≤ 60s.
+- Music plays from UI and from code with ≤ 50ms perceived latency.
+- Pause/Step always responsive; no UI freezes.
+- Export `.bop` restores session exactly (code + paint + mode).
+
+**End.**
diff --git a/app.js b/app.js
index b749300..0f5f960 100644
--- a/app.js
+++ b/app.js
@@ -1,11 +1,105 @@
+const STEPS_PER_TICK = 2048;
+const TIME_BUDGET_MS = 8;
+
+function createRunner() {
+  let interpreter = null;
+  let running = false;
+  let scheduledHandle = null;
+
+  function clearSchedule() {
+    if (scheduledHandle !== null) {
+      clearTimeout(scheduledHandle);
+      scheduledHandle = null;
+    }
+  }
+
+  function reset() {
+    running = false;
+    interpreter = null;
+    clearSchedule();
+  }
+
+  function initApi(interp, globalObject) {
+    const consoleObject = interp.createObject(interp.OBJECT);
+    const logFunction = interp.createNativeFunction(function () {
+      const args = Array.from(arguments).map(arg => interp.pseudoToNative(arg));
+      console.log('[bitbop]', ...args);
+    });
+    interp.setProperty(consoleObject, 'log', logFunction);
+    interp.setProperty(globalObject, 'console', consoleObject);
+  }
+
+  function scheduleNextTick() {
+    if (!running || scheduledHandle !== null) return;
+    scheduledHandle = setTimeout(() => {
+      scheduledHandle = null;
+      tick();
+    }, 0);
+  }
+
+  function tick() {
+    if (!running || !interpreter) return;
+    const start = performance.now();
+    let steps = 0;
+    let hasMore = true;
+
+    try {
+      while (
+        hasMore &&
+        steps < STEPS_PER_TICK &&
+        performance.now() - start < TIME_BUDGET_MS
+      ) {
+        hasMore = interpreter.step();
+        steps += 1;
+      }
+    } catch (err) {
+      console.error('bitbop runtime error:', err);
+      reset();
+      return;
+    }
+
+    if (hasMore) {
+      scheduleNextTick();
+    } else {
+      reset();
+      console.info('bitbop program finished.');
+    }
+  }
+
+  function run(code) {
+    reset();
+    try {
+      interpreter = new Interpreter(code, initApi);
+      running = true;
+    } catch (err) {
+      console.error('bitbop compile error:', err);
+      reset();
+      return;
+    }
+    scheduleNextTick();
+  }
+
+  return { run };
+}
+
 document.addEventListener('DOMContentLoaded', () => {
-  let editor = ace.edit("editor-pane", {
-    mode: "ace/mode/javascript",
-    theme: "ace/theme/monokai",
-    fontSize: "12px",
+  const editor = ace.edit('editor-pane', {
+    mode: 'ace/mode/javascript',
+    theme: 'ace/theme/monokai',
+    fontSize: '12px',
     tabSize: 2,
     useSoftTabs: true,
     showPrintMargin: false
   });
-});
 
+  const runner = createRunner();
+  const playButton = document.getElementById('play');
+
+  if (playButton) {
+    playButton.addEventListener('click', () => {
+      runner.run(editor.getValue());
+    });
+  }
+
+  window.bitbop = { editor, runner };
+});