feat(f5): configurable pen pressure curve (floor + gamma) pure core
Some checks failed
CI / Windows build (push) Has been cancelled

The user's repeated "可配置笔" ask, as a pure value type: raw normalized
pressure is pre-shaped into [floor, 1] via a min-width floor (the plan's
marker fixed-pressure floor) and a gamma response (γ<1 = more sensitive at light
touch, γ>1 = firmer). Clamps out-of-range + NaN inputs; endpoints anchored at
floor and 1. Widget-free/storage-free; PenConfig + the canvas wire it later
(live-path, on-device validated).

flutter analyze lib/editor clean; 145/145 tests (+6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 03:20:59 +08:00
parent 852eb389ee
commit eedb52d2f5
2 changed files with 89 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
// 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).
//
// 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;
/// Maps raw normalized pressure to a shaped response in `[floor, 1]`.
class PressureCurve {
const PressureCurve({this.floor = 0.0, this.gamma = 1.0})
: assert(floor >= 0.0 && floor < 1.0),
assert(gamma > 0.0);
/// 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.
final double gamma;
/// 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 shaped = gamma == 1.0 ? p : math.pow(p, gamma).toDouble();
return floor + (1.0 - floor) * shaped;
}
}

View File

@@ -0,0 +1,49 @@
// Tests for the configurable pen-pressure response (F5).
import 'package:flutter_test/flutter_test.dart';
import 'package:badnote/editor/input/pressure_curve.dart';
void main() {
test('linear is the identity over [0,1]', () {
const c = PressureCurve.linear;
expect(c.apply(0.0), closeTo(0.0, 1e-9));
expect(c.apply(0.5), closeTo(0.5, 1e-9));
expect(c.apply(1.0), closeTo(1.0, 1e-9));
});
test('floor raises the minimum, endpoints stay anchored at floor and 1', () {
const c = PressureCurve(floor: 0.4);
expect(c.apply(0.0), closeTo(0.4, 1e-9)); // floor
expect(c.apply(1.0), closeTo(1.0, 1e-9)); // full
expect(c.apply(0.5), closeTo(0.4 + 0.6 * 0.5, 1e-9)); // 0.7
});
test('gamma < 1 boosts light pressure (more sensitive)', () {
const c = PressureCurve(gamma: 0.5);
// pow(0.25, 0.5) = 0.5 → output 0.5 > input 0.25
expect(c.apply(0.25), closeTo(0.5, 1e-9));
expect(c.apply(0.25), greaterThan(0.25));
});
test('gamma > 1 dampens light pressure (firmer)', () {
const c = PressureCurve(gamma: 2.0);
expect(c.apply(0.5), closeTo(0.25, 1e-9)); // 0.5^2
expect(c.apply(0.5), lessThan(0.5));
});
test('output is clamped: out-of-range and NaN inputs are safe', () {
const c = PressureCurve(floor: 0.2);
expect(c.apply(-1.0), closeTo(0.2, 1e-9)); // clamped to 0 → floor
expect(c.apply(2.0), closeTo(1.0, 1e-9)); // clamped to 1 → full
expect(c.apply(double.nan), closeTo(0.2, 1e-9)); // NaN → 0 → floor
});
test('marker-style high floor compresses the range toward full width', () {
const marker = PressureCurve(floor: 0.9);
expect(marker.apply(0.0), closeTo(0.9, 1e-9));
expect(marker.apply(1.0), closeTo(1.0, 1e-9));
// Even a light touch stays near full width.
expect(marker.apply(0.1), greaterThan(0.9));
});
}