// 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). // // 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. // // 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; /// 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; /// 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, } /// Maps raw normalized pressure to a shaped response in `[floor, 1]`. class PressureCurve { const PressureCurve({ this.floor = 0.0, this.gamma = 1.0, this.shape, }) : assert(floor >= 0.0 && floor < 1.0), assert(gamma > 0.0); /// 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); } } /// 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; /// >1 = firmer. Unused when [shape] is [PressureCurveShape.logarithmic]. final double gamma; /// Optional named shape. When [PressureCurveShape.logarithmic], [apply] uses /// the log formula; otherwise (or when null) uses `p^gamma`. final PressureCurveShape? shape; /// 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); 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(); } return floor + (1.0 - floor) * shaped; } }