Files
nj/app/lib/features/editor/widgets/stroke_renderer.dart
iven e07da7addb 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
2026-06-01 13:18:36 +08:00

187 lines
5.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 笔画渲染工具 — 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<BrushType, _BrushConfig> _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] 作为乘数影响最终笔画大小。
/// [isComplete] 控制端点处理:已完成笔画传 true实时绘制传 false。
/// 当 [points] 少于 2 个时返回空列表(无法构成笔画)。
List<Offset> pointsToOutline(
List<StrokePoint> 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<Offset> 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;
}
}