/** Something that hops from stop to stop in an arc, round and round. A pure function of time. */ export type Point = { readonly x: number; readonly y: number }; const easeInOut = (t: number): number => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2); const wrap = (index: number, length: number): number => ((index % length) + length) % length; /** * Where the hopper is at `time`. It rests on `stops[0]` at time 0, reaches the next stop every * `framesPerHop`, and returns from the last stop to the first. The arc of a hop rises by * `arcRatio` times the distance covered. Negative times are fine. */ export const hopAt = (time: number, stops: readonly Point[], framesPerHop: number, arcRatio: number): Point => { const hop = Math.floor(time / framesPerHop); const from = stops[wrap(hop, stops.length)]; const to = stops[wrap(hop + 1, stops.length)]; if (!from || !to) throw new Error("hopAt needs at least one stop"); const t = easeInOut(time / framesPerHop - hop); const distance = Math.hypot(to.x - from.x, to.y - from.y); return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t - Math.sin(Math.PI * t) * distance * arcRatio, }; };