marceloeduardo.com/funslop/eye for you

funslop · 25 September 2026

eye for you

The 1998 EYE4U Flash intro, rebuilt frame by frame as live code, with every cut landing on a downbeat of its original soundtrack. Directed by Marcelo, made by Claude.

Built in one sitting, 12:03–16:24 · 11 prompts · 487 tool calls · tokens: 712 k output, 824 input, 1.6 M cache writes, 194.6 M cache reads

the beat map

This follows the player above. It scrolls through the grid Claude fitted to the soundtrack: bars, beats, the kicks it detected and all 112 catalogued events. Click anywhere to jump there.

intro – bpm bar – · beat – 0:00.00
–Press play in the frame above.
downbeatbeat kick-drum energydetected kick cuthit movestagger distance to its beat

then and now

The same soundtrack under both. The rebuild is time-warped onto the beat, so the two drift apart by a few frames and meet again at every cut.

1998 capturerebuild

Left: the original site as screen-captured, 854 × 480, with the Flash player showing frames at an irregular 16–21 fps. Right: the rebuild, rendered at 1080p60. Download the side-by-side (13 MB).

135.1BPM in 4/4, re-fitted in 7 blocks
222kick drums detected in the soundtrack
112events catalogued from 2,544 frames
12cuts, each exactly on a downbeat
47msmedian off-beat for other hits (1998: 89 ms)
0.2mspicture-to-sound sync on this page

how it was made

Everything here was made by Claude, Anthropic's AI model, working as an agent in Claude Code. It watched the capture frame by frame, listened to the track, wrote its own analysis and measuring tools, built the animation and the renderer, and checked each result against the original. Marcelo directed in a handful of plain-English notes and judged what came back. The rule throughout was to measure, never guess.

day25 September 202612:03–16:24, 4 h 21 min start to finish
direction11 promptsfrom Marcelo, in plain English
work487 tool callsacross 412 responses from Claude Opus 5.5
tokens written712 kcode, analysis, prose and tool calls
tokens read196.2 M99% re-read from the prompt cache (194.6 M), 1.6 M newly cached, 824 uncached

Counted from this session's transcript when the page was published. Each response re-reads the whole conversation so far, which is why cache reads dominate.

  1. 01

    watch

    The 84.9-second capture (854 × 480, 30 fps) was split into its 2,544 frames and read as contact sheets. Every change on screen went into a catalogue with its time and a one-line description: 112 events, made up of 56 hits, 33 moves, 11 staggered builds and 12 hard cuts.

    Fifty frames of the 1998 capture in a grid
    The whole capture at a glance, one frame every 1.7 seconds.
  2. 02

    listen

    The soundtrack was taken apart in Python with numpy and librosa: first a harmonic/percussive split, then spectral flux per frequency bin to find the kick drum. A single matched filter failed because the kick band ripples, so the detector works bin by bin. It found 222 kicks.

    The tempo is 135.1 BPM in 4/4. But the site restarts its music loop at every section change, so a single grid would drift. Instead the grid is re-anchored and re-fitted in 7 blocks, each landing within 1.4–6.8 ms (median) of its kicks. The beat map above is this grid.

    blocksectionstartsbpmkicks on gridmedian error
  3. 03

    storyboard

    Before writing any animation, Claude laid all 112 events on the grid as a storyboard. Each event got a rule: cuts go on downbeats, interface hits on beats or eighths, and a few stay where they were. Marcelo approved the storyboard before any code was written.

    The chart below shows how far each event sits from the beat, in the 1998 original and in the rebuild.

    Distance from the nearest beat (or eighth, for events meant for eighths) for the 97 non-cut events. In 1998 the median was 89 ms; in the rebuild it is 47 ms. The 12 cuts, not shown here, land exactly on their downbeats.
    All 112 events ↓
    idtyperule1998beatrebuild vs beaton screen
  4. 04

    build

    The rebuild is one HTML page with one SVG stage, drawn in the capture's own 854 × 480 units and scaled to 1080p. A single pure function, seek(t), computes every attribute from the time. There are no CSS animations, no timers and no state carried from frame to frame, so any frame can be drawn in any order and always comes out the same. The outlines were traced from the capture's pixels and fitted to circles and ellipses wherever they were geometric.

    async function seek(t) {                 // the only entry point
      const s = warp(t);                     // rebuild time → 1998 time
      for (const sc of SCENES) {
        const on = active(sc.windows, s);
        show(sc.g, on);
        if (on) sc.draw(s, t);               // every attribute, from time alone
      }
      drawCursor(s, t);
      await Promise.all(PENDING);            // wait for image decodes
    }
    Traced outlines drawn over source frames
    Traced outlines (dashed) checked against source frames.
  5. 05

    time warp

    Snapping each event straight onto its beat made the motion stutter. So the rebuild plays the original animation through a time warp: one smooth curve from rebuild time to 1998 time, solved as a quadratic program with OSQP.

    Cuts are hard constraints, so they land exactly on their downbeats. Playback speed stays between 0.5× and 2.3×, and it changes by at most 1.4% per 1/120 s, so you never see it step. Within those limits, 88 events are pulled toward their beats as soft targets and land a median 46 ms away (worst 156 ms). The other 9 would have forced time to run backwards or past the speed limit, so they ride along between their neighbours.

    How fast the 1998 animation plays inside the rebuild (1 = original speed). Orange ticks mark the cuts.
  6. 06

    measure motion

    Motion that is hard to keyframe by hand was measured instead. The drop spin, the iris and the red sunburst were fitted frame by frame by silhouette matching.

    The flower and the eye were rebuilt as the Flash tweens they originally were. The flower rides one cubic Bézier motion guide, eased by arc length, with a constant spin. The eye is three chained tweens. The 1998 player showed frames at an irregular 16–21 fps, so every measurement was first re-timed onto a steady Flash-frame clock, and only then fitted.

    Flower path in the 1998 capture, all frames overlaid1998 capture
    Flower path in the rebuild, all frames overlaidrebuild
    The flower's flight with every frame overlaid. The 1998 player dropped and bunched frames; the rebuild samples the same arc evenly.
  7. 07

    smooth

    “everything is janky and motion is not smooth, shapes jump around each frame. review it frame by frame and make sure the motions are smooth and interpolation is perfect”Marcelo, on the first render

    That note led Claude to build two meters. One is an audit that steps seek() at 60 Hz and flags every sudden change in an element's on-screen velocity. The other is an optical-flow jerk meter for rendered video.

    The fixes were the smooth warp above; Bessel tangents instead of monotone splines, which stop and start at every key; stable slots for pooled shapes; and eased holds. Two more notes followed: the flower's arc dipped, and the eye trembled on its way in. Both were fixed by measuring the original again, not by adjusting by eye.

    Frame-to-frame jerk from optical flow, as 1-second means. First render: mean 7.0 px, with 347 frames above 10 px. Released render: mean 4.0 px, with 220 frames above 10 px. 57% of those remaining frames sit within 0.15 s of a catalogued event, where the original pops a shape in on purpose.
    Rows of consecutive rendered frames
    Consecutive 60 fps frames from the render, each blended from 9 sub-frames.
  8. 08

    render and ship

    For video, Playwright drives headless Chromium. Each 60 fps frame is the average of 9 sub-frames spread across a 180° shutter, blended with ffmpeg, and the soundtrack is normalised to −14 LUFS.

    The player on this page renders live instead. The soundtrack is decoded once and played through Web Audio, and the picture clock follows the time actually reaching your speakers, to within about 0.2 ms. That keeps every cut on its beat in the browser too.