diff --git a/oven/kidlisp-mini/eval.mjs b/oven/kidlisp-mini/eval.mjs index 323843b7b..00d1a9a36 100644 --- a/oven/kidlisp-mini/eval.mjs +++ b/oven/kidlisp-mini/eval.mjs @@ -114,9 +114,12 @@ function parse(source) { advance(); return { type: 'timing-once', ms: t.value }; } else { - // Regular atom + // Regular atom - try to parse as number const token = advance(); - return token.value; + const value = token.value; + // Try to parse as float, fallback to string + const num = parseFloat(value); + return isNaN(num) ? value : num; } } diff --git a/oven/test_animation_frames.mjs b/oven/test_animation_frames.mjs new file mode 100644 index 000000000..727bd48ef --- /dev/null +++ b/oven/test_animation_frames.mjs @@ -0,0 +1,92 @@ +// Test animation frame progression and timing +import { KidLispMini, parse } from './kidlisp-mini/eval.mjs'; +import { TimingEngine } from './kidlisp-mini/timing.mjs'; + +const source = `fade:red-blue-black-blue-red +(ink (? rainbow white 0) (1s... 24 64)) +(line w/2 0 w/2 h) +(spin (2s... -1.125 1.125)) +(zoom 1.1) +(0.5s (contrast 1.05)) +(scroll (? -0.1 0 0.1) (? -0.1 0 0.1)) +(ink (? cyan yellow magenta) 8) +(circle w/2 h/2 (? 2 4 8))`; + +const ast = parse(source); +const kidlisp = new KidLispMini(); +const timing = new TimingEngine(42); // seed + +// Track API calls +const apiCalls = []; +const api = { + screen: { width: 1280, height: 800 }, + wipe: (r, g, b, a) => apiCalls.push({ fn: 'wipe', args: [r, g, b, a] }), + ink: (r, g, b, a) => apiCalls.push({ fn: 'ink', args: [r, g, b, a] }), + line: (x0, y0, x1, y1) => apiCalls.push({ fn: 'line', args: [x0, y0, x1, y1] }), + circle: (cx, cy, r, mode) => apiCalls.push({ fn: 'circle', args: [cx, cy, r, mode] }), + fadeBackground: (colors, frame) => apiCalls.push({ fn: 'fadeBackground', colorCount: colors.length, frame }), + scroll: (dx, dy) => apiCalls.push({ fn: 'scroll', args: [dx, dy] }), + spin: (angle) => apiCalls.push({ fn: 'spin', args: [angle] }), + zoom: (factor) => apiCalls.push({ fn: 'zoom', args: [factor] }), + contrast: (factor) => apiCalls.push({ fn: 'contrast', args: [factor] }), +}; + +kidlisp.setApi(api); + +console.log('=== DEBUG: API and Environment ==='); +console.log('API set:', !!kidlisp.api); +console.log('API.ink:', !!kidlisp.api?.ink); +console.log('globalEnv.ink:', !!kidlisp.globalEnv.ink); +console.log(); + +console.log('=== FRAME-BY-FRAME ANIMATION TEST ===\n'); + +// Test frames 0, 30 (0.5s at 60fps), 60 (1s), 120 (2s) +const testFrames = [0, 30, 60, 120]; + +testFrames.forEach(frameNum => { + apiCalls.length = 0; // reset + kidlisp.frameCount = frameNum; + + console.log(`Frame ${frameNum} (${(frameNum/60).toFixed(2)}s):`); + + // Evaluate only key expressions to see what's happening + // expr 1: ink with timing (1s... 24 64) + const inkExpr = ast[1]; + const inkResult = kidlisp.evalExpr(inkExpr, kidlisp.globalEnv, api, frameNum); + console.log(` ink evalExpr result: ${inkResult}`); + const inkCall = apiCalls.find(c => c.fn === 'ink'); + console.log(` ink API called: ${!!inkCall}`); + console.log(` ink alpha arg: ${inkCall?.args[3]} (interpolating 24→64 over 1s)`); + + // expr 3: spin with timing (2s... -1.125 1.125) + apiCalls.length = 0; + const spinExpr = ast[3]; + const spinResult = kidlisp.evalExpr(spinExpr, kidlisp.globalEnv, api, frameNum); + const spinCall = apiCalls.find(c => c.fn === 'spin'); + const expectedSpin = -1.125 + ((1.125 - (-1.125)) * ((frameNum * 16) % 2000) / 2000); + const spinAngle = spinCall?.args[0]; + console.log(` spin API called: ${!!spinCall}`); + console.log(` spin angle: ${typeof spinAngle === 'number' ? spinAngle.toFixed(4) : spinAngle} (expected: ${expectedSpin.toFixed(4)})`); + + // expr 5: 0.5s (contrast 1.05) + apiCalls.length = 0; + const contrastExpr = ast[5]; + const contrastResult = kidlisp.evalExpr(contrastExpr, kidlisp.globalEnv, api, frameNum); + console.log(` 0.5s contrast result: ${contrastResult?.toFixed(4)} (should be 0→1 over 0.5s)`); + + console.log(); +}); + +console.log('=== TIMING FORM EVALUATION ==='); +console.log('Checking timing form handling...'); + +// Check if timing form [timing, startVal, endVal] is being evaluated +const testTimingExpr = [{ type: 'timing-repeating', ms: 1000 }, 24, 64]; +console.log('Test expr: [timing-repeating(1000ms), 24, 64]'); +for (let f = 0; f <= 60; f += 15) { + const result = kidlisp.evalExpr(testTimingExpr, kidlisp.globalEnv, api, f); + const progress = (f * 16) % 1000 / 1000; + const expected = 24 + (64 - 24) * progress; + console.log(` Frame ${f}: result=${result?.toFixed(1)}, expected=${expected.toFixed(1)}`); +} diff --git a/oven/test_ink_fix.mjs b/oven/test_ink_fix.mjs new file mode 100644 index 000000000..8e8380e55 --- /dev/null +++ b/oven/test_ink_fix.mjs @@ -0,0 +1,46 @@ +// Test ink function with new color+alpha pattern +import { KidLispMini, parse } from './kidlisp-mini/eval.mjs'; + +const source = `(ink (? white black) (1s... 32 224)) +(circle w/2 h/2 100)`; + +const ast = parse(source); +const kidlisp = new KidLispMini(); + +// Mock API to track calls +const calls = []; +const api = { + screen: { width: 1280, height: 800 }, + ink: (...args) => { + calls.push({ fn: 'ink', args }); + console.log(` ink(${args.map(a => Array.isArray(a) ? `[${a}]` : a).join(', ')})`); + }, + circle: (cx, cy, r) => { + calls.push({ fn: 'circle', args: [cx, cy, r] }); + console.log(` circle(${cx}, ${cy}, ${r})`); + }, +}; + +kidlisp.setApi(api); + +console.log('=== TEST: ink with color + alpha override ===\n'); + +// Test frames +for (let frame of [0, 30, 60]) { + calls.length = 0; + console.log(`Frame ${frame} (${(frame/60).toFixed(2)}s):`); + + kidlisp.frameCount = frame; + for (const expr of ast) { + kidlisp.evalExpr(expr, kidlisp.globalEnv, api, frame); + } + + const inkCall = calls.find(c => c.fn === 'ink'); + if (inkCall) { + const [color, alpha] = inkCall.args; + console.log(` → Color: ${Array.isArray(color) ? `[${color.join(',')}]` : color}, Alpha: ${alpha}`); + } + console.log(); +} + +console.log('✅ ink pattern test complete'); diff --git a/oven/test_spin_timing.mjs b/oven/test_spin_timing.mjs new file mode 100644 index 000000000..580f279fc --- /dev/null +++ b/oven/test_spin_timing.mjs @@ -0,0 +1,47 @@ +// Test spin with timing form +import { KidLispMini, parse } from './kidlisp-mini/eval.mjs'; + +const source = `(spin (2s... -1.125 1.125))`; + +const ast = parse(source); +console.log('Parsed AST:', JSON.stringify(ast, null, 2)); + +const kidlisp = new KidLispMini(); +const calls = []; +const api = { + screen: { width: 1280, height: 800 }, + spin: (angle) => { + calls.push({ fn: 'spin', angle }); + console.log(` spin(${angle.toFixed(4)})`); + }, +}; + +kidlisp.setApi(api); + +console.log('\n=== TEST: spin with timing form ===\n'); + +// Test frames at different points in the 2-second cycle +const testFrames = [0, 30, 60, 120]; + +testFrames.forEach(frame => { + calls.length = 0; + const ms = 2000; + const progress = (frame * 16) % ms / ms; + const expectedAngle = -1.125 + (1.125 - (-1.125)) * progress; + + console.log(`Frame ${frame} (${(frame/60).toFixed(2)}s, progress=${progress.toFixed(3)}):`); + + kidlisp.frameCount = frame; + const expr = ast[0]; + console.log(` Expr: ${JSON.stringify(expr)}`); + kidlisp.evalExpr(expr, kidlisp.globalEnv, api, frame); + + const call = calls[0]; + if (call) { + console.log(` Result: ${call.angle.toFixed(4)} (expected: ${expectedAngle.toFixed(4)})`); + if (Math.abs(call.angle - expectedAngle) > 0.01) { + console.log(` ⚠️ OFF BY ${(call.angle - expectedAngle).toFixed(4)}`); + } + } + console.log(); +});