import { MOTION } from './design-tokens.js'; import { routePoints } from './geometry.js'; // Timing follows actual path length, so a piece beside an exit has no fixed // wait. Cruising speed is one constant for every piece — until the knee. Past // it the piece accelerates instead of taking longer, because uncapped linear // timing ran a single departure to seventeen seconds and reads as a stall // rather than as a journey. Callers may override the ceiling to compare // settings; labs/exit-timing does exactly that. export function exitTiming(route,{knee=MOTION.exitMaxTravelMs,maxMs=MOTION.exitLongMaxMs}={}) { const points=routePoints({...route,end:null},{size:1,ox:0,oy:0}); const distance=points.slice(1).reduce((sum,p,i)=>sum+Math.hypot(p.x-points[i].x,p.y-points[i].y),0); const linear=distance*MOTION.exitMsPerUnit; // Below the knee nothing changes: same speed as before, same feel. const base=linear<=knee?linear :Math.min(maxMs,knee+(linear-knee)*Math.max(0,(maxMs-knee))/Math.max(1,linear-knee)*(1-Math.exp(-(linear-knee)/Math.max(1,maxMs-knee)))); const rampMs=Math.min(MOTION.exitRampMs,base/3); const travelMs=base+rampMs; const duration=travelMs+MOTION.exitEffectMs; return {duration,travelMs,distance,travelFraction:travelMs/duration,capped:linear>knee}; } // Zero endpoint velocity: a soft launch and a settled finish, without overshoot. export function smoothMotion(value) { const t=Math.max(0,Math.min(1,value)); return t*t*(3-2*t); } // Fixed-duration acceleration and braking; the middle travels at a shared speed. export function travelProgress(progress,travelMs){ const t=Math.max(0,Math.min(1,progress))*travelMs; const ramp=Math.min(MOTION.exitRampMs,travelMs/4),area=travelMs-ramp; if(ttravelMs-ramp)return 1-(travelMs-t)**2/(2*ramp*area); return (t-ramp/2)/area; }