{"slug": "little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver", "title": "Little Red Dog by the Sea: building a pixel art TV screensaver", "summary": "Developer Matt Horn built a pixel art Android TV screensaver called Little Red Dog by the Sea, with Claude Fable 5.2 writing all the code and image files and Gemini's Nano Banana 2.1 (gemini-nano-banana-2.1) cleaning phone photos and sketching early layouts. The app renders a fixed 480x270 canvas scaled 8x to 4K with nearest-neighbor filtering, using semantic color slots so 12 monthly calendars and 6 lighting states share one set of art files, and baked Floyd-Steinberg dithering across 48 sky tones, 16 sea tones, and 16 sand tones. Horn moved the art pipeline to Python for faster iteration, then ported it back to Java, adding a frame-hash manifest in unit tests so the Java output cannot drift from Python by a single pixel.", "body_md": "# Little Red Dog by the Sea: building a pixel art TV screensaver with Claude\n\n· Matt Horn\n\n*One day on the beach, sped up across dawn, daylight, dusk, and night.*\n\nThis week, I learned I could [connect to my Google TV using a coding agent](https://github.com/Gathin23/cleanyourtv), and took the opportunity to start removing some bloatware. I got curious when I saw a few examples of screensavers with [pixel art koi fish](https://github.com/Gathin23/cleanyourtv/tree/main/addons/koi-pond) on GitHub. A few days later, I finished a [pixel art screensaver of my dog](https://github.com/matt-w-horn/little-red-dog-by-the-sea) on a Northern California beach, complete with tides, migrating wildflowers, holidays, and hourly light shifts. Claude wrote all of the code and image files.\n\nGemini’s image generation model ([Nano Banana 2](https://ai.google.dev/gemini-api/docs/image-generation), gemini-nano-banana-2.1) cleaned clutter out of my phone photos and sketched early layouts. From there, Claude Fable 5.2 wrote the [Java Android TV app](https://github.com/matt-w-horn/little-red-dog-by-the-sea/tree/main/app) (zero network access, zero Android permissions) alongside the [Python scripts](https://github.com/matt-w-horn/little-red-dog-by-the-sea/tree/main/tools) that generate every shipped pixel under [art/](https://github.com/matt-w-horn/little-red-dog-by-the-sea/tree/main/art).\n\n## Color slots, baked dithering, and Python-to-Java parity\n\nMost of the design grew out of two hard constraints: a fixed 480x270 canvas scaled 8x to 4K with nearest-neighbor filtering, and an indexed palette model. Pixels don’t store literal RGB colors; they store semantic color slots, like paint by numbers. Separating geometry from [palette tables](https://github.com/matt-w-horn/little-red-dog-by-the-sea/tree/main/art/palettes) lets 12 monthly calendars and 6 lighting states (dawn, day, dusk, cloudy, rain, and night) share one set of art files with zero redraws.\n\nEarly versions showed harsh horizontal bands across the sky and sea. For the gradients, I used baked [Floyd-Steinberg dithering](https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering) ([tools/scene_dither.py](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/tools/scene_dither.py)) across 48 sky tones, 16 sea tones, and 16 sand tones. Computing it once stops gradients from shimmering on every change (though a little shimmer might be nice in the ocean).\n\nClaude was iterating on this first in Java, where every tweak waited behind a full Gradle build and ADB deploy. To speed up the build, test, and review loop, I moved most of the art pipeline to [Python](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/tools/scene.py), then ported it back to [Java](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/main/java/io/matthorn/littlereddog/Scene.java) for the app. A frame-hash manifest ([art/reference/manifest.json](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/reference/manifest.json)) runs in [unit tests](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/test/java/io/matthorn/littlereddog/SceneReferenceTest.java) so Java can’t drift from Python by a single pixel.\n\n*Scene evolution across the working history: early banded gradients, baked 48/16/16-tone Floyd-Steinberg dithering, and the shipped beach with doghouse, surf ribbons, and seasonal plants.*\n\n## The only evidence is visual\n\nVerification was a challenge. The art kept regressing, especially the 40-pixel-tall dog sprites across six poses. Small changes to a collar or paw would turn into big mistakes, garbled images, and a litany of errors.\n\nEarly on, I set a rule: prose confidence doesn’t count, and the only evidence is visual.\n\n1. When generating new pixel art, Claude has to load a cleaned reference photo first.\n2. When modifying existing art, Claude has to load the current sprite first.\n3. After each edit, Claude overlays the sprite at 50% opacity over the photo. Finished sheets go to a reviewer subagent to describe aloud what it sees, then approved elements lock so they won’t change.\n\nPhone snapshots came with couches, awkward angles, and a patterned wrap on his hind leg. Gemini first isolated each pose onto a blue key background, then traced a flat-region study before we mapped silhouettes onto the grid.\n\n*Reference pipeline for the sleeping side pose: cropped phone shot, Gemini cleanup removing the leg wrap, flat-region study, three-part [dog_side.png](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_side.png) sprite, and 50% opacity overlay check.*\n\n## Why nine subagents approved a two-tailed dog\n\nScaling judgment is hard, especially for ambiguous tasks like “represent my dog using pixel art”. At 40 pixels tall, pixel art is pretty far from a photo, so what does it even mean for a dog to be properly represented?\n\nAt one point, I came back to a session that had garbled two sleeping poses (side and curl). On the side pose, the model gave him two separate tails: one straight tail sticking left, plus the curled tail tucked underneath. Nine Fable subagents looked at those candidates, and the ninth approved both. Subsequent redraws went further off the rails with stiff 45-degree stilt legs, a pinched neck, and a side-profile head grafted onto the curled pose, before I rolled back to the earlier baseline.\n\n*Pre-squash regression progression for the side and curl poses: approved baseline, the two-tailed edit passed by nine subagents, subsequent stilt-leg and head-graft redraw failures, and the final shipped sprites.*\n\nLooking into how the harness and vision encoder processed images surfaced two mechanical failure modes:\n\n1. **Patch-vector encoding and information density:** Vision models don’t inspect a pixel grid directly; they divide an image into fixed patches and project each patch into a vector embedding, after the harness first downscales any image over roughly 2,000 pixels on a side. That makes review sheets an information-density problem, not just a resolution problem. Packing multiple poses and lighting states onto one contact sheet crams an entire 40-pixel sprite into a handful of patch vectors, compressing two-pixel neck seams and extra tails into the same representation before the model evaluates them.\n2. **Perspective hallucination:** Overhead couch photos of a curled dog hid his lower face and foreshortened his spine. Asking a code model to reconstruct missing camera angles on a pixel grid broke proportions every time.\n\nOnce I saw a pattern, we’d turn it into a guideline for that class of error. We locked standing and sleeping silhouettes to orthographic side-view ratios matching his build (1:1 shoulder-to-rump torso square, legs at 50% of shoulder height, head at 40%), then added a shading rule: “decide where the sun is before you place a single shadow”.\n\nWhen text rules still fell short on the curled pose, adding another camera angle solved it. Gemini re-photographed the overhead shot from a low eye-level angle. Python area-averaged that cutout onto the pixel grid via [Image.Resampling.BOX](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Resampling.BOX), and a local alignment script flagged outline mismatches in 50% magenta beside magnified quadrant crops.\n\n*Fixing camera foreshortening on the curled pose: overhead snapshot, Gemini low-angle re-photography, Python box-filter reduction, 50% magenta outline diff, and the shipped [dog_curl.png](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_curl.png) sprite.*\n\n## Harmonizing six poses under one sun (PR #6)\n\nWe also made some minor adjustments to the dog sprites in [PR #6](https://github.com/matt-w-horn/little-red-dog-by-the-sea/pull/6). Once all six silhouettes held together, they still didn’t quite look like the same dog under the same sky. Commit [f273347](https://github.com/matt-w-horn/little-red-dog-by-the-sea/commit/f273347ea20907c50d62f8429985d62a3dba182c) restored the [standing dog’s](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog.png) original front half while keeping its newer head, collar, haunch, and tail. Plotting all six sprites in false color by semantic role then explained why the rest felt flatter: the standing pose carried broad yellow `COAT_TOP` highlight patches across its back and shoulder, while the other five poses only had a one-pixel rim.\n\n*Semantic palette roles in false color (`COAT_TOP` yellow, `COAT_REAR` pink, `COAT_FRONT` green, `COAT_UNDER` dark brown, `COAT_FAR` navy, `WHITE` white).*\n\nCopying the standing pose’s highlight strip literally looked wrong on curled or prone shapes, so commit [4c23383](https://github.com/matt-w-horn/little-red-dog-by-the-sea/commit/4c2338315539e9647103663f5479172f3b16a69c) relit each pose from its own geometry. The [belly](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_belly.png) and [side](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_side.png) poses scale `COAT_TOP` depth by how steeply the back faces up; upright [sit](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_sit.png) and [sphinx](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_sphinx.png) use an ellipsoid-mass script lit by a high left sun calibrated on the standing pose. On [curl](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/dog_curl.png), twelve refinement steps walked the back closer to the cutout contour, tilted the face down a pixel, widened a cream muzzle bridge above the nose, moved the breathing rectangle in [tools/poses/curl.json](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/tools/poses/curl.json) to row 8, and re-exported [art/reference/manifest.json](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/reference/manifest.json) ([743d8e3](https://github.com/matt-w-horn/little-red-dog-by-the-sea/commit/743d8e3694633b46c4d6de76ce84271b8b099bd2) updated the GIFs).\n\n*Final curl pass at 10x beside the close-up snout photo and dawn, day, dusk, and night renders.*\n\n## Tuning for a 4K living-room TV\n\nSome problems were hard to spot without a large screen. Early night scenes left the stars and clock too bright. They also kept beach sand at 52% of daytime brightness, so the shoreline read like snow. Because I wanted this running 24/7, I spent a few hours calibrating brightness across day and night so it wouldn’t pollute the house with blue light. Dropping [night sand](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/palettes/night.json) to 34% brightness (cooled +15% blue to offset the evening warmth filter) and dimming a warm translucent clock after 21:30 settled the room.\n\n*Top: zoomed horizon gradients before and after baked 48/16/16 Floyd-Steinberg dithering ([tools/scene_dither.py](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/tools/scene_dither.py)). Bottom: early 0.52x night glare vs. tuned 0.34x blue-cooled sand and dimmed clock ([art/palettes/night.json](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/art/palettes/night.json)).*\n\nAlong the way, there was a barely perceptible shudder on the shooting star crossing the sky. Beach art ticks at 8 fps (125 ms), while the shooting star interpolates on every 60 Hz panel refresh (16.7 ms vsync). Claude helped me profile the app with [simpleperf](https://developer.android.com/ndk/guides/simpleperf) over ADB. Drawing one 480x270 frame on the UI thread took 22 ms despite using only 18.8% of one of the TV’s four cores, so every 8 Hz art tick missed a vsync deadline and dropped one frame in eight.\n\nI suggested Claude try a multithreaded pipeline that renders two ticks ahead. Offloading rendering onto a background producer thread inside a four-slot [FrameRing](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/main/java/io/matthorn/littlereddog/FrameRing.java) buffer, paired with 256-entry [Dim](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/main/java/io/matthorn/littlereddog/Dim.java) lookup tables on the display thread in [SceneView](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/main/java/io/matthorn/littlereddog/SceneView.java), stopped the stutter (0 ticks dropped, 0.4 ms average callback delay).\n\n## Context compaction and catastrophic forgetting\n\nA lot of the work was simply managing the model. Fable 5.2 is a beast, and it’ll burn through tokens quickly if you don’t pay attention. Loading all 63 reference files at startup eats roughly 220,000 tokens (~3,500 tokens per picture) before any code gets written. Over a 24-hour sprite session, context compacted 7 times, including 3 automatic [context compactions](https://platform.claude.com/docs/en/build-with-claude/context-windows) near 967,000 tokens.\n\nEvery compaction caused [catastrophic forgetting](https://en.wikipedia.org/wiki/Catastrophic_interference) on subsequent art edits until I saw why. Compaction keeps a short text summary while dropping fine geometry rules and flushing image attachments out of the active window, leaving the model to edit pixels from prose summaries alone. Re-loading locked sprites, active rules, and the two or three reference photos for the pose in progress right after every compaction turned out to be essential. It doesn’t happen automatically.\n\nNear the end, I ran `/code-review` passes to deduplicate shared helpers, trim the dead foam fallback, tighten [Python](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/tools/scene.py) and [Java](https://github.com/matt-w-horn/little-red-dog-by-the-sea/blob/main/app/src/main/java/io/matthorn/littlereddog/Scene.java) parity checks, and bring render time down from 22 ms to 14.4 ms.\n\n*All six harmonized poses from PR #6, the animated surprise trigger, and the transition into night lighting.*\n\nI gave the screensaver my dog’s nickname, [“Little Red Dog”](https://github.com/matt-w-horn/little-red-dog-by-the-sea). More to come.", "url": "https://wpnews.pro/news/little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver", "canonical_source": "https://matthorn.io/posts/2026-11-14-little-red-dog/", "published_at": "2026-11-15 15:08:49+00:00", "updated_at": "2026-11-15 15:23:45.640678+00:00", "lang": "en", "topics": ["generative-ai", "ai-tools", "developer-tools"], "entities": ["Matt Horn", "Claude Fable 5.2", "Gemini Nano Banana 2.1", "Google TV", "Little Red Dog by the Sea"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver", "markdown": "https://wpnews.pro/news/little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver.md", "text": "https://wpnews.pro/news/little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver.txt", "jsonld": "https://wpnews.pro/news/little-red-dog-by-the-sea-building-a-pixel-art-tv-screensaver.jsonld"}}