UNPKG

html-trajectory

Version:

Animate HTML elements along a curved path from their current position to a target element. Eg. a product photo flying into the cart icon.

141 lines (114 loc) 4.56 kB
export default function projectile( flyingId: string, targetId: string, options?: { moveX?: boolean; moveY?: boolean; duration?: number; acceleration?: number; scale?: number; onTransitionEnd?: () => void; removeOriginal?: boolean; resetTransformation?: boolean; } ) { const flying = document.getElementById(flyingId) as HTMLElement | null; const target = document.getElementById(targetId) as HTMLElement | null; // Try to get the field element first, fall back to document.body if not found const field = document.getElementById('field') as HTMLElement | null; const containerElement = field || document.body; if (!flying || !target) { throw 'Invalid IDs provided to projectile function'; } const clone = flying.cloneNode(true) as HTMLElement; clone.removeAttribute('id'); clone.style.position = 'absolute'; clone.style.pointerEvents = 'none'; clone.style.margin = '0'; // Reset transformation if option is enabled if (options?.resetTransformation) { clone.style.transform = 'none'; clone.style.transformOrigin = 'center center'; } // Add html-trajectory-cloned class clone.classList.add("html-trajectory-cloned"); containerElement.appendChild(clone); const computedStyle = window.getComputedStyle(flying); const transform = computedStyle.transform; let baseRotation = 0; if (transform && transform !== 'none') { const matrixMatch = transform.match(/^matrix\(([-\d.,\s]+)\)$/); if (matrixMatch) { const values = matrixMatch[1].split(',').map(v => parseFloat(v.trim())); if (values.length >= 6) { const [a, b, c, d] = values; baseRotation = Math.atan2(b, a) * (180 / Math.PI); } } } clone.getBoundingClientRect(); const flyingRect = flying.getBoundingClientRect(); const containerRect = containerElement.getBoundingClientRect(); const targetRect = target.getBoundingClientRect(); const startX = flyingRect.left - containerRect.left; const startY = flyingRect.top - containerRect.top; const endX = targetRect.left - containerRect.left + (targetRect.width - clone.offsetWidth) / 2; const endY = targetRect.top - containerRect.top + (targetRect.height - clone.offsetHeight) / 2; clone.style.left = `${startX}px`; clone.style.top = `${startY}px`; const duration = (options?.duration ?? 1) * 1000; const userAcceleration = options?.acceleration ?? 4; const acceleration = userAcceleration * 100; const moveX = options?.moveX ?? true; const moveY = options?.moveY ?? true; const targetScale = options?.scale ?? 1; const removeOriginal = options?.removeOriginal ?? true; if (removeOriginal) { flying.remove(); } const distanceX = endX - startX; const distanceY = endY - startY; const velocityX = distanceX / (duration / 1000); const velocityYInitial = (distanceY - 0.5 * acceleration * Math.pow(duration / 1000, 2)) / (duration / 1000); let startTime: number | undefined; function animate(currentTime: number) { if (startTime === undefined) { startTime = currentTime; } const elapsed = (currentTime - startTime) / 1000; // in seconds const t = Math.min(elapsed, duration / 1000); const x = moveX ? startX + velocityX * t : startX; const y = moveY ? startY + velocityYInitial * t + 0.5 * acceleration * t * t : startY; const deltaT = 0.001; const nextT = Math.min(t + deltaT, duration / 1000); const nextX = moveX ? startX + velocityX * nextT : startX; const nextY = moveY ? startY + velocityYInitial * nextT + 0.5 * acceleration * nextT * nextT : startY; const dx = nextX - x; const dy = nextY - y; const angleRad = Math.atan2(dy, dx); const angleDeg = angleRad * (180 / Math.PI); const dynamicRotation = angleDeg + 90; const finalRotation = baseRotation + dynamicRotation; let scaleFactor = 1; if (t > (duration / 1000) * 0.66) { const progress = (t - (duration / 1000) * 0.66) / ((duration / 1000) * 0.34); scaleFactor = 1 + (targetScale - 1) * progress; } clone.style.transform = `rotate(${finalRotation}deg) scale(${scaleFactor})`; clone.style.left = `${x}px`; clone.style.top = `${y}px`; if (t < duration / 1000) { requestAnimationFrame(animate); } else { try { if (options?.onTransitionEnd) { options.onTransitionEnd(); } } catch (e) { console.error("Error in onTransitionEnd callback", e); } clone.remove(); } } requestAnimationFrame(animate); }