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 手势,支持双指缩放和旋转
189 lines
5.8 KiB
Dart
189 lines
5.8 KiB
Dart
// 笔画渲染工具 — 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;
|
||
}
|
||
}
|