SOURCE / PINNED RELEASE
Made of little things.
Sketch Loop
- Release
- 14966bba4a2a…
- Author-recorded commit
- e68361295d81…
- License
- LICENSE
- Author’s source reference
- nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-929bc2a88cb
Archive hash verified: 5e0385e3c08f944f…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { runInNewContext } from 'node:vm';
import test from 'node:test';
import ts from 'typescript';
const require=createRequire(import.meta.url);
async function load(path, dependencies={}) {
const source=await readFile(new URL(path,import.meta.url),'utf8');
const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const module={exports:{}};
runInNewContext(compiled,{module,exports:module.exports,performance,require:name=>dependencies[name]??require(name)});
return module.exports;
}
const {Interpreter,parseSketch}=await load('../src/interpreter.ts');
const {examples}=await load('../src/sketch.ts',{'./interpreter.js':{Interpreter,parseSketch}});
const {frameDelays}=await load('../src/export.ts',{'./sketch.js':{},gifenc:{}});
test('runs the supplied BusyMozaic syntax and preserves accumulating state',()=>{
let calls=0,canvasCalls=0;
const vm=new Interpreter({createCanvas:(w,h)=>{assert.equal(w,450);assert.equal(h,300);canvasCalls++;return 0;},background:()=>0,fill:()=>{},rect:()=>{calls++;},abs:Math.abs});
vm.run(parseSketch(examples[0].code).ast);
for(let i=0;i<60;i++)vm.invoke('draw');
assert.equal(vm.value('t'),60);assert.equal(canvasCalls,1);assert.equal(calls,4500);
});
test('accepts a collapsed social post with markdown hashtag links and escaped operators',()=>{
const raw='s=6;t=0//BusyMozaic [#p5js](https://x.com/hashtag/p5js?src=hashtag_click) [#processing](https://x.com/hashtag/processing?src=hashtag_click) draw=\\_=>{t++||createCanvas(2\\*(W=225),2\\*(H=150))+background(0) for(x=0;x<2\\*W;x+=s){y=(x^t)%(2\\*H);y=s\\*\\~\\~(y/s) u=1.7\\*(x-W);v=1.7\\*(y-H)+150;d=\\~\\~abs(u\\*\\*3/4E4-u+3\\*v-v\\*\\*2/1E2) p=(d/s)%(s&t);fill(d%256,(d%128)\\*2,(d%32)\\*8);rect(x-p,y-p,2\\*p)}}';
const vm=new Interpreter({createCanvas:()=>0,background:()=>0,fill:()=>{},rect:()=>{},abs:Math.abs});
vm.run(parseSketch(raw).ast);vm.invoke('draw');assert.equal(vm.value('t'),1);
});
test('supports local variables, functions, branching and loops',()=>{
const vm=new Interpreter({});vm.run(parseSketch('let total=0; function add(n){return n*2;} draw=()=>{for(let i=0;i<5;i++){if(i===2)continue;total+=add(i);}}').ast);vm.invoke('draw');assert.equal(vm.value('total'),16);
});
test('stops infinite loops and recursive calls',()=>{
const vm=new Interpreter({});assert.throws(()=>vm.run(parseSketch('while(true){}').ast),/budget/);
assert.throws(()=>vm.run(parseSketch('function again(){again()} again()').ast),/recursion/);
});
test('does not expose host globals or native function constructors',()=>{
for(const source of ['window.napplet','globalThis','this','({}).constructor','sin.constructor','sin["con"+"structor"]','({})["__proto__"]','new Function("return 1")','import("x")']){
const vm=new Interpreter({sin:Math.sin});assert.throws(()=>vm.run(parseSketch(source).ast),undefined,source);
}
});
test('limits strings, arrays and source size',()=>{
const vm=new Interpreter({});
assert.throws(()=>vm.run(parseSketch('a="hello";while(true)a=a+a').ast),/String size/);
assert.throws(()=>vm.run(parseSketch('a=[];a[10000000]=1').ast),/Array index/);
assert.throws(()=>parseSketch(' '.repeat(24001)),/24,000/);
});
test('GIF centisecond delays sum exactly to requested durations at every FPS',()=>{
for(const duration of [1,1.1,2.3,5,30])for(const fps of [10,15,20,25,30]){
const delays=frameDelays(duration,fps);
assert.equal(delays.reduce((a,b)=>a+b,0),duration*1000);
assert.equal(delays.length,Math.round(duration*fps));
assert.ok(delays.every(d=>d>=20&&d%10===0));
}
});