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; }
|
.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>
|
||||||
|
|||||||
Reference in New Issue
Block a user