fix: soft-clamp pinch zoom and Krita-inspired brush opacity
All checks were successful
CI / Windows build (push) Successful in 10m28s
All checks were successful
CI / Windows build (push) Successful in 10m28s
Hard SDROP avalanches froze lastRaw while zoom still crawled; soft-clamp and re-anchor instead. Ballpoint is near-solid, pencil uses soft √p without multiply stacking; PDF ink falls back to nearest page during zoom settle. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -154,10 +154,9 @@ const Map<BrushKind, BrushProfile> kBrushPresets = {
|
||||
opacity: 1.0,
|
||||
blendMultiply: false,
|
||||
),
|
||||
// Ballpoint — spec §4: size~2.2, thinning 0.15, smoothing 0.5,
|
||||
// streamline 0.55, near-constant width, linear pressure (gamma 1.0). The
|
||||
// "tell" is pressure → OPACITY (0.55 + 0.45·pressureAvg, resolved per-stroke
|
||||
// in resolveStrokeOpacity); the flat opacity field below is the solid cap.
|
||||
// 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.
|
||||
BrushKind.ballpoint: BrushProfile(
|
||||
kind: BrushKind.ballpoint,
|
||||
baseWidthFraction: 0.0022,
|
||||
@@ -192,22 +191,21 @@ const Map<BrushKind, BrushProfile> kBrushPresets = {
|
||||
opacity: 0.35,
|
||||
blendMultiply: true,
|
||||
),
|
||||
// Pencil — spec §4: size~3, thinning 0.5, smoothing 0.5, streamline 0.4,
|
||||
// pressure pre-warped to √p (Sqrt = pressureGamma 0.5). Pressure → OPACITY
|
||||
// (0.35 + 0.55·pressureAvg, resolveStrokeOpacity) makes it lighter/scratchy;
|
||||
// the 0.9 field is the solid cap. TODO(brush-texture): paper grain deferred.
|
||||
// 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.
|
||||
BrushKind.pencil: BrushProfile(
|
||||
kind: BrushKind.pencil,
|
||||
baseWidthFraction: 0.003,
|
||||
pressureGamma: 0.5,
|
||||
pfThinning: 0.5,
|
||||
pfStreamline: 0.4,
|
||||
pfSmoothing: 0.5,
|
||||
pfThinning: 0.45,
|
||||
pfStreamline: 0.35,
|
||||
pfSmoothing: 0.45,
|
||||
simulatePressure: false,
|
||||
capStart: true,
|
||||
capEnd: true,
|
||||
taper: false,
|
||||
opacity: 0.9,
|
||||
opacity: 0.88,
|
||||
blendMultiply: false,
|
||||
),
|
||||
};
|
||||
@@ -225,30 +223,39 @@ BrushProfile brushProfileFor(BrushKind kind) => kBrushPresets[kind]!;
|
||||
// `_PageOverlayPainter`) call [resolveStrokePaint] so they can never diverge.
|
||||
|
||||
/// Resolve the EFFECTIVE per-stroke opacity in [0,1] for [profile], given the
|
||||
/// stroke's AVERAGE pressure [pressureAvg] (already gamma-pre-warped at
|
||||
/// capture, but for opacity we want the raw feel of "how hard you pressed", so
|
||||
/// callers pass the mean of each point's `pressure ?? 0.5`).
|
||||
/// stroke's AVERAGE pressure [pressureAvg].
|
||||
///
|
||||
/// PER-STROKE (not per-segment): one alpha for the whole stroke this increment.
|
||||
/// The spec (§3/§4) ties ballpoint/pencil opacity to pressure; fountain pen and
|
||||
/// highlighter use the profile's flat [BrushProfile.opacity]. Per-point opacity
|
||||
/// (splitting into pressure-banded sub-strokes — spec §4) is deferred.
|
||||
/// Krita-inspired (not a full brush engine): ballpoint stays essentially solid
|
||||
/// (width carries the pressure feel); pencil uses a soft √p curve capped below
|
||||
/// 1 so light strokes stay grey without multiply-style mud; fountain/highlighter
|
||||
/// use the flat profile opacity. Per-dab / textured brushes remain deferred.
|
||||
double resolveStrokeOpacity(BrushProfile profile, {double pressureAvg = 0.5}) {
|
||||
final p = pressureAvg.clamp(0.0, 1.0);
|
||||
switch (profile.kind) {
|
||||
// Spec §4: ballpoint "tell" is pressure → opacity (near-constant width).
|
||||
// Solid ink — tiny residual so "hover contact" can't punch full black holes
|
||||
// into overlapping strokes, but no 0.55 floor translucency stacking.
|
||||
case BrushKind.ballpoint:
|
||||
return (0.55 + 0.45 * p).clamp(0.0, 1.0);
|
||||
// Spec §4: pencil darkens with pressure (firm, quick-darkening √p feel).
|
||||
return (0.92 + 0.08 * p).clamp(0.0, 1.0);
|
||||
// Soft graphite: √p darkens quickly under pressure, capped by profile.
|
||||
case BrushKind.pencil:
|
||||
return (0.35 + 0.55 * p).clamp(0.0, 1.0);
|
||||
// Fountain pen (solid 1.0) + highlighter (flat 0.35) use the profile value.
|
||||
final soft = 0.50 + 0.38 * _sqrt01(p);
|
||||
return soft.clamp(0.0, profile.opacity);
|
||||
case BrushKind.fountainPen:
|
||||
case BrushKind.highlighter:
|
||||
return profile.opacity.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
double _sqrt01(double v) {
|
||||
if (v <= 0) return 0;
|
||||
if (v >= 1) return 1;
|
||||
var x = v;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
x = 0.5 * (x + v / x);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/// Multiply [opacity] (0..1) into [argb]'s existing alpha channel and return the
|
||||
/// new ARGB int. Keeps any alpha the capture path already baked in (e.g. the
|
||||
/// highlighter's 0x80 translucent capture) so this composes WITHOUT
|
||||
|
||||
Reference in New Issue
Block a user