feat(pen): brush opacity + highlighter multiply
Some checks failed
CI / Windows build (push) Has been cancelled
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:
@@ -88,10 +88,9 @@ List<Offset> freehandOutlinePoints({
|
||||
|
||||
/// Build perfect_freehand [pf.StrokeOptions] from a [BrushProfile] (spec §4).
|
||||
///
|
||||
/// TODO(brush-opacity): [BrushProfile.opacity] / [BrushProfile.blendMultiply]
|
||||
/// are NOT consumed here — geometry only this increment. The caller still paints
|
||||
/// fill color (with its own alpha) and BlendMode.srcOver; per-stroke opacity /
|
||||
/// BlendMode.multiply for ballpoint/pencil/highlighter lands later.
|
||||
/// Geometry only: [BrushProfile.opacity] / [BrushProfile.blendMultiply] are
|
||||
/// consumed by the painters' [paintForEditorStroke] / `paintForStroke` (via
|
||||
/// [resolveStrokePaint]), NOT here — this stays a pure outline recipe.
|
||||
pf.StrokeOptions _optionsFromBrush(
|
||||
BrushProfile brush, {
|
||||
required double size,
|
||||
@@ -171,3 +170,36 @@ Path buildStrokeOutline(
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Mean point pressure (`pressure ?? 0.5`) of an [EditorStroke], for the
|
||||
/// per-stroke opacity resolution (spec §3/§4 tie ballpoint/pencil opacity to
|
||||
/// pressure).
|
||||
double _avgPressure(EditorStroke stroke) {
|
||||
if (stroke.points.isEmpty) return 0.5;
|
||||
var sum = 0.0;
|
||||
for (final p in stroke.points) {
|
||||
sum += p.pressure ?? 0.5;
|
||||
}
|
||||
return sum / stroke.points.length;
|
||||
}
|
||||
|
||||
/// THE single fill [Paint] for an [EditorStroke], with the brush's resolved
|
||||
/// opacity (multiplied into the color's alpha) and blend mode applied — closes
|
||||
/// TODO(brush-opacity). Shared by the committed [Picture] and live painters so
|
||||
/// the EditorStroke render path composites exactly like the PenStroke one.
|
||||
///
|
||||
/// The stroke is drawn as ONE fill polygon, so a highlighter's own self-overlap
|
||||
/// never darkens; cross-stroke overlap darkens via [BlendMode.multiply]
|
||||
/// (marker build-up). TODO(brush-texture): pencil paper grain still deferred.
|
||||
Paint paintForEditorStroke(EditorStroke stroke) {
|
||||
final resolved = resolveStrokePaint(
|
||||
stroke.brush,
|
||||
stroke.color,
|
||||
pressureAvg: _avgPressure(stroke),
|
||||
);
|
||||
return Paint()
|
||||
..color = resolved.color
|
||||
..blendMode = resolved.blendMode
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user