feat(pen): apply pressure-response curve for natural feel
"手写笔就是一个带压感的手指,没有特殊适配" — correct. The new editor fed RAW LINEAR stylus pressure into perfect_freehand, and the pressureGamma config (a slider in pen-settings) was read ONLY by that slider's UI and NEVER applied to a stroke. Dead wiring, like the rest. Wire it for real: - PenCanvas applies PressureCurve(floor, gamma) at capture, so stored pressure carries the feel and live + PDF export replay identically. - Natural defaults: gamma 0.7 (light touches register more width, rnote/ OneNote-like) + floor 0.12 (thin strokes keep body, not scratchy). - pen_editor threads PenConfig.pressureGamma into the canvas — the slider now actually changes stroke width. - pen_config: natural default + one-time migration of the legacy inert gamma 1.0, guarded by a marker so a deliberate 1.0 still sticks. Tests: pen_config_gamma_migration (4) + pressure_curve (6) pass; pen widget regressions green. flutter analyze: 0 issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
|
||||
import 'pressure_curve.dart' show kNaturalPressureGamma;
|
||||
|
||||
/// Action that can be triggered by a hardware pen button or the eraser end.
|
||||
enum PenButtonAction {
|
||||
@@ -22,7 +23,7 @@ class PenConfig {
|
||||
const PenConfig({
|
||||
this.sideButton = PenButtonAction.eraser,
|
||||
this.eraserEnd = PenButtonAction.eraser,
|
||||
this.pressureGamma = 1.0,
|
||||
this.pressureGamma = kNaturalPressureGamma,
|
||||
this.palmRejectionMs = 150.0,
|
||||
this.fingerDrawing = false,
|
||||
this.penWidth = 0.004,
|
||||
@@ -107,7 +108,8 @@ class PenConfig {
|
||||
eraserEnd:
|
||||
PenButtonAction.values.asNameMap()[json['eraserEnd'] as String? ?? ''] ??
|
||||
PenButtonAction.eraser,
|
||||
pressureGamma: (json['pressureGamma'] as num?)?.toDouble() ?? 1.0,
|
||||
pressureGamma:
|
||||
(json['pressureGamma'] as num?)?.toDouble() ?? kNaturalPressureGamma,
|
||||
palmRejectionMs: (json['palmRejectionMs'] as num?)?.toDouble() ?? 150.0,
|
||||
fingerDrawing: json['fingerDrawing'] as bool? ?? false,
|
||||
penWidth: (json['penWidth'] as num?)?.toDouble() ?? 0.004,
|
||||
@@ -161,6 +163,9 @@ class PenConfigController extends ChangeNotifier {
|
||||
/// The SharedPreferences key under which [PenConfig] JSON is stored.
|
||||
static const prefsKey = 'pen_config_v1';
|
||||
|
||||
/// Marker so the legacy-gamma migration in [load] runs at most once.
|
||||
static const _gammaMigratedKey = 'pen_config_gamma_migrated_v1';
|
||||
|
||||
final SharedPreferences _prefs;
|
||||
PenConfig _value;
|
||||
|
||||
@@ -184,6 +189,18 @@ class PenConfigController extends ChangeNotifier {
|
||||
config = const PenConfig();
|
||||
}
|
||||
}
|
||||
// One-time migration: before this build, pressureGamma was never applied to
|
||||
// strokes (a dead slider), so a stored 1.0 is the legacy inert default, not
|
||||
// a deliberate "linear feel" choice. Upgrade it ONCE to the natural curve so
|
||||
// the pen feels right out of the box. Guarded by a marker key so that, after
|
||||
// migrating, the user is free to set gamma back to 1.0 and have it stick.
|
||||
if (!(prefs.getBool(_gammaMigratedKey) ?? false)) {
|
||||
if (config.pressureGamma == 1.0) {
|
||||
config = config.copyWith(pressureGamma: kNaturalPressureGamma);
|
||||
await prefs.setString(prefsKey, jsonEncode(config.toJson()));
|
||||
}
|
||||
await prefs.setBool(_gammaMigratedKey, true);
|
||||
}
|
||||
return PenConfigController._(prefs, config);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
|
||||
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;
|
||||
|
||||
/// Maps raw normalized pressure to a shaped response in `[floor, 1]`.
|
||||
class PressureCurve {
|
||||
const PressureCurve({this.floor = 0.0, this.gamma = 1.0})
|
||||
|
||||
Reference in New Issue
Block a user