/** Quintic paths retain velocity when interrupted; arc and acceleration settle to zero. */ export function sampleMotion(motion, now) { const t = Math.max(0, Math.min(1, (now - motion.started) / motion.duration)); const t2 = t * t, t3 = t2 * t, t4 = t3 * t, t5 = t4 * t; const ease = 10 * t3 - 15 * t4 + 6 * t5; const carry = t - 6 * t3 + 8 * t4 - 3 * t5; const arch = 64 * t3 * (1 - t) ** 3; const de = 30 * t2 * (1 - t) ** 2; const dc = 1 - 18 * t2 + 32 * t3 - 15 * t4; const da = 192 * t2 * (1 - t) ** 2 * (1 - 2 * t); const seconds = motion.duration / 1000; const value = {}, velocity = {}; for (const key of ['x', 'y', 'scale']) { const delta = motion.to[key] - motion.from[key]; const v = motion.velocity[key] || 0; const bend = motion.arc[key] || 0; value[key] = motion.from[key] + delta * ease + v * seconds * carry + bend * arch; velocity[key] = delta * de / seconds + v * dc + bend * da / seconds; } return { value, velocity, done: t === 1 }; } export function transition(node, target, now, immediate = false) { const current = node.motion ? sampleMotion(node.motion, now) : { value: node, velocity: {} }; const from = { x: current.value.x, y: current.value.y, scale: current.value.scale }; const dx = target.x - from.x, dy = target.y - from.y; const distance = Math.hypot(dx, dy); const bend = Math.min(36, distance * 0.055); node.motion = { from, to: target, velocity: current.velocity, arc: { x: -dy / (distance || 1) * bend, y: dx / (distance || 1) * bend }, started: now, duration: immediate ? 1 : Math.min(950, 600 + distance * 0.22), }; }