slightly more UI animations
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user