Files
BlockSeasons/lib/ui/widgets/board_painter.dart
T
airkjw 3138fc4b08 Add playable core UI: board painter, drag-and-drop, HUD, result overlay
CustomPainter board with gems/ghost/clear-flash, finger-lifted drag
with snap preview, combo text effect, HUD chips, phase overlays with
rescue stubs, demo stage. E2E widget test drives a real drag gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:19:34 +09:00

138 lines
3.8 KiB
Dart

import 'package:flutter/material.dart';
import '../../game/models/cell.dart';
import '../../game/models/grid.dart';
import '../../game/models/piece.dart';
import '../theme/palette.dart';
import 'board_geometry.dart';
import 'piece_painter.dart';
/// Drag ghost preview: a piece hovering at a snapped anchor.
class GhostSpec {
const GhostSpec({
required this.piece,
required this.anchorX,
required this.anchorY,
required this.legal,
});
final Piece piece;
final int anchorX;
final int anchorY;
final bool legal;
}
class BoardPainter extends CustomPainter {
const BoardPainter({
required this.grid,
required this.ghost,
required this.flashProgress,
required this.flashRows,
required this.flashCols,
});
final GridState grid;
final GhostSpec? ghost;
/// 1.0 -> 0.0 while the clear flash fades.
final double flashProgress;
final List<int> flashRows;
final List<int> flashCols;
@override
void paint(Canvas canvas, Size size) {
final geo = BoardGeometry(boardSize: size.width);
final radius = Radius.circular(geo.cellSize * 0.18);
final inset = geo.cellSize * 0.05;
final bg = Paint()..color = GamePalette.boardBackground;
canvas.drawRRect(
RRect.fromRectAndRadius(Offset.zero & size, const Radius.circular(12)),
bg,
);
for (var y = 0; y < GridState.size; y++) {
for (var x = 0; x < GridState.size; x++) {
final rect = geo.cellRect(x, y).deflate(inset);
final cell = grid.cellAt(x, y);
final paint = Paint()
..color = switch (cell.type) {
CellType.empty => GamePalette.emptyCell,
CellType.filled => GamePalette.tile(cell.colorId),
CellType.gem => GamePalette.emptyCell,
};
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
if (cell.type == CellType.gem) {
_paintGem(canvas, rect);
} else if (cell.type == CellType.filled) {
final highlight = Paint()
..color = Colors.white.withValues(alpha: 0.15);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(
rect.left, rect.top, rect.width, rect.height * 0.32),
radius,
),
highlight,
);
}
}
}
final g = ghost;
if (g != null) {
paintPiece(
canvas,
g.piece,
cellSize: geo.cellSize,
origin: Offset(g.anchorX * geo.cellSize, g.anchorY * geo.cellSize),
overrideColor:
g.legal ? GamePalette.ghostLegal : GamePalette.ghostIllegal,
);
}
if (flashProgress > 0) {
final flash = Paint()
..color = Colors.white.withValues(alpha: 0.75 * flashProgress);
for (final y in flashRows) {
canvas.drawRect(
Rect.fromLTWH(0, y * geo.cellSize, size.width, geo.cellSize),
flash,
);
}
for (final x in flashCols) {
canvas.drawRect(
Rect.fromLTWH(x * geo.cellSize, 0, geo.cellSize, size.height),
flash,
);
}
}
}
void _paintGem(Canvas canvas, Rect rect) {
final center = rect.center;
final r = rect.width * 0.32;
final path = Path()
..moveTo(center.dx, center.dy - r)
..lineTo(center.dx + r, center.dy)
..lineTo(center.dx, center.dy + r)
..lineTo(center.dx - r, center.dy)
..close();
canvas.drawPath(path, Paint()..color = GamePalette.gem);
canvas.drawPath(
path,
Paint()
..color = Colors.white.withValues(alpha: 0.6)
..style = PaintingStyle.stroke
..strokeWidth = rect.width * 0.05,
);
}
@override
bool shouldRepaint(BoardPainter old) =>
old.grid != grid ||
old.ghost != ghost ||
old.flashProgress != flashProgress;
}