perf(app): 手写引擎性能优化 — 双层架构 + 光栅化缓存 + O(1) 点缓冲
性能优化: - 新建 StrokeRasterCache: 已完成笔画光栅化为 ui.Image 合成位图 - 新建 CachedStrokesPainter: 每帧仅 drawImage,O(1) 开销 - 新建 ActiveStrokePainter: 仅渲染当前笔画,isComplete: false - _currentPoints 改为可变缓冲区 + ValueNotifier 驱动,消除 O(N²) 列表拷贝 - 双层 Stack 架构: 已缓存层(不随指针移动重绘) + 实时层(仅当前笔画) Bug 修复: - 橡皮擦 saveLayer 合成: BlendMode.dstOut 在离屏缓冲区中正确工作 - pointsToOutline 新增 isComplete 参数: 实时绘制传 false,完成笔画传 true - 模式切换不再销毁 HandwritingCanvas: IgnorePointer 替代 if/else 分支 架构改进: - 提取 createPaintForStroke() 为顶层函数,供缓存和 Painter 共用 - 移除旧 StrokePainter 类,由双层 Painter 替代 - LayoutBuilder 跟踪画布尺寸,尺寸变化时缓存自动失效 文件变更: - 新建 stroke_cache.dart (~210 行) - 新建 cached_strokes_painter.dart (~35 行) - 新建 active_stroke_painter.dart (~70 行) - 重写 handwriting_canvas.dart (~300 行) - 重构 stroke_renderer.dart (~185 行, 移除旧 Painter) - 修改 editor_page.dart (IgnorePointer 模式切换) 验证: flutter analyze 0 error
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
// 笔画渲染器 — 将 StrokePoint 列表转换为 Flutter 可绘制路径。
|
||||
// 笔画渲染工具 — perfect_freehand 参数配置 + 路径构建 + 画笔 Paint 创建
|
||||
//
|
||||
// 核心流程:
|
||||
// StrokePoint[] → perfect_freehand.getStroke() → Point[] (轮廓多边形)
|
||||
// → buildStrokePath() → Path → CustomPainter.drawPath()
|
||||
// → buildStrokePath() → Path → Canvas.drawPath()
|
||||
//
|
||||
// 每种画笔(pen/pencil/marker/eraser)拥有独立的 perfect_freehand 参数,
|
||||
// 产生不同的视觉效果。
|
||||
// 本文件提供纯函数工具,供多个 Painter 和缓存系统复用:
|
||||
// - ActiveStrokePainter(当前笔画实时渲染)
|
||||
// - CachedStrokesPainter(合成位图绘制)
|
||||
// - StrokeRasterCache(单笔画光栅化)
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:perfect_freehand/perfect_freehand.dart' as pf;
|
||||
@@ -77,12 +79,14 @@ const Map<BrushType, _BrushConfig> _brushConfigs = {
|
||||
/// 将 [StrokePoint] 列表转换为 perfect_freehand 轮廓点列表。
|
||||
///
|
||||
/// [brushType] 决定渲染参数,[width] 作为乘数影响最终笔画大小。
|
||||
/// [isComplete] 控制端点处理:已完成笔画传 true,实时绘制传 false。
|
||||
/// 当 [points] 少于 2 个时返回空列表(无法构成笔画)。
|
||||
List<Offset> pointsToOutline(
|
||||
List<StrokePoint> points,
|
||||
BrushType brushType,
|
||||
double width,
|
||||
) {
|
||||
double width, {
|
||||
bool isComplete = true,
|
||||
}) {
|
||||
if (points.length < 2) return const [];
|
||||
|
||||
final config = _brushConfigs[brushType]!;
|
||||
@@ -101,7 +105,7 @@ List<Offset> pointsToOutline(
|
||||
smoothing: config.smoothing,
|
||||
streamline: config.streamline,
|
||||
simulatePressure: config.simulatePressure,
|
||||
isComplete: true, // 已完成笔画,启用端点处理
|
||||
isComplete: isComplete,
|
||||
);
|
||||
|
||||
// 转换为 Flutter Offset
|
||||
@@ -131,7 +135,7 @@ Path buildStrokePath(List<Offset> outlinePoints) {
|
||||
/// 将 CSS 十六进制颜色字符串 (#RRGGBB) 解析为 Flutter [Color]。
|
||||
///
|
||||
/// 如果解析失败,回退到默认的深色文字色 #2D2420。
|
||||
Color _parseHexColor(String hex) {
|
||||
Color parseHexColor(String hex) {
|
||||
final hexStr = hex.replaceFirst('#', '');
|
||||
if (hexStr.length != 6) return const Color(0xFF2D2420);
|
||||
|
||||
@@ -142,94 +146,41 @@ Color _parseHexColor(String hex) {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 笔画绘制器
|
||||
// 画笔 Paint 创建
|
||||
// ============================================================
|
||||
|
||||
/// 自定义 [CustomPainter],负责将所有笔画渲染到 Canvas。
|
||||
/// 根据画笔类型创建对应的 [Paint] 对象。
|
||||
///
|
||||
/// 接收 [completedStrokes](已完成笔画列表)和可选的 [currentStroke]
|
||||
/// (正在绘制中的笔画),分别渲染。
|
||||
///
|
||||
/// 渲染策略:
|
||||
/// - 钢笔/铅笔:正常不透明绘制
|
||||
/// - 马克笔:半透明 (0.4 opacity) 模拟荧光笔效果
|
||||
/// - 橡皮擦:使用 [BlendMode.dstOut] 擦除底层内容
|
||||
class StrokePainter extends CustomPainter {
|
||||
final List<Stroke> completedStrokes;
|
||||
final Stroke? currentStroke;
|
||||
/// 供 StrokePainter、StrokeRasterCache、ActiveStrokePainter 共用。
|
||||
/// - 钢笔/铅笔:不透明实心绘制
|
||||
/// - 马克笔:半透明 (0.4 opacity) 模拟荧光笔
|
||||
/// - 橡皮擦:使用 [BlendMode.dstOut] 擦除(需配合 saveLayer)
|
||||
Paint createPaintForStroke(Stroke stroke) {
|
||||
final color = parseHexColor(stroke.color);
|
||||
|
||||
StrokePainter({
|
||||
required this.completedStrokes,
|
||||
this.currentStroke,
|
||||
});
|
||||
switch (stroke.brushType) {
|
||||
case BrushType.pen:
|
||||
case BrushType.pencil:
|
||||
// 钢笔和铅笔:不透明实心绘制
|
||||
return Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// 先绘制已完成的笔画
|
||||
for (final stroke in completedStrokes) {
|
||||
_drawStroke(canvas, stroke);
|
||||
}
|
||||
case BrushType.marker:
|
||||
// 马克笔:半透明模拟荧光笔
|
||||
return Paint()
|
||||
..color = color.withValues(alpha: 0.4)
|
||||
..style = PaintingStyle.fill
|
||||
..isAntiAlias = true;
|
||||
|
||||
// 再绘制正在进行的笔画(覆盖在已完成笔画之上)
|
||||
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);
|
||||
case BrushType.eraser:
|
||||
// 橡皮擦:使用 dstOut 混合模式擦除底层像素
|
||||
// 必须在 saveLayer 内使用才能正确合成
|
||||
return Paint()
|
||||
..color = const Color(0xFFFFFFFF)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.dstOut
|
||||
..isAntiAlias = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user