DEV Community

heavy shield
heavy shield

Posted on

I Made Spider-Man Swing Without Animating a Single Frame

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:

  1. Observe: Get current position (x, y) and velocity (vx, vy) from the browser.
  2. 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.
  3. Act: Send the anchor point or release command to the browser.
  4. 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())
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Running It

  1. Save server.py and run python server.py.
  2. Open index.html in a browser.
  3. 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_WIDTH and 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.

Top comments (1)

Collapse
 
koda2026 profile image
Harun - solo dev •

the distinction that an "agent" doesn't need an llm is so important right now. everyone is throwing transformers at problems that a simple state machine + physics loop can solve way more efficiently.

your observe-decide-act loop for the pendulum is elegant. i use a similar loop structure for my ai coding mentor's sandbox (observe code -> decide safety -> act in iframe), though my "decide" step uses an llm instead of pure math.

curious about the websocket latency though. at 30hz, if a packet drops or the network jitters, does the browser interpolate the pendulum state locally to smooth it out, or does it just wait for the next server update? handling that jitter is usually the hardest part of real-time client-server physics.

massive respect for the "explicit termination" point. infinite loops are the silent killer of agent architectures. 🐯