import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:math' as math; class AnimatedDrawWheel extends StatefulWidget { final List> members; final Function(String winnerId) onDrawComplete; final String? serverSeed; final String? clientSeed; final int? nonce; const AnimatedDrawWheel({ super.key, required this.members, required this.onDrawComplete, this.serverSeed, this.clientSeed, this.nonce, }); @override State createState() => _AnimatedDrawWheelState(); } class _AnimatedDrawWheelState extends State with TickerProviderStateMixin { late AnimationController _spinController; late AnimationController _pulseController; late Animation _spinAnimation; late Animation _pulseAnimation; bool _isSpinning = false; bool _isDrawComplete = false; String? _winnerId; int _selectedIndex = 0; @override void initState() { super.initState(); _spinController = AnimationController( duration: const Duration(seconds: 4), vsync: this, ); _pulseController = AnimationController( duration: const Duration(milliseconds: 1000), vsync: this, ); _spinAnimation = Tween( begin: 0, end: 1, ).animate(CurvedAnimation( parent: _spinController, curve: Curves.easeOut, )); _pulseAnimation = Tween( begin: 1.0, end: 1.2, ).animate(CurvedAnimation( parent: _pulseController, curve: Curves.easeInOut, )); _spinController.addStatusListener((status) { if (status == AnimationStatus.completed) { _completeDraw(); } }); // Auto-start the draw after a short delay Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { _startDraw(); } }); } @override void dispose() { _spinController.dispose(); _pulseController.dispose(); super.dispose(); } void _startDraw() { if (_isSpinning) return; print('🎰 Starting draw animation...'); setState(() { _isSpinning = true; _isDrawComplete = false; }); // Calculate the winner using provably fair algorithm _calculateWinner(); print('🎯 Winner calculated: ${_winnerId} (index: $_selectedIndex)'); // Start spinning animation _spinController.forward(); _pulseController.repeat(reverse: true); } void _calculateWinner() { // Use provably fair algorithm to determine winner final serverSeed = widget.serverSeed ?? 'default_server_seed'; final clientSeed = widget.clientSeed ?? 'default_client_seed'; final nonce = widget.nonce ?? DateTime.now().millisecondsSinceEpoch; // Create a deterministic hash from the seeds and nonce final combinedSeed = '$serverSeed:$clientSeed:$nonce'; final hash = _generateHash(combinedSeed); // Convert hash to a number and select winner final randomValue = _hashToNumber(hash); _selectedIndex = randomValue % widget.members.length; _winnerId = widget.members[_selectedIndex]['id']; } String _generateHash(String input) { // Simple hash function for demonstration // In production, use a proper cryptographic hash 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) { // Pad with zeros if hash is too short hash = hash.padRight(10, '0'); } return int.parse(hash.substring(0, 10)); } void _completeDraw() { print('🎉 Draw completed! Winner: $_winnerId'); setState(() { _isSpinning = false; _isDrawComplete = true; }); _pulseController.stop(); _pulseController.reset(); // Notify parent of the winner if (_winnerId != null) { widget.onDrawComplete(_winnerId!); } } @override Widget build(BuildContext context) { return Container( width: 400.w, height: 400.w, child: Stack( alignment: Alignment.center, children: [ // Outer ring Container( width: 400.w, height: 400.w, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.purple.shade300, width: 8.w, ), boxShadow: [ BoxShadow( color: Colors.purple.shade200, blurRadius: 20.r, spreadRadius: 5.r, ), ], ), ), // Spinning wheel AnimatedBuilder( animation: _spinAnimation, builder: (context, child) { return Transform.rotate( angle: _spinAnimation.value * 2 * math.pi * 5 + (_selectedIndex * 2 * math.pi / widget.members.length), child: Container( width: 360.w, height: 360.w, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ Colors.purple.shade50, Colors.purple.shade100, Colors.purple.shade200, ], ), ), child: CustomPaint( painter: WheelPainter( members: widget.members, selectedIndex: _isDrawComplete ? _selectedIndex : -1, ), ), ), ); }, ), // Center circle with pointer Container( width: 80.w, height: 80.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, border: Border.all(color: Colors.purple.shade400, width: 3.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10.r, offset: Offset(0, 5.h), ), ], ), child: Icon( Icons.casino, color: Colors.purple.shade600, size: 40.w, ), ), // Pointer arrow Positioned( top: 20.h, child: Container( width: 0, height: 0, child: CustomPaint( painter: PointerPainter(), size: Size(20.w, 40.h), ), ), ), // Winner highlight if (_isDrawComplete) AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { return Transform.scale( scale: _pulseAnimation.value, child: Container( width: 400.w, height: 400.w, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.green.shade400, width: 4.w, ), ), ), ); }, ), ], ), ); } } class WheelPainter extends CustomPainter { final List> members; final int selectedIndex; WheelPainter({ required this.members, required this.selectedIndex, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width / 2 - 20; final paint = Paint() ..style = PaintingStyle.fill; final textPainter = TextPainter( textDirection: TextDirection.ltr, textAlign: TextAlign.center, ); for (int i = 0; i < members.length; i++) { final startAngle = (i * 2 * math.pi / members.length) - math.pi / 2; final sweepAngle = 2 * math.pi / members.length; // Color based on selection if (i == selectedIndex) { paint.color = Colors.green.shade300; } else { paint.color = (i % 2 == 0) ? Colors.purple.shade100 : Colors.purple.shade200; } // Draw segment canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle, true, paint, ); // Draw member name final memberName = members[i]['name'] ?? 'Member ${i + 1}'; final text = TextSpan( text: memberName.length > 8 ? '${memberName.substring(0, 8)}...' : memberName, style: TextStyle( color: Colors.purple.shade800, fontSize: 12.sp, fontWeight: FontWeight.w600, ), ); textPainter.text = text; textPainter.layout(); final textAngle = startAngle + sweepAngle / 2; final textRadius = radius * 0.7; final textX = center.dx + textRadius * math.cos(textAngle) - textPainter.width / 2; final textY = center.dy + textRadius * math.sin(textAngle) - textPainter.height / 2; canvas.save(); canvas.translate(textX, textY); canvas.rotate(textAngle + math.pi / 2); textPainter.paint(canvas, Offset.zero); canvas.restore(); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } class PointerPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.red.shade600 ..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; }