animate game.html: card draw, modals, entry reveals, button press, toast transitions

This commit is contained in:
2026-08-08 20:57:09 +02:00
parent 7071858390
commit 0ec748f67f
+111 -4
View File
@@ -739,6 +739,73 @@
.entry-actions { justify-content: flex-start; } .entry-actions { justify-content: flex-start; }
} }
[x-cloak] { display: none !important; } [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> </style>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -1147,9 +1214,11 @@
<!-- Live update toast --> <!-- Live update toast -->
<div x-show="liveUpdate" <div x-show="liveUpdate"
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-250"
x-transition:enter-start="opacity-0 transform translate-y-2" x-transition:enter-start="opacity-0 translate-y-2"
x-transition:leave="transition ease-in duration-300" 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" 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"> 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 Chronicle updated
@@ -1176,7 +1245,13 @@
</footer> </footer>
<!-- Modal — all sections use x-show (not x-if) so Alpine bindings are always live --> <!-- 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" @click.stop>
<div class="modal-title" x-text="modalTitle()"></div> <div class="modal-title" x-text="modalTitle()"></div>
@@ -2426,5 +2501,37 @@
} }
} }
</script> </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> </body>
</html> </html>