Back to Sketch Loop
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.

tests/sketch.test.mjs
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));
  }
});