From: Danilo M. Date: Tue, 21 Apr 2026 21:38:44 +0000 (+0200) Subject: feat: add Archimedean spiral layout engine for tag cloud X-Git-Tag: release_22042026-1342~24 X-Git-Url: https://git.danix.xyz/?a=commitdiff_plain;h=740506603dc9be03c3337dfa469d79e27c45fbbf;p=danix.xyz-2.git feat: add Archimedean spiral layout engine for tag cloud --- 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 index 0000000..6965565 --- /dev/null +++ b/themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js @@ -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'; + }); +});