Last active
January 17, 2026 02:03
-
-
Save snoble/2540c34cc4de713623c5ff774d349fc4 to your computer and use it in GitHub Desktop.
BurritoScript Bouncing Ball Demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Bouncing Ball - BurritoScript</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: #0f0f1a; | |
| color: #e4e4e7; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 440px; | |
| width: 100%; | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| margin-bottom: 16px; | |
| color: #a78bfa; | |
| } | |
| canvas { | |
| display: block; | |
| background: #1a1a2e; | |
| border-radius: 8px; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .controls { | |
| margin-top: 16px; | |
| padding: 16px; | |
| background: #1a1a2e; | |
| border-radius: 8px; | |
| } | |
| .control-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .control-row label { | |
| width: 100px; | |
| font-size: 14px; | |
| color: #a1a1aa; | |
| } | |
| .control-row input[type="range"] { | |
| flex: 1; | |
| accent-color: #a78bfa; | |
| } | |
| .control-row span { | |
| width: 50px; | |
| text-align: right; | |
| font-family: monospace; | |
| } | |
| .buttons { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 16px; | |
| } | |
| button { | |
| flex: 1; | |
| padding: 10px; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 14px; | |
| cursor: pointer; | |
| transition: opacity 0.15s; | |
| } | |
| button:hover { opacity: 0.9; } | |
| .btn-primary { background: #3b82f6; color: white; } | |
| .btn-secondary { background: #52525b; color: #e4e4e7; } | |
| .footer { | |
| margin-top: 16px; | |
| font-size: 12px; | |
| color: #71717a; | |
| text-align: center; | |
| } | |
| .footer a { color: #a78bfa; text-decoration: none; } | |
| .stats { | |
| position: absolute; | |
| top: 8px; | |
| left: 8px; | |
| font-size: 11px; | |
| color: rgba(255,255,255,0.5); | |
| font-family: monospace; | |
| } | |
| .canvas-wrapper { | |
| position: relative; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Bouncing Ball</h1> | |
| <div class="canvas-wrapper"> | |
| <canvas id="canvas" width="400" height="300"></canvas> | |
| <div class="stats" id="stats"></div> | |
| </div> | |
| <div class="controls"> | |
| <div class="control-row"> | |
| <label>Gravity</label> | |
| <input type="range" | |
| id="ctrl-gravity" | |
| min="0" | |
| max="1500" | |
| step="10" | |
| value="500"> | |
| <span id="val-gravity">500</span> | |
| </div> | |
| <div class="control-row"> | |
| <label>Bounciness</label> | |
| <input type="range" | |
| id="ctrl-bounciness" | |
| min="0" | |
| max="100" | |
| step="1" | |
| value="80"> | |
| <span id="val-bounciness">80</span> | |
| </div> | |
| <div class="buttons"> | |
| <button class="btn-secondary" id="reset">Reset</button> | |
| <button class="btn-primary" id="toggle">Pause</button> | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| Built with <a href="https://github.com/anthropics/burritoscript">BurritoScript</a> | |
| <br> | |
| <small>Effect analysis discovered: 4 state reads, 1 canvas bindings</small> | |
| </div> | |
| </div> | |
| <script> | |
| // ========================================================================== | |
| // STATE (from BurritoScript initialState) | |
| // ========================================================================== | |
| let state = { | |
| "x": 200, | |
| "y": 50, | |
| "vx": 100, | |
| "vy": 0, | |
| "gravity": 500, | |
| "bounciness": 80, | |
| "running": true, | |
| "width": 400, | |
| "height": 300, | |
| "ballRadius": 20 | |
| }; | |
| const initialState = {"x":200,"y":50,"vx":100,"vy":0,"gravity":500,"bounciness":80,"running":true,"width":400,"height":300,"ballRadius":20}; | |
| // ========================================================================== | |
| // SUBSCRIPTIONS (discovered by Effect analysis) | |
| // ========================================================================== | |
| const subscriptions = new Map([ | |
| ['ball', ["main"]], | |
| ['config', ["main"]] | |
| ]); | |
| // ========================================================================== | |
| // HELPERS | |
| // ========================================================================== | |
| const getPath = (obj, path) => path.reduce((acc, key) => acc?.[key], obj); | |
| const setPath = (obj, path, value) => { | |
| if (path.length === 0) return value; | |
| if (path.length === 1) return { ...obj, [path[0]]: value }; | |
| return { ...obj, [path[0]]: setPath(obj[path[0]] ?? {}, path.slice(1), value) }; | |
| }; | |
| // ========================================================================== | |
| // UPDATE FUNCTION (pure, from .burrito.ts) | |
| // ========================================================================== | |
| function update(state, dt) { | |
| if (!state.running) return state; | |
| const { x, y, vx, vy, gravity, bounciness: rawBounciness, width, height, ballRadius } = state; | |
| const bounciness = rawBounciness / 100; | |
| // Apply gravity | |
| const vy1 = vy + gravity * dt; | |
| // Update position | |
| const x1 = x + vx * dt; | |
| const y1 = y + vy1 * dt; | |
| // Bounce off left wall | |
| const afterLeftWall = | |
| x1 - ballRadius < 0 | |
| ? { x: ballRadius, vx: -vx * bounciness } | |
| : { x: x1, vx }; | |
| // Bounce off right wall | |
| const afterRightWall = | |
| afterLeftWall.x + ballRadius > width | |
| ? { x: width - ballRadius, vx: -afterLeftWall.vx * bounciness } | |
| : afterLeftWall; | |
| // Bounce off ceiling | |
| const afterCeiling = | |
| y1 - ballRadius < 0 | |
| ? { y: ballRadius, vy: -vy1 * bounciness } | |
| : { y: y1, vy: vy1 }; | |
| // Bounce off floor | |
| const afterFloor = | |
| afterCeiling.y + ballRadius > height | |
| ? { | |
| y: height - ballRadius, | |
| vy: Math.abs(afterCeiling.vy) < 10 ? 0 : -afterCeiling.vy * bounciness, | |
| } | |
| : afterCeiling; | |
| return { | |
| ...state, | |
| x: afterRightWall.x, | |
| y: afterFloor.y, | |
| vx: afterRightWall.vx, | |
| vy: afterFloor.vy, | |
| }; | |
| } | |
| // ========================================================================== | |
| // RENDER FUNCTION (pure, from .burrito.ts) | |
| // ========================================================================== | |
| function render(ctx, state) { | |
| const { x, y, vx, vy, width, height, ballRadius } = state; | |
| // Background gradient | |
| const bgGradient = ctx.createLinearGradient(0, 0, 0, height); | |
| bgGradient.addColorStop(0, '#1a1a2e'); | |
| bgGradient.addColorStop(1, '#16162a'); | |
| ctx.fillStyle = bgGradient; | |
| ctx.fillRect(0, 0, width, height); | |
| // Grid | |
| ctx.strokeStyle = 'rgba(255,255,255,0.03)'; | |
| ctx.lineWidth = 1; | |
| for (let gx = 0; gx < width; gx += 40) { | |
| ctx.beginPath(); | |
| ctx.moveTo(gx, 0); | |
| ctx.lineTo(gx, height); | |
| ctx.stroke(); | |
| } | |
| for (let gy = 0; gy < height; gy += 40) { | |
| ctx.beginPath(); | |
| ctx.moveTo(0, gy); | |
| ctx.lineTo(width, gy); | |
| ctx.stroke(); | |
| } | |
| // Shadow | |
| ctx.beginPath(); | |
| ctx.ellipse(x, height - 5, ballRadius * 0.8, ballRadius * 0.2, 0, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(0,0,0,0.3)'; | |
| ctx.fill(); | |
| // Ball with gradient | |
| const ballGradient = ctx.createRadialGradient( | |
| x - ballRadius * 0.3, y - ballRadius * 0.3, 0, | |
| x, y, ballRadius | |
| ); | |
| ballGradient.addColorStop(0, '#ff6b8a'); | |
| ballGradient.addColorStop(1, '#e94560'); | |
| ctx.beginPath(); | |
| ctx.arc(x, y, ballRadius, 0, Math.PI * 2); | |
| ctx.fillStyle = ballGradient; | |
| ctx.fill(); | |
| // Velocity vector | |
| const speed = Math.sqrt(vx * vx + vy * vy); | |
| if (speed > 10) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x, y); | |
| ctx.lineTo(x + vx * 0.05, y + vy * 0.05); | |
| ctx.strokeStyle = 'rgba(102, 126, 234, 0.6)'; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| } | |
| } | |
| // ========================================================================== | |
| // ANIMATION LOOP | |
| // ========================================================================== | |
| const canvas = document.getElementById('canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const statsEl = document.getElementById('stats'); | |
| let running = true; | |
| let lastTime = performance.now(); | |
| let frameCount = 0; | |
| let fps = 0; | |
| let lastFpsUpdate = lastTime; | |
| function animate(now) { | |
| const dt = Math.min((now - lastTime) / 1000, 0.1); | |
| lastTime = now; | |
| frameCount++; | |
| if (now - lastFpsUpdate > 1000) { | |
| fps = frameCount; | |
| frameCount = 0; | |
| lastFpsUpdate = now; | |
| } | |
| if (running && state.running !== false) { | |
| state = update(state, dt); | |
| } | |
| render(ctx, state); | |
| statsEl.textContent = fps + ' fps'; | |
| requestAnimationFrame(animate); | |
| } | |
| // ========================================================================== | |
| // CONTROLS | |
| // ========================================================================== | |
| document.getElementById('ctrl-gravity').addEventListener('input', (e) => { | |
| const value = parseFloat(e.target.value); | |
| state = setPath(state, ["gravity"], value); | |
| document.getElementById('val-gravity').textContent = value; | |
| render(ctx, state); | |
| }); | |
| document.getElementById('ctrl-bounciness').addEventListener('input', (e) => { | |
| const value = parseFloat(e.target.value); | |
| state = setPath(state, ["bounciness"], value); | |
| document.getElementById('val-bounciness').textContent = value; | |
| render(ctx, state); | |
| }); | |
| document.getElementById('toggle').addEventListener('click', function() { | |
| running = !running; | |
| this.textContent = running ? 'Pause' : 'Resume'; | |
| this.className = running ? 'btn-primary' : 'btn-secondary'; | |
| }); | |
| document.getElementById('reset').addEventListener('click', function() { | |
| state = JSON.parse(JSON.stringify(initialState)); | |
| running = true; | |
| document.getElementById('toggle').textContent = 'Pause'; | |
| document.getElementById('toggle').className = 'btn-primary'; | |
| }); | |
| // Start | |
| requestAnimationFrame(animate); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment