summaryrefslogtreecommitdiffstats
path: root/assets/js/tag-cloud-spiral.js
blob: bed464523fb4b264c03a22d5ac2557609f132bd0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
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 and remove flex layout
    container.style.position = 'relative';
    container.style.display = 'block';
    container.classList.remove('flex', 'flex-wrap');

    var padding = -2; // px gap between tags (negative allows ~2px edge overlap)
    var aStep = 0.2; // radians per spiral step
    var rScale = (containerWidth * 0.013); // spiral tightness

    var minTop = Infinity, maxBottom = -Infinity;

    links.forEach(function (link, i) {
      var w = sizes[i].w;
      var h = sizes[i].h;
      var seed = hashAngle(link.href);
      var theta = seed;
      var placed_rect;

      // Step along spiral until no collision
      for (var attempt = 0; attempt < 3000; 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 (32px)
    container.style.height = (maxBottom - minTop + 48) + 'px';
  });
});