board_painter.dart 12 KB

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