{"slug": "how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme", "title": "How to put crisp, animated pixel art in a GitHub profile README", "summary": "A developer built Pixelbolt, a pixel-art tool that AI agents drive with JSON recipes, and used it to generate animated pixel-art headers, tool cards and footers for a GitHub profile README. The animations are real fluid simulations shaded as pixel art, delivered as SVG wrappers that embed a sprite sheet with image-rendering: pixelated and step-end keyframe animation so the art stays crisp at any README width. The finished profile's 40 SVGs total 531,619 bytes, and the approach was tested in Chrome 155, Firefox 157 and WebKit 26.6 at 1x and 2x.", "body_md": "*Written with AI help (Claude), checked against the project's docs.*\n\nThe header, tool cards, link buttons and footer of the GitHub profile at [github.com/skulitom](https://github.com/skulitom) are now pixel art that moves: a night workbench where a flask boils and steam rolls up past a window, and a small scene for each tool. AI agents made all of it with [Pixelbolt](https://github.com/skulitom/Pixelbolt), a pixel-art tool that agents drive with JSON recipes. The liquid, mist and steam are real fluid simulations, shaded as pixel art. And it stays sharp at every width GitHub shows it at.\n\nThis post covers the README trick that keeps the pixels crisp, the simulations behind the steam, and how one command rebuilds every pixel to the same bytes.\n\nA profile README is Markdown plus a little HTML. No JavaScript, no `<style>`, no `style=\"\"`. You get images.\n\nPixel art needs whole pixels. The usual move is a GIF with `width=\"100%\"`, so it fills the column. But the browser scales that image with smoothing, and GitHub's column is almost never a whole multiple of your art. These are the widths we measured:\n\n| Viewport | README column | Cell of a two-column table | \n|---|---|---|\n| 1440 px | 846 px | 395.5 px | \n| 1280 px | 831 px | 388 px | \n| 1012 px | 563 px | 254 px | \n| 768 px | 383 px | 164 px | \n| 390 px phone (2x screen) | 308 px | 126.5 px | \n\nHere is one frame of a 192x100 card at 846 px wide, rendered by Chrome at 1x on a page that sizes images the way GitHub does. Left, as a plain image. Right, the same frame through the SVG wrapper described below.\n\nAn SVG file is an image as far as the README is concerned, but inside it you can write CSS. Two properties do the work:\n\n`image-rendering: pixelated` on the embedded sprite sheet, so it scales without smoothing.`@keyframes` animation with `step-end` timing that moves the sheet one frame at a time behind a clip.\nThis is the Inkbolt card's wrapper, trimmed (one keyframe stop per frame, 72 in all):\n\n```\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"384\" height=\"200\" viewBox=\"0 0 192 100\"\n     shape-rendering=\"crispEdges\">\n<style>\nimage{image-rendering:pixelated;image-rendering:crisp-edges;image-rendering:pixelated}\n.a{animation-timing-function:step-end;animation-iteration-count:infinite}\n@keyframes k0{0.0000%{transform:translate(-192px,0px)}1.3889%{transform:translate(-384px,0px)} /* ... */}\n.l0{animation-name:k0;animation-duration:5040ms;transform:translate(-192px,0px)}\n</style>\n<defs><clipPath id=\"c0\"><rect x=\"0\" y=\"0\" width=\"192\" height=\"100\"/></clipPath></defs>\n<g clip-path=\"url(#c0)\">\n  <image class=\"a l0\" width=\"1536\" height=\"1000\" href=\"data:image/png;base64,...\"/>\n</g>\n</svg>\n```\n\nThe sheet is 1536x1000: eight columns and ten rows of 192x100 frames. 72 frames at 70 ms each is 5,040 ms, so each frame owns 100/72 = 1.3889% of the animation. The `viewBox` is in art pixels, so the README can scale the whole SVG to any width and every art pixel stays a hard-edged block.\n\nIn the README it is an ordinary image:\n\n```\n<picture>\n  <source media=\"(prefers-reduced-motion: reduce)\" srcset=\"assets/v4/inkbolt-still.svg\">\n  <img src=\"assets/v4/inkbolt.svg\" width=\"100%\" alt=\"An ink drop falls into water and blooms into a vortex ring\">\n</picture>\n```\n\nWe tested this in Chrome 155, Firefox 157 and WebKit 26.6 at 1x and 2x, and on github.com itself, before drawing any real art. It is cheap: a 96-frame, 208x162 stress sheet came to about 530 KB, and the finished profile's 40 SVGs (the header, phone header, four cards, footer and seven link buttons with their phone-width versions, each with a motionless twin) total 531,619 bytes.\n\nThings that caught us out:\n\n`data:` URI.`prefers-reduced-motion` query, but no engine we tested applied it to an SVG shown through `<img>`. The motionless twin goes in a `<picture>` source instead.`<text>` drawn over the art, so they stay sharp at any size. Screen readers read the `alt`, so describe the scene there.\nPixelbolt compiles a fluid source into an ordinary recipe of palette pixels. Gases use stable fluids (advection, a pressure solve, vorticity confinement, buoyancy from temperature); liquids are FLIP particles, four per cell, on the same grid. \"Look\" layers then map fields such as density, speed, depth, light and shadow onto palette colours with ordered dither, so no colour outside the palette can appear, and looping effects blend their last frames into the first.\n\nThe header's steam is a 128x60 gas grid over 96 frames, fed by eight billows a loop at uneven intervals. One billow and the look, trimmed from the real source:\n\n```\n{\n  \"format\": \"pixelforge-fluid\",\n  \"width\": 128, \"height\": 60,\n  \"effects\": { \"steam\": {\n    \"frames\": 96, \"duration\": 70, \"loop\": true, \"seed\": 5,\n    \"buoyancy\": 0.2, \"vorticity\": 0.35, \"dissipation\": 0.04, \"drag\": 0.09,\n    \"emitters\": [\n      { \"name\": \"billow-1\", \"at\": [86, 53], \"radius\": 3, \"rate\": 1, \"start\": 0, \"end\": 3,\n        \"density\": 3.6, \"temperature\": 0.9, \"velocity\": [-0.3, -1.4], \"expand\": 2.0, \"vortex\": 0.3 }\n    ],\n    \"look\": [\n      { \"field\": \"density\", \"colors\": [\"u\", \"v\"], \"thresholds\": [0.16, 0.34], \"dither\": 0.85 },\n      { \"field\": \"shadow\",  \"colors\": [\"v\", \"t\"], \"thresholds\": [0.25, 0.55], \"dither\": 0 },\n      { \"field\": \"light\",   \"colors\": [\"x\", \"F\", \"G\"], \"thresholds\": [0.22, 0.5, 0.82], \"dither\": 0 }\n    ]\n  } }\n}\n```\n\n(`pixelforge-fluid` is the format's name from before the tool was renamed Pixelbolt.) The boiling liquid under the steam is a separate 44x50 FLIP simulation. Every piece shares one palette of 70 colours in 12 ramps; the header loop uses 44 of them.\n\nThe agents didn't get there first time. Three things they found:\n\n`\"wind\": [0, -0.08]`, removed the downward tails without changing the look.\nThe recipes live in the profile repo's [`art/`](https://github.com/skulitom/skulitom/tree/main/art) folder. Small Python generators write the JSON recipes and fluid sources, Pixelbolt runs the simulations and draws every pixel, and a short wrapper script packs the sheets into the SVGs:\n\n```\nbash art/build.sh\n```\n\nIt fetches Pixelbolt at the pinned commit (`8c5f662`), needs Node 20+ and Python 3.11+ with Pillow, and takes about 45 seconds. From a clean copy it rebuilt all 40 SVGs byte for byte. That works because Pixelbolt's simulation is deterministic by design: a fixed timestep, fixed iteration counts, seeded noise, and only arithmetic, square roots and polynomial trigonometry, so a source gives the same recipe on every machine and in the browser. (The one caveat in the notes: the Python generators use Python's `math` module, so another platform's maths library could in principle move a pixel.)\n\nThe portfolio at [skulitom.github.io](https://skulitom.github.io) uses the same art, played on a canvas. Under the workbench there are three buttons. \"Add heat\" and \"Stir the flask\" don't switch to a pre-made clip: Pixelbolt's fluid compiler runs in a Web Worker in the page, re-simulates the steam or the liquid with your change, and plays the new loop.\n\nOn the live site, on a desktop Ryzen 9 7950X3D, \"Add heat\" took 932 ms in Chrome, 795 ms in Firefox and 793 ms in WebKit. (The recording above, of the same page just before it went live, shows 957 ms.) To get under a second, the live version simulates a 48-frame loop with 12 pressure iterations; the pre-rendered art uses longer settings. One limit showed up: an emitter's start/end window can't wrap around the loop, so a click near the end of the loop is moved to frame 0.\n\nPixelbolt has no npm dependencies. From a clone:\n\n```\nnode bin/pixelbolt.js compile examples/steam-vent.fluid.json --out output/steam.json\nnode bin/pixelbolt.js gif output/steam.json --scale 4 --out output/steam.gif\n```\n\nThe repo has ten fluid presets (campfire, torch, steam, magic, explosion, smoke puff, waterfall, drip, potion, splash), and its fluid guide covers emitters, looks and loops. Limits worth knowing: a fluid canvas is at most 128x128, a frame at most 256x256, gravity and obstacles are static, and emitters move only on circles and ellipses (or by chaining time windows along a path). It runs on the CPU, with no GPU and no model calls.\n\nTo see how the wrapper is built, read `art/tools/wrap.py` in the profile repo: it turns a Pixelbolt sheet and atlas into a self-contained animated SVG and its still twin.\n\n`art/`", "url": "https://wpnews.pro/news/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme", "canonical_source": "https://dev.to/rivskiy_skulitom_af400ad1/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme-1cpd", "published_at": "2026-10-17 22:58:20+00:00", "updated_at": "2026-10-17 23:07:24.640657+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "generative-ai", "ai-tools"], "entities": ["Pixelbolt", "GitHub", "Claude", "Chrome", "Firefox", "WebKit", "skulitom"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme", "markdown": "https://wpnews.pro/news/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme.md", "text": "https://wpnews.pro/news/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme.txt", "jsonld": "https://wpnews.pro/news/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme.jsonld"}}