← All notes

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.

Chibi
Mini
Realistic
Ball
Watcher
Detailed

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.

cat/sprite.ts
// 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.

SSWWNWN
idle
walk
run
sit
sleep

sit-sw · never played

Frame
4

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.

cat/top-engine.ts
// 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
Pointer is
300px away
cat/roaming-cat.tsx
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.