feat(fx): extract clock + reveal + stagger from v7-5 (line 1567-1624)

3 SSR-safe utilities in one module:
- fxClock() — live Thai Buddhist calendar (id=fx-time, id=fx-date)
- fxReveal() — add .revealed to .fx-reveal on scroll (with 2 failsafes)
- fxStagger() — add .staggered to .fx-stagger on scroll (with 2 failsafes)
- fxInit() — convenience to run all 3

Typed with HTMLElement generics. Uses standard IntersectionObserver.
Namespaced .fx-* classes — no collision with legacy src/lib/animations.ts
(which uses .reveal / counterUp for bento components).

Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 1.3
This commit is contained in:
Kunthawat Greethong
2026-06-13 17:45:58 +07:00
parent 9eea9cbe6d
commit 582998a340

148
src/lib/fx-animations.ts Normal file
View File

@@ -0,0 +1,148 @@
/**
* MOREMINIMORE FX-ANIMATIONS v1
* Extracted from Desktop/moreminomore-mockup-v7-5.html bottom script (lines 1567-1624)
*
* 3 utilities — all SSR-safe (no-op on server):
* 1. fxClock() — live Thai clock + Buddhist date for utility bar
* 2. fxReveal() — add .revealed to .fx-reveal elements on scroll
* 3. fxStagger() — add .staggered to .fx-stagger elements on scroll
*
* Note: these are SEPARATE from src/lib/animations.ts (which uses .reveal class).
* v7-5 design system uses .fx-reveal / .fx-stagger to avoid collision with the
* legacy bento animations.
*
* Usage in Base.astro or any .astro file:
* <script>
* import { fxInit } from '../lib/fx-animations';
* if (document.readyState === 'loading') {
* document.addEventListener('DOMContentLoaded', fxInit);
* } else {
* fxInit();
* }
* </script>
*/
/* ------------------------------------------------------------------ */
/* 1. LIVE CLOCK (utility bar) */
/* ------------------------------------------------------------------ */
/** Days in Thai (Buddhist calendar: Sun=0, Sat=6) */
const THAI_DAYS = ['อา.', 'จ.', 'อ.', 'พ.', 'พฤ.', 'ศ.', 'ส.'];
/** Months in Thai */
const THAI_MONTHS = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];
/**
* Start the live clock. Looks for elements with id="fx-time" and id="fx-date".
* Sets text every second.
*/
export function fxClock(): void {
if (typeof window === 'undefined') return;
function update(): void {
const now = new Date();
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
const time = '⏱ ' + hh + ':' + mm + ':' + ss;
const date = '📅 ' + THAI_DAYS[now.getDay()] + ' ' + now.getDate() + ' ' + THAI_MONTHS[now.getMonth()] + ' ' + (now.getFullYear() + 543);
const t = document.getElementById('fx-time');
const d = document.getElementById('fx-date');
if (t) t.textContent = time;
if (d) d.textContent = date;
}
update();
setInterval(update, 1000);
}
/* ------------------------------------------------------------------ */
/* 2. REVEAL-ON-SCROLL (.fx-reveal) */
/* ------------------------------------------------------------------ */
/**
* Add .revealed to .fx-reveal elements as they enter viewport.
* Includes 2 failsafes:
* - Force-reveal all after 1.5s (in case observer never fires)
* - Force-reveal all after 100ms (in case section is already in view)
*/
export function fxReveal(): void {
if (typeof window === 'undefined') return;
const reveals = document.querySelectorAll<HTMLElement>('.fx-reveal');
if (reveals.length === 0) return;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.05, rootMargin: '0px 0px -40px 0px' }
);
reveals.forEach((r) => observer.observe(r));
}
// Failsafe 1: force-reveal all after 1.5s
setTimeout(() => {
reveals.forEach((r) => r.classList.add('revealed'));
}, 1500);
// Failsafe 2: trigger immediately for visible sections (100ms)
setTimeout(() => {
reveals.forEach((r) => r.classList.add('revealed'));
}, 100);
}
/* ------------------------------------------------------------------ */
/* 3. STAGGER-ON-SCROLL (.fx-stagger) */
/* ------------------------------------------------------------------ */
/**
* Add .staggered to .fx-stagger elements as they enter viewport.
* Same failsafe pattern as fxReveal.
*/
export function fxStagger(): void {
if (typeof window === 'undefined') return;
const staggers = document.querySelectorAll<HTMLElement>('.fx-stagger');
if (staggers.length === 0) return;
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('staggered');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.05, rootMargin: '0px 0px -40px 0px' }
);
staggers.forEach((s) => observer.observe(s));
}
// Failsafes
setTimeout(() => {
staggers.forEach((s) => s.classList.add('staggered'));
}, 1500);
setTimeout(() => {
staggers.forEach((s) => s.classList.add('staggered'));
}, 100);
}
/* ------------------------------------------------------------------ */
/* CONVENIENCE: init all 3 in one call */
/* ------------------------------------------------------------------ */
/** Run all fx animations. Safe to call multiple times (idempotent via querySelector). */
export function fxInit(): void {
fxClock();
fxReveal();
fxStagger();
}