export type Vec2 = { x: number; y: number }; export function add(a: Vec2, b: Vec2): Vec2 { return { x: a.x + b.x, y: a.y + b.y }; } export function sub(a: Vec2, b: Vec2): Vec2 { return { x: a.x - b.x, y: a.y - b.y }; } export function scale(a: Vec2, s: number): Vec2 { return { x: a.x * s, y: a.y * s }; } export function length(a: Vec2): number { return Math.hypot(a.x, a.y); } export function normalize(a: Vec2): Vec2 { const len = length(a); return len > 0 ? scale(a, 1 / len) : { x: 0, y: 0 }; } export function dot(a: Vec2, b: Vec2): number { return a.x * b.x + a.y * b.y; } export function rotate90(a: Vec2): Vec2 { return { x: -a.y, y: a.x }; } // Point le plus proche du point p sur le segment [a,b]. export function closestPointOnSegment(p: Vec2, a: Vec2, b: Vec2): { point: Vec2; t: number } { const ab = sub(b, a); const abLenSq = dot(ab, ab); const t = abLenSq > 0 ? Math.max(0, Math.min(1, dot(sub(p, a), ab) / abLenSq)) : 0; return { point: add(a, scale(ab, t)), t }; } // Catmull-Rom uniforme : passe exactement par chaque point de contrôle, // samplesPerSegment points intermédiaires entre chaque paire de points. export function catmullRom(points: Vec2[], samplesPerSegment: number): Vec2[] { if (points.length < 2) return points.slice(); const result: Vec2[] = []; const at = (i: number) => points[Math.max(0, Math.min(points.length - 1, i))]; for (let i = 0; i < points.length - 1; i++) { const p0 = at(i - 1); const p1 = at(i); const p2 = at(i + 1); const p3 = at(i + 2); for (let s = 0; s < samplesPerSegment; s++) { const t = s / samplesPerSegment; const t2 = t * t; const t3 = t2 * t; result.push({ x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3), y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3), }); } } result.push(points[points.length - 1]); return result; }