diff --git a/public/css/style.css b/public/css/style.css index 26be891..b4b0323 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1998,12 +1998,6 @@ input:checked+.slider:before { display: flex !important; flex-direction: column; border-color: var(--accent-indigo); - animation: globeExpandIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} - -/* Collapse animation (plays while .expanded is still active to keep fixed positioning) */ -.globe-card.expanded.globe-collapsing { - animation: globeCollapseOut 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; } /* Ensure children are visible */ @@ -2011,28 +2005,6 @@ input:checked+.slider:before { opacity: 1 !important; } -@keyframes globeExpandIn { - from { - opacity: 0; - transform: scale(0.82); - } - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes globeCollapseOut { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.82); - } -} - .globe-card.expanded .globe-body { height: calc(90vh - 120px) !important; /* Explicit calc height for ECharts reliability */ width: 100% !important; diff --git a/public/js/app.js b/public/js/app.js index 14cd531..bc218b5 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -207,25 +207,85 @@ dom.btnChangePassword.addEventListener('click', saveChangePassword); } - // Globe expansion + // Globe expansion (FLIP animation) + let savedGlobeRect = null; + function expandGlobe() { if (dom.globeCard.classList.contains('expanded')) return; + + // FLIP Step 1 — First: save original rect + savedGlobeRect = dom.globeCard.getBoundingClientRect(); + + // FLIP Step 2 — Last: apply expanded state dom.globeCard.classList.add('expanded'); dom.btnExpandGlobe.classList.add('active'); - dom.globeCard.addEventListener('animationend', function onExpand() { - dom.globeCard.removeEventListener('animationend', onExpand); - if (myMap2D) myMap2D.resize(); + + // Resize ECharts IMMEDIATELY so the map renders at full size + // This prevents the "flash" — content is correctly sized throughout + if (myMap2D) myMap2D.resize(); + + // FLIP Step 3 — Invert: calculate and apply inverse transform + const endRect = dom.globeCard.getBoundingClientRect(); + const scaleX = savedGlobeRect.width / endRect.width; + const scaleY = savedGlobeRect.height / endRect.height; + const dx = (savedGlobeRect.left + savedGlobeRect.width / 2) - (endRect.left + endRect.width / 2); + const dy = (savedGlobeRect.top + savedGlobeRect.height / 2) - (endRect.top + endRect.height / 2); + + dom.globeCard.style.willChange = 'transform, box-shadow'; + dom.globeCard.style.transition = 'none'; + dom.globeCard.style.transform = `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`; + dom.globeCard.style.boxShadow = '0 0 0 0 transparent, 0 0 0 0 transparent'; + + // Force reflow to commit the inverse state + dom.globeCard.offsetHeight; + + // FLIP Step 4 — Play: animate to final state + dom.globeCard.style.transition = 'transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s ease'; + dom.globeCard.style.transform = ''; + dom.globeCard.style.boxShadow = ''; + + dom.globeCard.addEventListener('transitionend', function onEnd(e) { + if (e.propertyName !== 'transform') return; + dom.globeCard.removeEventListener('transitionend', onEnd); + dom.globeCard.style.transition = ''; + dom.globeCard.style.willChange = ''; }); } function collapseGlobe() { if (!dom.globeCard.classList.contains('expanded')) return; if (dom.globeCard.classList.contains('globe-collapsing')) return; + + if (!savedGlobeRect) { + dom.globeCard.classList.remove('expanded'); + dom.btnExpandGlobe.classList.remove('active'); + if (myMap2D) requestAnimationFrame(() => myMap2D.resize()); + return; + } + dom.globeCard.classList.add('globe-collapsing'); - dom.globeCard.addEventListener('animationend', function onCollapse() { - dom.globeCard.removeEventListener('animationend', onCollapse); + + // Calculate transform from expanded back to original position + const expandedRect = dom.globeCard.getBoundingClientRect(); + const scaleX = savedGlobeRect.width / expandedRect.width; + const scaleY = savedGlobeRect.height / expandedRect.height; + const dx = (savedGlobeRect.left + savedGlobeRect.width / 2) - (expandedRect.left + expandedRect.width / 2); + const dy = (savedGlobeRect.top + savedGlobeRect.height / 2) - (expandedRect.top + expandedRect.height / 2); + + dom.globeCard.style.willChange = 'transform, box-shadow'; + dom.globeCard.style.transition = 'transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s ease'; + dom.globeCard.style.transform = `translate(${dx}px, ${dy}px) scale(${scaleX}, ${scaleY})`; + dom.globeCard.style.boxShadow = '0 0 0 0 transparent, 0 0 0 0 transparent'; + + dom.globeCard.addEventListener('transitionend', function onEnd(e) { + if (e.propertyName !== 'transform') return; + dom.globeCard.removeEventListener('transitionend', onEnd); dom.globeCard.classList.remove('expanded', 'globe-collapsing'); dom.btnExpandGlobe.classList.remove('active'); + dom.globeCard.style.transition = ''; + dom.globeCard.style.transform = ''; + dom.globeCard.style.boxShadow = ''; + dom.globeCard.style.willChange = ''; if (myMap2D) requestAnimationFrame(() => myMap2D.resize()); }); }