You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

71 lines
1.8 KiB

interface FlipEntry {
el: Element
rect: DOMRect
}
let cachedReducedMotion: boolean | null = null
function prefersReducedMotion(): boolean {
if (cachedReducedMotion === null) {
cachedReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
return cachedReducedMotion
}
export function useFlipAnimation() {
let firstEntries: FlipEntry[] | null = null
let runningAnims: Animation[] = []
function capture(container: HTMLElement, selector: string) {
cancelRunning()
firstEntries = []
container.querySelectorAll(selector).forEach((el) => {
firstEntries!.push({ el, rect: el.getBoundingClientRect() })
})
}
function play(
container: HTMLElement,
selector: string,
options: { duration?: number; easing?: string } = {},
) {
if (!firstEntries || prefersReducedMotion()) {
firstEntries = null
return
}
const { duration = 420, easing = 'cubic-bezier(0.16, 1, 0.3, 1)' } = options
const firstByEl = new Map<Element, DOMRect>()
for (const entry of firstEntries) {
firstByEl.set(entry.el, entry.rect)
}
container.querySelectorAll(selector).forEach((el) => {
const first = firstByEl.get(el)
if (!first) return
const last = el.getBoundingClientRect()
const dx = first.left - last.left
const dy = first.top - last.top
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) return
const anim = el.animate(
[
{ transform: `translate(${dx}px, ${dy}px)`, opacity: '1' },
{ transform: 'translate(0, 0)', opacity: '1' },
],
{ duration, easing, fill: 'both' },
)
runningAnims.push(anim)
})
firstEntries = null
}
function cancelRunning() {
for (const a of runningAnims) a.cancel()
runningAnims = []
}
return { capture, play, cancelRunning }
}