{
  "name": "path-utils",
  "type": "registry:lib",
  "description": "SVG path measuring, sampling, auto-fit and waypoint helpers",
  "dependencies": [
    "@remotion/paths"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/bases/default/lib/path-utils.ts",
      "type": "registry:lib",
      "content": "import {\n  evolvePath,\n  getBoundingBox,\n  getLength,\n  getPointAtLength,\n  getTangentAtLength,\n} from \"@remotion/paths\";\n\nexport type PathPoint = {\n  x: number;\n  y: number;\n};\n\n/** Point on a path plus the heading of the curve at that point, in degrees. */\nexport type PathSample = PathPoint & {\n  angle: number;\n};\n\n/**\n * Measuring a path parses its `d` string, which is far too expensive to redo\n * for every path on every frame. Paths are static strings, so results are\n * cached by the string itself.\n */\nconst lengthCache = new Map<string, number>();\nconst boxCache = new Map<string, ReturnType<typeof getBoundingBox>>();\n\nexport function pathLength(d: string): number {\n  const cached = lengthCache.get(d);\n  if (cached !== undefined) {\n    return cached;\n  }\n  const length = getLength(d);\n  lengthCache.set(d, length);\n  return length;\n}\n\nexport function pathBoundingBox(d: string) {\n  const cached = boxCache.get(d);\n  if (cached !== undefined) {\n    return cached;\n  }\n  const box = getBoundingBox(d);\n  boxCache.set(d, box);\n  return box;\n}\n\nexport function clampProgress(progress: number) {\n  return Math.min(1, Math.max(0, progress));\n}\n\nexport function getPathDrawStyles(progress: number, path: string) {\n  const evolution = evolvePath(clampProgress(progress), path);\n  return {\n    strokeDasharray: evolution.strokeDasharray,\n    strokeDashoffset: evolution.strokeDashoffset,\n  };\n}\n\n/**\n * A viewBox that tightly frames the artwork, so callers can hand over any path\n * — a logo exported at the origin or one sitting at x=940 in a 1024 canvas —\n * without hand-computing coordinates. `padding` is in path units and leaves\n * room for the stroke, which straddles the geometry and would otherwise clip.\n */\nexport function fitViewBox(paths: string | string[], padding = 0): string {\n  const list = (Array.isArray(paths) ? paths : [paths]).filter(Boolean);\n  const boxes = list.map(pathBoundingBox);\n\n  if (boxes.length === 0) {\n    return \"0 0 100 100\";\n  }\n\n  const x1 = Math.min(...boxes.map((box) => box.x1));\n  const y1 = Math.min(...boxes.map((box) => box.y1));\n  const x2 = Math.max(...boxes.map((box) => box.x2));\n  const y2 = Math.max(...boxes.map((box) => box.y2));\n\n  return [\n    x1 - padding,\n    y1 - padding,\n    Math.max(1, x2 - x1 + padding * 2),\n    Math.max(1, y2 - y1 + padding * 2),\n  ].join(\" \");\n}\n\n/**\n * Sample a path by arc length rather than by segment index, so a cursor or a\n * comet head travels at an even speed no matter how the path was authored.\n */\nexport function samplePath(d: string, progress: number): PathSample {\n  const length = pathLength(d);\n  const at = clampProgress(progress) * length;\n  // Both return null for a path with no drawable segments (an empty `d`, or a\n  // lone moveto), which is a valid input a caller can build from waypoints.\n  const point = getPointAtLength(d, at) ?? { x: 0, y: 0 };\n  const tangent = getTangentAtLength(d, at) ?? { x: 1, y: 0 };\n\n  return {\n    x: point.x,\n    y: point.y,\n    angle: (Math.atan2(tangent.y, tangent.x) * 180) / Math.PI,\n  };\n}\n\n/**\n * Build a path through waypoints. `smoothing` of 0 gives straight hops; higher\n * values round the corners with a cardinal spline whose control points are\n * derived from the neighbouring points, so the curve stays close to the line.\n */\nexport function waypointsToPath(\n  points: readonly PathPoint[],\n  smoothing = 0.25,\n): string {\n  if (points.length === 0) {\n    return \"\";\n  }\n  if (points.length === 1) {\n    return `M ${points[0].x} ${points[0].y}`;\n  }\n\n  const segments = [`M ${points[0].x} ${points[0].y}`];\n\n  for (let index = 0; index < points.length - 1; index += 1) {\n    const previous = points[index - 1] ?? points[index];\n    const from = points[index];\n    const to = points[index + 1];\n    const next = points[index + 2] ?? to;\n\n    if (smoothing <= 0) {\n      segments.push(`L ${to.x} ${to.y}`);\n      continue;\n    }\n\n    const c1 = {\n      x: from.x + ((to.x - previous.x) / 2) * smoothing,\n      y: from.y + ((to.y - previous.y) / 2) * smoothing,\n    };\n    const c2 = {\n      x: to.x - ((next.x - from.x) / 2) * smoothing,\n      y: to.y - ((next.y - from.y) / 2) * smoothing,\n    };\n\n    segments.push(`C ${c1.x} ${c1.y} ${c2.x} ${c2.y} ${to.x} ${to.y}`);\n  }\n\n  return segments.join(\" \");\n}\n\n/**\n * Arc-length progress at which each waypoint sits, so callers can fire events\n * — a click, a label — exactly when the traveller reaches a point instead of\n * at a guessed frame. Smoothing moves a waypoint off the straight line, so the\n * nearest sample is found by scanning rather than by segment ratio.\n */\nexport function waypointProgress(\n  d: string,\n  points: readonly PathPoint[],\n  samples = 240,\n): number[] {\n  const length = pathLength(d);\n  if (length === 0) {\n    return points.map(() => 0);\n  }\n\n  return points.map((point) => {\n    let bestProgress = 0;\n    let bestDistance = Infinity;\n\n    for (let step = 0; step <= samples; step += 1) {\n      const progress = step / samples;\n      const sample = getPointAtLength(d, progress * length);\n      if (!sample) {\n        continue;\n      }\n      const distance = (sample.x - point.x) ** 2 + (sample.y - point.y) ** 2;\n      if (distance < bestDistance) {\n        bestDistance = distance;\n        bestProgress = progress;\n      }\n    }\n\n    return bestProgress;\n  });\n}\n"
    }
  ]
}