Drawing 25 creatures in code, with no image files

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

Creature Camp is a Chrome side-panel game whose art is a program rather than a folder of pictures. Twenty-five species, ten hats, five scenes and every expression are drawn at run time from about 102 KB of generator code in art/. The only image files in the whole package are the four toolbar icons Chrome requires.

This is the second note on this build. The first one was about the instruments that lied. This one is about the drawing, because the two sprites at the top of the project page are not pictures of the game — they are the game’s renderer, running on a WordPress page.

Why draw creatures in code instead of shipping PNGs?

Four reasons, in the order they actually mattered. A generated creature has no licence surface — nothing was bought, borrowed or generated from someone else’s training set, so the privacy and provenance claims on the project page cost nothing to keep. It has no load: an extension that draws its own art has no image requests to make, which is what lets the whole thing sit behind a content policy that forbids network connections. It is parameterised: a creature is a species record plus an expression plus an optional hat, so a new mood costs a branch rather than twenty-five new files. And it is small — the average creature is about 5.6 KB of SVG generated on demand, against a sprite sheet that would have to carry every combination.

How does one generator make twenty-five different animals?

By body plan, not by drawing. Every species record names one of six plans — chibi, quadruped, winged, serpent, wisp or colossus — and the plan decides the ordered list of parts and where they attach. A fox kit and a raccoon are the same chibi skeleton with different proportions, ears and tails; a newt and an otter are both serpents; the stump spirit is the only colossus.

Colour is a five-tone palette per species — shadow, base, light, highlight, outline — indexed by role rather than written as hex in the drawing code, with one light source for the whole cast. That single constraint is what makes twenty-five separately-authored creatures read as one set instead of twenty-five styles. The feature that turns a shape into a creature is the eyes, and every plan draws them, including the ones with no face to speak of.

Hats were added late and could have been a nightmare: ten hats across twenty-five head shapes is 250 placements. It was one line instead, because the face routine already records where it put the head. The drawing kit stashes that anchor on the context, and renderHat(id, head) positions any hat on any species from it. A hat is never authored per creature.

What are the sprites animated with?

CSS, aimed at named groups the generator emits: cc-idle for the breathing scale, cc-blink for the eyelid, cc-wag for a tail, cc-glow for a firefly’s lantern. No animation library, no JavaScript tick driving a transform per frame.

That choice pays twice. It honours prefers-reduced-motion with a media query rather than a code path. And because a CSS animation can be paused by a single class on the body, the camp can go completely still when nobody is looking at it — measured on twenty friends: 60 style recalculations a second while watched, 0.2 a second when still, and about 15.4% of one core down to 0.97%. Art that is drawn by the browser can also be stopped by the browser.

What goes wrong with generated SVG?

The failure mode worth knowing about: a dangling url(#id) reference renders as nothing at all. No error, no console warning, no fallback colour — the shape simply is not painted. An earlier project shipped twenty creatures with no contact shadow for exactly this reason, and nothing in the test suite noticed, because the markup was present and well-formed.

The fix is structural rather than vigilant. Every gradient and clip path the generator emits is namespaced with a per-instance id, so six creatures on one page cannot collide — which is the exact case this site hits, since a project page is one document holding several of them. The check is one line and it runs over the whole set: collect every id, collect every url(#…), and assert the difference is empty.

Two more that cost real time. Under a strict content policy an extension page may not carry inline style attributes, so per-creature colour tinting had to become generated CSS classes rather than a style string — the browser silently drops the attribute and the creature renders in its default palette. And a probe that scanned my own markup for external references counted the SVG namespace declaration as one: http://www.w3.org/2000/svg is a URL, and a regular expression that matches the scheme rather than the whole address will tell you your self-contained file has six remote dependencies.

Why is the game’s renderer running on a WordPress page?

Because the alternative is a screenshot, and a screenshot goes stale the first time the art changes. The sprites on the Creature Camp page are produced by importing the same module the extension calls and writing its output into the page, so the marketing surface cannot drift from the product.

Three constraints shaped how much of it went up. Coordinates are rounded to two decimals, because below a pixel at that size the precision is invisible and the bytes are not. Nothing may reference an external file. And no ampersand may appear inside anything script-shaped on this site — a WordPress render can entity-encode one and kill the block silently, which is how a page here once shipped dead behind a fully green test run.

Would I do it this way again?

For a set, yes. The economics invert somewhere around ten entities: below that, drawing each one by hand is faster; above it, every change is a change to one generator rather than to N files, and the uniqueness of the set becomes something you can test rather than something you hope for. The same approach draws the crystal critters in Lumenreel and the card art in Prismwar; between them that is several hundred entities and no art folder.

For one hero character, no. A generator is a way of being consistent across many things, and consistency is not what a single portrait needs.

The camp itself is on its project page, the rest of the browser games are in the arcade, and the full index is on the projects page.