Four attempts at one cat
He’s somewhere on this page. Characters, code, 3D and generated pixels, for one small black cat.
There’s a cat on this page, sitting beside a paragraph and reading it with you. He belongs to the page: scroll, and he goes up with the text. Move your pointer and he comes over, walking when you’re close, running when you’re far. Here’s the same one, held still so you can look at him properly.
One: rows of characters
He started as text. Six concepts, each a grid of characters where every letter stands for a color. It’s the fastest way to find out what a cat at that size even is, and you can change an ear by changing a letter.
Two: all in code
Then I drew him procedurally: no sprite sheet, 880 lines of poses, each frame computed and written into a tiny buffer. It weighs nothing and every pose can be tweaked. That’s him, still running.
It also looks exactly like what it is, a very patient program’s idea of a cat.
// The cat is drawn procedurally into a tiny indexed buffer instead of shipping
// a sprite sheet: every frame is a Pose, and drawCat() turns a Pose into pixels.
// Coordinates face right; the engine mirrors them when the cat faces left.Three: capsules and spheres
So I tried 3D: capsules and spheres on a small skeleton, rendered tiny and scaled up without smoothing. Real rotation for free, in any direction.
But it read as a 3D model, not as a drawing, and 3D isn’t my strong suit.
Four: PixelLab, and a lot of checking
The cat you see is generated with PixelLab from one reference drawing, one strip per action and per view. PixelLab only makes five of the eight directions, from facing you round by the left to facing away. The other three are those, mirrored. Here are all of them.
sit-sw · never played
A generator doesn’t know it’s making the same cat again and again. Some strips came out wrong: he stands up instead of lying down, or turns into another drawing. I didn’t regenerate those forever. I just never let him do it from there.
// From those directions the cat first turns
// to the nearest one whose strip is good.
const REDIRECT = {
sit: { 1: 0, 7: 0 },
sleep: { 3: 2, 4: 2, 5: 6 },
};It became a personality. He turns around before lying down, which is what a cat would do anyway.
Paws that don’t slide
My first version had a nice curve from distance to speed. He looked like he was on ice. A walk cycle has a stride, and if the ground moves at any other speed the paws slide. So he has exactly two speeds, tied to the frames: one sprite pixel per frame to walk, three to run.
- run
- gait
- 3
- sprite px per frame
- 72
- px per second
const gait = dist > 220 ? "run" : dist > 14 * SCALE ? "walk" : null;
if (gait) cat.want = towards(dx, dy);
// in the frame callback: travel is 1 or 3
b.x += STEP[cat.dir][0] * travel * SCALE;
b.y += STEP[cat.dir][1] * travel * SCALE;Going up and down the screen, a step only counts for 75%. The camera looks at him from above, so depth is foreshortened. You’d never name it, but without it he floats.
Part of the page
A mascot pinned to the window is a sticker. It floats over the content and has nothing to do with it. So he’s positioned in the page itself, in page coordinates, and passes under the header. Scrolling carries him with the paragraph he’s reading, and if an image loads and pushes the text down, he moves with it.
When your pointer stops, he sits after five seconds, then walks back to whatever you’re reading. Scroll and he follows you down the page. Leave him alone for twenty seconds and he falls asleep, until you move or the song starts. Click him and you get hearts and a jump.
Small things
His eyes are recolored at render time: the iris is the only saturated yellow in the palette, so one test per pixel finds it. Idle strips don’t end where they start, so they play back and forth rather than jumping. Hearts and z’s are still written as rows of characters, like the very first attempt.
All of him fits in under 200 KB, and he only loads on this page.