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.
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. It 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 and the viewer fetches whichever one it needs. No server, no database, no build step.
The SVGs 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, and every edge pixel 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, driving 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 an identical image will not be built again.
This image 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 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. I should note, this tool was authored in New England.
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 resides.
The date and time picker exists as a wink at nostalgia. This piece about time is 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 and no bundler, and 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 portfolio is built in Astro and deployed to Cloudflare Pages. It embeds the viewer cross-origin in an iframe, and a cross-origin host cannot reach into the document to measure it. So the host sizes the frame itself instead of asking the viewer how tall it is. The home feed gives the tile the poster's aspect ratio. The case-study overlay gives it a fixed panel at 88vh. Either way scrolling="no" clips the sketch's own overflow, and the iframe loads with loading="lazy" so the p5 runtime holds off until the piece is near the viewport.
The viewer is a dark piece, so the iframe is created with color-scheme: dark. That hands the cross-origin document a dark UA backdrop and kills the white flash that used to show while it fetched, before its own theme logic ran. The frame stays hidden until its load event fires, then reveals in one step.
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 responds to the selected moment instead of the current one: season, moon phase, hour, drift. Setting _mockDate back to null returns the piece to live time.
The calendar reads the clock. The generator hands you the clock.
The viewer at the top of this page shows what this moment looks like and nothing else. The full generator at davidpreli.com/tools/pokedex puts the engine on a control panel. The six values the clock was quietly setting all year sit on sliders down the left side, and they are yours to move.
Leaves and leaf size decide how much grows off the silhouette and how big each mark runs. Winter thins or floods the cold-weather marks the season usually governs on its own. Topo, contrast, and detail shape the Perlin field underneath the growth. Push leaves to 6 and the body vanishes under its own garden. Pull winter up in the middle of July and you get a summer Pokémon caught in a frost it has no business having.
Randomize rerolls every slider at once. Reset hands the controls back to the clock. Export png writes the current frame to disk, the one render out of the whole year you decided to keep.
Move the date to the night you were born. Turn the leaves up until the face is gone. Export it. That render does not exist anywhere else, and now the moment sits in your downloads folder.