chitfund/luckychit/lib/shared/widgets/alternative_draw_animations...

1091 lines
34 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;
int _currentSelectedIndex = 0;
@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();
// 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<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;
}