import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math' as math; import 'dart:async'; class ParticleDrawAnimation extends StatefulWidget { final List> members; final Function(String winnerId) onDrawComplete; final String? serverSeed; final String? clientSeed; final int? nonce; final Duration animationDuration; const ParticleDrawAnimation({ super.key, required this.members, required this.onDrawComplete, this.serverSeed, this.clientSeed, this.nonce, this.animationDuration = const Duration(seconds: 5), }); @override State createState() => _ParticleDrawAnimationState(); } class _ParticleDrawAnimationState extends State with TickerProviderStateMixin { late AnimationController _particleController; late AnimationController _winnerController; late Animation _particleAnimation; late Animation _winnerAnimation; bool _isAnimating = false; bool _isComplete = false; String? _winnerId; Map? _winnerData; List _particles = []; Timer? _particleTimer; @override void initState() { super.initState(); _particleController = AnimationController( duration: widget.animationDuration, vsync: this, ); _winnerController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); _particleAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _particleController, curve: Curves.easeInOut, )); _winnerAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _winnerController, curve: Curves.elasticOut, )); _particleController.addStatusListener((status) { if (status == AnimationStatus.completed) { _completeAnimation(); } }); // Auto-start after delay Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _startAnimation(); }); } @override void dispose() { _particleController.dispose(); _winnerController.dispose(); _particleTimer?.cancel(); super.dispose(); } void _startAnimation() { if (_isAnimating) return; setState(() { _isAnimating = true; }); _calculateWinner(); _generateParticles(); _particleController.forward(); } void _calculateWinner() { final serverSeed = widget.serverSeed ?? 'default_server_seed'; final clientSeed = widget.clientSeed ?? 'default_client_seed'; final nonce = widget.nonce ?? DateTime.now().millisecondsSinceEpoch; final combinedSeed = '$serverSeed:$clientSeed:$nonce'; final hash = _generateHash(combinedSeed); final randomValue = _hashToNumber(hash); final selectedIndex = randomValue % widget.members.length; _winnerId = widget.members[selectedIndex]['id']; _winnerData = widget.members[selectedIndex]; } void _generateParticles() { _particles.clear(); final random = math.Random(); // Generate particles for each member for (int i = 0; i < widget.members.length; i++) { final member = widget.members[i]; final isWinner = member['id'] == _winnerId; // Create multiple particles per member for (int j = 0; j < (isWinner ? 20 : 5); j++) { _particles.add(Particle( id: '${member['id']}_$j', memberId: member['id'], memberName: member['name'] ?? 'Unknown', x: random.nextDouble() * 300.w, y: random.nextDouble() * 400.h, vx: (random.nextDouble() - 0.5) * 2, vy: (random.nextDouble() - 0.5) * 2, size: random.nextDouble() * 8 + 4, color: isWinner ? Colors.green.shade400 : Colors.primaries[random.nextInt(Colors.primaries.length)].shade400, isWinner: isWinner, )); } } } void _completeAnimation() { setState(() { _isAnimating = false; _isComplete = true; }); _winnerController.forward(); widget.onDrawComplete(_winnerId!); } String _generateHash(String input) { int hash = 0; for (int i = 0; i < input.length; i++) { hash = ((hash << 5) - hash + input.codeUnitAt(i)) & 0xffffffff; } return hash.abs().toString(); } int _hashToNumber(String hash) { if (hash.length < 10) { hash = hash.padRight(10, '0'); } return int.parse(hash.substring(0, 10)); } @override Widget build(BuildContext context) { return Container( width: 300.w, height: 400.h, child: Column( children: [ // Title Text( 'Particle Draw', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: Colors.indigo.shade700, ), ), SizedBox(height: 20.h), // Particle Display Expanded( child: Container( width: 280.w, height: 300.h, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(16.r), border: Border.all(color: Colors.indigo.shade400, width: 2.w), ), child: AnimatedBuilder( animation: _particleAnimation, builder: (context, child) { return CustomPaint( painter: ParticlePainter( particles: _particles, animationValue: _particleAnimation.value, isComplete: _isComplete, ), size: Size(280.w, 300.h), ); }, ), ), ), // Winner Display if (_isComplete) AnimatedBuilder( animation: _winnerAnimation, builder: (context, child) { return Transform.scale( scale: _winnerAnimation.value, child: Container( margin: EdgeInsets.only(top: 20.h), padding: EdgeInsets.all(16.w), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.green.shade400, Colors.green.shade600], ), borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.green.shade300, blurRadius: 10.r, offset: Offset(0, 5.h), ), ], ), child: Column( children: [ Icon( Icons.emoji_events, color: Colors.white, size: 32.w, ), SizedBox(height: 8.h), Text( 'WINNER!', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( _winnerData?['name'] ?? 'Unknown', style: TextStyle( fontSize: 16.sp, color: Colors.white.withOpacity(0.9), ), ), ], ), ), ); }, ) else if (_isAnimating) Container( margin: EdgeInsets.only(top: 20.h), child: Text( 'Particles swirling...', style: TextStyle( fontSize: 16.sp, color: Colors.indigo.shade600, ), ), ), ], ), ); } } class Particle { final String id; final String memberId; final String memberName; double x; double y; double vx; double vy; final double size; final Color color; final bool isWinner; Particle({ required this.id, required this.memberId, required this.memberName, required this.x, required this.y, required this.vx, required this.vy, required this.size, required this.color, required this.isWinner, }); void update() { x += vx; y += vy; // Bounce off walls if (x <= 0 || x >= 280) vx = -vx; if (y <= 0 || y >= 300) vy = -vy; // Keep particles in bounds x = x.clamp(0, 280); y = y.clamp(0, 300); } } class ParticlePainter extends CustomPainter { final List particles; final double animationValue; final bool isComplete; ParticlePainter({ required this.particles, required this.animationValue, required this.isComplete, }); @override void paint(Canvas canvas, Size size) { final paint = Paint()..style = PaintingStyle.fill; for (final particle in particles) { // Update particle position particle.update(); // Apply animation effects final alpha = isComplete && particle.isWinner ? 1.0 : (0.3 + 0.7 * animationValue); paint.color = particle.color.withOpacity(alpha); // Draw particle canvas.drawCircle( Offset(particle.x, particle.y), particle.size * (isComplete && particle.isWinner ? 1.5 : 1.0), paint, ); // Draw member name for winner particles if (isComplete && particle.isWinner) { final textPainter = TextPainter( text: TextSpan( text: particle.memberName.length > 8 ? '${particle.memberName.substring(0, 8)}...' : particle.memberName, style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ); textPainter.layout(); textPainter.paint( canvas, Offset( particle.x - textPainter.width / 2, particle.y + particle.size + 5, ), ); } } // Draw connecting lines for winner particles if (isComplete) { final winnerParticles = particles.where((p) => p.isWinner).toList(); if (winnerParticles.isNotEmpty) { paint.color = Colors.green.withOpacity(0.3); paint.strokeWidth = 2; paint.style = PaintingStyle.stroke; for (int i = 0; i < winnerParticles.length - 1; i++) { canvas.drawLine( Offset(winnerParticles[i].x, winnerParticles[i].y), Offset(winnerParticles[i + 1].x, winnerParticles[i + 1].y), paint, ); } } } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }