68 lines
2.0 KiB
TypeScript
68 lines
2.0 KiB
TypeScript
/**
|
|
* useCountUp — RAF-tween анимация числа (Quiet Luxury KPI cards).
|
|
*
|
|
* - easeOutQuint easing
|
|
* - respects prefers-reduced-motion (instant value)
|
|
* - re-animates when target ref changes
|
|
*
|
|
* Spec: docs/superpowers/plans/2026-05-12-portal-redesign-quiet-luxury-plan.md (Task 6).
|
|
*/
|
|
import { ref, watch, type Ref } from 'vue';
|
|
|
|
export interface CountUpOptions {
|
|
duration?: number; // ms
|
|
precision?: number; // знаков после запятой
|
|
}
|
|
|
|
export interface CountUpHandle {
|
|
display: Ref<number>;
|
|
start: () => void;
|
|
}
|
|
|
|
const easeOutQuint = (t: number): number => 1 - Math.pow(1 - t, 5);
|
|
|
|
function prefersReducedMotion(): boolean {
|
|
if (typeof window === 'undefined' || !window.matchMedia) return false;
|
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
}
|
|
|
|
export function useCountUp(target: Ref<number>, opts: CountUpOptions = {}): CountUpHandle {
|
|
const duration = opts.duration ?? 600;
|
|
const precision = opts.precision ?? 0;
|
|
const display = ref(0);
|
|
let raf: number | null = null;
|
|
let startTime = 0;
|
|
let fromValue = 0;
|
|
|
|
function tick(now: number): void {
|
|
const elapsed = now - startTime;
|
|
const t = Math.min(elapsed / duration, 1);
|
|
const eased = easeOutQuint(t);
|
|
const value = fromValue + (target.value - fromValue) * eased;
|
|
display.value = precision === 0 ? Math.round(value) : parseFloat(value.toFixed(precision));
|
|
if (t < 1) {
|
|
raf = requestAnimationFrame(tick);
|
|
} else {
|
|
display.value = target.value;
|
|
raf = null;
|
|
}
|
|
}
|
|
|
|
function start(): void {
|
|
if (prefersReducedMotion()) {
|
|
display.value = target.value;
|
|
return;
|
|
}
|
|
if (raf !== null) cancelAnimationFrame(raf);
|
|
fromValue = display.value;
|
|
startTime = performance.now();
|
|
raf = requestAnimationFrame(tick);
|
|
}
|
|
|
|
watch(target, () => {
|
|
if (display.value !== target.value) start();
|
|
});
|
|
|
|
return { display, start };
|
|
}
|