// board_painter.dart import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:puzzleweave/play/board.dart'; import 'package:puzzleweave/play/piece.dart'; import 'package:puzzleweave/skin/skin.dart'; import 'package:logging/logging.dart'; final Logger _log = Logger('board_painter.dart'); class BoardPainter extends CustomPainter { final Board board; final AnimationController prepareAnimation; // 碎片边框宽度 static const double _pieceStrokeWidth = 1.0; // 边框画笔 (外边框,黑色) final Paint _outerBorderPaint = Paint() ..color = SkinHelper.outLineBorderColor ..style = PaintingStyle.stroke ..strokeWidth = _pieceStrokeWidth ..isAntiAlias = true; // 边框画笔 (内边框,白色) final Paint _innerBorderPaint = Paint() ..color = SkinHelper.innerLineBorderColor ..style = PaintingStyle.stroke ..strokeWidth = _pieceStrokeWidth ..isAntiAlias = true; BoardPainter({required this.board, required this.prepareAnimation}) : super(repaint: Listenable.merge([board.boardNotifier, prepareAnimation])); // 触发重绘 @override void paint(Canvas canvas, Size size) { switch (board.status) { case BoardStatus.loading: _paintLoading(canvas, size); break; case BoardStatus.preparing: _paintPreparing(canvas, size); break; case BoardStatus.shuffle: _paintShuffle(canvas, size); break; case BoardStatus.playing: _paintPlaying(canvas, size); break; case BoardStatus.success: _paintSuccess(canvas, size); break; } } _paintLoading(Canvas canvas, Size size) { // _log.info('_paintLoading'); // 绘制整个背景底色 canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height), Paint() ..color = SkinHelper.wholeBgColor ..style = PaintingStyle.fill, ); } _paintPreparing(Canvas canvas, Size size) { // _log.info('_paintPreparing'); // 绘制整个背景底色, 但不绘制核心宫格游戏区 canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height), Paint() ..color = SkinHelper.wholeBgColor ..style = PaintingStyle.fill, ); if (prepareAnimation.isAnimating) { _paintBackground(canvas, size, (prepareAnimation.value * 255).toInt()); } else { _paintBackground(canvas, size, 255); } } _paintShuffle(Canvas canvas, Size size) { // _log.info('_paintShuffle'); // 绘制背景 if (board.backgroundPicture != null) { canvas.drawPicture(board.backgroundPicture!); } // 绘制所有卡片 for (final piece in board.pieces) { _drawDealingPiece(canvas, size, piece); } } /// 绘制进行中的游戏界面 _paintPlaying(Canvas canvas, Size size) { // _log.info('_paintPlaying'); // 1. 绘制背景 if (board.backgroundPicture != null) { canvas.drawPicture(board.backgroundPicture!); } // 2. 收集所有不重复的群组(避免重复绘制) // final Set drawnGroups = {}; for (final piece in board.pieces) { _drawPiece(canvas, size, piece); // if (piece.group != null) { // if (!drawnGroups.contains(piece.group)) { // drawnGroups.add(piece.group!); // _drawGroup(canvas, size, piece.group!); // } // } else { // _drawPiece(canvas, size, piece); // } } } _paintSuccess(Canvas canvas, Size size) { final cornerRadius = board.cornerRadius; final targetRect = board.finalRect; final rrect = RRect.fromRectAndRadius(targetRect, Radius.circular(cornerRadius)); final sourceRect = Rect.fromLTWH(0, 0, board.image.width.toDouble(), board.image.height.toDouble()); final Paint outerBorderPaint = Paint() ..color = SkinHelper.outLineBorderColor ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..isAntiAlias = true; // 边框画笔 final Paint innerBorderPaint = Paint() ..color = SkinHelper.innerLineBorderColor ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..isAntiAlias = true; final outerRRect = RRect.fromRectAndRadius(targetRect.deflate(0.5), Radius.circular(cornerRadius)); final innerRRect = RRect.fromRectAndRadius(targetRect.deflate(1.5), Radius.circular(cornerRadius)); // 1. 绘制整个屏幕背景 canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height), Paint() ..color = SkinHelper.wholeBgColor ..style = PaintingStyle.fill, ); canvas.save(); canvas.clipRRect(rrect); canvas.drawImageRect(board.image, sourceRect, targetRect, Paint()..isAntiAlias = true); canvas.restore(); // 绘制边框 canvas.drawRRect(outerRRect, outerBorderPaint); canvas.drawRRect(innerRRect, innerBorderPaint); } void _drawDealingPiece(Canvas canvas, Size size, Piece piece) { final w = piece.width; final h = piece.height; final storage64 = Float64List.fromList(piece.transform.storage); canvas.save(); canvas.transform(storage64); // 应用平移 // --- 核心改造:完全使用 cardPicture 绘制未翻转的卡牌 --- if (!piece.isFlipped && board.cardPicture != null) { // 一次 drawPicture 调用完成:图片、裁剪、外边框、内边框 canvas.drawPicture(board.cardPicture!); } else if (piece.isFlipped) { // 保持翻转状态的绘制逻辑不变 (这是原始图片碎片,不是卡牌背面) final cornerRadius = board.cornerRadius; final rrect = RRect.fromRectAndRadius(Rect.fromLTWH(0, 0, w, h), Radius.circular(cornerRadius)); // 绘制图片 (需要裁剪) canvas.save(); canvas.clipRRect(rrect); final Rect dstRect = Rect.fromLTWH(0, 0, w, h); canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true); canvas.restore(); // 绘制边框 (使用动态 Path,因为这可能是连接后的碎片) canvas.save(); // 再次保存,用于边框 // 这里的 transform 已经应用了,所以直接绘制 path final List paths = _getPiecePath(piece); final Path outerBorderPath = paths[1]; final Path innerBorderPath = paths[2]; canvas.drawPath(outerBorderPath, _outerBorderPaint); // 外边框,黑色 canvas.drawPath(innerBorderPath, _innerBorderPaint); // 内边框,白色 canvas.restore(); } canvas.restore(); // 恢复 Canvas 状态 (移除 piece.transform) } /// 绘制单个碎片 void _drawPiece(Canvas canvas, Size size, Piece piece) { final double w = piece.width; final double h = piece.height; // 1. 准备变换矩阵和动态 Path final Float64List storage64 = Float64List.fromList(piece.transform.storage); // 核心:根据 borders 状态动态生成 Path final List paths = _getPiecePath(piece); final Path piecePath = paths[0]; final Path outerBorderPath = paths[1]; final Path innerBorderPath = paths[2]; canvas.save(); canvas.transform(storage64); // 应用平移 // 裁剪区域:使用动态生成的 Path 来裁剪图片,实现圆角/尖角混合 canvas.clipPath(piecePath); // 绘制图片:只有落在 rrect 内部的部分图片会被绘制 final Rect dstRect = Rect.fromLTWH(0, 0, w, h); canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true); canvas.restore(); // 恢复 Canvas 状态 (移除裁剪和 transform) // --- 绘制边框 --- // 为了确保边框线宽向外延伸的部分不会被裁剪,我们需要在裁剪区域被移除后重新绘制。 canvas.save(); canvas.transform(storage64); // 重新应用平移 // 绘制碎片边框 canvas.drawPath(outerBorderPath, _outerBorderPaint); // 外边框,黑色 canvas.drawPath(innerBorderPath, _innerBorderPaint); // 内边框,白色 canvas.restore(); } /// 绘制整个群组(核心实现) void _drawGroup(Canvas canvas, Size size, PieceGroup group) { if (group.pieces.isEmpty) return; // 1. 准备群组基础数据 final topLeftPiece = group.topLeftPiece; group.generateGroupPaths(); // 确保路径已生成 // 2. 群组基准点变换(基于左上角碎片的绝对位置) final baseTransform = Float64List.fromList(topLeftPiece.transform.storage); // 3. 计算群组内碎片的相对偏移(相对于左上角碎片) final minR = topLeftPiece.curRow; final minC = topLeftPiece.curCol; final pieceWidth = topLeftPiece.width; final pieceHeight = topLeftPiece.height; // 4. 绘制群组图片(整体裁剪 + 拼接碎片) canvas.save(); canvas.transform(baseTransform); // 移动到群组基准点 // 4.1 应用群组裁剪路径(确保超出群组范围的内容不显示) if (group.groupClipPath != null) { canvas.clipPath(group.groupClipPath!); } // 4.2 绘制群组内所有碎片的图片部分(相对位置拼接) for (final piece in group.pieces) { // 计算碎片在群组内的相对坐标(相对于左上角碎片) final dr = piece.curRow - minR; final dc = piece.curCol - minC; final dx = dc * pieceWidth; final dy = dr * pieceHeight; // 绘制碎片图片到群组内的对应位置 final dstRect = Rect.fromLTWH(dx, dy, pieceWidth, pieceHeight); canvas.drawImageRect(board.image, piece.sourceRect, dstRect, Paint()..isAntiAlias = true); } canvas.restore(); // 恢复裁剪和基准变换 // 5. 绘制群组边框(外边框 + 内边框) canvas.save(); canvas.transform(baseTransform); // 重新应用基准变换 if (group.groupOutLinePath != null) { canvas.drawPath(group.groupOutLinePath!, _outerBorderPaint); } if (group.groupInnerLinePath != null) { canvas.drawPath(group.groupInnerLinePath!, _innerBorderPaint); } canvas.restore(); } List _getPiecePath(Piece piece) { // 如果是单个碎片,直接使用 piece 自身的方法获取路径 if (piece.path == null || piece.outLinePath == null || piece.innerLinePath == null) { return piece.generatePaths(); } else { return [piece.path!, piece.outLinePath!, piece.innerLinePath!]; } } // 绘制背景(已经被drawPicture取代) void _paintBackground(Canvas canvas, Size size, int alpha) { final targetRect = board.targetRect; final pieceLogicalWidth = board.pieceLogicalWidth; final pieceLogicalHeight = board.pieceLogicalHeight; // --- 静态绘制配置 --- final double cornerRadius = board.cornerRadius; const double strokeWidth = 1.0; // 拼图槽位的线宽 final double halfStroke = strokeWidth / 2.0; // 1. 绘制整个屏幕背景 canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height), Paint() ..color = SkinHelper.wholeBgColor ..style = PaintingStyle.fill, ); // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容) final slotFillPaint = Paint() ..color = SkinHelper.slotBgColor.withAlpha(alpha) ..style = PaintingStyle.fill; final slotStrokePaint = Paint() ..color = SkinHelper.slotBorderColor.withAlpha(alpha) ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; for (int r = 0; r < board.rows; r++) { for (int c = 0; c < board.cols; c++) { // 计算当前槽位的边界 (Canvas坐标系) final left = targetRect.left + c * pieceLogicalWidth; final top = targetRect.top + r * pieceLogicalHeight; final right = left + pieceLogicalWidth; final bottom = top + pieceLogicalHeight; // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke); final slotRRect = RRect.fromRectAndRadius(slotRect, Radius.circular(cornerRadius)); // 绘制填充 canvas.drawRRect(slotRRect, slotFillPaint); // 绘制描边 canvas.drawRRect(slotRRect, slotStrokePaint); } } } @override bool shouldRepaint(covariant BoardPainter oldDelegate) { // 优化:只在必要时重绘 return oldDelegate.board != board || oldDelegate.prepareAnimation != prepareAnimation || oldDelegate.board.status != board.status; } @override bool shouldRebuildSemantics(covariant BoardPainter oldDelegate) => false; }