animate game.html: card draw, modals, entry reveals, button press, toast transitions
This commit is contained in:
@@ -739,6 +739,73 @@
|
||||
.entry-actions { justify-content: flex-start; }
|
||||
}
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
/* ── Animation ──────────────────────────────────────────────────── */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
|
||||
/* Alpine x-transition utility classes (used by toast + modals) */
|
||||
.transition { transition-property: opacity, transform; }
|
||||
.ease-out { transition-timing-function: cubic-bezier(0.22,1,.36,1); }
|
||||
.ease-in { transition-timing-function: cubic-bezier(0.4,0,1,1); }
|
||||
.duration-150 { transition-duration: 150ms; }
|
||||
.duration-200 { transition-duration: 200ms; }
|
||||
.duration-250 { transition-duration: 250ms; }
|
||||
.duration-300 { transition-duration: 300ms; }
|
||||
.opacity-0 { opacity: 0; }
|
||||
.opacity-100 { opacity: 1; }
|
||||
.translate-y-2 { transform: translateY(8px); }
|
||||
.translate-y-0 { transform: translateY(0); }
|
||||
|
||||
/* Card draw — comes up from below with a slight tilt */
|
||||
.card-frame {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1) rotate(0deg);
|
||||
transition: opacity 0.45s cubic-bezier(0.22,1,.36,1),
|
||||
transform 0.45s cubic-bezier(0.22,1,.36,1);
|
||||
}
|
||||
@starting-style {
|
||||
.card-frame {
|
||||
opacity: 0;
|
||||
transform: translateY(42px) scale(0.92) rotate(-1.5deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal backdrop fade */
|
||||
.modal-overlay {
|
||||
transition: opacity 0.22s cubic-bezier(0.22,1,.36,1);
|
||||
opacity: 1;
|
||||
}
|
||||
@starting-style { .modal-overlay { opacity: 0; } }
|
||||
|
||||
/* Modal inner panel slides up */
|
||||
.modal {
|
||||
transition: opacity 0.3s cubic-bezier(0.22,1,.36,1),
|
||||
transform 0.3s cubic-bezier(0.22,1,.36,1);
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
@starting-style {
|
||||
.modal {
|
||||
opacity: 0;
|
||||
transform: translateY(18px) scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
/* Chronicle entry scroll reveals */
|
||||
.entry[data-reveal] {
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
transition: opacity 0.5s cubic-bezier(0.22,1,.36,1),
|
||||
transform 0.5s cubic-bezier(0.22,1,.36,1);
|
||||
}
|
||||
.entry[data-reveal].revealed { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* Button press */
|
||||
.btn:active {
|
||||
transform: translateY(1px) scale(0.98);
|
||||
transition-duration: 0.06s !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
@@ -1147,9 +1214,11 @@
|
||||
|
||||
<!-- Live update toast -->
|
||||
<div x-show="liveUpdate"
|
||||
x-transition:enter="transition ease-out duration-200"
|
||||
x-transition:enter-start="opacity-0 transform translate-y-2"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:enter="transition ease-out duration-250"
|
||||
x-transition:enter-start="opacity-0 translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
style="position:fixed;bottom:1.5rem;right:1.5rem;z-index:400;background:var(--panel);border:1px solid rgba(112,121,78,.5);color:var(--moss);font:700 .62rem/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;padding:.55rem 1rem;pointer-events:none">
|
||||
Chronicle updated
|
||||
@@ -1176,7 +1245,13 @@
|
||||
</footer>
|
||||
|
||||
<!-- Modal — all sections use x-show (not x-if) so Alpine bindings are always live -->
|
||||
<div class="modal-overlay" x-show="showModal" @click="closeModal()" @keydown.escape.window="closeModal()">
|
||||
<div class="modal-overlay" x-show="showModal" @click="closeModal()" @keydown.escape.window="closeModal()"
|
||||
x-transition:enter="transition ease-out duration-250"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0">
|
||||
<div class="modal" @click.stop>
|
||||
<div class="modal-title" x-text="modalTitle()"></div>
|
||||
|
||||
@@ -2426,5 +2501,37 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
// Scroll reveals for chronicle entries.
|
||||
// Uses MutationObserver so it works with Alpine's dynamic rendering.
|
||||
const revealObs = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
const delay = +(e.target.dataset.revealDelay || 0)
|
||||
setTimeout(() => e.target.classList.add('revealed'), delay)
|
||||
revealObs.unobserve(e.target)
|
||||
}
|
||||
})
|
||||
}, { threshold: 0.04, rootMargin: '0px 0px -24px 0px' })
|
||||
|
||||
const observe = (el) => {
|
||||
if (!el.classList?.contains('entry') || el.hasAttribute('data-reveal')) return
|
||||
const siblings = Array.from(el.parentElement?.querySelectorAll('.entry') || [])
|
||||
el.dataset.revealDelay = Math.min(siblings.indexOf(el) * 45, 200)
|
||||
el.setAttribute('data-reveal', '')
|
||||
revealObs.observe(el)
|
||||
}
|
||||
|
||||
// Watch for entries Alpine adds dynamically
|
||||
new MutationObserver((muts) => {
|
||||
muts.forEach(m => m.addedNodes.forEach(node => {
|
||||
if (node.nodeType !== 1) return
|
||||
if (node.classList?.contains('entry')) observe(node)
|
||||
node.querySelectorAll?.('.entry').forEach(observe)
|
||||
}))
|
||||
}).observe(document.body, { childList: true, subtree: true })
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user