fix: PDF finger ink, chrome UX, OneNote pens, and pen physics
Some checks failed
CI / Windows build (push) Has been cancelled

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>
This commit is contained in:
2026-08-06 16:01:01 +08:00
parent 85af037b7d
commit f31dd0fb52
17 changed files with 835 additions and 234 deletions

View File

@@ -135,16 +135,15 @@ class BrushProfile {
/// highlighter ≈ 0.02 so the existing pen/highlighter visuals are PRESERVED as
/// the fountainPen/highlighter presets (no regression).
const Map<BrushKind, BrushProfile> kBrushPresets = {
// Fountain pen — spec §4: size~6, thinning 0.9, smoothing 0.55,
// streamline 0.45, simulatePressure false, taper on, pressure pre-warped to
// p² (Pow2 / quadratic = pressureGamma 2.0). Solid ink (opacity 1.0).
// Fountain pen — Surface feel: lower streamline (less lag), higher thinning
// for expressive width, pressure pre-warped to p² (Pow2 / quadratic).
// Solid ink (opacity 1.0). See also tipVelocityWidthScale (ink starvation).
BrushKind.fountainPen: BrushProfile(
kind: BrushKind.fountainPen,
baseWidthFraction: 0.006,
pressureGamma: 2.0,
// Was 0.9 — too aggressive on short CJK strokes (width collapses mid-glyph).
pfThinning: 0.65,
pfStreamline: 0.4,
pfThinning: 0.75,
pfStreamline: 0.22,
pfSmoothing: 0.5,
simulatePressure: false,
capStart: true,
@@ -154,15 +153,14 @@ const Map<BrushKind, BrushProfile> kBrushPresets = {
opacity: 1.0,
blendMultiply: false,
),
// Ballpoint — Krita-inspired "ink pen": near-constant width, SOLID opacity.
// Pressure modulates WIDTH slightly (thinning 0.15), NOT alpha — translucent
// srcOver stacking looked like accidental multiply when strokes overlapped.
// Ballpoint — near-constant width, SOLID opacity. Lower streamline (~0.35)
// for lower latency; thinning 0.12 keeps width almost flat.
BrushKind.ballpoint: BrushProfile(
kind: BrushKind.ballpoint,
baseWidthFraction: 0.0022,
pressureGamma: 1.0,
pfThinning: 0.15,
pfStreamline: 0.55,
pfThinning: 0.12,
pfStreamline: 0.35,
pfSmoothing: 0.5,
simulatePressure: false,
capStart: true,
@@ -171,18 +169,14 @@ const Map<BrushKind, BrushProfile> kBrushPresets = {
opacity: 1.0,
blendMultiply: false,
),
// Highlighter — spec §4: size~22, thinning 0.0 (constant width),
// smoothing 0.4, streamline 0.5, square (uncapped) ends, translucent +
// multiply build-up. opacity 0.35 / blendMultiply true are APPLIED via
// resolveStrokePaint: the 0.35 is multiplied INTO the color's existing alpha
// (the capture path ships a 0x80 / 50% translucent color), and the stroke
// composites with BlendMode.multiply (cross-stroke overlap darkens = marker).
// Highlighter — flat width (thinning 0), square (uncapped) ends, translucent +
// multiply build-up. streamline 0.3 for a bit less lag on broad strokes.
BrushKind.highlighter: BrushProfile(
kind: BrushKind.highlighter,
baseWidthFraction: 0.02,
pressureGamma: 1.0,
pfThinning: 0.0,
pfStreamline: 0.5,
pfStreamline: 0.3,
pfSmoothing: 0.4,
simulatePressure: false,
capStart: false,
@@ -191,15 +185,13 @@ const Map<BrushKind, BrushProfile> kBrushPresets = {
opacity: 0.35,
blendMultiply: true,
),
// Pencil — Krita-inspired: soft graphite, moderate translucency via √p, but
// NEVER multiply blend (only highlighter uses multiply). Cap ~0.88 so overlaps
// darken gently under srcOver without turning into marker blobs.
// Pencil — soft graphite via √p, moderate translucency. streamline 0.25.
BrushKind.pencil: BrushProfile(
kind: BrushKind.pencil,
baseWidthFraction: 0.003,
pressureGamma: 0.5,
pfThinning: 0.45,
pfStreamline: 0.35,
pfStreamline: 0.25,
pfSmoothing: 0.45,
simulatePressure: false,
capStart: true,

View File

@@ -0,0 +1,33 @@
// lib/editor/engine/pen_physics.dart
//
// Simple physical tip model: modulate stroke width by tip velocity so fountain
// ink feels slightly thinner when moving fast (starvation), while ballpoint
// stays nearly velocity-invariant.
//
// TODO(pen-physics-wire): wired at capture in PenCanvas._toNormalized via
// tip velocity × pressure. PDF editor path still uses brush gamma only.
import 'brush.dart';
/// Modulate width fraction by tip velocity (page-normalized units per second).
///
/// Fountain: faster → slightly thinner (ink starvation feel).
/// Ballpoint: nearly ignore velocity.
/// Pencil: mild thinning at speed.
/// Highlighter: ignore velocity (flat marker).
double tipVelocityWidthScale(BrushKind kind, double speedNormPerSec) {
final speed =
speedNormPerSec.isNaN || speedNormPerSec < 0 ? 0.0 : speedNormPerSec;
// Reference: ~2 page-widths/sec ≈ fast handwriting; clamp influence to [0,1].
final t = (speed / 2.0).clamp(0.0, 1.0);
switch (kind) {
case BrushKind.fountainPen:
return 1.0 - 0.15 * t;
case BrushKind.ballpoint:
return 1.0 - 0.02 * t;
case BrushKind.pencil:
return 1.0 - 0.08 * t;
case BrushKind.highlighter:
return 1.0;
}
}

View File

@@ -11,7 +11,7 @@ class PredictedPoint {
}
class StrokePredictor {
StrokePredictor({this.lookaheadMs = 12});
StrokePredictor({this.lookaheadMs = 8});
/// How far ahead to project, in milliseconds of recent velocity.
final double lookaheadMs;