chitfund/luckychit/lib/shared/widgets/animated_draw_wheel.dart

375 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math' as math;
class AnimatedDrawWheel extends StatefulWidget {
final List<Map<String, dynamic>> 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<AnimatedDrawWheel> createState() => _AnimatedDrawWheelState();
}
class _AnimatedDrawWheelState extends State<AnimatedDrawWheel>
with TickerProviderStateMixin {
late AnimationController _spinController;
late AnimationController _pulseController;
late Animation<double> _spinAnimation;
late Animation<double> _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<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _spinController,
curve: Curves.easeOut,
));
_pulseAnimation = Tween<double>(
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<Map<String, dynamic>> 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;
}