All files / signature-pad/src/ink filterPoints.ts

100% Statements 15/15
100% Branches 4/4
100% Functions 3/3
100% Lines 13/13

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84                                        855x 855x 855x                                                 326x                                               6x 5x 5x 5x 9x 9x 5x 5x     5x    
/**
 * Min-distance point filtering — the first stage of the ink pipeline
 * (SPEC §4.2). Pure and framework-free.
 */
import type { SignaturePoint } from "../types";
 
/**
 * Euclidean distance between two points, in CSS px.
 *
 * @example
 * ```ts
 * import { pointDistance } from "@usefy/signature-pad/headless";
 *
 * pointDistance({ x: 0, y: 0 }, { x: 3, y: 4 }); // 5
 * ```
 */
export function pointDistance(
  a: Pick<SignaturePoint, "x" | "y">,
  b: Pick<SignaturePoint, "x" | "y">,
): number {
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  return Math.sqrt(dx * dx + dy * dy);
}
 
/**
 * The single accept/drop predicate shared by the batch {@link filterPoints}
 * and the engine's incremental capture path — both MUST agree or replay
 * would diverge from live drawing.
 *
 * A point passes when it is at least `minDistance` px from the previously
 * kept point. `minDistance <= 0` accepts everything (including exact
 * duplicates).
 *
 * @example
 * ```ts
 * import { passesMinDistance } from "@usefy/signature-pad/headless";
 *
 * passesMinDistance({ x: 0, y: 0 }, { x: 2, y: 0 }, 2);   // true (exactly at threshold)
 * passesMinDistance({ x: 0, y: 0 }, { x: 1.9, y: 0 }, 2); // false
 * ```
 */
export function passesMinDistance(
  prev: Pick<SignaturePoint, "x" | "y">,
  next: Pick<SignaturePoint, "x" | "y">,
  minDistance: number,
): boolean {
  return pointDistance(prev, next) >= minDistance;
}
 
/**
 * Drop points closer than `minDistance` to the last **kept** point.
 * The first point is always kept. Pure — the input array is not mutated;
 * kept points are the original references.
 *
 * @example
 * ```ts
 * import { filterPoints } from "@usefy/signature-pad/headless";
 *
 * const pts = [
 *   { x: 0, y: 0, time: 0, pressure: 0 },
 *   { x: 1, y: 0, time: 5, pressure: 0 },  // 1px from (0,0) — dropped
 *   { x: 3, y: 0, time: 10, pressure: 0 }, // 3px from (0,0) — kept
 * ];
 * filterPoints(pts, 2).map((p) => p.x); // [0, 3]
 * ```
 */
export function filterPoints(
  points: readonly SignaturePoint[],
  minDistance: number,
): SignaturePoint[] {
  if (points.length === 0) return [];
  const kept: SignaturePoint[] = [points[0]];
  let last = points[0];
  for (let i = 1; i < points.length; i++) {
    const p = points[i];
    if (passesMinDistance(last, p, minDistance)) {
      kept.push(p);
      last = p;
    }
  }
  return kept;
}