// 笔画渲染器 — 将 StrokePoint 列表转换为 Flutter 可绘制路径。 // // 核心流程: // StrokePoint[] → perfect_freehand.getStroke() → Point[] (轮廓多边形) // → buildStrokePath() → Path → CustomPainter.drawPath() // // 每种画笔(pen/pencil/marker/eraser)拥有独立的 perfect_freehand 参数, // 产生不同的视觉效果。 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: 8, thinning: 0.7, smoothing: 0.5, simulatePressure: true, ), /// 铅笔:细线,轻微压感,高平滑度产生自然线条 BrushType.pencil: _BrushConfig( size: 4, thinning: 0.3, smoothing: 0.7, 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] 作为乘数影响最终笔画大小。 /// 当 [points] 少于 2 个时返回空列表(无法构成笔画)。 List pointsToOutline( List points, BrushType brushType, double width, ) { 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: true, // 已完成笔画,启用端点处理 ); // 转换为 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); } // ============================================================ // 笔画绘制器 // ============================================================ /// 自定义 [CustomPainter],负责将所有笔画渲染到 Canvas。 /// /// 接收 [completedStrokes](已完成笔画列表)和可选的 [currentStroke] /// (正在绘制中的笔画),分别渲染。 /// /// 渲染策略: /// - 钢笔/铅笔:正常不透明绘制 /// - 马克笔:半透明 (0.4 opacity) 模拟荧光笔效果 /// - 橡皮擦:使用 [BlendMode.dstOut] 擦除底层内容 class StrokePainter extends CustomPainter { final List completedStrokes; final Stroke? currentStroke; StrokePainter({ required this.completedStrokes, this.currentStroke, }); @override void paint(Canvas canvas, Size size) { // 先绘制已完成的笔画 for (final stroke in completedStrokes) { _drawStroke(canvas, stroke); } // 再绘制正在进行的笔画(覆盖在已完成笔画之上) if (currentStroke != null) { _drawStroke(canvas, currentStroke!); } } /// 渲染单条笔画。 void _drawStroke(Canvas canvas, Stroke stroke) { final outlinePoints = pointsToOutline( stroke.points, stroke.brushType, stroke.width, ); if (outlinePoints.isEmpty) return; final path = buildStrokePath(outlinePoints); // 根据画笔类型选择不同的绘制策略 final paint = _createPaint(stroke); canvas.drawPath(path, paint); } /// 根据画笔类型创建对应的 [Paint] 对象。 Paint _createPaint(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 混合模式擦除底层像素 return Paint() ..color = const Color(0xFFFFFFFF) // 颜色无关紧要,blendMode 决定效果 ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut ..isAntiAlias = true; } } /// 判断是否需要重绘。 /// /// 始终比较引用:当 strokes 列表或 currentStroke 发生变化时重绘。 /// 这是最安全的策略,因为笔画数据是不可变的(freezed), /// 引用变化意味着内容一定变化。 @override bool shouldRepaint(StrokePainter oldDelegate) { return !identical(completedStrokes, oldDelegate.completedStrokes) || !identical(currentStroke, oldDelegate.currentStroke); } }