Text-first ES5 lab for ages 10–18 with JS-Interpreter, preemptable execution, Turtle + Paint graphics, and a playable/programmatic music keyboard.
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.
interpreter.step()
loop with time budget.budgetMs
(~1–2ms) or maxSteps
per tick.// 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();
}
Main/UI thread <—postMessage—> Worker (JS-Interpreter)
Ace, Inputs, Canvas compositor User code, API shims, Stepper
Batched per frame to limit overhead.
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};
paintCanvas
(persistent bitmap).turtleCanvas
(non-persistent path, redrawn each frame from turtle state log or incremental ops).requestAnimationFrame
merges layers to compositeCanvas
.id
, type
, min/max/step/value
, rateLimitMs
.Inputs.get(id)
returns scalar/boolean/string.currentTime + δ
.Programmatic:
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.
setup(function init(){ /* once */ });
loop(function tick(dt){ /* ~60 fps or mode rate */ });
const x = Inputs.get('slider1'); // number|boolean|string
Turtle.reset();
Turtle.home();
Turtle.penDown();
Turtle.penUp();
Turtle.color('#37a');
Turtle.width(2);
Turtle.turn(90);
Turtle.move(20);
Paint.clear();
Paint.stroke([ {x:10,y:10}, {x:40,y:40} ], {color:'#000', width:3, alpha:1});
Paint.fill(100, 120, '#ff0');
Debug.watch('x', () => x);
Debug.breakIf(() => x > 100);
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.
{type:'RUN', code:string, seed:number, mode:string}
{type:'PAUSE'|'RESUME'|'STEP'|'STOP'}
{type:'INPUTS', patch: Record<string,number|boolean|string>}
{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.
ace/src-min
.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).loop
registered, engine calls it with dt
on each tick (simulated via shared clock).window
, document
, fetch
, eval
.A4=440
. Parse C#4
, Db4
, MIDI ints (60 = C4).A=5ms, D=50ms, S=0.8, R=80ms
..bop
JSON (code, mode, inputs, paint bitmap optional as PNG data URL).default-src 'self'
; disable eval
./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)
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);
});
});
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);
};
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..bop
restores session exactly (code + paint + mode).End.