// =========================================================================== // FILE: lib/ui/game/game_screen.dart // =========================================================================== import 'dart:ui'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../logic/game_controller.dart'; import '../../core/theme_manager.dart'; import '../../core/app_colors.dart'; import 'board_painter.dart'; import 'score_board.dart'; import '../../models/game_board.dart'; class GameScreen extends StatefulWidget { const GameScreen({super.key}); @override State createState() => _GameScreenState(); } class _GameScreenState extends State with TickerProviderStateMixin { late AnimationController _blinkController; bool _gameOverDialogShown = false; bool _opponentLeftDialogShown = false; @override void initState() { super.initState(); _blinkController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), )..repeat(reverse: true); } @override void dispose() { _blinkController.dispose(); super.dispose(); } void _showGameOverDialog(BuildContext context, GameController game, ThemeColors theme, AppThemeType themeType) { _gameOverDialogShown = true; showDialog( barrierDismissible: false, context: context, builder: (dialogContext) => Consumer( builder: (context, controller, child) { if (!controller.isGameOver) { WidgetsBinding.instance.addPostFrameCallback((_) { if (Navigator.canPop(dialogContext)) Navigator.pop(dialogContext); _gameOverDialogShown = false; }); return const SizedBox(); } int red = controller.board.scoreRed; int blue = controller.board.scoreBlue; bool playerBeatCPU = controller.isVsCPU && red > blue; String nameRed = controller.isOnline ? controller.onlineHostName.toUpperCase() : "TU"; String nameBlue = controller.isOnline ? controller.onlineGuestName.toUpperCase() : (themeType == AppThemeType.cyberpunk ? "VERDE" : "BLU"); if (controller.isVsCPU) nameBlue = "CPU"; String winnerText = ""; Color winnerColor = theme.text; if (red > blue) { winnerText = "VINCE $nameRed!"; winnerColor = theme.playerRed; } else if (blue > red) { winnerText = "VINCE $nameBlue!"; winnerColor = theme.playerBlue; } else { winnerText = "PAREGGIO!"; winnerColor = theme.text; } return AlertDialog( backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: winnerColor.withOpacity(0.5), width: 2)), title: Text("FINE PARTITA", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontWeight: FontWeight.bold, fontSize: 22)), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text(winnerText, textAlign: TextAlign.center, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: winnerColor)), const SizedBox(height: 20), Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration(color: theme.text.withOpacity(0.05), borderRadius: BorderRadius.circular(15)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text("$nameRed: $red", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerRed)), Text(" - ", style: TextStyle(fontSize: 18, color: theme.text)), Text("$nameBlue: $blue", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: theme.playerBlue)), ], ), ), if (controller.isVsCPU) ...[ const SizedBox(height: 15), Text("Difficoltà CPU: Livello ${controller.cpuLevel}", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: theme.text.withOpacity(0.7))), ], if (controller.isOnline) ...[ const SizedBox(height: 20), if (controller.rematchRequested && !controller.opponentWantsRematch) Text("In attesa di $nameBlue...", style: TextStyle(color: Colors.amber, fontWeight: FontWeight.bold, fontStyle: FontStyle.italic)), if (controller.opponentWantsRematch && !controller.rematchRequested) Text("$nameBlue vuole la rivincita!", style: TextStyle(color: Colors.greenAccent, fontWeight: FontWeight.bold)), if (controller.rematchRequested && controller.opponentWantsRematch) Text("Avvio nuova partita...", style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), ] ], ), actionsPadding: const EdgeInsets.only(left: 20, right: 20, bottom: 20, top: 10), actionsAlignment: MainAxisAlignment.center, actions: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (playerBeatCPU) ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.increaseLevelAndRestart(); }, child: const Text("PROSSIMO LIVELLO ➔", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ) else if (controller.isOnline) ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: controller.rematchRequested ? Colors.grey : (winnerColor == theme.text ? theme.playerBlue : winnerColor), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: controller.rematchRequested ? null : () { controller.requestRematch(); }, child: Text(controller.opponentWantsRematch ? "ACCETTA RIVINCITA" : "CHIEDI RIVINCITA", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 1.0)), ) else ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: winnerColor == theme.text ? theme.playerBlue : winnerColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), elevation: 5), onPressed: () { Navigator.pop(dialogContext); _gameOverDialogShown = false; controller.startNewGame(controller.board.radius, vsCPU: controller.isVsCPU); }, child: const Text("RIGIOCA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, letterSpacing: 2)), ), const SizedBox(height: 12), OutlinedButton( style: OutlinedButton.styleFrom(foregroundColor: theme.text, side: BorderSide(color: theme.text.withOpacity(0.3), width: 2), padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { if (controller.isOnline) controller.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, child: Text("TORNA AL MENU", style: TextStyle(fontWeight: FontWeight.bold, color: theme.text, fontSize: 14, letterSpacing: 1.5)), ), ], ) ], ); } ) ); } @override Widget build(BuildContext context) { final themeManager = context.watch(); final themeType = themeManager.currentThemeType; final theme = themeManager.currentColors; final gameController = context.watch(); WidgetsBinding.instance.addPostFrameCallback((_) { if (gameController.opponentLeft && !_opponentLeftDialogShown) { _opponentLeftDialogShown = true; showDialog( barrierDismissible: false, context: context, builder: (dialogContext) => AlertDialog( backgroundColor: theme.background, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text("VITTORIA A TAVOLINO!", textAlign: TextAlign.center, style: TextStyle(color: theme.playerRed, fontWeight: FontWeight.bold)), content: Text("L'avversario ha abbandonato la stanza.\nSei il vincitore incontestato!", textAlign: TextAlign.center, style: TextStyle(color: theme.text, fontSize: 16)), actionsAlignment: MainAxisAlignment.center, actions: [ ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: theme.playerBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(dialogContext); Navigator.pop(context); }, child: const Text("MENU PRINCIPALE", style: TextStyle(fontWeight: FontWeight.bold)), ) ], ) ); } else if (gameController.board.isGameOver && !_gameOverDialogShown) { _showGameOverDialog(context, gameController, theme, themeType); } }); String? bgImage; if (themeType == AppThemeType.wood) bgImage = 'assets/images/wood_bg.jpg'; if (themeType == AppThemeType.doodle) bgImage = 'assets/images/doodle_bg.jpg'; Color indicatorColor = themeType == AppThemeType.cyberpunk ? Colors.white : Colors.black; Widget emojiBar = const SizedBox(); if (gameController.isOnline && !gameController.isGameOver) { final List emojis = ['😂', '😡', '😱', '🥳', '👀']; emojiBar = Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: themeType == AppThemeType.cyberpunk ? Colors.black.withOpacity(0.6) : Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(30), border: Border.all(color: themeType == AppThemeType.cyberpunk ? theme.playerBlue.withOpacity(0.3) : Colors.black12, width: 2), ), child: Row( mainAxisSize: MainAxisSize.min, children: emojis.map((e) => GestureDetector( onTap: () => gameController.sendReaction(e), child: Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(e, style: const TextStyle(fontSize: 22))), )).toList(), ), ); } Widget gameContent = SafeArea( child: Stack( children: [ Column( children: [ const ScoreBoard(), Expanded( child: Center( child: Padding( padding: const EdgeInsets.all(10.0), child: AspectRatio( aspectRatio: 1, child: LayoutBuilder( builder: (context, constraints) { return GestureDetector( behavior: HitTestBehavior.opaque, onTapDown: (details) => _handleTap(details.localPosition, constraints.maxWidth, gameController, themeType), child: AnimatedBuilder( animation: _blinkController, builder: (context, child) { return CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: BoardPainter(board: gameController.board, theme: theme, themeType: themeType, blinkValue: _blinkController.value), ); } ), ); } ), ), ), ), ), Padding( padding: const EdgeInsets.only(bottom: 20.0, left: 20.0, right: 20.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (gameController.isVsCPU) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(color: indicatorColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: indicatorColor.withOpacity(0.3))), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.smart_toy_rounded, size: 16, color: indicatorColor), const SizedBox(width: 8), Text("LIVELLO CPU: ${gameController.cpuLevel}", style: TextStyle(color: indicatorColor, fontWeight: FontWeight.bold, fontSize: 13, letterSpacing: 1.0)), ], ), ) else emojiBar, Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 5)]), child: TextButton.icon( style: TextButton.styleFrom(backgroundColor: bgImage != null ? Colors.black87 : theme.background, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20), side: BorderSide(color: Colors.white.withOpacity(0.1), width: 1))), icon: Icon(Icons.exit_to_app, color: bgImage != null ? Colors.white : theme.text, size: 20), onPressed: () { gameController.disconnectOnlineGame(); Navigator.pop(context); }, label: Text("ESCI", style: TextStyle(color: bgImage != null ? Colors.white : theme.text, fontWeight: FontWeight.bold, fontSize: 14)), ), ), ], ), ) ], ), if (gameController.myReaction != null) Positioned( top: 80, left: gameController.isHost ? 30 : null, right: gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.myReaction!), ), if (gameController.opponentReaction != null) Positioned( top: 80, left: !gameController.isHost ? 30 : null, right: !gameController.isHost ? null : 30, child: _BouncingEmoji(emoji: gameController.opponentReaction!), ), ], ), ); return PopScope( canPop: true, onPopInvoked: (didPop) { gameController.disconnectOnlineGame(); }, child: Scaffold( backgroundColor: bgImage != null ? Colors.transparent : theme.background, body: CustomPaint( painter: themeType == AppThemeType.minimal ? FullScreenGridPainter(Colors.black.withOpacity(0.06)) : null, child: Container( decoration: bgImage != null ? BoxDecoration(image: DecorationImage(image: AssetImage(bgImage), fit: BoxFit.cover, colorFilter: themeType == AppThemeType.doodle ? ColorFilter.mode(Colors.white.withOpacity(0.7), BlendMode.lighten) : null)) : null, child: Stack( children: [ if (gameController.isTimeMode && !gameController.isCPUThinking && !gameController.isGameOver && gameController.timeLeft > 0 && gameController.timeLeft <= 5) Positioned.fill(child: BlitzBackgroundEffect(timeLeft: gameController.timeLeft, color: theme.playerRed)), if (gameController.effectText.isNotEmpty) Positioned.fill(child: SpecialEventBackgroundEffect(text: gameController.effectText, color: gameController.effectColor)), Positioned.fill(child: gameContent), // ========================================== // EFFETTI VISIVI (VFX) DI FINE PARTITA // ========================================== if (gameController.isGameOver && gameController.board.scoreRed != gameController.board.scoreBlue) Positioned.fill( child: IgnorePointer( child: WinnerVFXOverlay( winnerColor: gameController.board.scoreRed > gameController.board.scoreBlue ? theme.playerRed : theme.playerBlue, themeType: themeType, ), ), ), ], ), ), ), ), ); } void _handleTap(Offset tapPos, double size, GameController controller, AppThemeType themeType) { final board = controller.board; if (board.isGameOver) return; int gridPoints = board.radius * 2 + 2; double spacing = size / gridPoints; double offset = spacing / 2; Line? closestLine; double minDistance = double.infinity; double maxTouchDistance = spacing * 0.4; for (var line in board.lines) { if (line.owner != Player.none || !line.isPlayable) continue; Offset screenP1 = Offset(line.p1.x * spacing + offset, line.p1.y * spacing + offset); Offset screenP2 = Offset(line.p2.x * spacing + offset, line.p2.y * spacing + offset); double dist = _distanceToSegment(tapPos, screenP1, screenP2); if (dist < minDistance && dist < maxTouchDistance) { minDistance = dist; closestLine = line; } } if (closestLine != null) { controller.handleLineTap(closestLine, themeType); } } double _distanceToSegment(Offset p, Offset a, Offset b) { double l2 = (a.dx - b.dx) * (a.dx - b.dx) + (a.dy - b.dy) * (a.dy - b.dy); if (l2 == 0) return (p - a).distance; double t = (((p.dx - a.dx) * (b.dx - a.dx) + (p.dy - a.dy) * (b.dy - a.dy)) / l2).clamp(0.0, 1.0); Offset projection = Offset(a.dx + t * (b.dx - a.dx), a.dy + t * (b.dy - a.dy)); return (p - projection).distance; } } // =========================================================================== // CLASSI PER IL MOTORE PARTICELLARE (VFX) DI FINE PARTITA // =========================================================================== class _Particle { double x, y; double vx, vy; Color color; double size; double angle; double spin; int type; // 0=cerchio, 1=quadrato, 2=triangolo _Particle({required this.x, required this.y, required this.vx, required this.vy, required this.color, required this.size, required this.angle, required this.spin, required this.type}); } class WinnerVFXOverlay extends StatefulWidget { final Color winnerColor; final AppThemeType themeType; const WinnerVFXOverlay({super.key, required this.winnerColor, required this.themeType}); @override State createState() => _WinnerVFXOverlayState(); } class _WinnerVFXOverlayState extends State with SingleTickerProviderStateMixin { late AnimationController _vfxController; final List<_Particle> _particles = []; final math.Random _rand = math.Random(); bool _initialized = false; @override void initState() { super.initState(); // L'animazione gira a 60fps per 4 secondi e poi si ferma _vfxController = AnimationController(vsync: this, duration: const Duration(seconds: 4)) ..addListener(() { _updateParticles(); }) ..forward(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (!_initialized) { _initParticles(MediaQuery.of(context).size); _initialized = true; } } void _initParticles(Size screenSize) { int particleCount = widget.themeType == AppThemeType.cyberpunk ? 150 : 100; // Lista di colori da mixare (colore vincitore + bianco + colori a tema) List palette = [widget.winnerColor, widget.winnerColor.withOpacity(0.7), Colors.white]; if (widget.themeType == AppThemeType.cyberpunk) { palette.add(Colors.cyanAccent); palette.add(Colors.yellowAccent); } else if (widget.themeType == AppThemeType.doodle) { palette.add(const Color(0xFF00008B)); // Inchiostro biro palette.add(Colors.redAccent); } else if (widget.themeType == AppThemeType.wood) { palette = [Colors.orangeAccent, Colors.yellow, Colors.red, Colors.white]; } for (int i = 0; i < particleCount; i++) { // Esplosione dal centro verso l'esterno double speed = _rand.nextDouble() * 20 + 5; double theta = _rand.nextDouble() * 2 * math.pi; _particles.add(_Particle( x: screenSize.width / 2, y: screenSize.height / 2, vx: speed * math.cos(theta), vy: speed * math.sin(theta) - 5, // Leggera spinta verso l'alto color: palette[_rand.nextInt(palette.length)], size: _rand.nextDouble() * 10 + 6, angle: _rand.nextDouble() * math.pi, spin: (_rand.nextDouble() - 0.5) * 0.5, type: _rand.nextInt(3), )); } } void _updateParticles() { setState(() { for (var p in _particles) { p.x += p.vx; p.y += p.vy; // Gravità e attrito if (widget.themeType == AppThemeType.cyberpunk) { p.vy += 0.1; // Gravità bassa (fluttuano di più) p.vx *= 0.98; // Attrito p.vy *= 0.98; } else if (widget.themeType == AppThemeType.wood) { p.vy -= 0.2; // Vanno verso l'alto come fumo/scintille! p.x += math.sin(p.y * 0.05) * 2; // Tremolio } else { p.vy += 0.5; // Gravità standard (coriandoli cadono) } p.angle += p.spin; p.size *= 0.99; // Si rimpiccioliscono nel tempo } }); } @override void dispose() { _vfxController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return CustomPaint( painter: _VFXPainter(particles: _particles, themeType: widget.themeType), child: Container(), ); } } class _VFXPainter extends CustomPainter { final List<_Particle> particles; final AppThemeType themeType; _VFXPainter({required this.particles, required this.themeType}); @override void paint(Canvas canvas, Size size) { for (var p in particles) { if (p.size < 0.5) continue; final paint = Paint() ..color = p.color ..style = PaintingStyle.fill; // Glow per il Cyberpunk if (themeType == AppThemeType.cyberpunk) { paint.maskFilter = const MaskFilter.blur(BlurStyle.solid, 4.0); } canvas.save(); canvas.translate(p.x, p.y); canvas.rotate(p.angle); if (themeType == AppThemeType.doodle) { // Stile schizzato paint.style = PaintingStyle.stroke; paint.strokeWidth = 2.0; if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size*2, height: p.size*2), paint); } } else if (themeType == AppThemeType.wood) { // Scintille rotonde e sfuocate paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0); canvas.drawCircle(Offset.zero, p.size, paint); } else { // Forme standard per Minimal e Cyberpunk if (p.type == 0) { canvas.drawCircle(Offset.zero, p.size, paint); } else if (p.type == 1) { canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: p.size * 2, height: p.size * 2), paint); } else { var path = Path() ..moveTo(0, -p.size) ..lineTo(p.size, p.size) ..lineTo(-p.size, p.size) ..close(); canvas.drawPath(path, paint); } } canvas.restore(); } } @override bool shouldRepaint(covariant _VFXPainter oldDelegate) => true; } // =========================================================================== // WIDGET INTERNI ESISTENTENTI // =========================================================================== class _BouncingEmoji extends StatefulWidget { final String emoji; const _BouncingEmoji({required this.emoji}); @override State<_BouncingEmoji> createState() => _BouncingEmojiState(); } class _BouncingEmojiState extends State<_BouncingEmoji> with SingleTickerProviderStateMixin { late AnimationController _ctrl; late Animation _anim; @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(milliseconds: 500))..repeat(reverse: true); _anim = Tween(begin: -10, end: 10).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeInOut)); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _anim, builder: (ctx, child) => Transform.translate( offset: Offset(0, _anim.value), child: Container( padding: const EdgeInsets.all(8), decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle, boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 5)]), child: Text(widget.emoji, style: const TextStyle(fontSize: 32)), ), ), ); } } class FullScreenGridPainter extends CustomPainter { final Color gridColor; FullScreenGridPainter(this.gridColor); @override void paint(Canvas canvas, Size size) { final Paint paperGridPaint = Paint()..color = gridColor..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); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class BlitzBackgroundEffect extends StatefulWidget { final int timeLeft; final Color color; const BlitzBackgroundEffect({super.key, required this.timeLeft, required this.color}); @override State createState() => _BlitzBackgroundEffectState(); } class _BlitzBackgroundEffectState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400))..repeat(reverse: true); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Container( color: widget.color.withOpacity(0.12 * _controller.value), child: Center( child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 2.0, sigmaY: 2.0), child: Text('${widget.timeLeft}', style: TextStyle(fontSize: 300, fontWeight: FontWeight.w900, color: widget.color.withOpacity(0.35 + (0.3 * _controller.value)), height: 1.0)), ), ), ); }, ); } } class SpecialEventBackgroundEffect extends StatefulWidget { final String text; final Color color; const SpecialEventBackgroundEffect({super.key, required this.text, required this.color}); @override State createState() => _SpecialEventBackgroundEffectState(); } class _SpecialEventBackgroundEffectState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; late Animation _opacityAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000))..forward(); _scaleAnimation = Tween(begin: 0.5, end: 1.5).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _opacityAnimation = Tween(begin: 0.9, end: 0.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeIn)); } @override void didUpdateWidget(covariant SpecialEventBackgroundEffect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { _controller.reset(); _controller.forward(); } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Center( child: Transform.scale( scale: _scaleAnimation.value, child: Opacity( opacity: _opacityAnimation.value, child: ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0), child: Text(widget.text, style: TextStyle(fontSize: 250, fontWeight: FontWeight.w900, color: widget.color, height: 1.0)), ), ), ), ); }, ); } }