Skip to content

Instantly share code, notes, and snippets.

@snoble
Last active January 17, 2026 02:03
Show Gist options
  • Select an option

  • Save snoble/2540c34cc4de713623c5ff774d349fc4 to your computer and use it in GitHub Desktop.

Select an option

Save snoble/2540c34cc4de713623c5ff774d349fc4 to your computer and use it in GitHub Desktop.
BurritoScript Bouncing Ball Demo
<!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