I built a Spider-Man swing effect that runs entirely in the browser, with no pre-rendered animation, no sprite sheets, and no video files. Instead, a small AI agent loop generates motion parameters in real time. Here’s how it works, with runnable Python code you can adapt.
Animation is expensive. Traditional 2D or 3D animation requires keyframes, interpolation curves, and often a rendering pipeline. For a simple web demo, that’s overkill. I wanted Spider-Man to swing across a city skyline using only procedural motion driven by an agent. The agent doesn’t generate pixels; it generates physics parameters (rope length, swing angle, release timing) that a canvas renderer uses to draw a stick-figure Spider-Man.
The result: a 60 FPS swing that feels dynamic, with zero pre-baked frames. The agent runs in Python (using a lightweight loop) and communicates with the browser via WebSocket. This post explains the problem, the solution, and the implementation.
Swinging is a physics problem: a pendulum with a changing pivot point. You could hardcode a sine wave, but it looks robotic. You could use a full physics engine, but that adds dependencies and complexity. I wanted an agent that decides when to release the web and where to attach the next one, based on the current state. The agent must:
This is a classic control loop. The challenge is making it robust and fast enough for real-time.
I built a simple agent loop in Python that runs at 30 Hz. It uses a state machine with explicit termination conditions:
The agent doesn’t use machine learning; it’s a rule-based controller. But it’s an “agent” in the sense of perceiving, deciding, and acting in a loop. I call it an “agent-style” loop because it’s inspired by AI agent architectures (observe-decide-act) but doesn’t use an LLM.
This code runs a WebSocket server that sends commands to the browser. It uses websockets and asyncio. Install with pip install websockets.
import asyncio
import json
import math
import websockets
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
GRAVITY = 0.5
SWING_SPEED = 0.1
MAX_ITERATIONS = 500
ANCHORS = [(100, 100), (250, 80), (400, 120), (550, 90), (700, 110)]
class SwingAgent:
def __init__(self):
self.x = 50.0
self.y = 300.0
self.vx = 5.0
self.vy = 0.0
self.anchor = None
self.angle = 0.0
self.angular_velocity = 0.0
self.iterations = 0
def observe(self, state):
self.x = state['x']
self.y = state['y']
self.vx = state['vx']
self.vy = state['vy']
def decide(self):
if self.anchor is None:
best = None
best_dist = float('inf')
for ax, ay in ANCHORS:
if ax > self.x and ax - self.x < 300:
dist = math.hypot(ax - self.x, ay - self.y)
if dist < best_dist:
best_dist = dist
best = (ax, ay)
if best:
self.anchor = best
dx = self.x - best[0]
dy = self.y - best[1]
self.angle = math.atan2(dx, dy)
self.angular_velocity = 0.0
return {'action': 'attach', 'anchor': best}
else:
length = math.hypot(self.x - self.anchor[0], self.y - self.anchor[1])
if length < 1:
length = 1
alpha = -GRAVITY / length * math.sin(self.angle)
self.angular_velocity += alpha * 0.1
self.angle += self.angular_velocity * 0.1
if abs(self.angle) < 0.1 and self.angular_velocity > 0:
self.anchor = None
return {'action': 'release', 'vx': self.vx, 'vy': self.vy}
else:
new_x = self.anchor[0] + length * math.sin(self.angle)
new_y = self.anchor[1] + length * math.cos(self.angle)
self.vx = (new_x - self.x) / 0.1
self.vy = (new_y - self.y) / 0.1
self.x = new_x
self.y = new_y
return {'action': 'swing', 'x': self.x, 'y': self.y}
return {'action': 'none'}
def should_terminate(self):
return self.x > SCREEN_WIDTH or self.iterations > MAX_ITERATIONS
async def handler(websocket):
agent = SwingAgent()
async for message in websocket:
state = json.loads(message)
agent.observe(state)
if agent.should_terminate():
await websocket.send(json.dumps({'action': 'stop'}))
break
command = agent.decide()
agent.iterations += 1
await websocket.send(json.dumps(command))
async def main():
async with websockets.serve(handler, "localhost", 8765):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
The client renders a stick figure and sends state updates. It uses the Canvas API.
<!DOCTYPE html>
<html>
<head><title>Spider-Man Swing</title></head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const ws = new WebSocket('ws://localhost:8765');
let state = { x: 50, y: 300, vx: 5, vy: 0 };
let anchor = null;
ws.onmessage = (event) => {
const cmd = JSON.parse(event.data);
if (cmd.action === 'attach') {
anchor = cmd.anchor;
} else if (cmd.action === 'release') {
anchor = null;
} else if (cmd.action === 'swing') {
state.x = cmd.x;
state.y = cmd.y;
} else if (cmd.action === 'stop') {
ws.close();
}
};
function draw() {
ctx.clearRect(0, 0, 800, 600);
// Draw buildings
ctx.fillStyle = '#333';
[[100, 100], [250, 80], [400, 120], [550, 90], [700, 110]].forEach(([x, y]) => {
ctx.fillRect(x - 20, y, 40, 600 - y);
});
// Draw web
if (anchor) {
ctx.beginPath();
ctx.moveTo(state.x, state.y);
ctx.lineTo(anchor[0], anchor[1]);
ctx.strokeStyle = 'white';
ctx.stroke();
}
// Draw Spider-Man (stick figure)
ctx.beginPath();
ctx.arc(state.x, state.y, 10, 0, Math.PI * 2);
ctx.fillStyle = 'red';
ctx.fill();
ctx.beginPath();
ctx.moveTo(state.x, state.y + 10);
ctx.lineTo(state.x, state.y + 30);
ctx.strokeStyle = 'red';
ctx.stroke();
// Send state
ws.send(JSON.stringify(state));
requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
server.py and run python server.py. index.html in a browser.
Security warning: This code uses eval? No. It doesn’t. But if you extend it to execute arbitrary code from the browser, never use eval or exec on untrusted input. Always validate and sanitize. The WebSocket server here only accepts JSON with numeric fields; it does not execute code.
x > SCREEN_WIDTH and a max iteration cap.
This approach is extensible: you could swap the rule-based agent for a reinforcement learning policy, or add more complex environments. But even this minimal version shows how an agent-style loop can create dynamic, interactive experiences without traditional animation.