--- /dev/null
+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';
+ });
+});