A generative art engine that renders a different illustration every moment of the year.
151 Pokémon across a calendar year. The image changes depending on when you open it.
Bulbasaur on February 27. Mew 366 days later. Each one drawn in the browser from the current hour, the season, and the phase of the moon. Open it at dawn and you get one thing. Open it at noon and the leaves are denser, the palette warmer, the animation faster. Come back in December and the marks are sparse. That is what December looks like.
It started as an illustration project. 151 drawings, one per Pokémon, done as clean flat SVGs. The engine does not display those drawings. It reads them, maps the silhouette, finds the edges, measures how deep the body is at every point along the outline, and uses all of that to decide where things grow. The SVG is the skeleton. The engine fills it in according to the time of day you showed up.
Nothing in the output is stored. Every time you load it the engine starts from scratch, reads the clock, and builds the image for that exact moment.
The whole thing runs as a single HTML file. The SVGs live in a separate GitHub repository. The viewer fetches whichever one it needs. No server, no database, no build step.
The SVGs are not decoration. They are the skeleton the engine builds around.
The original illustrations were drawn as clean, flat SVGs — one per Pokémon, each a silhouette with enough definition to be readable at small sizes. When the engine loads one, it rasterizes it to a 500×500 pixel canvas and runs a pixel-level analysis. Every edge pixel gets a normal vector computed via a Sobel operator. Every edge pixel also gets a thickness measurement — a ray cast inward to find out how deep the body is at that point. Thin areas like ears and tails get sparse, fine marks. Wide areas like the torso get dense layered growth.
The four SVGs below are pulled live from the GitHub repository. This is the same fetch call the engine makes when you load the viewer.
Time of day, season, moon phase, and Pokémon type. Each one controls a different dimension of the output.
151 Pokémon distributed across 366 days using a simple linear formula. Day 0 is Bulbasaur. Day 365 is Mew. The lookup runs in the browser on every page load — no server call, no database. The viewer also has a date and time picker so you can move anywhere in the year and see what that moment looks like.
var dayOffset = Math.floor((d.getTime() - START_MS) / 86400000);
var num = Math.min(151, Math.floor(dayOffset * 150 / 365) + 1);The SVG is fetched from raw GitHub, base64-encoded, and drawn to an offscreen 500×500 canvas. The pixel data is read into a typed array. Every edge pixel gets a normal vector via Sobel convolution and a thickness value via ray-casting inward. This produces three arrays: edge pixel coordinates, outward angles, and body depth at each point. The rest of the engine reads from those arrays.
Four functions read the current moment and return values the renderer uses directly. getHours() returns fractional hours including minutes and seconds — this drives color temperature and bug population. getAnnualData() returns the day of year, a seasonal multiplier (peaks in summer, drops in winter), and an annual hue shift tied to a sine curve. getMoon() calculates lunar phase from a known epoch and returns a 0–1 factor that scales leaf density. getDrift() is a composite of time and season that controls overall animation energy.
All four route through a _now() wrapper instead of calling new Date() directly. When the picker is in use, _mockDate overrides the real clock. Every downstream calculation responds instantly.
Two leaf populations grow from the edge data — one for each of the Pokémon's types. Type 1 leaves grow outward along the surface normals. Type 2 leaves grow inward. Each leaf is a Bézier curve with hue derived from the type color table, shifted by season and time of day. Length, width, and density are all modulated by the moon factor, the season multiplier, and the local body thickness measured in step 2. A separate winter mark set thins out as summer approaches and fills in as it recedes.
The topo background is a multi-pass Perlin noise field rendered before the leaves. Each Pokémon name is hashed to seed a mulberry32 PRNG, so the same name always produces the same noise field. The only variation between two renders of the same Pokémon is wall-clock time.
The engine runs at 22fps. Each frame restores a static background image captured after the topo pass, then advances a 0–1 animation clock and redraws every leaf at a position offset by a sine function seeded with that leaf's own noise value. Each leaf oscillates on its own phase. The result looks like breathing rather than animation — everything moves but nothing goes anywhere. Click the canvas to reposition the silhouette and randomize all parameters. Scroll to zoom.
The image you see right now does not exist anywhere. It was built when you loaded the page and it will not be built again in exactly this way.
Most images are made once. This one is made continuously. Open it at 7am in January, you get one thing. Open it at 7pm in July and the hue has shifted, the leaves are packed in, the drift is at full energy. The engine is always reading its context. It does not know you are looking at it, but it knows what time it is.
The Pokémon framing is not decorative. The original 151 were designed as a closed system, a taxonomy with its own internal logic about types and habitats and how things relate to each other. Using that system as a calendar gives the year a shape it would not otherwise have. February is Bulbasaur. October is around Gengar. New Year's Eve is Dragonite or Dragonair, depending on the year.
At 3am in February it is sparse and cold and a little grim. That is the correct output for 3am in February.
The moon was not planned. Once the seasonal and time-of-day signals were in place it was the obvious third variable, something that shifts on its own schedule without asking for attention. A full moon in July looks different from a new moon in July. Most people will not know why. That gap between cause and perception is where the piece lives.
The date and time picker exists because a piece about time should be navigable. Jump to the winter solstice at midnight. Jump to the summer solstice at noon. The distance between those two moments is the actual content.
The viewer is a single self-contained HTML file. It loads p5.js from a CDN, fetches SVGs from GitHub, and runs entirely in the browser. There is no framework, no bundler, no server. The whole thing weighs about 58KB before the p5 library.
Hosting on GitHub Pages meant the SVG repository and the viewer could live side by side. The viewer fetches from raw.githubusercontent.com — a direct CDN link to whatever is in the repo at that moment. Adding a new SVG to the repo makes it immediately available to the viewer without any other change.
The viewer is embedded in the portfolio site via iframe. Cross-origin iframe height reporting uses postMessage — the viewer measures its own content height after the SVG loads and sends that value to the parent page, which resizes the iframe to match. A 50ms debounce prevents the resize from triggering Cargo's ResizeObserver in a loop.
All time-sensitive functions route through _now() rather than calling new Date() directly. When the picker is in use, _mockDate overrides the real clock and every downstream calculation — season, moon phase, hour, drift — responds to the selected moment instead of the current one. Setting _mockDate back to null returns the piece to live time.