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
text/typescript
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);
}