Files
ece.suwa3d.com/src/utils/fly-image.ts
T
2026-11-03 14:13:25 +08:00

104 lines
2.9 KiB
TypeScript

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<void> {
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 }
}