board_painter.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. // board_painter.dart
  2. import 'dart:math';
  3. import 'dart:typed_data';
  4. import 'package:flutter/material.dart';
  5. import 'package:image_puzzle/play/board.dart';
  6. import 'package:image_puzzle/play/piece.dart';
  7. import 'package:logging/logging.dart';
  8. final Logger _log = Logger('board_painter.dart');
  9. class BoardPainter extends CustomPainter {
  10. final Board board;
  11. final AnimationController prepareAnimation;
  12. // 碎片边框宽度
  13. static const double _pieceStrokeWidth = 1.0;
  14. // 边框画笔 (外边框,黑色)
  15. final Paint _outerBorderPaint = Paint()
  16. ..color = Colors.black
  17. ..style = PaintingStyle.stroke
  18. ..strokeWidth = _pieceStrokeWidth
  19. ..isAntiAlias = true;
  20. // 边框画笔 (内边框,白色)
  21. final Paint _innerBorderPaint = Paint()
  22. ..color = Colors.white
  23. ..style = PaintingStyle.stroke
  24. ..strokeWidth = _pieceStrokeWidth
  25. ..isAntiAlias = true;
  26. // 卡片背面画笔
  27. final Paint _cardBackPaint = Paint()..isAntiAlias = true;
  28. BoardPainter({required this.board, required this.prepareAnimation}) : super(repaint: Listenable.merge([board.boardNotifier, prepareAnimation])); // 触发重绘
  29. @override
  30. void paint(Canvas canvas, Size size) {
  31. switch (board.status) {
  32. case BoardStatus.loading:
  33. _paintLoading(canvas, size);
  34. break;
  35. case BoardStatus.preparing:
  36. _paintPreparing(canvas, size);
  37. break;
  38. case BoardStatus.shuffle:
  39. _paintShuffle(canvas, size);
  40. break;
  41. case BoardStatus.playing:
  42. _paintPlaying(canvas, size);
  43. break;
  44. case BoardStatus.success:
  45. _paintSuccess(canvas, size);
  46. break;
  47. }
  48. }
  49. _paintLoading(Canvas canvas, Size size) {
  50. _log.info('_paintLoading');
  51. // 绘制整个背景底色
  52. canvas.drawRect(
  53. Rect.fromLTWH(0, 0, size.width, size.height),
  54. Paint()
  55. ..color = Colors.lightGreen
  56. ..style = PaintingStyle.fill,
  57. );
  58. }
  59. _paintPreparing(Canvas canvas, Size size) {
  60. _log.info('_paintPreparing');
  61. // 绘制整个背景底色, 但不绘制核心宫格游戏区
  62. canvas.drawRect(
  63. Rect.fromLTWH(0, 0, size.width, size.height),
  64. Paint()
  65. ..color = Colors.lightGreen
  66. ..style = PaintingStyle.fill,
  67. );
  68. if (prepareAnimation.isAnimating) {
  69. _paintBackground(canvas, size, (prepareAnimation.value * 255).toInt());
  70. } else {
  71. _paintBackground(canvas, size, 255);
  72. }
  73. }
  74. _paintShuffle(Canvas canvas, Size size) {
  75. _log.info('_paintShuffle');
  76. // 绘制背景
  77. if (board.backgroundPicture != null) {
  78. canvas.drawPicture(board.backgroundPicture!);
  79. }
  80. // 绘制所有卡片
  81. for (final piece in board.pieces) {
  82. _drawDealingPiece(canvas, size, piece);
  83. }
  84. }
  85. /// 绘制进行中的游戏界面
  86. _paintPlaying(Canvas canvas, Size size) {
  87. _log.info('_paintPlaying');
  88. // 1. 绘制背景
  89. if (board.backgroundPicture != null) {
  90. canvas.drawPicture(board.backgroundPicture!);
  91. }
  92. // 2. 收集所有不重复的群组(避免重复绘制)
  93. final Set<PieceGroup> drawnGroups = {};
  94. for (final piece in board.pieces) {
  95. _drawPiece(canvas, size, piece);
  96. // if (piece.group != null) {
  97. // if (!drawnGroups.contains(piece.group)) {
  98. // drawnGroups.add(piece.group!);
  99. // // 调用更新后的 _drawGroup 方法
  100. // _drawGroup(canvas, size, piece.group!);
  101. // }
  102. // } else {
  103. // _drawPiece(canvas, size, piece);
  104. // }
  105. }
  106. }
  107. _paintSuccess(Canvas canvas, Size size) {
  108. // 成功状态的绘制逻辑,目前和 playing 相同
  109. _paintPlaying(canvas, size);
  110. }
  111. void _drawDealingPiece(Canvas canvas, Size size, Piece piece) {
  112. final Paint outerBorderPaint = Paint()
  113. ..color = Colors.black
  114. ..style = PaintingStyle.stroke
  115. ..strokeWidth = 1.0
  116. ..isAntiAlias = true;
  117. // 边框画笔
  118. final Paint innerBorderPaint = Paint()
  119. ..color = Colors.white
  120. ..style = PaintingStyle.stroke
  121. ..strokeWidth = 2.0
  122. ..isAntiAlias = true;
  123. final w = piece.width;
  124. final h = piece.height;
  125. final storage64 = Float64List.fromList(piece.transform.storage);
  126. final rrect = RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, w, h), Radius.circular(8.0));
  127. final outerRRect = RRect.fromRectAndRadius(Rect.fromLTWH(0.5, 0.5, w - 1.0, h - 1.0), Radius.circular(8.0));
  128. final innerRRect = RRect.fromRectAndRadius(Rect.fromLTWH(2.5, 2.5, w - 5.0, h - 5.0), Radius.circular(8.0));
  129. final Rect dstRect = Rect.fromLTWH(0, 0, w, h);
  130. canvas.save();
  131. canvas.transform(storage64); // 应用平移
  132. canvas.clipRRect(rrect);
  133. // 绘制图片:只有落在 rrect 内部的部分图片会被绘制
  134. if (!piece.isFlipped) {
  135. // _log.info("show card");
  136. final Rect sourceRect = Rect.fromLTWH(0, 0, board.cardImage.width.toDouble(), board.cardImage.height.toDouble());
  137. canvas.drawImageRect(board.cardImage, sourceRect, dstRect, Paint()..isAntiAlias = true);
  138. } else {
  139. // _log.info("show image");
  140. canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true);
  141. }
  142. canvas.restore(); // 恢复 Canvas 状态 (移除裁剪和 transform)
  143. // --- 绘制边框 ---
  144. // 为了确保边框线宽向外延伸的部分不会被裁剪,我们需要在裁剪区域被移除后重新绘制。
  145. canvas.save();
  146. canvas.transform(storage64); // 重新应用平移
  147. if (!piece.isFlipped) {
  148. canvas.drawRRect(outerRRect, outerBorderPaint);
  149. canvas.drawRRect(innerRRect, innerBorderPaint);
  150. } else {
  151. final List<Path> paths = _getPiecePath(piece);
  152. final Path outerBorderPath = paths[1];
  153. final Path innerBorderPath = paths[2];
  154. canvas.drawPath(outerBorderPath, _outerBorderPaint); // 外边框,黑色
  155. canvas.drawPath(innerBorderPath, _innerBorderPaint); // 内边框,白色
  156. }
  157. canvas.restore();
  158. }
  159. /// 绘制单个碎片
  160. void _drawPiece(Canvas canvas, Size size, Piece piece) {
  161. final double w = piece.width;
  162. final double h = piece.height;
  163. // 1. 准备变换矩阵和动态 Path
  164. final Float64List storage64 = Float64List.fromList(piece.transform.storage);
  165. // 核心:根据 borders 状态动态生成 Path
  166. final List<Path> paths = _getPiecePath(piece);
  167. final Path piecePath = paths[0];
  168. final Path outerBorderPath = paths[1];
  169. final Path innerBorderPath = paths[2];
  170. canvas.save();
  171. canvas.transform(storage64); // 应用平移
  172. // 裁剪区域:使用动态生成的 Path 来裁剪图片,实现圆角/尖角混合
  173. canvas.clipPath(piecePath);
  174. // 绘制图片:只有落在 rrect 内部的部分图片会被绘制
  175. final Rect dstRect = Rect.fromLTWH(0, 0, w, h);
  176. canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true);
  177. canvas.restore(); // 恢复 Canvas 状态 (移除裁剪和 transform)
  178. // --- 绘制边框 ---
  179. // 为了确保边框线宽向外延伸的部分不会被裁剪,我们需要在裁剪区域被移除后重新绘制。
  180. canvas.save();
  181. canvas.transform(storage64); // 重新应用平移
  182. // 绘制碎片边框
  183. canvas.drawPath(outerBorderPath, _outerBorderPaint); // 外边框,黑色
  184. canvas.drawPath(innerBorderPath, _innerBorderPaint); // 内边框,白色
  185. canvas.restore();
  186. }
  187. /// 绘制整个群组(核心实现)
  188. void _drawGroup(Canvas canvas, Size size, PieceGroup group) {
  189. if (group.pieces.isEmpty) return;
  190. // 1. 准备群组基础数据
  191. final topLeftPiece = group.topLeftPiece;
  192. group.generateGroupPaths(); // 确保路径已生成
  193. // 2. 群组基准点变换(基于左上角碎片的绝对位置)
  194. final baseTransform = Float64List.fromList(topLeftPiece.transform.storage);
  195. // 3. 计算群组内碎片的相对偏移(相对于左上角碎片)
  196. final minR = topLeftPiece.curRow;
  197. final minC = topLeftPiece.curCol;
  198. final pieceWidth = topLeftPiece.width;
  199. final pieceHeight = topLeftPiece.height;
  200. // 4. 绘制群组图片(整体裁剪 + 拼接碎片)
  201. canvas.save();
  202. canvas.transform(baseTransform); // 移动到群组基准点
  203. // 4.1 应用群组裁剪路径(确保超出群组范围的内容不显示)
  204. if (group.groupClipPath != null) {
  205. canvas.clipPath(group.groupClipPath!);
  206. }
  207. // 4.2 绘制群组内所有碎片的图片部分(相对位置拼接)
  208. for (final piece in group.pieces) {
  209. // 计算碎片在群组内的相对坐标(相对于左上角碎片)
  210. final dr = piece.curRow - minR;
  211. final dc = piece.curCol - minC;
  212. final dx = dc * pieceWidth;
  213. final dy = dr * pieceHeight;
  214. // 绘制碎片图片到群组内的对应位置
  215. final dstRect = Rect.fromLTWH(dx, dy, pieceWidth, pieceHeight);
  216. canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true);
  217. }
  218. canvas.restore(); // 恢复裁剪和基准变换
  219. // 5. 绘制群组边框(外边框 + 内边框)
  220. canvas.save();
  221. canvas.transform(baseTransform); // 重新应用基准变换
  222. if (group.groupOutLinePath != null) {
  223. canvas.drawPath(group.groupOutLinePath!, _outerBorderPaint);
  224. }
  225. if (group.groupInnerLinePath != null) {
  226. canvas.drawPath(group.groupInnerLinePath!, _innerBorderPaint);
  227. }
  228. canvas.restore();
  229. }
  230. List<Path> _getPiecePath(Piece piece) {
  231. // 如果是单个碎片,直接使用 piece 自身的方法获取路径
  232. if (piece.path == null || piece.outLinePath == null || piece.innerLinePath == null) {
  233. return piece.generatePaths();
  234. } else {
  235. return [piece.path!, piece.outLinePath!, piece.innerLinePath!];
  236. }
  237. }
  238. // 绘制背景(已经被drawPicture取代)
  239. void _paintBackground(Canvas canvas, Size size, int alpha) {
  240. final targetRect = board.targetRect;
  241. final pieceLogicalWidth = board.pieceLogicalWidth;
  242. final pieceLogicalHeight = board.pieceLogicalHeight;
  243. // --- 静态绘制配置 ---
  244. const double cornerRadius = 8.0;
  245. const double strokeWidth = 1.0; // 拼图槽位的线宽
  246. final double halfStroke = strokeWidth / 2.0;
  247. // 1. 绘制整个屏幕背景
  248. canvas.drawRect(
  249. Rect.fromLTWH(0, 0, size.width, size.height),
  250. Paint()
  251. ..color = Colors.lightGreen
  252. ..style = PaintingStyle.fill,
  253. );
  254. // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容)
  255. final slotFillPaint = Paint()
  256. ..color = Colors.green.withAlpha(alpha)
  257. ..style = PaintingStyle.fill;
  258. final slotStrokePaint = Paint()
  259. ..color = Color(0xff26600c).withAlpha(alpha)
  260. ..style = PaintingStyle.stroke
  261. ..strokeWidth = strokeWidth;
  262. for (int r = 0; r < board.rows; r++) {
  263. for (int c = 0; c < board.cols; c++) {
  264. // 计算当前槽位的边界 (Canvas坐标系)
  265. final left = targetRect.left + c * pieceLogicalWidth;
  266. final top = targetRect.top + r * pieceLogicalHeight;
  267. final right = left + pieceLogicalWidth;
  268. final bottom = top + pieceLogicalHeight;
  269. // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
  270. final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke);
  271. final slotRRect = RRect.fromRectAndRadius(slotRect, const Radius.circular(cornerRadius));
  272. // 绘制填充
  273. canvas.drawRRect(slotRRect, slotFillPaint);
  274. // 绘制描边
  275. canvas.drawRRect(slotRRect, slotStrokePaint);
  276. }
  277. }
  278. }
  279. @override
  280. bool shouldRepaint(covariant BoardPainter oldDelegate) {
  281. // 检查不可变的核心数据是否发生变化
  282. if (oldDelegate.board != board || oldDelegate.prepareAnimation != prepareAnimation) {
  283. return true;
  284. }
  285. // 如果 board 和 animation controller 引用没有变化,
  286. // 由于构造函数中已经添加了监听器,理论上只需要在监听器触发时重绘。
  287. // 但为了确保,可以比较 board 的状态,因为它决定了执行哪个绘制逻辑。
  288. return oldDelegate.board.status != board.status;
  289. }
  290. }