343 lines
No EOL
22 KiB
Dart
343 lines
No EOL
22 KiB
Dart
// ===========================================================================
|
|
// FILE: lib/ui/game/board_painter.dart
|
|
// ===========================================================================
|
|
|
|
import 'dart:math';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../models/game_board.dart';
|
|
import '../../core/app_colors.dart';
|
|
|
|
class BoardPainter extends CustomPainter {
|
|
final GameBoard board;
|
|
final ThemeColors theme;
|
|
final AppThemeType themeType;
|
|
final double blinkValue;
|
|
|
|
final bool isOnline;
|
|
final bool isVsCPU;
|
|
final bool isSetupPhase;
|
|
final Player myPlayer;
|
|
final Player jokerTurn;
|
|
|
|
BoardPainter({
|
|
required this.board,
|
|
required this.theme,
|
|
required this.themeType,
|
|
required this.isOnline,
|
|
required this.isVsCPU,
|
|
required this.isSetupPhase,
|
|
required this.myPlayer,
|
|
required this.jokerTurn,
|
|
this.blinkValue = 0.0
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
if (themeType == AppThemeType.doodle) {
|
|
final Paint paperGridPaint = Paint()
|
|
..color = Colors.grey.withOpacity(0.3)
|
|
..strokeWidth = 1.0
|
|
..style = PaintingStyle.stroke;
|
|
|
|
double paperStep = 20.0;
|
|
for (double i = 0; i <= size.width; i += paperStep) {
|
|
canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
|
|
}
|
|
for (double i = 0; i <= size.height; i += paperStep) {
|
|
canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
|
|
}
|
|
}
|
|
|
|
int gridPoints = board.columns + 1;
|
|
double spacing = size.width / gridPoints;
|
|
double offset = spacing / 2;
|
|
Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);
|
|
|
|
for (var box in board.boxes) {
|
|
Offset p1 = getScreenPos(box.x, box.y);
|
|
Offset p2 = getScreenPos(box.x + 1, box.y + 1);
|
|
Rect rect = Rect.fromPoints(p1, p2);
|
|
|
|
if (box.type == BoxType.invisible) {
|
|
if (box.isRevealed) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (box.type == BoxType.ice && box.owner == Player.none) {
|
|
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
|
|
}
|
|
|
|
if (box.owner != Player.none) {
|
|
final boxPaint = Paint()
|
|
..style = PaintingStyle.fill
|
|
..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);
|
|
|
|
if (themeType == AppThemeType.wood) {
|
|
_drawFlameBox(canvas, rect, box.owner == Player.red);
|
|
} else if (themeType == AppThemeType.doodle) {
|
|
Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
|
|
_drawScribbleBox(canvas, rect, penColor);
|
|
} else if (themeType == AppThemeType.arcade) {
|
|
_drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
|
} else if (themeType == AppThemeType.grimorio) {
|
|
_drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
|
} else {
|
|
canvas.drawRect(rect, boxPaint);
|
|
}
|
|
}
|
|
|
|
if (box.hiddenJokerOwner != null) {
|
|
Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;
|
|
|
|
if (box.isJokerRevealed) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
|
|
} else {
|
|
bool canSee = false;
|
|
if (isOnline || isVsCPU) {
|
|
canSee = box.hiddenJokerOwner == myPlayer;
|
|
} else {
|
|
canSee = false;
|
|
}
|
|
if (canSee) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (box.type == BoxType.gold) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber);
|
|
} else if (box.type == BoxType.bomb) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple);
|
|
} else if (box.type == BoxType.swap) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent);
|
|
} else if (box.type == BoxType.ice) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent);
|
|
} else if (box.type == BoxType.multiplier) {
|
|
_drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent);
|
|
}
|
|
}
|
|
|
|
for (var line in board.lines) {
|
|
if (!line.isPlayable) continue;
|
|
|
|
Offset p1 = getScreenPos(line.p1.x, line.p1.y);
|
|
Offset p2 = getScreenPos(line.p2.x, line.p2.y);
|
|
|
|
if (line.isIceCracked) {
|
|
_drawCrackedIceLine(canvas, p1, p2, blinkValue);
|
|
continue;
|
|
}
|
|
|
|
bool isLastMove = (line == board.lastMove);
|
|
Color lineColor = line.owner == Player.none
|
|
? theme.gridLine.withOpacity(0.4)
|
|
: (line.owner == Player.red ? theme.playerRed : theme.playerBlue);
|
|
|
|
if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
|
|
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
|
|
}
|
|
|
|
if (themeType == AppThemeType.wood) {
|
|
if (line.owner == Player.none) {
|
|
canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
|
|
} else {
|
|
Color headColor = lineColor;
|
|
if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
|
|
_drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
|
|
}
|
|
} else if (themeType == AppThemeType.cyberpunk) {
|
|
_drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
|
} else if (themeType == AppThemeType.doodle) {
|
|
Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
|
|
if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
|
|
_drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
|
} else if (themeType == AppThemeType.arcade) {
|
|
_drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
|
} else if (themeType == AppThemeType.grimorio) {
|
|
_drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
|
|
} else {
|
|
if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
|
|
canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
|
|
}
|
|
}
|
|
|
|
final dotPaint = Paint()..style = PaintingStyle.fill;
|
|
Set<Dot> activeDots = {};
|
|
for (var line in board.lines) {
|
|
if (line.isPlayable) {
|
|
activeDots.add(line.p1); activeDots.add(line.p2);
|
|
}
|
|
}
|
|
|
|
for (var dot in activeDots) {
|
|
Offset pos = getScreenPos(dot.x, dot.y);
|
|
if (themeType == AppThemeType.wood) {
|
|
canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
|
|
} else if (themeType == AppThemeType.cyberpunk) {
|
|
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
|
|
canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
|
|
} else if (themeType == AppThemeType.doodle) {
|
|
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
|
|
} else if (themeType == AppThemeType.arcade) {
|
|
canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9));
|
|
canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background);
|
|
} else if (themeType == AppThemeType.grimorio) {
|
|
canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0));
|
|
Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close();
|
|
canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8));
|
|
} else {
|
|
canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
|
|
}
|
|
}
|
|
}
|
|
|
|
void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) {
|
|
TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
|
|
textPainter.text = TextSpan(
|
|
text: String.fromCharCode(icon.codePoint),
|
|
style: TextStyle(
|
|
color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
|
|
fontSize: rect.width * 0.45,
|
|
fontFamily: icon.fontFamily,
|
|
package: icon.fontPackage,
|
|
shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
|
|
),
|
|
);
|
|
textPainter.layout();
|
|
textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
|
|
}
|
|
|
|
void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
|
|
Paint crackPaint = Paint()
|
|
..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
|
|
..strokeWidth = 3.0
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round
|
|
..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);
|
|
|
|
canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);
|
|
|
|
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
|
|
double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
|
|
|
|
Path crack = Path()..moveTo(p1.dx, p1.dy);
|
|
int zigzags = 6;
|
|
for (int i=1; i<zigzags; i++) {
|
|
double d = len * (i / zigzags);
|
|
Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
|
double offset = (i % 2 == 0 ? 3.0 : -3.0);
|
|
crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
|
|
}
|
|
crack.lineTo(p2.dx, p2.dy);
|
|
canvas.drawPath(crack, crackPaint);
|
|
}
|
|
|
|
void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
|
|
double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
|
|
for (double y = rect.top; y < rect.bottom; y += pixelSize) {
|
|
for (double x = rect.left; x < rect.right; x += pixelSize) {
|
|
int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
|
|
if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
|
|
}
|
|
}
|
|
canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
|
|
}
|
|
|
|
void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
|
|
canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
|
|
Offset c = rect.center; double r = rect.width * 0.35;
|
|
Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
|
canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
|
|
Path p = Path();
|
|
for(int i=0; i<3; i++) {
|
|
double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
|
|
if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
|
|
}
|
|
p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
|
|
}
|
|
|
|
void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
|
double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
|
|
Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
|
|
Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
|
|
for(double d = 0; d <= len; d += pixelSize + 1.0) {
|
|
Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
|
canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
|
|
if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
|
|
}
|
|
if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
|
|
}
|
|
|
|
void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
|
if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
|
|
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
|
|
canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
|
|
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
|
|
Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
|
|
Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
|
|
double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
|
|
for(int i = 0; i <= segments; i++) {
|
|
double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
|
|
double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
|
|
Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
|
|
if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
|
|
}
|
|
Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
|
|
canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
|
|
}
|
|
|
|
void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
|
|
final rand = Random((baseRect.left + baseRect.top).toInt());
|
|
Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
|
|
Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
|
|
canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
|
|
void drawFlameLayer(double scale, Color color, double tipOffsetX) {
|
|
Path path = Path(); double fw = w * scale; double fh = h * scale;
|
|
path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
|
|
canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
|
|
}
|
|
double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
|
|
}
|
|
|
|
void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
|
|
final rand = Random((baseRect.left + baseRect.top).toInt());
|
|
final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
|
|
final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
|
|
path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
|
|
for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
|
|
canvas.drawPath(path, paint);
|
|
}
|
|
|
|
void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
|
|
int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
|
|
canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
|
|
if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
|
|
canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
|
|
canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
|
|
}
|
|
|
|
void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
|
double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
|
|
canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
|
|
if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
|
|
canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
|
|
}
|
|
|
|
void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
|
|
final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
|
|
double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
|
|
final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
|
|
final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
|
|
final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
|
|
}
|
|
|
|
@override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
|
|
}
|
|
|
|
class Vector2 {
|
|
final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
|
|
Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
|
|
} |