Abie Maxey

Design theme

Customize ~ more themes
The Blog
Building October 2026 9 min read

A hero that looks wherever your cursor goes.

One photo of me, one photo of headphones, and a few chats in Julius later there is an animated version of me on my portfolio that follows your cursor, blinks when you leave her alone, and weighs less than a single photo. Five steps, every prompt, and the hour she looked the wrong way.

The finished hero on an iPad: an animated Abie with pink headphones at a laptop, Hi I'm Abie on the left, a Building Right Now list on the right

The finished hero. Try it live at abiemaxey.com/hello2

01 ~ The point

What you get

Move your mouse to the left of the page and she looks left. Move it right, she follows. Leave, and she drifts back to eye contact and blinks every few seconds. There is no video playing. It is one still image, 33 frames wide, and a few hundred lines of code deciding which frame to show.

Three frames from the strip: looking left, looking at the camera, looking right

The whole range of motion is a line: far left on one end, far right on the other, eye contact in the middle. Your cursor's position across the page picks a spot on that line. Everything that feels alive about it is how she gets from one spot to the next.

I saw a reel of a developer's portfolio where the character on the page reacted to the cursor, and I wanted one with me in it. The whole thing came out of one Julius chat and an afternoon of finishing. Here it is in the same five steps I use in the video.

02 ~ Step 1

A photo and a pair of headphones

Front facing works best. I used a plain studio photo of me, and because I wanted her to wear the headphones I actually wear, I added a product shot of them as a second image. Two inputs in the same message, so the character and the prop get generated together instead of pasted on later.

Left: a front-facing photo of Abie in a white top. Right: the first animated render, waving at a laptop with pink headphones around her neck
Step 1 to step 2 · my reference photo, and the very first render that came back.

03 ~ Step 2

The animated me

This is the prompt that turned the photo into her. Everything after it was a one-line follow-up in the same chat.

Julius · image prompt (with the photo and the headphones attached)
Turn the person in the first photo into a Pixar-style 3D character. Keep her face, hair, skin tone and smile recognisable. She is sitting at a laptop on a plain white desk, wearing the headphones from the second photo, front view, looking at the camera. Soft studio light, clean white background, no text.

The first render waved at me with the headphones around her neck, which was cute and wrong. Three short follow-ups fixed it, one change at a time. The file names Julius gives you are the first words of each prompt, which turned out to be a nice diary of the session.

~/Downloads ~ what Julius saved, in order
23:57 image_20261013235713.jpg
23:58 Same_character,_same_framing,_same_… .jpg
00:02 remove_the_b_logo_… .jpg
01:49 make_her_looking_at_the_… _2K.jpg
06:57 Animate_character_looking_around_1080p_… .mp4
Julius · the three follow-ups
Same character, same framing, same headphones. Put the headphones on her ears and have her typing on the laptop, both hands on the keyboard.

Remove the b logo from the headphones. Keep everything else identical.

Make her looking at the camera, hands resting by the laptop, small smile. Same character, same framing. Render at 2K.
Four renders in order: waving with headphones on her neck, typing with headphones on, the same without the logo, and the final still looking at the camera
The four renders, in order. Top left is the first; bottom right is the one I animated.

Don't rewrite the prompt when something is off. Say what to change and what to keep. “Same character, same framing” did more for consistency than any style word.

04 ~ Step 3

Animate it

The final still goes back in with an animate prompt. Two things in it matter more than the rest: a locked camera, so every frame lines up with every other frame, and a return to centre, so there is a resting pose to come home to.

Julius · video prompt (with the final still attached)
Animate character looking around. Locked camera, no zoom, no cuts. She starts looking at the camera, turns her head to look to one side, then the other, glances up once, blinks naturally, and comes back to looking at the camera. Hands stay still. White background stays white. 8 seconds.

What came back, with every fourth frame laid out so you can read it like a comic:

A 6 by 8 grid of frames from the clip, every fourth frame, showing her turning one way, looking up, turning the other way, blinking and returning to centre
Every fourth frame of the 8-second clip. 192 frames at 24fps, camera never moves.

05 ~ Step 4

The interactive hero

Then the clip goes back into Julius with the prompt that does the engineering. This is the one people ask for. I keep it word for word:

Julius · the interactive hero prompt (with the clip attached)
Create an interactive hero using the uploaded animation. Use FFmpeg to extract every frame, combine them into a single horizontal sprite sheet, and build a frame-based animation system in vanilla JavaScript. Make the character naturally track the mouse by smoothly scrubbing through the frames based on cursor position, with eased interpolation and a smooth return to the idle pose when the mouse leaves. Preserve the original composition, keep the character centered, and make the interaction feel premium and responsive.

Julius ran ffmpeg, extracted the frames, packed them, wrote the spring, and handed back a page I could open in a browser. That page is real and it works:

Julius's own build on an iPad: a motion study page titled She follows your lead, with the character in a rounded card and an idle badge
What Julius built from that prompt. Export it, open index.html, move the mouse.

Two numbers from its README made me rebuild it for the site: 361 frames, and a sprite sheet 57,760 pixels wide by 88 tall. Browsers cap one image dimension at 16383 pixels, so the big strip could only be a blurry fallback and the sharp frames loaded one file at a time. For a demo that is fine. For my home page I wanted one image, sharp, with the frames chosen on purpose.

So I kept the idea and the prompt and did the frame picking by hand. First, extract every frame, cropped to her and the laptop:

zsh ~ ffmpeg
$ffmpeg -i clip.mp4 -vf "crop=800:864:560:100" -start_number 0 frames/%03d.png
$ls frames | wc -l
192

Then measure how different each frame is from the next. Mean squared error is a crude number, but it shows where the turn happens and where she is only holding a pose:

python ~ neighbour deltas
deltas 0..23: [1, 1, 1, 6, 25, 48, 57, 84, 107, 127, 149, 167, 182, 183, 173, 165, 159, 133, 103, 80, 60, 48, 67, 112]
deltas 92..104: [91, 65, 34, 11, 2, 2, 6, 10, 14, 24, 48, 87, 123]
closest frame to frame 0 among the second centre: 118 (mse 92)

Frames 0 to 3 are the same picture. Frames 8 to 22 are the turn, every one earning its place. Frames 92 to 99 are a hold with only the eyes drifting. So the strip keeps every frame of the two turns and thins the holds. The blink in the middle of the return is skipped, because a cursor can park anywhere and nobody wants to park on closed eyes. Two blink frames go at the end of the strip for an idle blink instead. 33 frames at 496 pixels is 16368 wide. Right under the cap.

The first six frames of the sprite strip side by side, her head turned left and gradually coming toward centre
The first six of 33 frames in the strip. Far left is index 0; eye contact is index 16.

The white background had to go so she sits on the page in light and dark mode. Her shirt is nearly the same white, so a plain colour key punched holes through it. A flood fill from the frame border fixes that: only white that touches the edge is background.

Six keyed frames on a dark background: no halo, the white shirt intact
Keyed frames checked on the dark theme. The shirt survives because it never touches the border.

The engine is the same shape Julius wrote, trimmed to what the strip needs. Cursor x becomes a target frame, a critically damped spring chases it, and each draw fades the next frame on top of the current one so motion between frames reads as motion rather than steps:

sprite-scrubber.ts (the loop, trimmed)
// cursor x in -1..1 -> a target frame on the strip
const u = clamp(x / 0.82, -1, 1);                 // fully turned before the edge
const eased = Math.sign(u) * (1 - (1 - Math.abs(u)) ** 1.6);
target = eased < 0 ? idle + eased * idle : idle + eased * (count - 1 - idle);

// critically damped spring: snappier while following, lazier on the way home
const k = following ? 170 : 55;
const c = 2 * Math.sqrt(k);
vel += (k * (target - pos) - c * vel) * dt;
pos += vel * dt;

// draw floor(pos), then fade floor(pos)+1 in by the fraction
const i = Math.floor(pos), f = pos - i;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(sheet, i * w, 0, w, h, 0, 0, w, h);
if (f > 0.005) {
  ctx.globalAlpha = f;
  ctx.drawImage(sheet, (i + 1) * w, 0, w, h, 0, 0, w, h);
  ctx.globalAlpha = 1;
}

First build, cursor on the right, she looked left. I had read the contact sheet backwards. Easy fix, rebuild the strip in the other order, but I wanted proof, so a few lines in the browser console compare the canvas against every frame of the strip and print which one is on screen while synthetic pointer events fire:

browser console ~ frame detector
pointer at +0.97 (far right), strip built backwards
[101,27] [208,26] [311,26] [414,26] [516,21] [617,13] [719,10] [822,7] [925,6] [1028,4] [1130,3]
same test after the rebuild
[100,25] [200,27] [300,27] ... pointerleave ... [1500,13] [1600,13] [2000,31] [2300,13]

The second trace is what a working hero looks like: she reaches the far end in two hundred milliseconds, holds, comes back to idle after I leave, and the 31 near the end is a blink.

06 ~ Step 5

The live link

The old hero was mid-rework in the same repo, so this one lived in a git worktree: a second folder, its own branch, its own dev server, nothing shared but history. The strip is built by a script in the repo, so changing my mind about a frame is two lists and a rerun, never hand-edited pixels.

zsh ~ abiemaxey
$git worktree add -b feature/hello2-sprite-hero ../abiemaxey-hello2 origin/dev
$python3 scripts/build-hello2-sheet.py ~/Downloads/Animate_character_looking_around.mp4
public/hello2/sheet.webp 16368x536 643 KB
src/components/hero2/sheet-manifest.json 33 frames, idle at 16

From there it was the usual route: a pull request into dev, a release from dev to main, and a wait for the production build to say READY before calling it live. A failed build keeps the old site up and tells nobody. It is live now at abiemaxey.com/hello2. On a phone, tap left or right of her.

07 ~ Copy this

Every prompt, and the checklist

Image

One front-facing photo of me plus a product shot of the headphones, with the character prompt below. Three follow-ups: same character typing, remove the logo, look at the camera.

Video

The final still plus the animate prompt. 8 seconds, locked camera, 24fps.

Hero

The clip plus the interactive hero prompt. Julius extracts the frames, packs the sheet and writes the vanilla JavaScript.

Site

The part I did by hand: picking the frames, keying the background, and dropping the engine into my Next.js portfolio.

  • Start from one front-facing photo and one product shot of the thing you want her wearing. Two inputs, one prompt.
  • Fix the character in follow-up messages, one change each: typing, remove the logo, look at the camera. Don't rewrite the whole prompt.
  • Animate the final still with a locked camera and a return to centre, or the frames won't line up.
  • Read a contact sheet before you pick frames. Keep every frame of a turn, thin the holds, skip the blinks.
  • Keep a single strip under 16383px wide. WebP and Safari both cap one dimension there.
  • Test the scrub with synthetic pointer events and read the frame back from the canvas. Eyeballing lies.
  • Mixing frames from two renders of the same character. Even a near match pops at the join.

The last one cost me an hour. A second clip arrived with an up-look I wanted, same character, same pose. Aligned and measured, the two renders were as different as two neighbouring frames mid-turn: bigger eyes, flatter headphones, slightly different scale. Any join at the centre would have popped. One source, or none.

Abie Maxey

Love,

Abie

Weekly Dispatch

The builder's diaries.

What I'm building, breaking, and figuring out.

  • AI tools I actually use to build faster
  • Behind-the-scenes of growing a business in Madrid
  • Community plays, client wins, and real revenue breakdowns
  • Grounded dispatches from someone building roots
Growing community ~ join Claude Maxxing on Skool

Drop your email.

Unsubscribe any time. No spam, ever.

Apply for Ship Yours