diff --git a/src/lib/fx-animations.ts b/src/lib/fx-animations.ts new file mode 100644 index 0000000..aab8618 --- /dev/null +++ b/src/lib/fx-animations.ts @@ -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: + * + */ + +/* ------------------------------------------------------------------ */ +/* 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('.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('.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(); +}