96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|||
|
|
const activeAnimations = new WeakMap();
|
||
|
|
const visibilityIntent = new WeakMap();
|
||
|
|
let panelSwapSequence = 0;
|
||
|
|
|
||
|
|
export function prefersReducedMotion() {
|
||
|
|
return reducedMotionQuery.matches;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function animateElement(element, keyframes, options = {}) {
|
||
|
|
if (!element || prefersReducedMotion() || typeof element.animate !== "function") {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
activeAnimations.get(element)?.cancel();
|
||
|
|
const animation = element.animate(keyframes, {
|
||
|
|
duration: 220,
|
||
|
|
easing: "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||
|
|
fill: "none",
|
||
|
|
...options,
|
||
|
|
});
|
||
|
|
activeAnimations.set(element, animation);
|
||
|
|
animation.addEventListener("finish", () => {
|
||
|
|
if (activeAnimations.get(element) === animation) activeAnimations.delete(element);
|
||
|
|
}, { once: true });
|
||
|
|
animation.addEventListener("cancel", () => {
|
||
|
|
if (activeAnimations.get(element) === animation) activeAnimations.delete(element);
|
||
|
|
}, { once: true });
|
||
|
|
return animation;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function revealElement(element, options = {}) {
|
||
|
|
if (!element) return null;
|
||
|
|
visibilityIntent.set(element, true);
|
||
|
|
element.classList.remove("hidden");
|
||
|
|
element.removeAttribute("hidden");
|
||
|
|
return animateElement(element, [
|
||
|
|
{ opacity: 0, transform: `translateY(${options.offset ?? 8}px) scale(${options.scale ?? 0.99})` },
|
||
|
|
{ opacity: 1, transform: "translateY(0) scale(1)" },
|
||
|
|
], { duration: options.duration ?? 220 });
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function concealElement(element, options = {}) {
|
||
|
|
if (!element || element.classList.contains("hidden") || element.hasAttribute("hidden")) return false;
|
||
|
|
visibilityIntent.set(element, false);
|
||
|
|
const animation = animateElement(element, [
|
||
|
|
{ opacity: 1, transform: "translateY(0) scale(1)" },
|
||
|
|
{ opacity: 0, transform: `translateY(${options.offset ?? -4}px) scale(${options.scale ?? 0.995})` },
|
||
|
|
], { duration: options.duration ?? 110, easing: "ease-in" });
|
||
|
|
if (animation) {
|
||
|
|
try { await animation.finished; } catch {}
|
||
|
|
}
|
||
|
|
if (visibilityIntent.get(element) === false) {
|
||
|
|
element.classList.add("hidden");
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function swapPanels(outgoing, incoming) {
|
||
|
|
if (!incoming || outgoing === incoming) return;
|
||
|
|
const sequence = ++panelSwapSequence;
|
||
|
|
if (outgoing) await concealElement(outgoing, { duration: 90, offset: -3 });
|
||
|
|
if (sequence !== panelSwapSequence) return;
|
||
|
|
revealElement(incoming, { duration: 190, offset: 6 });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function staggerChildren(container, options = {}) {
|
||
|
|
if (!container || prefersReducedMotion()) return;
|
||
|
|
const children = Array.from(container.children).slice(0, options.limit ?? 12);
|
||
|
|
children.forEach((child, index) => {
|
||
|
|
animateElement(child, [
|
||
|
|
{ opacity: 0, transform: `translateY(${options.offset ?? 7}px)` },
|
||
|
|
{ opacity: 1, transform: "translateY(0)" },
|
||
|
|
], {
|
||
|
|
duration: options.duration ?? 200,
|
||
|
|
delay: index * (options.interval ?? 24),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function emphasizeElement(element, tone = "rgba(56, 189, 248, 0.28)") {
|
||
|
|
return animateElement(element, [
|
||
|
|
{ transform: "scale(1)", boxShadow: "0 0 0 0 transparent" },
|
||
|
|
{ transform: "scale(1.012)", boxShadow: `0 0 0 3px ${tone}` },
|
||
|
|
{ transform: "scale(1)", boxShadow: "0 0 0 0 transparent" },
|
||
|
|
], { duration: 420, easing: "ease-out" });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function animateTextChange(element) {
|
||
|
|
return animateElement(element, [
|
||
|
|
{ opacity: 0.45, transform: "translateY(3px)" },
|
||
|
|
{ opacity: 1, transform: "translateY(0)" },
|
||
|
|
], { duration: 160 });
|
||
|
|
}
|