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'; }); });