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.
Introduction
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.
Problem
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:
- Observe the current position and velocity.
- Choose an anchor point (a building) within range.
- Compute the swing arc.
- Decide when to release to maintain forward momentum.
- Repeat until the character reaches the right edge of the screen.
This is a classic control loop. The challenge is making it robust and fast enough for real-time.
Solution
I built a simple agent loop in Python that runs at 30 Hz. It uses a state machine with explicit termination conditions:
- Observe: Get current position (x, y) and velocity (vx, vy) from the browser.
- Decide: If no web is attached, find the nearest building anchor within a cone ahead. If a web is attached, compute the pendulum angle and check if the character is at the bottom of the swing and moving forward. If so, release.
- Act: Send the anchor point or release command to the browser.
- Terminate: Stop when x > screen width or after 500 iterations (safety).
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.
Implementation
Python Agent (server.py)
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 # Configuration SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 GRAVITY = 0.5 SWING_SPEED = 0.1 MAX_ITERATIONS = 500 # Building anchors (x, y) - top of buildings 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: # Find nearest anchor ahead within range 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 # Initialize angle from vertical 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: # Pendulum physics 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 # Check release condition: at bottom and moving forward if abs(self.angle) < 0.1 and self.angular_velocity > 0: # Release self.anchor = None return {'action': 'release', 'vx': self.vx, 'vy': self.vy} else: # Update position from pendulum 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()) Browser Client (index.html)
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> Running It
- Save
server.pyand runpython server.py. - Open
index.htmlin a browser. - Watch Spider-Man swing. The agent loop terminates when he reaches the right edge or after 500 iterations.
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.
Key Takeaways
- Agent loops can drive animation: You don’t need an LLM to have an observe-decide-act loop. A simple rule-based controller can produce emergent motion.
- Explicit termination is critical: Without a stop condition, the agent would run forever. I used
x > SCREEN_WIDTHand a max iteration cap. - Physics can be simple: A pendulum model with release timing is enough for a convincing swing.
- Real-time constraints: The loop runs at 30 Hz; the browser interpolates. This keeps CPU usage low.
- No frames were animated: The motion is generated procedurally from parameters, not from pre-rendered images.
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.

