// 笔画渲染工具 — perfect_freehand 参数配置 + 路径构建 + 画笔 Paint 创建 // // 核心流程: // StrokePoint[] → perfect_freehand.getStroke() → Point[] (轮廓多边形) // → buildStrokePath() → Path → Canvas.drawPath() // // 本文件提供纯函数工具,供多个 Painter 和缓存系统复用: // - ActiveStrokePainter(当前笔画实时渲染) // - CachedStrokesPainter(合成位图绘制) // - StrokeRasterCache(单笔画光栅化) import 'package:flutter/widgets.dart'; import 'package:perfect_freehand/perfect_freehand.dart' as pf; import 'stroke_model.dart'; // ============================================================ // 画笔参数配置 // ============================================================ /// perfect_freehand 参数集,按画笔类型区分。 /// /// 参考设计规格 v1.2:钢笔细致、铅笔柔和、马克笔粗半透明、橡皮擦大范围。 class _BrushConfig { final double size; final double thinning; final double smoothing; final double streamline; final bool simulatePressure; const _BrushConfig({ required this.size, required this.thinning, required this.smoothing, this.streamline = 0.5, required this.simulatePressure, }); } /// 各画笔的渲染参数。 const Map _brushConfigs = { /// 钢笔:粗壮平滑,模拟签字笔效果 BrushType.pen: _BrushConfig( size: 10, thinning: 0.65, smoothing: 0.65, streamline: 0.6, simulatePressure: true, ), /// 铅笔:纤细有质感,保留书写抖动 BrushType.pencil: _BrushConfig( size: 3, thinning: 0.4, smoothing: 0.35, streamline: 0.3, simulatePressure: true, ), /// 马克笔:粗线,几乎无压感变化,不模拟压力 BrushType.marker: _BrushConfig( size: 16, thinning: 0.1, smoothing: 0.5, simulatePressure: false, ), /// 橡皮擦:最大范围,无压感变化 BrushType.eraser: _BrushConfig( size: 32, thinning: 0, smoothing: 0.5, simulatePressure: false, ), }; // ============================================================ // 点转换工具函数 // ============================================================ /// 将 [StrokePoint] 列表转换为 perfect_freehand 轮廓点列表。 /// /// [brushType] 决定渲染参数,[width] 作为乘数影响最终笔画大小。 /// [isComplete] 控制端点处理:已完成笔画传 true,实时绘制传 false。 /// 当 [points] 少于 2 个时返回空列表(无法构成笔画)。 List pointsToOutline( List points, BrushType brushType, double width, { bool isComplete = true, }) { if (points.length < 2) return const []; final config = _brushConfigs[brushType]!; final widthMultiplier = width / 3.0; // 3.0 是默认宽度,缩放到用户选择 // 转换为 perfect_freehand 的 Point 格式 final pfPoints = points .map((p) => pf.Point(p.x, p.y, p.pressure)) .toList(growable: false); // 调用 getStroke 获取轮廓多边形 final outline = pf.getStroke( pfPoints, size: config.size * widthMultiplier, thinning: config.thinning, smoothing: config.smoothing, streamline: config.streamline, simulatePressure: config.simulatePressure, isComplete: isComplete, ); // 转换为 Flutter Offset return outline.map((p) => Offset(p.x, p.y)).toList(growable: false); } // ============================================================ // 路径构建 // ============================================================ /// 将轮廓点列表构建为 Flutter [Path]。 /// /// 使用 [Path.addPolygon] 直接构建闭合多边形, /// 比 lineTo 逐点连接更高效,且自动闭合。 Path buildStrokePath(List outlinePoints) { if (outlinePoints.isEmpty) return Path(); final path = Path(); path.addPolygon(outlinePoints, true); // true = 自动闭合 return path; } // ============================================================ // 颜色解析工具 // ============================================================ /// 将 CSS 十六进制颜色字符串 (#RRGGBB) 解析为 Flutter [Color]。 /// /// 如果解析失败,回退到默认的深色文字色 #2D2420。 Color parseHexColor(String hex) { final hexStr = hex.replaceFirst('#', ''); if (hexStr.length != 6) return const Color(0xFF2D2420); final value = int.tryParse(hexStr, radix: 16); if (value == null) return const Color(0xFF2D2420); return Color(0xFF000000 + value); } // ============================================================ // 画笔 Paint 创建 // ============================================================ /// 根据画笔类型创建对应的 [Paint] 对象。 /// /// 供 StrokePainter、StrokeRasterCache、ActiveStrokePainter 共用。 /// - 钢笔/铅笔:不透明实心绘制 /// - 马克笔:半透明 (0.4 opacity) 模拟荧光笔 /// - 橡皮擦:使用 [BlendMode.dstOut] 擦除(需配合 saveLayer) Paint createPaintForStroke(Stroke stroke) { final color = parseHexColor(stroke.color); switch (stroke.brushType) { case BrushType.pen: case BrushType.pencil: // 钢笔和铅笔:不透明实心绘制 return Paint() ..color = color ..style = PaintingStyle.fill ..isAntiAlias = true; case BrushType.marker: // 马克笔:半透明模拟荧光笔 return Paint() ..color = color.withValues(alpha: 0.4) ..style = PaintingStyle.fill ..isAntiAlias = true; case BrushType.eraser: // 橡皮擦:使用 dstOut 混合模式擦除底层像素 // 必须在 saveLayer 内使用才能正确合成 return Paint() ..color = const Color(0xFFFFFFFF) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut ..isAntiAlias = true; } }