| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- // 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<PieceGroup> 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<Path> 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<Path> 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<Path> _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;
- }
|