1141 lines
38 KiB
Dart
1141 lines
38 KiB
Dart
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<Map<String, dynamic>> 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<CardFlipDrawAnimation> createState() => _CardFlipDrawAnimationState();
|
|
}
|
|
|
|
class _CardFlipDrawAnimationState extends State<CardFlipDrawAnimation>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _flipController;
|
|
late AnimationController _scaleController;
|
|
late Animation<double> _flipAnimation;
|
|
late Animation<double> _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<double>(
|
|
begin: 0,
|
|
end: 1,
|
|
).animate(CurvedAnimation(
|
|
parent: _flipController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
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<SlotMachineDrawAnimation> createState() => _SlotMachineDrawAnimationState();
|
|
}
|
|
|
|
class _SlotMachineDrawAnimationState extends State<SlotMachineDrawAnimation>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _slotController;
|
|
late Animation<double> _slotAnimation;
|
|
late AnimationController _pulseController;
|
|
late Animation<double> _pulseAnimation;
|
|
|
|
bool _isAnimating = false;
|
|
bool _isComplete = false;
|
|
String? _winnerId;
|
|
List<String> _displayNames = [];
|
|
Timer? _slotTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_slotController = AnimationController(
|
|
duration: const Duration(milliseconds: 400),
|
|
vsync: this,
|
|
);
|
|
|
|
_slotAnimation = Tween<double>(
|
|
begin: 0,
|
|
end: 1,
|
|
).animate(CurvedAnimation(
|
|
parent: _slotController,
|
|
curve: Curves.linear,
|
|
));
|
|
|
|
_pulseController = AnimationController(
|
|
duration: const Duration(milliseconds: 800),
|
|
vsync: this,
|
|
);
|
|
|
|
_pulseAnimation = Tween<double>(
|
|
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();
|
|
});
|
|
|
|
_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();
|
|
});
|
|
|
|
_slotController.forward().then((_) {
|
|
_slotController.reset();
|
|
});
|
|
|
|
currentSpin++;
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _completeAnimation() {
|
|
final winnerName = widget.members.firstWhere((m) => m['id'] == _winnerId)['name'];
|
|
|
|
setState(() {
|
|
_isAnimating = false;
|
|
_isComplete = true;
|
|
// Show winner in center position (index 2) with padding names
|
|
_displayNames = [
|
|
_displayNames.isNotEmpty ? _displayNames[0] : '',
|
|
_displayNames.length > 1 ? _displayNames[1] : '',
|
|
winnerName, // Center - the winner!
|
|
_displayNames.length > 3 ? _displayNames[3] : '',
|
|
_displayNames.length > 4 ? _displayNames[4] : '',
|
|
];
|
|
});
|
|
|
|
_pulseController.stop();
|
|
|
|
// Delay callback slightly for visual effect
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
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: List.generate(5, (index) {
|
|
final displayIndex = index < _displayNames.length ? index : 0;
|
|
final name = _displayNames[displayIndex];
|
|
final isWinner = _isComplete && index == 2; // Center position
|
|
final isCenterHighlight = _isAnimating && index == 2; // Always highlight center
|
|
|
|
return Expanded(
|
|
child: AnimatedBuilder(
|
|
animation: _pulseAnimation,
|
|
builder: (context, child) {
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
curve: Curves.easeInOut,
|
|
transform: Matrix4.identity()
|
|
..scale(isCenterHighlight || isWinner ? 1.05 : 1.0),
|
|
child: Container(
|
|
width: double.infinity,
|
|
margin: EdgeInsets.symmetric(vertical: 2.h, horizontal: 4.w),
|
|
decoration: BoxDecoration(
|
|
gradient: isWinner
|
|
? LinearGradient(
|
|
colors: [
|
|
Colors.green.shade600,
|
|
Colors.green.shade700,
|
|
],
|
|
)
|
|
: isCenterHighlight
|
|
? LinearGradient(
|
|
colors: [
|
|
Colors.orange.shade600,
|
|
Colors.red.shade600,
|
|
],
|
|
)
|
|
: LinearGradient(
|
|
colors: [
|
|
Colors.blue.shade700,
|
|
Colors.blue.shade800,
|
|
],
|
|
),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
border: Border.all(
|
|
color: isWinner || isCenterHighlight
|
|
? Colors.white.withOpacity(0.6)
|
|
: Colors.white.withOpacity(0.1),
|
|
width: isWinner || isCenterHighlight ? 2.w : 1.w,
|
|
),
|
|
boxShadow: isWinner
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.green.shade300,
|
|
blurRadius: 12.r,
|
|
spreadRadius: 3.r,
|
|
),
|
|
]
|
|
: isCenterHighlight
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.orange.shade300.withOpacity(0.6),
|
|
blurRadius: 8.r,
|
|
spreadRadius: 2.r,
|
|
),
|
|
]
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 4.r,
|
|
offset: Offset(0, 2.h),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
name.length > 15 ? '${name.substring(0, 15)}...' : name,
|
|
style: TextStyle(
|
|
fontSize: isWinner || isCenterHighlight ? 18.sp : 15.sp,
|
|
fontWeight: isWinner || isCenterHighlight
|
|
? FontWeight.w900
|
|
: FontWeight.w600,
|
|
color: Colors.white,
|
|
letterSpacing: 0.5,
|
|
shadows: [
|
|
Shadow(
|
|
color: Colors.black.withOpacity(0.5),
|
|
blurRadius: 3.r,
|
|
offset: Offset(1, 1),
|
|
),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
},
|
|
),
|
|
|
|
// 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<NumberRouletteDrawAnimation> createState() => _NumberRouletteDrawAnimationState();
|
|
}
|
|
|
|
class _NumberRouletteDrawAnimationState extends State<NumberRouletteDrawAnimation>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _rouletteController;
|
|
late Animation<double> _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<double>(
|
|
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;
|
|
}
|