@@ -9478,6 +9478,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private onPointerMoveFromPointerDownHandler(
|
private onPointerMoveFromPointerDownHandler(
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
) {
|
) {
|
||||||
|
// Per-stroke One Euro Filter state for mouse freedraw smoothing.
|
||||||
|
// Reference: Casiez et al. 2012 "1€ Filter: A Simple Speed-based Low-pass
|
||||||
|
// Filter for Noisy Input in Interactive Systems".
|
||||||
|
//
|
||||||
|
// The filter adapts its EMA alpha to the current speed of the pointer:
|
||||||
|
// slow movement → low cutoff → heavy smoothing (removes jitter)
|
||||||
|
// fast movement → high cutoff → near-raw tracking (minimal lag)
|
||||||
|
//
|
||||||
|
// State variables (reset at stroke start via null sentinel):
|
||||||
|
let emaX: number | null = null; // smoothed position x
|
||||||
|
let emaY: number | null = null; // smoothed position y
|
||||||
|
let emaDx = 0; // smoothed derivative x
|
||||||
|
let emaDy = 0; // smoothed derivative y
|
||||||
|
let prevRawX: number | null = null; // previous raw x for derivative
|
||||||
|
let prevRawY: number | null = null;
|
||||||
|
let prevTs: number | null = null; // previous event timestamp (ms)
|
||||||
|
|
||||||
|
// Tuning constants:
|
||||||
|
// MIN_CUTOFF – cutoff frequency (Hz) when speed ≈ 0; smaller = more smoothing
|
||||||
|
// BETA – rate at which cutoff grows with speed; larger = quicker adaptation
|
||||||
|
// D_CUTOFF – fixed cutoff for the derivative pre-filter
|
||||||
|
const MIN_CUTOFF = 0.5;
|
||||||
|
const BETA = 0.01;
|
||||||
|
const D_CUTOFF = 2.0;
|
||||||
|
|
||||||
|
/** Compute EMA alpha from cutoff frequency (Hz) and timestep (seconds). */
|
||||||
|
const emaAlpha = (cutoff: number, dt: number): number => {
|
||||||
|
const tau = 1 / (2 * Math.PI * cutoff);
|
||||||
|
return 1 / (1 + tau / dt);
|
||||||
|
};
|
||||||
|
|
||||||
const handler = (event: PointerEvent) => {
|
const handler = (event: PointerEvent) => {
|
||||||
if (this.state.openDialog?.name === "elementLinkSelector") {
|
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||||
return;
|
return;
|
||||||
@@ -10185,8 +10216,38 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
for (const ev of allEvents) {
|
for (const ev of allEvents) {
|
||||||
const coords = viewportCoordsToSceneCoords(ev, this.state);
|
const coords = viewportCoordsToSceneCoords(ev, this.state);
|
||||||
const dx = coords.x - newElement.x;
|
const rawDx = coords.x - newElement.x;
|
||||||
const dy = coords.y - newElement.y;
|
const rawDy = coords.y - newElement.y;
|
||||||
|
|
||||||
|
// One Euro Filter: speed-adaptive low-pass for mouse events.
|
||||||
|
const dt = Math.max(
|
||||||
|
prevTs !== null ? (ev.timeStamp - prevTs) / 1000 : 1 / 60,
|
||||||
|
0.001,
|
||||||
|
); // seconds
|
||||||
|
|
||||||
|
// 1. Smooth the derivative (fixed D_CUTOFF pre-filter).
|
||||||
|
const alphaD = emaAlpha(D_CUTOFF, dt);
|
||||||
|
const rawDxDt = prevRawX !== null ? (rawDx - prevRawX) / dt : 0;
|
||||||
|
const rawDyDt = prevRawY !== null ? (rawDy - prevRawY) / dt : 0;
|
||||||
|
emaDx = alphaD * rawDxDt + (1 - alphaD) * emaDx;
|
||||||
|
emaDy = alphaD * rawDyDt + (1 - alphaD) * emaDy;
|
||||||
|
|
||||||
|
// 2. Adaptive cutoff: higher speed → higher cutoff → less lag.
|
||||||
|
const speed = Math.sqrt(emaDx * emaDx + emaDy * emaDy);
|
||||||
|
const cutoff = MIN_CUTOFF + BETA * speed;
|
||||||
|
const alphaP = emaAlpha(cutoff, dt);
|
||||||
|
|
||||||
|
// 3. Smooth position with adaptive alpha.
|
||||||
|
emaX = emaX === null ? rawDx : alphaP * rawDx + (1 - alphaP) * emaX;
|
||||||
|
emaY = emaY === null ? rawDy : alphaP * rawDy + (1 - alphaP) * emaY;
|
||||||
|
|
||||||
|
prevRawX = rawDx;
|
||||||
|
prevRawY = rawDy;
|
||||||
|
prevTs = ev.timeStamp;
|
||||||
|
|
||||||
|
const dx = emaX;
|
||||||
|
const dy = emaY;
|
||||||
|
|
||||||
if (!lastPoint || lastPoint[0] !== dx || lastPoint[1] !== dy) {
|
if (!lastPoint || lastPoint[0] !== dx || lastPoint[1] !== dy) {
|
||||||
const pt = pointFrom<LocalPoint>(dx, dy);
|
const pt = pointFrom<LocalPoint>(dx, dy);
|
||||||
newPoints.push(pt);
|
newPoints.push(pt);
|
||||||
|
|||||||
Reference in New Issue
Block a user