Initial release of 8BitDo Control Center
Provide a Linux desktop controller configuration app with evdev input, uinput remapping, profiles, calibration, and reconnect recovery. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
98
src/components/inputs/CurveEditor.tsx
Normal file
98
src/components/inputs/CurveEditor.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
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<SVGSVGElement | SVGCircleElement>): 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<SVGSVGElement>) {
|
||||
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<SVGCircleElement>) {
|
||||
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 (
|
||||
<div>
|
||||
<svg
|
||||
viewBox={`0 0 ${SIZE} ${SIZE}`}
|
||||
className="w-full max-w-[220px] cursor-crosshair rounded-lg border border-border bg-surface-2"
|
||||
onPointerDown={addPoint}
|
||||
aria-label="Custom response curve editor"
|
||||
>
|
||||
{[0.25, 0.5, 0.75].map((value) => (
|
||||
<g key={value}>
|
||||
<line x1={PADDING} x2={SIZE - PADDING} y1={PADDING + value * PLOT_SIZE} y2={PADDING + value * PLOT_SIZE} className="stroke-border" />
|
||||
<line y1={PADDING} y2={SIZE - PADDING} x1={PADDING + value * PLOT_SIZE} x2={PADDING + value * PLOT_SIZE} className="stroke-border" />
|
||||
</g>
|
||||
))}
|
||||
<polyline points={line} fill="none" className="stroke-accent" strokeWidth={2.5} />
|
||||
{sorted.map((point, index) => {
|
||||
const svg = pointToSvg(point);
|
||||
return (
|
||||
<circle
|
||||
key={`${point.x}-${point.y}-${index}`}
|
||||
cx={svg.x}
|
||||
cy={svg.y}
|
||||
r={5}
|
||||
className="cursor-grab fill-accent stroke-surface active:cursor-grabbing"
|
||||
strokeWidth={2}
|
||||
onPointerDown={(event) => {
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) movePoint(index, event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
<p className="mt-1 text-xs text-fg-subtle">Click to add a point; drag points to adjust the curve.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user