[object Object]

← back to Particle Text

Starling: save motion graphic as animated GIF (dependency-free GIF89a: direct-render capture, median-cut palette, LZW) + copy source code

76daa36ba0f981ac216772f1ce3752b6b97b2557 · 2026-07-25 09:14:33 -0700 · Steve

Files touched

Diff

commit 76daa36ba0f981ac216772f1ce3752b6b97b2557
Author: Steve <steve@designerwallcoverings.com>
Date:   Sat Jul 25 09:14:33 2026 -0700

    Starling: save motion graphic as animated GIF (dependency-free GIF89a: direct-render capture, median-cut palette, LZW) + copy source code
---
 index.html | 53 +++++++++++++++++++++++++++++++++++++----------------
 1 file changed, 37 insertions(+), 16 deletions(-)

diff --git a/index.html b/index.html
index e028b39..9752e14 100644
--- a/index.html
+++ b/index.html
@@ -469,23 +469,22 @@
     function put(code, size) { cur |= code << bits; bits += size; while (bits >= 8) { out.push(cur & 255); cur = cur >>> 8; bits -= 8; } }
     var clear = 1 << minCode, eoi = clear + 1;
     var size, next, dict;
-    function reset() { size = minCode + 1; next = eoi + 1; dict = new Map(); }
-    reset();
+    function init() { size = minCode + 1; next = eoi + 1; dict = new Map(); }
+    init();
     put(clear, size);
     if (idx.length) {
-      var prev = idx[0];
+      var key = idx[0];                          // running code (starts as the first literal)
       for (var i = 1; i < idx.length; i++) {
-        var c = idx[i], key = (prev << 8) | c;
-        if (dict.has(key)) { prev = dict.get(key); }
+        var k = idx[i], ck = (key << 8) | k;
+        if (dict.has(ck)) { key = dict.get(ck); }
         else {
-          put(prev, size);
-          dict.set(key, next++);
-          if (next === (1 << size) && size < 12) size++;
-          if (next > 4095) { put(clear, size); reset(); }
-          prev = c;
+          put(key, size);
+          if (next === 4096) { put(clear, size); init(); }   // dict full → reset
+          else { if (next >= (1 << size)) size++; dict.set(ck, next++); } // grow size BEFORE assigning
+          key = k;
         }
       }
-      put(prev, size);
+      put(key, size);
     }
     put(eoi, size);
     if (bits > 0) out.push(cur & 255);
@@ -522,11 +521,24 @@
     gw -= gw % 2; gh -= gh % 2;
     var off = document.createElement("canvas"); off.width = gw; off.height = gh;
     var octx = off.getContext("2d");
-    var frames = [], TOTAL = 40, DELAY = 6; // ~16fps, ~2.4s
+    var sx = gw / W, sy = gh / H;                 // map particle coords into the GIF frame
+    var dotR = Math.max(1, Math.min(gw, gh) / 230); // crisp, visible dot at GIF scale
+    var frames = [], TOTAL = 40, DELAY = 6;       // ~16fps, ~2.4s
+    octx.fillStyle = "#05060a"; octx.fillRect(0, 0, gw, gh);
     scatterAll();
     var f = 0;
     var iv = setInterval(function () {
-      octx.drawImage(canvas, 0, 0, gw, gh);
+      // render particles straight at GIF resolution (preserves brightness + additive glow)
+      octx.globalCompositeOperation = "source-over";
+      octx.fillStyle = "rgba(5,6,10,0.34)"; octx.fillRect(0, 0, gw, gh); // motion trail
+      octx.globalCompositeOperation = "lighter";
+      for (var pi = 0; pi < particles.length; pi++) {
+        var pp = particles[pi];
+        octx.fillStyle = pp.color;
+        octx.beginPath();
+        octx.arc(pp.x * sx, pp.y * sy, dotR, 0, 6.2832);
+        octx.fill();
+      }
       frames.push(octx.getImageData(0, 0, gw, gh).data.slice());
       if (++f >= TOTAL) { clearInterval(iv); encode(); }
       else toast("Recording GIF… " + Math.round(f / TOTAL * 100) + "%", 900);
@@ -535,11 +547,20 @@
     function encode() {
       toast("Encoding GIF…", 6000);
       setTimeout(function () {
-        var cols = [];
-        for (var fi = 0; fi < frames.length; fi += 3) {
+        // Sample for the palette: keep EVERY bright particle pixel (they're sparse but are the
+        // whole point), and only a thin slice of the dark background — otherwise median-cut spends
+        // all 256 slots on near-black and quantizes the glow away.
+        var cols = [], dc = 0;
+        for (var fi = 0; fi < frames.length; fi += 2) {
           var d = frames[fi];
-          for (var i = 0; i < d.length; i += 28) cols.push((d[i] << 16) | (d[i + 1] << 8) | d[i + 2]);
+          for (var i = 0; i < d.length; i += 4) {
+            var r = d[i], g = d[i + 1], b = d[i + 2];
+            var bright = r > g ? r : g; if (b > bright) bright = b;
+            if (bright > 40) cols.push((r << 16) | (g << 8) | b);
+            else if ((dc++ & 15) === 0) cols.push((r << 16) | (g << 8) | b);
+          }
         }
+        if (cols.length < 512) for (var i = 0; i < 512; i++) cols.push(0x05060a);
         var pal = buildPalette(cols);
         var cache = new Int16Array(262144); for (var i = 0; i < cache.length; i++) cache[i] = -1;
         var gif = GifWriter(gw, gh, pal);

← 7a75395 auto-save: 2026-07-25T09:05:11 (1 files) — index.html  ·  back to Particle Text  ·  5x sweep 1: add inline favicon (kills /favicon.ico 404 conso 34f7be7 →