feat(pen): brush opacity + highlighter multiply
Some checks failed
CI / Windows build (push) Has been cancelled

Honor each brush's opacity/blend so the brushes feel distinct
(closes TODO(brush-opacity)).

- Shared paint resolver: a stroke's color alpha is multiplied by its
  brush opacity; ballpoint/pencil opacity is tied to pressure
  (per-stroke average this increment) so a ballpoint reads lighter
  than a solid fountain pen.
- Highlighter paints with BlendMode.multiply and draws once, so
  cross-stroke overlap darkens like a real marker while self-overlap
  doesn't.
- Applied across BOTH render paths (PenCanvas static/live painters and
  the PDF _PageOverlayPainter).

Pencil paper-grain texture still deferred (TODO brush-texture); brush
kind is not yet serialized (TODO brush-persist — next). analyze clean,
tests green.
This commit is contained in:
2026-06-24 23:27:11 +08:00
parent 24d13642fd
commit 6c2dd71b82
7 changed files with 349 additions and 65 deletions

View File

@@ -50,7 +50,7 @@ import '../persistence/sidecar_repository.dart';
import '../ui/pen_settings_page.dart';
import '../ui/thumbnail_grid.dart';
import 'editor_tool.dart';
import 'ink_painters.dart' show buildStrokePath;
import 'ink_painters.dart' show buildStrokePath, paintForStroke;
import 'input_diagnostics.dart';
import 'pen_palette_widgets.dart';
import 'pen_stroke.dart';
@@ -1689,13 +1689,10 @@ class _PageOverlayPainter extends CustomPainter {
final path =
buildStrokePath(stroke, size, isComplete: true, thinning: thinning);
if (path.getBounds().isEmpty) continue;
canvas.drawPath(
path,
Paint()
..color = Color(stroke.color)
..style = PaintingStyle.fill
..isAntiAlias = true,
);
// Single drawPath per stroke ⇒ highlighter self-overlap never darkens;
// cross-stroke overlap darkens via BlendMode.multiply (closes
// TODO(brush-opacity); shared resolver with the PenCanvas painters).
canvas.drawPath(path, paintForStroke(stroke));
}
// 3. Live stroke — read from the notifier at paint time, only for this page.
@@ -1704,13 +1701,7 @@ class _PageOverlayPainter extends CustomPainter {
final path = buildStrokePath(live.stroke, size,
isComplete: false, thinning: thinning);
if (!path.getBounds().isEmpty) {
canvas.drawPath(
path,
Paint()
..color = Color(live.stroke.color)
..style = PaintingStyle.fill
..isAntiAlias = true,
);
canvas.drawPath(path, paintForStroke(live.stroke));
}
}