export type FlyRect = { left: number top: number width: number height: number } function prefersReducedMotion() { return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches } /** 二次贝塞尔:起点 → 控制点 → 终点 */ function quad(p0: number, p1: number, p2: number, t: number) { const u = 1 - t return u * u * p0 + 2 * u * t * p1 + t * t * p2 } function easeInOut(t: number) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2 } /** 将图片从源矩形沿抛物线飞到目标矩形,动画结束后移除 */ export function flyImageToRect(url: string, from: FlyRect, to: FlyRect, duration = 860): Promise { if (typeof document === 'undefined' || !url || from.width <= 0 || from.height <= 0) { return Promise.resolve() } if (prefersReducedMotion()) return Promise.resolve() const ghost = document.createElement('img') ghost.src = url ghost.alt = '' ghost.className = 'jd-img-fly' ghost.setAttribute('aria-hidden', 'true') Object.assign(ghost.style, { position: 'fixed', left: `${from.left}px`, top: `${from.top}px`, width: `${from.width}px`, height: `${from.height}px`, margin: '0', padding: '0', border: '0', borderRadius: '12px', objectFit: 'contain', objectPosition: 'center', background: '#e8eaed', zIndex: '10000', pointerEvents: 'none', boxShadow: '0 12px 28px rgba(15, 23, 42, 0.22)', transformOrigin: 'top left', willChange: 'transform, opacity, border-radius', } as CSSStyleDeclaration) document.body.appendChild(ghost) const sxEnd = to.width / Math.max(1, from.width) const syEnd = to.height / Math.max(1, from.height) const dx = to.left - from.left const dy = to.top - from.top const lift = Math.min(160, Math.max(72, Math.hypot(dx, dy) * 0.28)) const cx = dx * 0.5 const cy = dy * 0.5 - lift const steps = 18 const keyframes: Keyframe[] = [] for (let i = 0; i <= steps; i += 1) { const raw = i / steps const t = easeInOut(raw) const x = quad(0, cx, dx, t) const y = quad(0, cy, dy, t) const scaleX = 1 + (sxEnd - 1) * t const scaleY = 1 + (syEnd - 1) * t const radius = 12 + (10 - 12) * t keyframes.push({ offset: raw, transform: `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`, opacity: String(1 - 0.08 * t), borderRadius: `${radius}px`, }) } return new Promise((resolve) => { const finish = () => { ghost.remove() resolve() } const anim = ghost.animate(keyframes, { duration, easing: 'linear', fill: 'forwards', }) anim.onfinish = finish anim.oncancel = finish }) } export function rectFromElement(el: Element | null | undefined): FlyRect | null { if (!el) return null const box = el.getBoundingClientRect() if (box.width <= 0 || box.height <= 0) return null return { left: box.left, top: box.top, width: box.width, height: box.height } }