2026-06-23 03:20:59 +08:00
|
|
|
|
// lib/editor/input/pressure_curve.dart
|
|
|
|
|
|
//
|
|
|
|
|
|
// Configurable pen-pressure response (F5 — the user's repeated "可配置笔" ask).
|
|
|
|
|
|
// Raw normalized stylus pressure [0,1] is pre-shaped here before it reaches
|
|
|
|
|
|
// perfect_freehand, giving two user-facing knobs:
|
|
|
|
|
|
// - [floor]: a minimum output (the plan's marker "min-width floor" — a fixed-
|
|
|
|
|
|
// pressure marker uses floor≈1.0; a pen uses 0.0).
|
|
|
|
|
|
// - [gamma]: the response exponent — γ<1 makes light touches register more
|
|
|
|
|
|
// width (more sensitive), γ>1 requires firmer pressure (less sensitive).
|
|
|
|
|
|
//
|
2026-08-06 16:01:01 +08:00
|
|
|
|
// Named [PressureCurveShape] presets mirror rnote-style curves (linear / soft /
|
|
|
|
|
|
// Pow2 / cubic / log / sqrt) via [PressureCurve.shaped]. Logarithmic uses
|
|
|
|
|
|
// ln(1+k·p)/ln(1+k); all others use p^gamma.
|
|
|
|
|
|
//
|
2026-06-23 03:20:59 +08:00
|
|
|
|
// Pure value type (widget-free, storage-free) so the full mapping is unit
|
|
|
|
|
|
// tested; PenConfig / the canvas wire it later (the wiring touches the live
|
|
|
|
|
|
// draw path and is validated on-device).
|
|
|
|
|
|
|
|
|
|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
2026-06-23 09:43:56 +08:00
|
|
|
|
/// Default pressure-response exponent. <1 so light-to-medium pressure registers
|
|
|
|
|
|
/// more width — the responsive, rnote/OneNote-like feel — instead of the raw
|
|
|
|
|
|
/// linear mapping that made the pen feel like a pressure-sensitive finger.
|
|
|
|
|
|
const double kNaturalPressureGamma = 0.7;
|
|
|
|
|
|
|
|
|
|
|
|
/// Default minimum shaped pressure: even the lightest touch keeps ~12% of the
|
|
|
|
|
|
/// dynamic range so thin strokes have body instead of scratchy near-zero width.
|
|
|
|
|
|
const double kNaturalPressureFloor = 0.12;
|
|
|
|
|
|
|
2026-08-06 16:01:01 +08:00
|
|
|
|
/// Steepness for [PressureCurveShape.logarithmic]: `ln(1+k·p)/ln(1+k)`.
|
|
|
|
|
|
const double kLogarithmicPressureK = 9.0;
|
|
|
|
|
|
|
|
|
|
|
|
/// Named rnote-style pressure-response shapes.
|
|
|
|
|
|
enum PressureCurveShape {
|
|
|
|
|
|
/// Identity: gamma 1.
|
|
|
|
|
|
linear,
|
|
|
|
|
|
|
|
|
|
|
|
/// Light-touch sensitive: gamma ≈ 0.6.
|
|
|
|
|
|
soft,
|
|
|
|
|
|
|
|
|
|
|
|
/// rnote Pow2 / fountain: gamma 2.
|
|
|
|
|
|
quadratic,
|
|
|
|
|
|
|
|
|
|
|
|
/// gamma 3.
|
|
|
|
|
|
cubic,
|
|
|
|
|
|
|
|
|
|
|
|
/// Log curve: ln(1+k·p)/ln(1+k).
|
|
|
|
|
|
logarithmic,
|
|
|
|
|
|
|
|
|
|
|
|
/// Pencil: gamma 0.5.
|
|
|
|
|
|
sqrt,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 03:20:59 +08:00
|
|
|
|
/// Maps raw normalized pressure to a shaped response in `[floor, 1]`.
|
|
|
|
|
|
class PressureCurve {
|
2026-08-06 16:01:01 +08:00
|
|
|
|
const PressureCurve({
|
|
|
|
|
|
this.floor = 0.0,
|
|
|
|
|
|
this.gamma = 1.0,
|
|
|
|
|
|
this.shape,
|
|
|
|
|
|
}) : assert(floor >= 0.0 && floor < 1.0),
|
2026-06-23 03:20:59 +08:00
|
|
|
|
assert(gamma > 0.0);
|
|
|
|
|
|
|
2026-08-06 16:01:01 +08:00
|
|
|
|
/// Named-shape factory. Sets [gamma] for power-law shapes; logarithmic
|
|
|
|
|
|
/// ignores gamma and uses [kLogarithmicPressureK] in [apply].
|
|
|
|
|
|
factory PressureCurve.shaped(
|
|
|
|
|
|
PressureCurveShape shape, {
|
|
|
|
|
|
double floor = 0.0,
|
|
|
|
|
|
}) {
|
|
|
|
|
|
switch (shape) {
|
|
|
|
|
|
case PressureCurveShape.linear:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 1.0, shape: shape);
|
|
|
|
|
|
case PressureCurveShape.soft:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 0.6, shape: shape);
|
|
|
|
|
|
case PressureCurveShape.quadratic:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 2.0, shape: shape);
|
|
|
|
|
|
case PressureCurveShape.cubic:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 3.0, shape: shape);
|
|
|
|
|
|
case PressureCurveShape.logarithmic:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 1.0, shape: shape);
|
|
|
|
|
|
case PressureCurveShape.sqrt:
|
|
|
|
|
|
return PressureCurve(floor: floor, gamma: 0.5, shape: shape);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 03:20:59 +08:00
|
|
|
|
/// Minimum output (>=0, <1). 0 = full dynamic range; raise toward 1 for a
|
|
|
|
|
|
/// fixed-pressure feel (marker).
|
|
|
|
|
|
final double floor;
|
|
|
|
|
|
|
|
|
|
|
|
/// Response exponent (>0). 1 = linear; <1 = more sensitive at light pressure;
|
2026-08-06 16:01:01 +08:00
|
|
|
|
/// >1 = firmer. Unused when [shape] is [PressureCurveShape.logarithmic].
|
2026-06-23 03:20:59 +08:00
|
|
|
|
final double gamma;
|
|
|
|
|
|
|
2026-08-06 16:01:01 +08:00
|
|
|
|
/// Optional named shape. When [PressureCurveShape.logarithmic], [apply] uses
|
|
|
|
|
|
/// the log formula; otherwise (or when null) uses `p^gamma`.
|
|
|
|
|
|
final PressureCurveShape? shape;
|
|
|
|
|
|
|
2026-06-23 03:20:59 +08:00
|
|
|
|
/// Linear, full-range pen response (identity).
|
|
|
|
|
|
static const PressureCurve linear = PressureCurve();
|
|
|
|
|
|
|
|
|
|
|
|
/// Shape [pressure] (clamped to [0,1]) into `[floor, 1]`.
|
|
|
|
|
|
double apply(double pressure) {
|
|
|
|
|
|
final p = pressure.isNaN ? 0.0 : pressure.clamp(0.0, 1.0);
|
2026-08-06 16:01:01 +08:00
|
|
|
|
final double shaped;
|
|
|
|
|
|
if (shape == PressureCurveShape.logarithmic) {
|
|
|
|
|
|
shaped = math.log(1.0 + kLogarithmicPressureK * p) /
|
|
|
|
|
|
math.log(1.0 + kLogarithmicPressureK);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
shaped = gamma == 1.0 ? p : math.pow(p, gamma).toDouble();
|
|
|
|
|
|
}
|
2026-06-23 03:20:59 +08:00
|
|
|
|
return floor + (1.0 - floor) * shaped;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|