Files
BadNote/lib/editor/input/pressure_curve.dart
Akiba So f31dd0fb52
Some checks failed
CI / Windows build (push) Has been cancelled
fix: PDF finger ink, chrome UX, OneNote pens, and pen physics
Wire finger drawing on PDF without breaking pinch; auto-hide page scrubber and fix bounce; share sticky tools with resize and per-page remember; side-button select; separate pen slots with colors; rnote pressure shapes plus tip-velocity width and lower stroke latency.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 16:01:01 +08:00

113 lines
4.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}
}