import 'package:flutter/material.dart'; import 'package:flutter/services.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; final List _baseNames = []; List _displayNames = []; int _currentIndex = 0; Timer? _slotTimer; @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, )); _prepareDisplayNames(); // 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 _prepareDisplayNames() { _baseNames.clear(); _baseNames.addAll( widget.members.map((m) => m['name']?.toString() ?? 'Unknown'), ); if (_baseNames.isEmpty) { _baseNames.add('Unknown'); } // Ensure we have at least 7 entries to fill the slot windows if (_baseNames.length < 7) { final original = List.from(_baseNames); while (_baseNames.length < 7) { _baseNames.addAll(original); } } _shuffleBaseNames(); _currentIndex = 0; _updateDisplayWindow(); } void _shuffleBaseNames() { final seedSource = DateTime.now().millisecondsSinceEpoch.toString(); final hash = _generateHash(seedSource); final randomValue = _hashToNumber(hash); final random = math.Random(randomValue); for (var i = _baseNames.length - 1; i > 0; i--) { final j = random.nextInt(i + 1); final temp = _baseNames[i]; _baseNames[i] = _baseNames[j]; _baseNames[j] = temp; } } void _updateDisplayWindow() { _displayNames = List.generate( 7, (index) => _baseNames[(_currentIndex + index) % _baseNames.length], ); } 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; final raw = widget.members[selectedIndex]['id']; _winnerId = raw?.toString() ?? ''; } 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(() { _currentIndex = (_currentIndex + 1) % _baseNames.length; _updateDisplayWindow(); }); _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(() { _currentIndex = (_currentIndex + 1) % _baseNames.length; _updateDisplayWindow(); }); _slotController.forward().then((_) { _slotController.reset(); }); currentSpin++; } ); } }); } void _completeAnimation() { // Always center the real winner — indexOf(name) breaks when names repeat. String winnerName = 'Unknown'; for (final m in widget.members) { if ('${m['id']}' == '${_winnerId}') { winnerName = m['name']?.toString() ?? 'Unknown'; break; } } final otherNames = widget.members .where((m) => '${m['id']}' != '${_winnerId}') .map((m) => m['name']?.toString() ?? 'Unknown') .toList(); final filler = otherNames.isEmpty ? [winnerName] : otherNames; final window = List.filled(7, ''); window[3] = winnerName; int o = 0; for (int i = 0; i < 7; i++) { if (i == 3) continue; window[i] = filler[o % filler.length]; o++; } HapticFeedback.mediumImpact(); setState(() { _isAnimating = false; _isComplete = true; _displayNames = window; }); _pulseController.stop(); Future.delayed(const Duration(milliseconds: 650), () { if (_winnerId != null && _winnerId!.isNotEmpty) { 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) { String? winnerName; if (_winnerId != null && _winnerId!.isNotEmpty) { for (final m in widget.members) { if ('${m['id']}' == '${_winnerId}') { winnerName = m['name']?.toString(); break; } } } final maxW = MediaQuery.sizeOf(context).width - 40.w; return Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: maxW.clamp(280.w, 400.w)), child: SizedBox( width: double.infinity, height: 480.h.clamp(380.h, 560.h), child: Column( children: [ Expanded( child: Container( width: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.grey.shade900, Colors.grey.shade800, ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.circular(20.r), border: Border.all(color: Colors.orange.shade400, width: 3.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.35), blurRadius: 18.r, offset: Offset(0, 10.h), ), ], ), child: Container( margin: EdgeInsets.all(18.w), decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(12.r), ), child: LayoutBuilder( builder: (context, constraints) { final rowH = constraints.maxHeight / 7; return Stack( clipBehavior: Clip.hardEdge, children: [ AnimatedBuilder( animation: _slotAnimation, builder: (context, child) { return Column( children: List.generate(7, (index) { final displayIndex = index < _displayNames.length ? index : 0; final name = _displayNames[displayIndex]; final isWinner = _isComplete && index == 3; final isCenterHighlight = _isAnimating && index == 3; return Expanded( child: AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { final double scale = isWinner || isCenterHighlight ? 1.08 : 1.0; final double fontSize = isWinner ? 24.sp : isCenterHighlight ? 20.sp : 18.sp; final FontWeight weight = isWinner ? FontWeight.w900 : isCenterHighlight ? FontWeight.w800 : FontWeight.w700; final List colors = isWinner ? [ Colors.green.shade500, Colors.green.shade600, ] : isCenterHighlight ? [ Colors.deepPurple.shade500, Colors.deepPurple.shade700, ] : [ Colors.blueGrey.shade700, Colors.blueGrey.shade900, ]; return AnimatedContainer( duration: const Duration( milliseconds: 220), curve: Curves.easeInOut, transform: Matrix4.identity() ..scale(scale), child: Container( width: double.infinity, margin: EdgeInsets.symmetric( vertical: 6.h, horizontal: 12.w, ), decoration: BoxDecoration( gradient: LinearGradient( colors: colors, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(10.r), border: Border.all( color: Colors.white.withOpacity( isWinner || isCenterHighlight ? 0.7 : 0.15), width: isWinner || isCenterHighlight ? 2.w : 1.w, ), boxShadow: [ BoxShadow( color: Colors.black .withOpacity(0.4), blurRadius: isWinner ? 14.r : 6.r, offset: Offset(0, 3.h), ), ], ), child: Center( child: Text( name.length > 22 ? '${name.substring(0, 22)}…' : name, style: TextStyle( fontSize: fontSize, fontWeight: weight, color: Colors.white, letterSpacing: 0.6, shadows: [ Shadow( color: Colors.black .withOpacity(0.5), blurRadius: 4.r, offset: const Offset(1.5, 1.5), ), ], ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ), ); }, ), ); }), ); }, ), if (_isComplete) Positioned( top: rowH * 3, left: 0, right: 0, height: rowH, child: IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.r), border: Border.all( color: Colors.amber.shade400, width: 2.5.w, ), boxShadow: [ BoxShadow( color: Colors.amber.withOpacity(0.35), blurRadius: 14.r, spreadRadius: 0.5, ), ], ), ), ), ), ], ); }, ), ), ), ), SizedBox(height: 16.h), if (_isComplete && winnerName != null) Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 14.h), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.green.shade50, Colors.green.shade100.withOpacity(0.85), ], ), borderRadius: BorderRadius.circular(14.r), border: Border.all(color: Colors.green.shade200, width: 1.5), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.emoji_events, color: Colors.amber.shade700, size: 26.w), SizedBox(width: 10.w), Flexible( child: Text( winnerName, style: TextStyle( fontSize: 19.sp, fontWeight: FontWeight.w800, color: Colors.green.shade900, height: 1.2, ), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ], ), ), ), ); } } // 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; }