]> danix's work - danix.xyz-2.git/commitdiff
feat: add Archimedean spiral layout engine for tag cloud
authorDanilo M. <redacted>
Tue, 21 Apr 2026 21:38:44 +0000 (23:38 +0200)
committerDanilo M. <redacted>
Tue, 21 Apr 2026 21:38:44 +0000 (23:38 +0200)
themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js [new file with mode: 0644]

diff --git a/themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js b/themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js
new file mode 100644 (file)
index 0000000..6965565
--- /dev/null
@@ -0,0 +1,121 @@
+document.addEventListener('DOMContentLoaded', function () {
+  var containers = document.querySelectorAll('[data-tag-cloud]');
+  if (!containers.length) return;
+
+  Array.prototype.forEach.call(containers, function (container) {
+    if (container.offsetWidth < 400) return;
+
+    var links = Array.prototype.slice.call(
+      container.querySelectorAll('.tag-cloud-link')
+    );
+    if (!links.length) return;
+
+    // Sort descending by weight (biggest first = placed near center)
+    links.sort(function (a, b) {
+      return parseFloat(b.dataset.weight) - parseFloat(a.dataset.weight);
+    });
+
+    // String hash → deterministic angle seed (0..2π)
+    function hashAngle(str) {
+      var h = 0;
+      for (var i = 0; i < str.length; i++) {
+        h = (h * 31 + str.charCodeAt(i)) & 0xffffffff;
+      }
+      return ((h >>> 0) / 0xffffffff) * 2 * Math.PI;
+    }
+
+    // AABB collision check
+    function overlaps(a, b) {
+      return !(
+        a.right  < b.left  ||
+        a.left   > b.right ||
+        a.bottom < b.top   ||
+        a.top    > b.bottom
+      );
+    }
+
+    var placed = [];
+    var containerWidth = container.offsetWidth;
+    var cx = containerWidth / 2;
+
+    // Measure each tag before repositioning
+    var sizes = links.map(function (link) {
+      var rect = link.getBoundingClientRect();
+      return { w: rect.width, h: rect.height };
+    });
+
+    // Switch container to relative positioning
+    container.style.position = 'relative';
+    container.style.display = 'block';
+
+    var padding = 8; // px gap between tags
+    var aStep = 0.3; // radians per spiral step
+    var rScale = (containerWidth * 0.018); // spiral tightness
+
+    var minTop = 0, maxBottom = 0;
+
+    links.forEach(function (link, i) {
+      var w = sizes[i].w;
+      var h = sizes[i].h;
+      var seed = hashAngle(link.textContent.trim());
+      var theta = seed;
+      var placed_rect;
+
+      // Step along spiral until no collision
+      for (var attempt = 0; attempt < 2000; attempt++) {
+        var r = rScale * theta;
+        var x = cx + r * Math.cos(theta) - w / 2;
+        var y = r * Math.sin(theta) - h / 2;
+
+        var candidate = { left: x, top: y, right: x + w, bottom: y + h };
+        var collision = false;
+
+        for (var j = 0; j < placed.length; j++) {
+          var p = placed[j];
+          var padded = {
+            left: p.left - padding,
+            top: p.top - padding,
+            right: p.right + padding,
+            bottom: p.bottom + padding
+          };
+          if (overlaps(candidate, padded)) {
+            collision = true;
+            break;
+          }
+        }
+
+        if (!collision) {
+          placed_rect = candidate;
+          break;
+        }
+        theta += aStep;
+      }
+
+      if (!placed_rect) {
+        // Fallback: just append to flow if spiral exhausted
+        link.style.position = 'static';
+        return;
+      }
+
+      placed.push(placed_rect);
+
+      link.style.position = 'absolute';
+      link.style.left = Math.round(placed_rect.left) + 'px';
+      link.style.top = Math.round(placed_rect.top) + 'px';
+
+      if (placed_rect.top < minTop) minTop = placed_rect.top;
+      if (placed_rect.bottom > maxBottom) maxBottom = placed_rect.bottom;
+    });
+
+    // Normalize: shift all tags so topmost is at y=16px
+    var offset = 16 - minTop;
+    links.forEach(function (link) {
+      if (link.style.position === 'absolute') {
+        link.style.top = (parseInt(link.style.top) + offset) + 'px';
+      }
+    });
+
+    // Set container height to fit all tags + 2rem bottom padding
+    container.style.height = (maxBottom - minTop + 48) + 'px';
+  });
+});