Il movimento era fatto con `@keyframes` e transizioni a durata fissa: nessuna di quelle animazioni può essere afferrata a metà. Se l'utente scorre o tocca mentre una è in corso, quella va avanti per conto suo e per ripartire deve prima finire. `lib/molla.ts` raccoglie i parametri, che sono i due di Apple in «Designing Fluid Interfaces» — rimbalzo e durata, non massa/rigidità/smorzamento. `molla.ui` (nessun sorpasso) è il default; `molla.slancio` si usa solo dopo un gesto che portava già inerzia. `proietta()` calcola dove finirebbe un elemento lanciato, con la stessa decelerazione esponenziale dello scroll iOS. Nei tre componenti: - `Reveal` compare quando entra davvero nel viewport. Prima partiva al mount, quindi gli elementi sotto la piega consumavano l'animazione a vuoto e l'utente li trovava già fermi. Il ritardo massimo scende da 480 a 200 ms. - `Barra` anima `scaleX` invece di `width`: la larghezza rifà il layout a ogni frame, la trasformazione la gestisce il compositore. - `Numero` usa una molla al posto dell'interpolazione a durata fissa: se il dato cambia a metà conteggio il numero cambia rotta da dov'è invece di ripartire da capo. Sparisce la prop `durata`, che nessuno passava. `lib/motion.ts` resta solo per il rilevamento del movimento ridotto (che qui guarda anche `deviceMemory`, cosa che `useReducedMotion` non fa) e per i coriandoli. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42 lines
1.4 KiB
TypeScript
42 lines
1.4 KiB
TypeScript
/**
|
|
* Quel che resta del motion "fatto a mano": il rilevamento del movimento
|
|
* ridotto (che qui tiene conto anche dei device deboli, cosa che
|
|
* `useReducedMotion` di motion non fa) e i coriandoli.
|
|
*
|
|
* Le animazioni di valore — conteggi, barre, comparse — sono passate a molle
|
|
* interrompibili: vedi `lib/molla.ts` e `components/motion/`.
|
|
*/
|
|
import { useEffect, useState } from "react";
|
|
|
|
/** True se l'utente ha chiesto meno movimento o il device è poco performante. */
|
|
export function useMotoRidotto() {
|
|
const [ridotto, setRidotto] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
const memoria = (navigator as Navigator & { deviceMemory?: number }).deviceMemory;
|
|
const debole = typeof memoria === "number" && memoria > 0 && memoria < 2;
|
|
const applica = () => setRidotto(mq.matches || debole);
|
|
applica();
|
|
mq.addEventListener("change", applica);
|
|
return () => mq.removeEventListener("change", applica);
|
|
}, []);
|
|
|
|
return ridotto;
|
|
}
|
|
|
|
/** Coriandoli leggeri, caricati solo al momento del bisogno. */
|
|
export async function coriandoli(ridotto = false) {
|
|
if (ridotto || typeof window === "undefined") return;
|
|
const { default: confetti } = await import("canvas-confetti");
|
|
confetti({
|
|
particleCount: 70,
|
|
spread: 65,
|
|
startVelocity: 32,
|
|
ticks: 120,
|
|
scalar: 0.9,
|
|
origin: { y: 0.7 },
|
|
disableForReducedMotion: true,
|
|
});
|
|
}
|