<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: rivskiy skulitom</title>
    <description>The latest articles on DEV Community by rivskiy skulitom (@rivskiy_skulitom_af400ad1).</description>
    <link>https://dev.to/rivskiy_skulitom_af400ad1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169087%2F6cc37a0a-1107-4cfa-8371-ecc96b245b1a.png</url>
      <title>DEV Community: rivskiy skulitom</title>
      <link>https://dev.to/rivskiy_skulitom_af400ad1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rivskiy_skulitom_af400ad1"/>
    <language>en</language>
    <item>
      <title>How to make a pixel art waterfall animation (free, in your browser or from the command line)</title>
      <dc:creator>rivskiy skulitom</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:23:19 +0000</pubDate>
      <link>https://dev.to/rivskiy_skulitom_af400ad1/how-to-make-a-pixel-art-waterfall-animation-free-in-your-browser-or-from-the-command-line-46io</link>
      <guid>https://dev.to/rivskiy_skulitom_af400ad1/how-to-make-a-pixel-art-waterfall-animation-free-in-your-browser-or-from-the-command-line-46io</guid>
      <description>&lt;p&gt;&lt;em&gt;Written with AI help (Claude), checked against the project's docs.&lt;/em&gt; Pixelbolt is made by our small lab, so we're not neutral; every number below comes from a run on 3 November 2026 with Pixelbolt 0.11.0 from npm.&lt;/p&gt;

&lt;p&gt;This is a looping pixel art waterfall for a 2D game: water leaves a mossy stone ledge in an arc, lands in a basin and spills over its lip. It's made with Pixelbolt, an MIT-licensed tool that simulates the water as particles on a pixel grid and bakes each frame into palette pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6k09bm7zblcaf7v68sa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6k09bm7zblcaf7v68sa.gif" alt="A pixel-art waterfall pours off a mossy stone ledge in an arc into a stone basin and spills over its lip" width="288" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The waterfall preset, unchanged, rendered by &lt;code&gt;pixelbolt gif&lt;/code&gt; at 6× on a dark background.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;48 × 64 pixels, 24 frames of 60 ms (a 1.44 s loop at 16.67 fps)&lt;/li&gt;
&lt;li&gt;9 palette colours on a transparent background (the GIF above is blended onto a dark colour and enlarged 6×)&lt;/li&gt;
&lt;li&gt;a 384 × 192 sprite sheet (8 × 3 cells), a TexturePacker-style atlas JSON with each frame's rectangle and duration, a GIF, an animated PNG and single frames&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The browser way
&lt;/h2&gt;

&lt;p&gt;The guide gives four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the waterfall in the playground from the guide's waterfall section (link at the end). The link carries the whole recipe, with nothing to install and no account.&lt;/li&gt;
&lt;li&gt;Change a value in the Recipe JSON box. The preview renders again after each edit.&lt;/li&gt;
&lt;li&gt;Choose a scale from 1× to 16× for the frame PNG and the GIF. The sheet and atlas stay at the art's own size.&lt;/li&gt;
&lt;li&gt;Download the sheet, the atlas PNG and JSON, the GIF, a frame PNG or the recipe. Copy link makes a link to your edited version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A note on versions: the guide's link and commands name Pixelbolt 0.9.1. The commands below use 0.11.0, the newest release. The recipe, size and timing are the same, but 0.11.0's liquid changes alter about 150 to 220 of each frame's 3,072 pixels. Its changelog says the waterfall's foot "splits into strands a little more".&lt;/p&gt;
&lt;h2&gt;
  
  
  The command-line way
&lt;/h2&gt;

&lt;p&gt;You need Node.js 20 or newer. The waterfall ships as a preset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.11.0 compile &lt;span class="nt"&gt;--preset&lt;/span&gt; waterfall &lt;span class="nt"&gt;--out&lt;/span&gt; waterfall.json
npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.11.0 gif waterfall.json &lt;span class="nt"&gt;--scale&lt;/span&gt; 6 &lt;span class="nt"&gt;--background&lt;/span&gt; &lt;span class="s2"&gt;"#17191d"&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; waterfall.gif
npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.11.0 render waterfall.json &lt;span class="nt"&gt;--out&lt;/span&gt; waterfall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compile&lt;/code&gt; runs the simulation and writes an ordinary recipe of frames. &lt;code&gt;gif&lt;/code&gt; enlarges it by a whole number and blends it onto one colour. &lt;code&gt;render&lt;/code&gt; writes a folder holding the sheet, the atlas JSON, the frames, an animated PNG, CSS, a canvas player and a preview page. It also writes &lt;code&gt;waterfall.tres&lt;/code&gt;, which the folder's README.txt calls a Godot 4 SpriteFrames file; we haven't tested that file. For engines in general, the guide says to slice the sheet as a grid of equal cells. Nothing is overwritten unless you add &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One knob, changed
&lt;/h2&gt;

&lt;p&gt;The guide names the settings under &lt;code&gt;effects&lt;/code&gt; → &lt;code&gt;waterfall&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;velocity&lt;/code&gt; on the &lt;code&gt;source&lt;/code&gt; emitter (&lt;code&gt;[1.1, 0]&lt;/code&gt;), which sets how far out the arc reaches&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pulse&lt;/code&gt;, which surges the flow four times a loop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;whitewater&lt;/code&gt; for spray and foam&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flip&lt;/code&gt;, livelier toward 1 and calmer toward 0&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;depth&lt;/code&gt; colour layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To edit the recipe, save it as &lt;code&gt;waterfall.fluid.json&lt;/code&gt; with the playground's Recipe JSON button, or download the same file from the npm package (in Windows PowerShell, type &lt;code&gt;curl.exe&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://cdn.jsdelivr.net/npm/pixelbolt@0.11.0/examples/waterfall.fluid.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compiling that file gives byte-for-byte the same result as the preset. We changed &lt;code&gt;velocity&lt;/code&gt; from &lt;code&gt;[1.1, 0]&lt;/code&gt; to &lt;code&gt;[1.6, 0]&lt;/code&gt; and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.11.0 compile waterfall.fluid.json &lt;span class="nt"&gt;--out&lt;/span&gt; waterfall-fast.json
npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.11.0 gif waterfall-fast.json &lt;span class="nt"&gt;--scale&lt;/span&gt; 6 &lt;span class="nt"&gt;--background&lt;/span&gt; &lt;span class="s2"&gt;"#17191d"&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; waterfall-fast.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmxr509dp5223ps367dl.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmxr509dp5223ps367dl.gif" alt="The same waterfall with faster water: the arc lands near the basin's right lip and the pool sits higher" width="288" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The same recipe with &lt;code&gt;velocity&lt;/code&gt; at &lt;code&gt;[1.6, 0]&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The arc now lands near the basin's right lip instead of mid-basin. Halfway down (row 30), the stream's centre moved from x 24–27 to x 29.5–30. More water reaches the basin: the pool's surface rose from rows 43–45 to row 39, more spills over the lip, and the drawn water went from 619–640 to 708–739 pixels a frame. Size, timing and loop didn't change.&lt;/p&gt;

&lt;p&gt;Not every value is accepted. A pulse period of 5 is refused: "a looping effect repeats every 24 frames, so the period must divide 24".&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from our run
&lt;/h2&gt;

&lt;p&gt;Node 24.19.0, Windows 11, a 16-core desktop CPU:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;compile&lt;/code&gt; took 3.02, 3.32 and 3.27 s through npx, which needs about 2.3 s just to start. Run with node on the same package, it took 1.03–1.07 s, about 0.3 s of that Node's start-up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gif&lt;/code&gt; and &lt;code&gt;render&lt;/code&gt; each took about 0.35 s with node.&lt;/li&gt;
&lt;li&gt;Every run gave byte-identical files.&lt;/li&gt;
&lt;li&gt;The GIF is 288 × 384 pixels and 41,809 bytes, the sheet 6,519 bytes and the atlas 9,599 bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Pixelbolt's physics notes describe one grid per effect, one cell per pixel, with the liquid carried by particles, four to a full cell. Each step moves the particles' motion onto the grid, applies gravity and cohesion, and solves for pressure. It then hands the velocities back to the particles and moves them, never into a solid. The maths uses only basic arithmetic, square roots, the project's own trigonometry and seeded hashing, with fixed step counts, so a recipe gives the same pixels on every run. The stated aim is the picture: "When accuracy and looks disagree, looks win."&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A fluid canvas is at most 128 × 128 pixels, with up to 16 emitters and 32 obstacles per effect.&lt;/li&gt;
&lt;li&gt;One liquid per effect: mixing several is "out of scope for now".&lt;/li&gt;
&lt;li&gt;Frames are at most 256 × 256 pixels, and a project holds at most 256 frames.&lt;/li&gt;
&lt;li&gt;It imports PNG only, doesn't edit GIF or Aseprite files, uses no image model and isn't meant for painted or photographic art.&lt;/li&gt;
&lt;li&gt;Spray and foam are still drawn as single pixels and dashes.&lt;/li&gt;
&lt;li&gt;It's tuned for looks, not engineering accuracy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The water guide's waterfall section: &lt;a href="https://skulitom.github.io/Pixelbolt/guides/pixel-art-water-animation/#waterfall" rel="noopener noreferrer"&gt;https://skulitom.github.io/Pixelbolt/guides/pixel-art-water-animation/#waterfall&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The fluid physics model: &lt;a href="https://github.com/skulitom/Pixelbolt/blob/main/docs/physics-model.md" rel="noopener noreferrer"&gt;https://github.com/skulitom/Pixelbolt/blob/main/docs/physics-model.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abotwrotethis</category>
      <category>pixelart</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to make a pixel art fire animation for Godot 4 (free, in your browser)</title>
      <dc:creator>rivskiy skulitom</dc:creator>
      <pubDate>Tue, 27 Oct 2026 17:19:40 +0000</pubDate>
      <link>https://dev.to/rivskiy_skulitom_af400ad1/how-to-make-a-pixel-art-fire-animation-for-godot-4-free-in-your-browser-4m86</link>
      <guid>https://dev.to/rivskiy_skulitom_af400ad1/how-to-make-a-pixel-art-fire-animation-for-godot-4-free-in-your-browser-4m86</guid>
      <description>&lt;p&gt;&lt;em&gt;Written with AI help (Claude), checked against the project's docs.&lt;/em&gt; Pixelbolt is made by our small lab, so we're not neutral; the measurements below come from a run on 27 October 2026 with Pixelbolt 0.9.1 (from npm) and Godot 4.6.3.&lt;/p&gt;

&lt;p&gt;You want a looping campfire in your Godot game, you'd rather not hand-animate two dozen frames, and you'd like to change how it burns. Here's one way that's free and runs in a browser: open a ready fire recipe in Pixelbolt's playground, tweak a couple of numbers, export a sprite sheet, and slice it in Godot as a plain grid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs58183hcoqhsi6wl7apx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs58183hcoqhsi6wl7apx.gif" alt="A pixel-art campfire: flames rise from crossed logs, embers drift up and grey smoke curls away" width="256" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The campfire recipe, unchanged, rendered by &lt;code&gt;pixelbolt gif&lt;/code&gt; at 8× on a dark background.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;p&gt;From the campfire recipe, unchanged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The animation:&lt;/strong&gt; 32 × 48 pixels, 24 frames of 70 ms each (1.68 s), looping, 9 colours on a transparent background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sprite sheet:&lt;/strong&gt; 256 × 144 pixels, 8 columns × 3 rows of 32 × 48 cells with no padding. The PNG is 3,375 bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The atlas JSON:&lt;/strong&gt; a TexturePacker-style hash with every frame's rectangle and its 70 ms duration, plus the animation's name, order and loop flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flames come from a small simulation of heat and smoke, not from hand-drawn frames, and a recipe renders to the same pixels every time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: open the campfire recipe
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://skulitom.github.io/Pixelbolt/playground/#v=0.9.1&amp;amp;r=hVTLjqMwEPyX2msP4h3Wx_zCHuYQ5WDAJBYGR8bMTBTx76s2IcNIK62lSCmqqa7udvNAZ90gPQRu-kuZzrqLeuvMrFsQPpSbtB0hEsIoBwWBRg63TjsFwqdu_RUiSwlXpS9XD5FXhJs0ynsF8cAMgV95mxdFBcIfRqUqywMjx6iusjTtQLCMuvhwSGsQ7gF1TZOzi_cVdWWbgFAzKmRWpQUIR0aVLGRWgtAzSmXSJBUWwnQfamsmdnJxdh5biBP6_xyW-V_ImWDsZWK5KIqivo9-nBWDmOyPxz3dM97I_ljX3zRTdc3sStZ8Vnql6rruX7Lf9Ea9ZAPdr_TKrBk2MtD9k9m8fZNbwA7ivBBU16nGh26-LoF4oHNyUBNEmhPa2UkfLswh5hbZG4R3syJMSrUQGeFTGhMaV1vv7cCtrGd7l2Nzh4ijvCB8WOd1o_3rQaunSd-eynEUlwWhsdbo8cKYM9t68rIxbOT0eA4eYp0S4SukvkPk8UI7-nkrQkC8BpSE1slPCIx2VFjOBDVo75VbpZ9r8FwBXpxTUlKenglOtnqeIEr-ywuQELwabspJP3OzkightGqcttp4w4xdSz3F9JZErDNaPYXonNAZ3fTKcXTJ1p_p1VCzoW8DWb4zkO0MTDfp-nVodh49CxUEozsFcaoo4fcaa2woD-9h-daf5dl4J7XZhijHi-HX0iKm9Hd8ZvUw11McZRRHh3NoXhhKXC1LWBPbh751WpkW4lX-PuscPg6c7erUdLWmnYJmUlIcscFW--vahIK_LzyNEQIf0swK3JZNfd_ufQYX6lnrev9HJnafpxRHZUrJz4TLeVmWvw" rel="noopener noreferrer"&gt;the campfire in the Pixelbolt playground&lt;/a&gt;. It's the same button as on the &lt;a href="https://skulitom.github.io/Pixelbolt/guides/pixel-art-fire-animation/" rel="noopener noreferrer"&gt;fire guide&lt;/a&gt;. The link carries the whole recipe and names Pixelbolt 0.9.1; the playground renders it in your browser, with nothing to install and no account.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: change how it burns
&lt;/h2&gt;

&lt;p&gt;The recipe is JSON. The fire's settings sit under &lt;code&gt;effects&lt;/code&gt; → &lt;code&gt;campfire&lt;/code&gt;, and the preview renders again after each edit. If the JSON breaks, the playground says what's wrong, usually with the line number. The guide's suggestions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;buoyancy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;How hard the heat rises. Higher lifts the flames higher and faster.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cooling&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.24&lt;/td&gt;
&lt;td&gt;The share of heat lost each frame. Raise it for a lower fire, lower it for taller flames.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vorticity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;Keeps small swirls alive, so the flames and smoke curl.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;flicker&lt;/code&gt; (on the fire emitter)&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;Variation from cell to cell and frame to frame that breaks the base into tongues.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;count&lt;/code&gt; (in the embers emitter's sparks)&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;Embers per frame. Fractions mean some frames get one and some don't.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What one change did in our run: the guide says to lower &lt;code&gt;cooling&lt;/code&gt; for taller flames. Pixelbolt's compile report gives the fire's bounding box for every frame. Averaged over the 24 frames, it was 23.8 px tall at the default 0.24, 24.9 px at 0.16 (hardly a change) and 28.6 px at 0.08 (clearly taller). At 0.08 the gas stays hot as it rises, so the grey smoke turns into red wisps. Change it in bigger steps than you'd expect, and watch the smoke.&lt;/p&gt;

&lt;p&gt;The torch recipe works the same way. For a blue torch, change the hex values of &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;W&lt;/code&gt; in its palette (coolest to hottest). We used &lt;code&gt;#1d3f8f&lt;/code&gt;, &lt;code&gt;#2f7de1&lt;/code&gt;, &lt;code&gt;#7cc8ff&lt;/code&gt; and &lt;code&gt;#e8f8ff&lt;/code&gt;, and it rendered as a blue flame.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: export a sheet Godot can slice
&lt;/h2&gt;

&lt;p&gt;Download &lt;strong&gt;Atlas PNG + JSON&lt;/strong&gt; (or just &lt;strong&gt;Sheet PNG&lt;/strong&gt;). Sheets come at the art's own size, one pixel per pixel, whatever scale you pick for the GIF and frame PNG. For a bigger sheet, add &lt;code&gt;"sheet": { "scale": 4 }&lt;/code&gt; at the top level of the recipe (whole numbers from 1 to 16), or keep it small and scale it in Godot with nearest-neighbour filtering.&lt;/p&gt;

&lt;p&gt;For Godot's grid slicing, Pixelbolt's &lt;a href="https://github.com/skulitom/Pixelbolt/blob/v0.9.1/docs/game-integration.md" rel="noopener noreferrer"&gt;game integration notes&lt;/a&gt; say: keep the default untrimmed layout and set &lt;code&gt;sheet.padding&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt;. The campfire's sheet already has no padding: 256 = 8 × 32 and 144 = 3 × 48.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same from the command line
&lt;/h2&gt;

&lt;p&gt;With Node.js 20 or newer, save the recipe (the playground's &lt;strong&gt;Recipe JSON&lt;/strong&gt; button saves &lt;code&gt;campfire.fluid.json&lt;/code&gt;, or take &lt;a href="https://cdn.jsdelivr.net/npm/pixelbolt@0.9.1/examples/campfire.fluid.json" rel="noopener noreferrer"&gt;the one in the 0.9.1 package&lt;/a&gt;) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.9.1 compile campfire.fluid.json &lt;span class="nt"&gt;--out&lt;/span&gt; campfire.json
npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.9.1 gif campfire.json &lt;span class="nt"&gt;--scale&lt;/span&gt; 4 &lt;span class="nt"&gt;--out&lt;/span&gt; campfire.gif
npx &lt;span class="nt"&gt;-y&lt;/span&gt; pixelbolt@0.9.1 render campfire.json &lt;span class="nt"&gt;--out&lt;/span&gt; campfire
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compile&lt;/code&gt; runs the simulation and writes an ordinary recipe of frames. &lt;code&gt;gif&lt;/code&gt; writes the animation. &lt;code&gt;render&lt;/code&gt; writes a folder with the sheet (&lt;code&gt;campfire.png&lt;/code&gt;), its atlas (&lt;code&gt;campfire.atlas.json&lt;/code&gt;), every frame as its own PNG, an animated PNG, CSS classes, a small canvas player, a preview page and the recipe. Pixelbolt won't overwrite existing files unless you add &lt;code&gt;--force&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In our run (Windows 11, Node 24), each &lt;code&gt;npx&lt;/code&gt; command took 2.0 to 2.6 seconds, mostly npx starting up; the compile itself took about a third of a second. &lt;code&gt;render&lt;/code&gt; wrote 32 files. The 4× GIF is 128 × 192 pixels and 22,385 bytes. The playground's GIF and atlas JSON came out byte-identical to the command line's, and its sheet PNG has the same pixels (it's a little bigger because the playground embeds the recipe in every PNG it exports, so you can drop the file back on the page and keep editing).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: play it in Godot 4
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Copy &lt;code&gt;campfire.png&lt;/code&gt; into your project.&lt;/li&gt;
&lt;li&gt;For crisp pixels, set &lt;strong&gt;Project Settings → Rendering → Textures → Canvas Textures → Default Texture Filter&lt;/strong&gt; to &lt;strong&gt;Nearest&lt;/strong&gt; (or set &lt;strong&gt;Texture → Filter&lt;/strong&gt; to Nearest on the node).&lt;/li&gt;
&lt;li&gt;Add an &lt;strong&gt;AnimatedSprite2D&lt;/strong&gt;. In the Inspector, set &lt;strong&gt;Sprite Frames&lt;/strong&gt; to a &lt;strong&gt;New SpriteFrames&lt;/strong&gt; and open it.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add frames from sprite sheet&lt;/strong&gt;, pick &lt;code&gt;campfire.png&lt;/code&gt;, set &lt;strong&gt;Horizontal&lt;/strong&gt; to 8 and &lt;strong&gt;Vertical&lt;/strong&gt; to 3, select all 24 cells and add them.&lt;/li&gt;
&lt;li&gt;Set the animation's speed to &lt;strong&gt;14.29 FPS&lt;/strong&gt; (1000 ÷ 70 ms), turn &lt;strong&gt;Loop&lt;/strong&gt; on, and turn on &lt;strong&gt;Autoplay on load&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What we checked: in Godot 4.6.3 we imported the sheet headless and built the same SpriteFrames from a short script that reads the atlas JSON: 24 frames at 14.2857 FPS, loop on. Every rectangle in the atlas matched the 8 × 3 grid, and an AnimatedSprite2D played frames 0 to 23, then looped back to 0 (28 frame changes in 2 seconds). We didn't click through the editor steps above or take a screenshot in Godot; they're Godot's standard SpriteFrames steps for the same grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Godot export.&lt;/strong&gt; Pixelbolt writes PNGs and a TexturePacker-style JSON. Godot doesn't read that JSON, so you enter the grid, the speed and the loop yourself. The files never state an FPS: divide 1000 by the frame time in milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uneven frame times and padding.&lt;/strong&gt; The guide's hand-drawn flame plays its 4 frames for 80 to 120 ms each, and its sheet has 1 px of padding. Set the recipe's &lt;code&gt;sheet&lt;/code&gt; padding to 0 before you export, then use each frame's duration from the JSON (in Godot, a frame's duration is a multiplier of the animation speed), or import the separate frame PNGs instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size.&lt;/strong&gt; A recipe is capped at 256 × 256 pixels and 256 frames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GIFs.&lt;/strong&gt; Pixels under 50% opacity become fully transparent, and frame delays are rounded to 10 ms. The PNG sheet keeps full alpha, so use the sheet in the game and the GIF for devlogs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The playground&lt;/strong&gt; needs a recent browser (JavaScript, module workers and CompressionStream) and loads the engine from jsDelivr. The &lt;strong&gt;Atlas PNG + JSON&lt;/strong&gt; button downloads two files; if your browser keeps only one, the page shows links to both for two minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What we tested:&lt;/strong&gt; Godot 4.6.3, headless, with the campfire. Not Godot 3, not the editor's clicks, and not the torch in Godot. Pixelbolt's own notes say engine integration "should be checked in your target engine", and that's fair advice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you'd rather not make your own
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://skulitom.itch.io/elemental-fx" rel="noopener noreferrer"&gt;Elemental FX pack&lt;/a&gt; has 27 effects made with Pixelbolt (elemental powers, impacts, fire, smoke, goo, lava and magic), each with its sheet, atlas JSON and a Godot 4 SpriteFrames file you set on an AnimatedSprite2D. It's $4.99 or more, or $3.49 in itch.io's Halloween Sale until 4 November, 18:00 UTC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pixelbolt on GitHub (MIT): &lt;a href="https://github.com/skulitom/Pixelbolt" rel="noopener noreferrer"&gt;https://github.com/skulitom/Pixelbolt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The six effect guides (fire, explosions, smoke, water, slime and magic): &lt;a href="https://skulitom.github.io/Pixelbolt/guides/" rel="noopener noreferrer"&gt;https://skulitom.github.io/Pixelbolt/guides/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The playground: &lt;a href="https://skulitom.github.io/Pixelbolt/playground/" rel="noopener noreferrer"&gt;https://skulitom.github.io/Pixelbolt/playground/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abotwrotethis</category>
      <category>godot</category>
      <category>gamedev</category>
      <category>pixelart</category>
    </item>
    <item>
      <title>How to nest laser-cut parts for free in your browser</title>
      <dc:creator>rivskiy skulitom</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:13:59 +0000</pubDate>
      <link>https://dev.to/rivskiy_skulitom_af400ad1/how-to-nest-laser-cut-parts-for-free-in-your-browser-1epc</link>
      <guid>https://dev.to/rivskiy_skulitom_af400ad1/how-to-nest-laser-cut-parts-for-free-in-your-browser-1epc</guid>
      <description>&lt;p&gt;&lt;em&gt;Written with AI help (Claude), checked against the project's docs. Disclosure: this account belongs to the small project that makes Nestbolt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nesting means arranging the parts of a drawing on your sheet before you cut, so they use less material. &lt;a href="https://github.com/skulitom/Nestbolt" rel="noopener noreferrer"&gt;Nestbolt&lt;/a&gt; is a free, MIT-licensed nesting tool for fixed sheets (laser, CNC, plasma, waterjet, fabric). Its &lt;a href="https://skulitom.github.io/Nestbolt/" rel="noopener noreferrer"&gt;browser app&lt;/a&gt; runs the nesting engine on your own computer in WebAssembly, and the page says it plainly: "Everything runs in this browser; files are never uploaded."&lt;/p&gt;

&lt;p&gt;Below is one real run, step by step, with the numbers the app showed. I ran it on 20 October 2026 in desktop Chrome on the hosted app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ershiq52wshqzicmink.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ershiq52wshqzicmink.png" alt="Nestbolt in the browser: six parts with quantities on the left, and on the right one 600 x 400 mm sheet holding 91 nested parts, with Sheets 1, Utilisation 64.6%, Placed 91/91 and Check valid above it" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Open the app
&lt;/h2&gt;

&lt;p&gt;Go to &lt;a href="https://skulitom.github.io/Nestbolt/" rel="noopener noreferrer"&gt;skulitom.github.io/Nestbolt&lt;/a&gt;. There is nothing to install and no account. If you just want to see how it behaves, it has five one-click examples: finger-jointed boxes, letters inside letters, a mechanical kit, a sewing pattern and interlocking gears.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Import your parts
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Import SVG, DXF or LightBurn&lt;/strong&gt; (or drop files on it). The importer treats each closed outline as a part and any outline inside it as a hole, so small parts can later sit inside larger ones.&lt;/p&gt;

&lt;p&gt;My file was a 420 x 70 mm SVG with six outlines: a cat ornament, a star, a coaster, a gear, a heart keyring and a name tag. The app listed them as six parts with their areas (26.1, 8.4, 17.8, 14.1, 14.1 and 16.9 cm²) and showed what its cleanup did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;laser-parts.svg: cleanup removed 0 exact duplicate segment(s), 1 zero-length segment(s); merged 0 overlapping collinear segment(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gaps up to 0.005 mm are joined automatically; larger repairs are listed with their positions. The import panel also shows which layers and colours it picked as cut lines, which is worth a look if your drawing has engraving or guide layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Set quantities, rotations and mirroring
&lt;/h2&gt;

&lt;p&gt;Each part gets a &lt;strong&gt;Qty&lt;/strong&gt; box, a &lt;strong&gt;Rotation&lt;/strong&gt; choice and a &lt;strong&gt;Mirror&lt;/strong&gt; tick box. Rotation options are 0/90/180/270° (the default), 0/180° (grain), Fixed 0°, Every 15° and Free. Use 0/180° when the grain or pattern direction matters.&lt;/p&gt;

&lt;p&gt;I typed 12 cats, 20 stars, 8 coasters, 16 gears, 20 hearts and 15 tags: 91 parts, default rotations, no mirroring.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Describe the sheet
&lt;/h2&gt;

&lt;p&gt;The default sheet is 600 x 400 mm with a 5 mm margin, and the preset list has Diode laser 300×200, CO2 bed 600×400, Plywood 1220×2440, two sheet-metal sizes and a 1500 mm fabric roll. You can add more sheet sizes, a count limit per size, a remnant outline (an odd-shaped leftover piece) and defects to avoid.&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Settings&lt;/strong&gt;: part spacing and edge spacing (1 mm each by default), the goal (&lt;strong&gt;Place every part&lt;/strong&gt;, &lt;strong&gt;Fill the sheets (most area)&lt;/strong&gt; or &lt;strong&gt;Fill by priority&lt;/strong&gt;) and the time budget (&lt;strong&gt;Time&lt;/strong&gt;, 30 s by default). I left all of these at their defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Press Nest
&lt;/h2&gt;

&lt;p&gt;The best layout found so far is drawn while the search runs. After the 30 seconds, my result read:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sheets&lt;/th&gt;
&lt;th&gt;Utilisation&lt;/th&gt;
&lt;th&gt;Placed&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;64.6%&lt;/td&gt;
&lt;td&gt;91/91&lt;/td&gt;
&lt;td&gt;valid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All 91 parts fit on one sheet, and the unused material was left as one clear strip, labelled &lt;strong&gt;offcut 600 × 49 mm&lt;/strong&gt;, along the top. The project's README says the last sheet is compressed towards one end so that the offcut stays usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check: valid&lt;/strong&gt; comes from an independent validator (Clipper2) that tests overlap, spacing, containment, rotations and quantities. According to the README, invalid layouts are never returned.&lt;/p&gt;

&lt;p&gt;The search is time-based, so results vary a little between runs: the same file and quantities gave 65.0% on 17 October, on an older build of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Download the sheets
&lt;/h2&gt;

&lt;p&gt;Each sheet has &lt;strong&gt;SVG&lt;/strong&gt; and &lt;strong&gt;DXF&lt;/strong&gt; buttons, and &lt;strong&gt;Download all sheets (ZIP)&lt;/strong&gt; gets them all at once. My &lt;code&gt;sheet-0.svg&lt;/code&gt; was 600 mm x 400 mm with all 91 parts as paths in one &lt;code&gt;CUT&lt;/code&gt; group. The name tags' rounded corners were still SVG arcs (&lt;code&gt;A 6 6 ...&lt;/code&gt;), not chains of short lines: the README says export is "transformed from the original curves rather than re-polygonised".&lt;/p&gt;

&lt;p&gt;Open the file in your laser or CAM software and check it there before cutting. &lt;strong&gt;Save&lt;/strong&gt; keeps the project in the browser, and &lt;strong&gt;Project ZIP&lt;/strong&gt; saves it as a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The hosted app runs a preview build from the project's &lt;code&gt;main&lt;/code&gt; branch. The stable release (v0.1.0) is a &lt;a href="https://github.com/skulitom/Nestbolt/releases" rel="noopener noreferrer"&gt;download&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;LightBurn project nesting (&lt;code&gt;.lbrn2&lt;/code&gt; and &lt;code&gt;.lbrn&lt;/code&gt;) and lead-in allowances are preview features.&lt;/li&gt;
&lt;li&gt;Common-line cutting is optional and still under development. It can increase material use, and its export needs CAM preparation so shared sections aren't cut twice.&lt;/li&gt;
&lt;li&gt;Nestbolt gives no machine-safety or fit certification. Review every sheet in your own cutting software.&lt;/li&gt;
&lt;li&gt;My numbers come from one run on one PC. A bigger job, or a slower computer, may need a longer time budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Other ways to run it
&lt;/h2&gt;

&lt;p&gt;The same engine also comes as command-line tools for Windows, Linux and macOS (Apple silicon), and as an MCP server, so an AI agent can import, nest, validate and export a job (&lt;a href="https://github.com/skulitom/Nestbolt/blob/main/docs/AGENT_INTERFACE.md" rel="noopener noreferrer"&gt;agent interface&lt;/a&gt;). The engine builds on &lt;a href="https://github.com/JeroenGar/jagua-rs" rel="noopener noreferrer"&gt;jagua-rs&lt;/a&gt; and &lt;a href="https://github.com/JeroenGar/sparrow" rel="noopener noreferrer"&gt;sparrow&lt;/a&gt; by Jeroen Gardeyn and collaborators.&lt;/p&gt;

&lt;p&gt;More guides on the project site: &lt;a href="https://skulitom.github.io/Nestbolt/free-nesting-software-laser-cutting/" rel="noopener noreferrer"&gt;free nesting for laser cutting&lt;/a&gt;, &lt;a href="https://skulitom.github.io/Nestbolt/nest-svg-dxf-in-browser/" rel="noopener noreferrer"&gt;nesting SVG and DXF in the browser&lt;/a&gt;, &lt;a href="https://skulitom.github.io/Nestbolt/nesting-examples/" rel="noopener noreferrer"&gt;the five examples&lt;/a&gt; and &lt;a href="https://skulitom.github.io/Nestbolt/results/" rel="noopener noreferrer"&gt;the benchmark results&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>abotwrotethis</category>
      <category>lasercutting</category>
      <category>opensource</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>How to put crisp, animated pixel art in a GitHub profile README</title>
      <dc:creator>rivskiy skulitom</dc:creator>
      <pubDate>Sat, 17 Oct 2026 22:58:20 +0000</pubDate>
      <link>https://dev.to/rivskiy_skulitom_af400ad1/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme-1cpd</link>
      <guid>https://dev.to/rivskiy_skulitom_af400ad1/how-to-put-crisp-animated-pixel-art-in-a-github-profile-readme-1cpd</guid>
      <description>&lt;p&gt;&lt;em&gt;Written with AI help (Claude), checked against the project's docs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The header, tool cards, link buttons and footer of the GitHub profile at &lt;a href="https://github.com/skulitom" rel="noopener noreferrer"&gt;github.com/skulitom&lt;/a&gt; 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 &lt;a href="https://github.com/skulitom/Pixelbolt" rel="noopener noreferrer"&gt;Pixelbolt&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxmksw8wkp235esfvgs7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flxmksw8wkp235esfvgs7.gif" alt="A night workbench in pixel art: a mint liquid boils in a round flask and its steam rolls up in shaded billows past a moonlit window" width="712" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: GitHub scales your GIF and blurs it
&lt;/h2&gt;

&lt;p&gt;A profile README is Markdown plus a little HTML. No JavaScript, no &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, no &lt;code&gt;style=""&lt;/code&gt;. You get images.&lt;/p&gt;

&lt;p&gt;Pixel art needs whole pixels. The usual move is a GIF with &lt;code&gt;width="100%"&lt;/code&gt;, 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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Viewport&lt;/th&gt;
&lt;th&gt;README column&lt;/th&gt;
&lt;th&gt;Cell of a two-column table&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1440 px&lt;/td&gt;
&lt;td&gt;846 px&lt;/td&gt;
&lt;td&gt;395.5 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1280 px&lt;/td&gt;
&lt;td&gt;831 px&lt;/td&gt;
&lt;td&gt;388 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1012 px&lt;/td&gt;
&lt;td&gt;563 px&lt;/td&gt;
&lt;td&gt;254 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768 px&lt;/td&gt;
&lt;td&gt;383 px&lt;/td&gt;
&lt;td&gt;164 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;390 px phone (2x screen)&lt;/td&gt;
&lt;td&gt;308 px&lt;/td&gt;
&lt;td&gt;126.5 px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3kmd7js08civ687waso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3kmd7js08civ687waso.png" alt="The same pixel-art frame twice at 846 px wide: on the left, a plain image with soft, smeared edges; on the right, the SVG wrapper with hard pixel edges" width="716" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: an SVG that plays a sprite sheet
&lt;/h2&gt;

&lt;p&gt;An SVG file is an image as far as the README is concerned, but inside it you can write CSS. Two properties do the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image-rendering: pixelated&lt;/code&gt; on the embedded sprite sheet, so it scales without smoothing.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;@keyframes&lt;/code&gt; animation with &lt;code&gt;step-end&lt;/code&gt; timing that moves the sheet one frame at a time behind a clip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the Inkbolt card's wrapper, trimmed (one keyframe stop per frame, 72 in all):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"384"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 192 100"&lt;/span&gt;
     &lt;span class="na"&gt;shape-rendering=&lt;/span&gt;&lt;span class="s"&gt;"crispEdges"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
image{image-rendering:pixelated;image-rendering:crisp-edges;image-rendering:pixelated}
.a{animation-timing-function:step-end;animation-iteration-count:infinite}
@keyframes k0{0.0000%{transform:translate(-192px,0px)}1.3889%{transform:translate(-384px,0px)} /* ... */}
.l0{animation-name:k0;animation-duration:5040ms;transform:translate(-192px,0px)}
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&amp;lt;clipPath&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"c0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"192"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&amp;lt;/clipPath&amp;gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;clip-path=&lt;/span&gt;&lt;span class="s"&gt;"url(#c0)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"a l0"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1536"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1000"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"data:image/png;base64,..."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 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 &lt;code&gt;viewBox&lt;/code&gt; is in art pixels, so the README can scale the whole SVG to any width and every art pixel stays a hard-edged block.&lt;/p&gt;

&lt;p&gt;In the README it is an ordinary image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;picture&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"(prefers-reduced-motion: reduce)"&lt;/span&gt; &lt;span class="na"&gt;srcset=&lt;/span&gt;&lt;span class="s"&gt;"assets/v4/inkbolt-still.svg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"assets/v4/inkbolt.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"An ink drop falls into water and blooms into a vortex ring"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/picture&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3es10ye4hm9p82aqg8x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs3es10ye4hm9p82aqg8x.gif" alt="Pixel-art card: an ink drop falls from a dip pen into a glass tank of water and blooms into a vortex ring" width="576" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We 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.&lt;/p&gt;

&lt;p&gt;Things that caught us out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Everything must be inline.&lt;/strong&gt; An SVG shown as an image can't load other files, so the PNG sheet goes in as a &lt;code&gt;data:&lt;/code&gt; URI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced motion doesn't reach inside.&lt;/strong&gt; The SVG can carry a &lt;code&gt;prefers-reduced-motion&lt;/code&gt; query, but no engine we tested applied it to an SVG shown through &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;. The motionless twin goes in a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; source instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The animation starts at frame 0 on every page load.&lt;/strong&gt; If frame 0 is a quiet hold, that's what visitors stare at. Order the sheet so it opens on a moment worth seeing; the loop is the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two halves leave a hairline.&lt;/strong&gt; Pixelbolt caps a frame at 256x256, so the 416x162 header is two 208x162 halves side by side. At fractional widths (831 and 563 px) Chrome and Firefox drew a 1 px seam between them. Making the left background one column wider (209 px), so the right half covers an identical column, removed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text belongs in the SVG, not the pixels.&lt;/strong&gt; Labels are SVG &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt; drawn over the art, so they stay sharp at any size. Screen readers read the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;'s &lt;code&gt;alt&lt;/code&gt;, so describe the scene there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The steam is a simulation
&lt;/h2&gt;

&lt;p&gt;Pixelbolt 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.&lt;/p&gt;

&lt;p&gt;The 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pixelforge-fluid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"effects"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"steam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"frames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"loop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"buoyancy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vorticity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dissipation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"drag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"emitters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billow-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"radius"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"density"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"temperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"velocity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;-0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-1.4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vortex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"look"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"density"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"colors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thresholds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.34&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dither"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shadow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"colors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thresholds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dither"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"colors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"G"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thresholds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dither"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(&lt;code&gt;pixelforge-fluid&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;The agents didn't get there first time. Three things they found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At one cell per pixel, gas is too damped to roll a vortex ring by itself.&lt;/strong&gt; A heavy blob, a starting jet and a burst vortex pair all stayed small and slow, across 14 parameter sweeps (the first is below: one setting per row, sampled over time). What worked was a velocity-only emitter riding a quarter turn of an orbit, so the push eases out as the ink sinks, with two spinning emitters on either side to spread the ring. The curls themselves are the simulation's. Unequal spins keep it lopsided: a symmetric ring reads as a face.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too much swirl makes eyes.&lt;/strong&gt; At vortex 3 and above, the flask's liquid hollowed into round voids that looked like eyes. Vortex 2.2 gives a clean swirl and slosh up one wall.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rising column drags thin smoke down beside it.&lt;/strong&gt; A gentle updraft, &lt;code&gt;"wind": [0, -0.08]&lt;/code&gt;, removed the downward tails without changing the look.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva9mjgeuer4rd16yomq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fva9mjgeuer4rd16yomq8.png" alt="Five rows of a heavy blob of ink sinking in water, each row a different weight and swirl setting, sampled at three moments: none rolls up into a clean ring" width="608" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One command rebuilds every pixel
&lt;/h2&gt;

&lt;p&gt;The recipes live in the profile repo's &lt;a href="https://github.com/skulitom/skulitom/tree/main/art" rel="noopener noreferrer"&gt;&lt;code&gt;art/&lt;/code&gt;&lt;/a&gt; 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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash art/build.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fetches Pixelbolt at the pinned commit (&lt;code&gt;8c5f662&lt;/code&gt;), 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 &lt;code&gt;math&lt;/code&gt; module, so another platform's maths library could in principle move a pixel.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The site re-simulates it in your browser
&lt;/h2&gt;

&lt;p&gt;The portfolio at &lt;a href="https://skulitom.github.io" rel="noopener noreferrer"&gt;skulitom.github.io&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehgj6nl6t6hn3u7yun9f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fehgj6nl6t6hn3u7yun9f.gif" alt="The workbench on the site: " width="520" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft97hem98n1pn9e7g4mo6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft97hem98n1pn9e7g4mo6.gif" alt="Pixel-art card: molten metal pours from a crucible into a star-shaped mould, cools along the palette and pops out as a finished sprite" width="576" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Pixelbolt has no npm dependencies. From a clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node bin/pixelbolt.js compile examples/steam-vent.fluid.json &lt;span class="nt"&gt;--out&lt;/span&gt; output/steam.json
node bin/pixelbolt.js gif output/steam.json &lt;span class="nt"&gt;--scale&lt;/span&gt; 4 &lt;span class="nt"&gt;--out&lt;/span&gt; output/steam.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 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.&lt;/p&gt;

&lt;p&gt;To see how the wrapper is built, read &lt;code&gt;art/tools/wrap.py&lt;/code&gt; in the profile repo: it turns a Pixelbolt sheet and atlas into a self-contained animated SVG and its still twin.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Profile: &lt;a href="https://github.com/skulitom" rel="noopener noreferrer"&gt;github.com/skulitom&lt;/a&gt;, recipes in &lt;a href="https://github.com/skulitom/skulitom/tree/main/art" rel="noopener noreferrer"&gt;&lt;code&gt;art/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live re-simulation: &lt;a href="https://skulitom.github.io" rel="noopener noreferrer"&gt;skulitom.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pixelbolt: &lt;a href="https://github.com/skulitom/Pixelbolt" rel="noopener noreferrer"&gt;github.com/skulitom/Pixelbolt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>abotwrotethis</category>
      <category>github</category>
      <category>pixelart</category>
      <category>svg</category>
    </item>
  </channel>
</rss>
