71 lines
1.9 KiB
Dart
71 lines
1.9 KiB
Dart
|
|
import '../models/ink_point.dart';
|
||
|
|
|
||
|
|
/// Stabilization level for hand-drawn strokes.
|
||
|
|
enum StabilizationLevel { none, light, medium, heavy }
|
||
|
|
|
||
|
|
/// Smooths pen input using an Exponential Moving Average (EMA) filter.
|
||
|
|
///
|
||
|
|
/// - **none**: no smoothing (passthrough)
|
||
|
|
/// - **light**: alpha = 0.6 (subtle smoothing)
|
||
|
|
/// - **medium**: alpha = 0.4 (moderate smoothing)
|
||
|
|
/// - **heavy**: alpha = 0.25 (strong smoothing, removes most tremor)
|
||
|
|
///
|
||
|
|
/// The formula applied to each coordinate independently:
|
||
|
|
/// x_smoothed = alpha * x_raw + (1 - alpha) * x_prev
|
||
|
|
class StrokeStabilizer {
|
||
|
|
final StabilizationLevel level;
|
||
|
|
|
||
|
|
double? _prevX;
|
||
|
|
double? _prevY;
|
||
|
|
|
||
|
|
StrokeStabilizer({this.level = StabilizationLevel.none});
|
||
|
|
|
||
|
|
double get _alpha {
|
||
|
|
switch (level) {
|
||
|
|
case StabilizationLevel.none:
|
||
|
|
return 1.0;
|
||
|
|
case StabilizationLevel.light:
|
||
|
|
return 0.6;
|
||
|
|
case StabilizationLevel.medium:
|
||
|
|
return 0.4;
|
||
|
|
case StabilizationLevel.heavy:
|
||
|
|
return 0.25;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Filters a raw point through the EMA, returning a smoothed point.
|
||
|
|
/// Returns the raw point unchanged if level is [StabilizationLevel.none].
|
||
|
|
InkPoint filter(InkPoint rawPoint) {
|
||
|
|
if (level == StabilizationLevel.none) return rawPoint;
|
||
|
|
|
||
|
|
final alpha = _alpha;
|
||
|
|
|
||
|
|
if (_prevX == null || _prevY == null) {
|
||
|
|
_prevX = rawPoint.x;
|
||
|
|
_prevY = rawPoint.y;
|
||
|
|
return rawPoint;
|
||
|
|
}
|
||
|
|
|
||
|
|
final smoothedX = alpha * rawPoint.x + (1 - alpha) * _prevX!;
|
||
|
|
final smoothedY = alpha * rawPoint.y + (1 - alpha) * _prevY!;
|
||
|
|
|
||
|
|
_prevX = smoothedX;
|
||
|
|
_prevY = smoothedY;
|
||
|
|
|
||
|
|
return InkPoint(
|
||
|
|
x: smoothedX,
|
||
|
|
y: smoothedY,
|
||
|
|
pressure: rawPoint.pressure,
|
||
|
|
tilt: rawPoint.tilt,
|
||
|
|
timestamp: rawPoint.timestamp,
|
||
|
|
pointerDeviceKind: rawPoint.pointerDeviceKind,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Resets the filter state. Call when starting a new stroke.
|
||
|
|
void reset() {
|
||
|
|
_prevX = null;
|
||
|
|
_prevY = null;
|
||
|
|
}
|
||
|
|
}
|