Files
nj/app/lib/features/editor/widgets/stroke_renderer.dart
iven 9fce34f4ef
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): 修复 4 个 Flutter 交互问题
1. 首页数据不刷新 — JournalRepository 添加 onJournalChanged
   Stream 变更通知,HomeBloc 订阅后自动刷新
2. 画笔再次点击不弹出面板 — 添加 ToolReactivated 事件,
   工具栏检测已激活工具时发出重新激活信号
3. 钢笔铅笔效果一样 — 调整 perfect_freehand 参数
   (pen: size 10/smooth 0.65, pencil: size 3/smooth 0.35)
4. 橡皮擦不生效 — ActiveStrokePainter 橡皮擦模式绘制
   半透明灰色反馈,笔画完成后 setState 触发 Layer 1 重绘
5. 贴纸文字无法缩放 — DraggableElement 用 Scale 手势
   替换 Pan 手势,支持双指缩放和旋转
2026-06-04 00:05:22 +08:00

189 lines
5.8 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: 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<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;
}
}