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:
148
src/lib/fx-animations.ts
Normal file
148
src/lib/fx-animations.ts
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user