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

1252 lines
43 KiB
Dart

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';
import '../../core/themes/draw_slot_theme.dart';
// 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;
final List<String> _baseNames = [];
List<String> _displayNames = [];
int _currentIndex = 0;
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,
));
_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<String>.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 ? <String>[winnerName] : otherNames;
final window = List<String>.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) {
final theme = Theme.of(context);
final scheme = theme.colorScheme;
final d = DrawSlotTheming(scheme, theme.brightness);
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 Semantics(
label: 'Slot machine draw',
child: 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(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_isAnimating)
Padding(
padding: EdgeInsets.only(bottom: 10.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 15.w,
height: 15.w,
child: CircularProgressIndicator(
strokeWidth: 2.2,
color: scheme.tertiary,
),
),
SizedBox(width: 10.w),
Text(
'Selecting winner…',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.onSurface,
),
),
],
),
),
Expanded(
child: Container(
width: double.infinity,
decoration: d.slotChassisDecoration(),
child: Padding(
padding: EdgeInsets.all(14.w),
child: DecoratedBox(
decoration: BoxDecoration(
color: d.slotInnerWell,
borderRadius: BorderRadius.circular(14.r),
border: Border.all(
color: scheme.outline.withOpacity(0.28),
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13.r),
child: LayoutBuilder(
builder: (context, constraints) {
final rowH = constraints.maxHeight / 7;
return Stack(
clipBehavior: Clip.hardEdge,
fit: StackFit.expand,
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 scale = isWinner ||
isCenterHighlight
? 1.05
: 1.0;
List<Color> rowColors;
if (isWinner) {
rowColors = d.rowColorsWinner();
} else if (isCenterHighlight) {
rowColors =
d.rowColorsSpinning();
} else {
rowColors = d.rowColorsIdle();
}
final borderColor = isWinner
? scheme.tertiary
.withOpacity(0.9)
: isCenterHighlight
? scheme.primary
.withOpacity(0.55)
: scheme.outline
.withOpacity(0.22);
return AnimatedContainer(
duration: const Duration(
milliseconds: 220),
curve: Curves.easeInOut,
transform: Matrix4.identity()
..scale(scale),
child: Container(
width: double.infinity,
margin:
EdgeInsets.symmetric(
vertical: 4.h,
horizontal: 10.w,
),
decoration: BoxDecoration(
gradient:
LinearGradient(
colors: rowColors,
begin: Alignment
.centerLeft,
end: Alignment
.centerRight,
),
borderRadius:
BorderRadius
.circular(12.r),
border: Border.all(
color: borderColor,
width: isWinner
? 2.w
: 1.w,
),
boxShadow: [
if (isWinner)
BoxShadow(
color: scheme
.primary
.withOpacity(
0.45),
blurRadius: 16.r,
offset: Offset(
0, 4.h),
),
],
),
child: Center(
child: Text(
name.length > 22
? '${name.substring(0, 22)}'
: name,
style: theme
.textTheme
.titleMedium
?.copyWith(
fontSize: isWinner
? 21.sp
: isCenterHighlight
? 18.sp
: 16.sp,
fontWeight: isWinner
? FontWeight.w900
: FontWeight
.w700,
color: isWinner
? d
.winnerRowText
: d.idleRowText,
letterSpacing: 0.2,
shadows: theme
.brightness ==
Brightness
.dark
? [
Shadow(
color: Colors
.black
.withOpacity(
0.45),
blurRadius:
3,
offset:
const Offset(
0,
1),
),
]
: null,
),
textAlign:
TextAlign.center,
maxLines: 1,
overflow: TextOverflow
.ellipsis,
),
),
),
);
},
),
);
}),
);
},
),
Positioned(
top: rowH * 2.5,
left: 8.w,
right: 8.w,
height: 1,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
scheme.tertiary.withOpacity(0.45),
Colors.transparent,
],
),
),
),
),
Positioned(
top: rowH * 4.5,
left: 8.w,
right: 8.w,
height: 1,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
scheme.tertiary.withOpacity(0.45),
Colors.transparent,
],
),
),
),
),
if (_isComplete)
Positioned(
top: rowH * 3,
left: 0,
right: 0,
height: rowH,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(12.r),
border: Border.all(
color: d.winnerFrameColor,
width: 2.5.w,
),
boxShadow: [
BoxShadow(
color: scheme.tertiary
.withOpacity(0.35),
blurRadius: 18.r,
spreadRadius: 0.5,
),
],
),
),
),
),
],
);
},
),
),
),
),
),
),
SizedBox(height: 14.h),
if (_isComplete && winnerName != null)
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 16.w, vertical: 16.h),
decoration: d.winnerSummaryDecoration(),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_events_rounded,
color: d.winnerSummaryIcon,
size: 28.w,
),
SizedBox(width: 12.w),
Flexible(
child: Text(
winnerName,
style: d.winnerSummaryTitle(theme.textTheme)
.copyWith(fontSize: 18.sp),
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<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;
}