(function () { 'use strict'; document.addEventListener('DOMContentLoaded', function () { var container = document.getElementById('keyword-container'); var tags = window.thalimTags; if (!container || !tags || !tags.length) return; // — init — var GAP = 14; var COLORS = ['#e0775d', '#7cc0c6', '#e05680', '#46ae51', '#bb8dd9']; var colorTimeouts = new Map(); // Mélange aléatoire pour varier la disposition à chaque chargement var shuffled = tags.slice().sort(function () { return Math.random() - 0.5; }); // Crée les éléments une seule fois (réutilisés à chaque layout) var items = shuffled.map(function (tag) { var el = document.createElement('a'); el.className = 'keyword'; el.href = tag.url; el.textContent = tag.name; el.addEventListener('mouseenter', function () { if (colorTimeouts.has(el)) clearTimeout(colorTimeouts.get(el)); el.style.color = COLORS[Math.floor(Math.random() * COLORS.length)]; colorTimeouts.set(el, setTimeout(function () { el.style.color = ''; colorTimeouts.delete(el); }, 2000)); }); container.appendChild(el); return { el: el, w: 0, h: 0 }; }); var gradOverlay = null; var lastLayoutWidth = 0; function layoutCloud() { var cw = container.offsetWidth; if (cw === lastLayoutWidth) return; lastLayoutWidth = cw; var cx = cw / 2; var isMobile = cw < 768; var a = cx - GAP; // demi-grand axe horizontal var b = Math.round(cw * (isMobile ? 0.45 : 0.20)); // demi-petit axe vertical (plus haut sur mobile) var R_V = Math.round(b * (isMobile ? 0.45 : 0.70)); // demi-axe vertical de la zone d'exclusion (plus petit sur mobile) var R_H = Math.round(b * (isMobile ? 0.70 : 1.15)); // demi-axe horizontal (plus petit sur mobile) // Re-mesurer les éléments (la taille peut changer avec le viewport) // +1 compense les arrondis sub-pixel sur écrans haute densité items.forEach(function (item) { var rect = item.el.getBoundingClientRect(); item.w = Math.ceil(rect.width) + 1; item.h = Math.ceil(rect.height) + 1; }); var placed = []; function hasOverlap(x, y, w, h) { for (var i = 0; i < placed.length; i++) { var p = placed[i]; if (x < p.x + p.w + GAP && x + w + GAP > p.x && y < p.y + p.h + GAP && y + h + GAP > p.y) return true; } return false; } // Placement contraint à l'anneau elliptique : // { intérieur ellipse (a, b) } ∩ { extérieur ellipse d'exclusion (R_H, R_V) } // Les candidats sont triés par distance à l'ellipse d'exclusion (croissante) : // les mots s'accumulent au plus près du centre avant de s'étendre. function findPos(w, h) { var candidates = []; for (var x = 0; x <= cw - w; x += 8) { var dx = (x + w / 2) - cx; var ratio = dx / a; if (Math.abs(ratio) >= 1) continue; // Limite verticale du centre du mot imposée par l'ellipse externe (avec marge h/2) var maxPcy = b * Math.sqrt(1 - ratio * ratio); if (maxPcy < h / 2) continue; var maxAbsY = maxPcy - h / 2; // Limite verticale minimale imposée par l'ellipse d'exclusion (R_H, R_V) var minAbsY = (Math.abs(dx) >= R_H) ? 0 : R_V * Math.sqrt(Math.max(0, 1 - (dx * dx) / (R_H * R_H))); if (minAbsY > maxAbsY) continue; for (var absY = minAbsY; absY <= maxAbsY; absY += 4) { // Distance normalisée à l'ellipse d'exclusion (0 = sur le bord) var nx = dx / R_H, ny = absY / R_V; var dist = Math.sqrt(nx * nx + ny * ny) - 1; var yB = Math.round(absY - h / 2); if (absY > 0) { candidates.push({ x: x, y: Math.round(-absY - h / 2), dist: dist }); } candidates.push({ x: x, y: yB, dist: dist }); } } // Trier par distance à l'ellipse d'exclusion croissante → attraction vers le centre candidates.sort(function (ca, cb) { return ca.dist - cb.dist; }); for (var i = 0; i < candidates.length; i++) { var c = candidates[i]; if (!hasOverlap(c.x, c.y, w, h)) return { x: c.x, y: c.y }; } return null; // aucune position dans l'anneau } // Place les mots dans l'anneau elliptique, collecte les débordements var overflow = []; items.forEach(function (item) { var pos = findPos(item.w, item.h); if (pos) { item.pos = { x: pos.x, y: pos.y, w: item.w, h: item.h }; placed.push(item.pos); item.el.style.left = pos.x + 'px'; item.el.style.top = pos.y + 'px'; } else { overflow.push(item); } }); // Placement des débordements en lignes centrées (style flex-wrap center) if (overflow.length) { var startY = placed.reduce(function (m, p) { return Math.max(m, p.y + p.h); }, 0) + GAP; var rows = []; var currentRow = []; var currentRowW = 0; overflow.forEach(function (item) { var needed = currentRowW > 0 ? item.w + GAP : item.w; if (currentRowW + needed > cw && currentRow.length > 0) { rows.push(currentRow); currentRow = []; currentRowW = 0; } currentRow.push(item); currentRowW += (currentRowW > 0 ? GAP : 0) + item.w; }); if (currentRow.length) rows.push(currentRow); var curY = startY; rows.forEach(function (row) { var rowW = row.reduce(function (s, item) { return s + item.w; }, 0) + (row.length - 1) * GAP; var rowH = row.reduce(function (m, item) { return Math.max(m, item.h); }, 0); var offsetX = Math.round((cw - rowW) / 2); row.forEach(function (item) { item.pos = { x: offsetX, y: curY, w: item.w, h: item.h }; placed.push(item.pos); item.el.style.left = offsetX + 'px'; item.el.style.top = curY + 'px'; offsetX += item.w + GAP; }); curY += rowH + GAP; }); } // Normalisation Y : cy=0 → shift px depuis le haut du conteneur var minY = items.reduce(function (m, item) { return Math.min(m, item.pos.y); }, Infinity); var shift = Math.max(0, GAP - minY); items.forEach(function (item) { item.pos.y += shift; item.el.style.top = item.pos.y + 'px'; }); // Hauteur basée sur le contenu réel (évite le débordement sur mobile) var maxPlacedY = items.reduce(function (m, item) { return Math.max(m, item.pos.y + item.pos.h); }, 0); container.style.height = (maxPlacedY + GAP) + 'px'; // Dégradé sur un overlay séparé pour permettre l'animation scale au survol if (gradOverlay) { container.removeChild(gradOverlay); } gradOverlay = document.createElement('div'); gradOverlay.style.cssText = 'position:absolute;inset:0;pointer-events:none;' + 'background:radial-gradient(circle ' + Math.round(R_V * 2) + 'px at ' + cx + 'px ' + shift + 'px,' + 'rgba(247,255,41,1) 0%,rgba(247,255,41,0.6) 16%,rgba(247,255,41,0.15) 55%,transparent 70%);' + 'transform-origin:' + cx + 'px ' + shift + 'px;' + 'transition:transform 0.4s ease;'; container.insertBefore(gradOverlay, container.firstChild); } // Premier layout layoutCloud(); // Événements du dégradé (référencent gradOverlay via closure) container.addEventListener('mouseenter', function () { if (!gradOverlay) return; gradOverlay.style.transition = 'transform 0.4s ease'; gradOverlay.style.transform = 'scale(0.82)'; }); container.addEventListener('mousemove', function (e) { if (!gradOverlay) return; var cw = container.offsetWidth; var cx = cw / 2; var rect = container.getBoundingClientRect(); var dx = (e.clientX - rect.left) - cx; var dy = (e.clientY - rect.top) - parseFloat(gradOverlay.style.transformOrigin.split(' ')[1]); var tx = (-dx * 0.09).toFixed(2); var ty = (-dy * 0.09).toFixed(2); gradOverlay.style.transition = 'transform 0.15s ease-out'; gradOverlay.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(0.82)'; }); container.addEventListener('mouseleave', function () { if (!gradOverlay) return; gradOverlay.style.transition = 'transform 0.9s cubic-bezier(0.25, 0.46, 0.45, 0.94)'; gradOverlay.style.transform = 'scale(1)'; }); // Resize avec debounce var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { lastLayoutWidth = 0; // forcer le recalcul layoutCloud(); }, 250); }); // Animation d'apparition au scroll var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { items.forEach(function (item, i) { item.el.style.animationDelay = (i * 0.03) + 's'; item.el.classList.add('keyword--visible'); }); observer.unobserve(container); } }); }, { threshold: 0.05 }); observer.observe(container); }); })();