import type { PointerEvent as ReactPointerEvent } from "react"; import type { CurvePoint } from "@/types/profile"; import { sampleCurve } from "@/utils/curveMath"; interface CurveEditorProps { points: CurvePoint[]; onChange: (points: CurvePoint[]) => void; } const SIZE = 220; const PADDING = 18; const PLOT_SIZE = SIZE - PADDING * 2; function pointToSvg(point: CurvePoint): { x: number; y: number } { return { x: PADDING + point.x * PLOT_SIZE, y: PADDING + (1 - point.y) * PLOT_SIZE }; } function svgToPoint(event: ReactPointerEvent): CurvePoint { const svg = event.currentTarget instanceof SVGSVGElement ? event.currentTarget : event.currentTarget.ownerSVGElement; if (!svg) return { x: 0, y: 0 }; const bounds = svg.getBoundingClientRect(); return { x: Math.min(1, Math.max(0, (event.clientX - bounds.left - PADDING) / PLOT_SIZE)), y: Math.min(1, Math.max(0, 1 - (event.clientY - bounds.top - PADDING) / PLOT_SIZE)), }; } /** Interactive, dependency-free custom response-curve point editor. */ export function CurveEditor({ points, onChange }: CurveEditorProps) { const sorted = [...points].sort((a, b) => a.x - b.x); const samples = sampleCurve("custom", sorted); const line = samples.map((point) => { const svg = pointToSvg(point); return `${svg.x},${svg.y}`; }).join(" "); function addPoint(event: ReactPointerEvent) { if ((event.target as Element).tagName !== "svg") return; const point = svgToPoint(event); onChange([...sorted, point].sort((a, b) => a.x - b.x)); } function movePoint(index: number, event: ReactPointerEvent) { const next = [...sorted]; const candidate = svgToPoint(event); // Endpoints remain anchored at input 0/1, preventing invalid curves. if (index === 0) candidate.x = 0; if (index === next.length - 1) candidate.x = 1; const lower = index === 0 ? 0 : next[index - 1].x + 0.01; const upper = index === next.length - 1 ? 1 : next[index + 1].x - 0.01; candidate.x = Math.min(upper, Math.max(lower, candidate.x)); next[index] = candidate; onChange(next); } return (
{[0.25, 0.5, 0.75].map((value) => ( ))} {sorted.map((point, index) => { const svg = pointToSvg(point); return ( { event.stopPropagation(); event.currentTarget.setPointerCapture(event.pointerId); }} onPointerMove={(event) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) movePoint(index, event); }} /> ); })}

Click to add a point; drag points to adjust the curve.

); }