import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math' as math; import 'dart:async'; // Base class for all draw animations abstract class DrawAnimation extends StatefulWidget { final List> members; final Function(String winnerId) onDrawComplete; final String? serverSeed; final String? clientSeed; final int? nonce; final Duration animationDuration; const DrawAnimation({ super.key, required this.members, required this.onDrawComplete, this.serverSeed, this.clientSeed, this.nonce, this.animationDuration = const Duration(seconds: 4), }); } // 1. Card Flip Animation - Shows member cards flipping rapidly class CardFlipDrawAnimation extends DrawAnimation { const CardFlipDrawAnimation({ super.key, required super.members, required super.onDrawComplete, super.serverSeed, super.clientSeed, super.nonce, super.animationDuration, }); @override State createState() => _CardFlipDrawAnimationState(); } class _CardFlipDrawAnimationState extends State with TickerProviderStateMixin { late AnimationController _flipController; late AnimationController _scaleController; late Animation _flipAnimation; late Animation _scaleAnimation; bool _isAnimating = false; bool _isComplete = false; String? _winnerId; int _currentIndex = 0; Timer? _flipTimer; @override void initState() { super.initState(); _flipController = AnimationController( duration: const Duration(milliseconds: 200), vsync: this, ); _scaleController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _flipAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _flipController, curve: Curves.easeInOut, )); _scaleAnimation = Tween( begin: 1.0, end: 1.1, ).animate(CurvedAnimation( parent: _scaleController, curve: Curves.elasticOut, )); // Auto-start after delay Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _startAnimation(); }); } @override void dispose() { _flipController.dispose(); _scaleController.dispose(); _flipTimer?.cancel(); super.dispose(); } void _startAnimation() { if (_isAnimating) return; setState(() { _isAnimating = true; }); _calculateWinner(); _startCardFlipping(); } 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']; } void _startCardFlipping() { final totalFlips = 20 + (widget.members.length * 2); int currentFlip = 0; _flipTimer = Timer.periodic(const Duration(milliseconds: 150), (timer) { if (currentFlip >= totalFlips) { timer.cancel(); _completeAnimation(); return; } setState(() { _currentIndex = currentFlip % widget.members.length; }); _flipController.forward().then((_) { _flipController.reset(); }); currentFlip++; }); } void _completeAnimation() { setState(() { _isAnimating = false; _isComplete = true; }); _scaleController.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( 'Card Flip Draw', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: Colors.purple.shade700, ), ), SizedBox(height: 20.h), // Card Display Expanded( child: AnimatedBuilder( animation: _flipAnimation, builder: (context, child) { return Transform( alignment: Alignment.center, transform: Matrix4.identity() ..setEntry(3, 2, 0.001) ..rotateY(_flipAnimation.value * math.pi), child: AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _isComplete ? _scaleAnimation.value : 1.0, child: Container( width: 250.w, height: 300.h, decoration: BoxDecoration( borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 10.r, offset: Offset(0, 5.h), ), ], ), child: _flipAnimation.value < 0.5 ? _buildCardFront() : _buildCardBack(), ), ); }, ), ); }, ), ), // Status SizedBox(height: 20.h), if (_isAnimating) Text( 'Flipping cards...', style: TextStyle( fontSize: 16.sp, color: Colors.orange.shade600, ), ) else if (_isComplete) Text( 'Winner Selected!', style: TextStyle( fontSize: 16.sp, color: Colors.green.shade600, fontWeight: FontWeight.bold, ), ), ], ), ); } Widget _buildCardFront() { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.purple.shade400, Colors.purple.shade600, ], ), borderRadius: BorderRadius.circular(16.r), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.casino, size: 60.w, color: Colors.white, ), SizedBox(height: 16.h), Text( 'LuckyChit', style: TextStyle( fontSize: 24.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'Draw', style: TextStyle( fontSize: 16.sp, color: Colors.white.withOpacity(0.8), ), ), ], ), ), ); } Widget _buildCardBack() { final member = widget.members[_currentIndex]; return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: _isComplete && _winnerId == member['id'] ? [Colors.green.shade400, Colors.green.shade600] : [Colors.blue.shade400, Colors.blue.shade600], ), borderRadius: BorderRadius.circular(16.r), ), child: Padding( padding: EdgeInsets.all(20.w), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircleAvatar( radius: 40.r, backgroundColor: Colors.white.withOpacity(0.2), child: Text( (member['name']?.isNotEmpty == true ? member['name'].substring(0, 1) : 'M').toUpperCase(), style: TextStyle( fontSize: 32.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), SizedBox(height: 16.h), Text( member['name'] ?? 'Unknown', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.white, ), textAlign: TextAlign.center, ), Text( member['mobile'] ?? '', style: TextStyle( fontSize: 14.sp, color: Colors.white.withOpacity(0.8), ), ), if (_isComplete && _winnerId == member['id']) ...[ SizedBox(height: 16.h), Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20.r), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.emoji_events, color: Colors.white, size: 20.w), SizedBox(width: 8.w), Text( 'WINNER!', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ], ), ), ], ], ), ), ); } } // 2. Slot Machine Animation - Numbers/names spinning like a slot machine class SlotMachineDrawAnimation extends DrawAnimation { const SlotMachineDrawAnimation({ super.key, required super.members, required super.onDrawComplete, super.serverSeed, super.clientSeed, super.nonce, super.animationDuration, }); @override State createState() => _SlotMachineDrawAnimationState(); } class _SlotMachineDrawAnimationState extends State with TickerProviderStateMixin { late AnimationController _slotController; late Animation _slotAnimation; late AnimationController _pulseController; late Animation _pulseAnimation; bool _isAnimating = false; bool _isComplete = false; String? _winnerId; List _displayNames = []; Timer? _slotTimer; int _currentSelectedIndex = 0; @override void initState() { super.initState(); _slotController = AnimationController( duration: const Duration(milliseconds: 400), vsync: this, ); _slotAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _slotController, curve: Curves.linear, )); _pulseController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _pulseAnimation = Tween( begin: 0.8, end: 1.2, ).animate(CurvedAnimation( parent: _pulseController, curve: Curves.easeInOut, )); // Prepare display names _displayNames = widget.members.map((m) => m['name']?.toString() ?? 'Unknown').toList(); // Auto-start after delay Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _startAnimation(); }); } @override void dispose() { _slotController.dispose(); _pulseController.dispose(); _slotTimer?.cancel(); super.dispose(); } void _startAnimation() { if (_isAnimating) return; setState(() { _isAnimating = true; }); _calculateWinner(); _startSlotMachine(); _pulseController.repeat(reverse: true); } 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']; } void _startSlotMachine() { final totalSpins = 25 + (widget.members.length * 2); int currentSpin = 0; _slotTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) { if (currentSpin >= totalSpins) { timer.cancel(); _completeAnimation(); return; } setState(() { // Shuffle the display names for effect _displayNames.shuffle(); // Update current selection pointer _currentSelectedIndex = currentSpin % _displayNames.length; }); _slotController.forward().then((_) { _slotController.reset(); }); currentSpin++; // Gradually slow down the animation if (currentSpin > totalSpins * 0.7) { // Slow down in the last 30% of spins timer.cancel(); _slotTimer = Timer.periodic( Duration(milliseconds: 350 + (currentSpin * 15)), (newTimer) { if (currentSpin >= totalSpins) { newTimer.cancel(); _completeAnimation(); return; } setState(() { _displayNames.shuffle(); _currentSelectedIndex = currentSpin % _displayNames.length; }); _slotController.forward().then((_) { _slotController.reset(); }); currentSpin++; } ); } }); } void _completeAnimation() { setState(() { _isAnimating = false; _isComplete = true; // Set the winner's name at the top _displayNames = [widget.members.firstWhere((m) => m['id'] == _winnerId)['name']]; }); _pulseController.stop(); 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( 'Slot Machine Draw', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: Colors.orange.shade700, ), ), SizedBox(height: 20.h), // Pointer/Indicator Container( width: 280.w, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.keyboard_arrow_down, color: Colors.red.shade600, size: 30.w, ), SizedBox(width: 8.w), Text( _isAnimating ? 'Selecting...' : 'Winner!', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.bold, color: _isAnimating ? Colors.orange.shade600 : Colors.green.shade600, ), ), SizedBox(width: 8.w), Icon( Icons.keyboard_arrow_down, color: Colors.red.shade600, size: 30.w, ), ], ), ), SizedBox(height: 10.h), // Slot Machine Display Expanded( child: Container( width: 280.w, decoration: BoxDecoration( color: Colors.grey.shade800, borderRadius: BorderRadius.circular(16.r), border: Border.all(color: Colors.orange.shade400, width: 3.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 15.r, offset: Offset(0, 8.h), ), ], ), child: Column( children: [ // Slot Windows Expanded( child: Container( margin: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(8.r), ), child: Stack( children: [ // Animated names AnimatedBuilder( animation: _slotAnimation, builder: (context, child) { return Column( children: _displayNames.take(5).map((name) { final index = _displayNames.indexOf(name); final isWinner = _isComplete && index == 0; final isCurrentlySelected = _isAnimating && index == _currentSelectedIndex; return Expanded( child: AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { return Transform.scale( scale: isCurrentlySelected ? _pulseAnimation.value : 1.0, child: Container( width: double.infinity, margin: EdgeInsets.symmetric(vertical: 2.h), decoration: BoxDecoration( color: isWinner ? Colors.green.shade600 : isCurrentlySelected ? Colors.red.shade600 : Colors.blue.shade600, borderRadius: BorderRadius.circular(4.r), boxShadow: isWinner ? [ BoxShadow( color: Colors.green.shade300, blurRadius: 8.r, spreadRadius: 2.r, ), ] : isCurrentlySelected ? [ BoxShadow( color: Colors.red.shade300, blurRadius: 6.r, spreadRadius: 1.r, ), ] : null, ), child: Center( child: Text( name.length > 12 ? '${name.substring(0, 12)}...' : name, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.white, shadows: isWinner ? [ Shadow( color: Colors.black.withOpacity(0.3), blurRadius: 2.r, offset: Offset(1, 1), ), ] : null, ), ), ), ), ); }, ), ); }).toList(), ); }, ), // Winner highlight if (_isComplete) Positioned( top: 0, left: 0, right: 0, child: Container( height: 60.h, decoration: BoxDecoration( color: Colors.green.shade400.withOpacity(0.3), borderRadius: BorderRadius.circular(4.r), ), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.emoji_events, color: Colors.yellow, size: 24.w), SizedBox(width: 8.w), Text( 'WINNER!', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, color: Colors.yellow, ), ), ], ), ), ), ), ], ), ), ), // Slot Machine Controls Container( padding: EdgeInsets.all(16.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Container( width: 40.w, height: 40.w, decoration: BoxDecoration( color: Colors.red.shade600, shape: BoxShape.circle, ), child: Icon(Icons.stop, color: Colors.white, size: 20.w), ), Container( width: 40.w, height: 40.w, decoration: BoxDecoration( color: Colors.green.shade600, shape: BoxShape.circle, ), child: Icon(Icons.play_arrow, color: Colors.white, size: 20.w), ), Container( width: 40.w, height: 40.w, decoration: BoxDecoration( color: Colors.blue.shade600, shape: BoxShape.circle, ), child: Icon(Icons.pause, color: Colors.white, size: 20.w), ), ], ), ), ], ), ), ), // Status SizedBox(height: 20.h), if (_isAnimating) Text( 'Slot machine spinning...', style: TextStyle( fontSize: 16.sp, color: Colors.orange.shade600, ), ) else if (_isComplete) Text( 'Winner: ${widget.members.firstWhere((m) => m['id'] == _winnerId)['name']}', style: TextStyle( fontSize: 16.sp, color: Colors.green.shade600, fontWeight: FontWeight.bold, ), ), ], ), ); } } // 3. Number Roulette Animation - Numbers spinning like a roulette wheel class NumberRouletteDrawAnimation extends DrawAnimation { const NumberRouletteDrawAnimation({ super.key, required super.members, required super.onDrawComplete, super.serverSeed, super.clientSeed, super.nonce, super.animationDuration, }); @override State createState() => _NumberRouletteDrawAnimationState(); } class _NumberRouletteDrawAnimationState extends State with TickerProviderStateMixin { late AnimationController _rouletteController; late Animation _rouletteAnimation; bool _isAnimating = false; bool _isComplete = false; String? _winnerId; int _selectedNumber = 0; @override void initState() { super.initState(); _rouletteController = AnimationController( duration: widget.animationDuration, vsync: this, ); _rouletteAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _rouletteController, curve: Curves.easeOut, )); _rouletteController.addStatusListener((status) { if (status == AnimationStatus.completed) { _completeAnimation(); } }); // Auto-start after delay Future.delayed(const Duration(milliseconds: 500), () { if (mounted) _startAnimation(); }); } @override void dispose() { _rouletteController.dispose(); super.dispose(); } void _startAnimation() { if (_isAnimating) return; setState(() { _isAnimating = true; }); _calculateWinner(); _rouletteController.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']; _selectedNumber = selectedIndex + 1; } void _completeAnimation() { setState(() { _isAnimating = false; _isComplete = true; }); 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( 'Number Roulette', style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: Colors.red.shade700, ), ), SizedBox(height: 20.h), // Roulette Display Expanded( child: AnimatedBuilder( animation: _rouletteAnimation, builder: (context, child) { return Container( width: 250.w, height: 250.w, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ Colors.red.shade50, Colors.red.shade100, Colors.red.shade200, ], ), border: Border.all(color: Colors.red.shade400, width: 4.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 15.r, offset: Offset(0, 8.h), ), ], ), child: Stack( children: [ // Numbers around the circle ...List.generate(widget.members.length, (index) { final angle = (index * 2 * math.pi / widget.members.length) - math.pi / 2; final currentAngle = _rouletteAnimation.value * 2 * math.pi * 5 + angle; final isSelected = _isComplete && index == (_selectedNumber - 1); return Positioned( left: 125.w + 100.w * math.cos(currentAngle) - 15.w, top: 125.w + 100.w * math.sin(currentAngle) - 15.w, child: Container( width: 30.w, height: 30.w, decoration: BoxDecoration( color: isSelected ? Colors.green.shade600 : Colors.red.shade600, shape: BoxShape.circle, border: Border.all( color: Colors.white, width: 2.w, ), ), child: Center( child: Text( '${index + 1}', style: TextStyle( fontSize: 12.sp, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ); }), // Center pointer Center( child: Container( width: 0, height: 0, child: CustomPaint( painter: PointerPainter(), size: Size(20.w, 40.h), ), ), ), // Winner highlight if (_isComplete) Center( child: Container( width: 60.w, height: 60.w, decoration: BoxDecoration( color: Colors.green.shade400.withOpacity(0.3), shape: BoxShape.circle, ), child: Center( child: Icon( Icons.emoji_events, color: Colors.yellow, size: 30.w, ), ), ), ), ], ), ); }, ), ), // Status SizedBox(height: 20.h), if (_isAnimating) Text( 'Spinning...', style: TextStyle( fontSize: 16.sp, color: Colors.red.shade600, ), ) else if (_isComplete) Column( children: [ Text( 'Winner: #$_selectedNumber', style: TextStyle( fontSize: 18.sp, color: Colors.green.shade600, fontWeight: FontWeight.bold, ), ), Text( widget.members.firstWhere((m) => m['id'] == _winnerId)['name'] ?? 'Unknown', style: TextStyle( fontSize: 16.sp, color: Colors.grey.shade600, ), ), ], ), ], ), ); } } class PointerPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.black ..style = PaintingStyle.fill; final path = Path(); path.moveTo(size.width / 2, 0); path.lineTo(0, size.height); path.lineTo(size.width, size.height); path.close(); canvas.drawPath(path, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }