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

405 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:math' as math;
import 'dart:async';
class ParticleDrawAnimation 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 ParticleDrawAnimation({
super.key,
required this.members,
required this.onDrawComplete,
this.serverSeed,
this.clientSeed,
this.nonce,
this.animationDuration = const Duration(seconds: 5),
});
@override
State<ParticleDrawAnimation> createState() => _ParticleDrawAnimationState();
}
class _ParticleDrawAnimationState extends State<ParticleDrawAnimation>
with TickerProviderStateMixin {
late AnimationController _particleController;
late AnimationController _winnerController;
late Animation<double> _particleAnimation;
late Animation<double> _winnerAnimation;
bool _isAnimating = false;
bool _isComplete = false;
String? _winnerId;
Map<String, dynamic>? _winnerData;
List<Particle> _particles = [];
Timer? _particleTimer;
@override
void initState() {
super.initState();
_particleController = AnimationController(
duration: widget.animationDuration,
vsync: this,
);
_winnerController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
_particleAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _particleController,
curve: Curves.easeInOut,
));
_winnerAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(CurvedAnimation(
parent: _winnerController,
curve: Curves.elasticOut,
));
_particleController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_completeAnimation();
}
});
// Auto-start after delay
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) _startAnimation();
});
}
@override
void dispose() {
_particleController.dispose();
_winnerController.dispose();
_particleTimer?.cancel();
super.dispose();
}
void _startAnimation() {
if (_isAnimating) return;
setState(() {
_isAnimating = true;
});
_calculateWinner();
_generateParticles();
_particleController.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'];
_winnerData = widget.members[selectedIndex];
}
void _generateParticles() {
_particles.clear();
final random = math.Random();
// Generate particles for each member
for (int i = 0; i < widget.members.length; i++) {
final member = widget.members[i];
final isWinner = member['id'] == _winnerId;
// Create multiple particles per member
for (int j = 0; j < (isWinner ? 20 : 5); j++) {
_particles.add(Particle(
id: '${member['id']}_$j',
memberId: member['id'],
memberName: member['name'] ?? 'Unknown',
x: random.nextDouble() * 300.w,
y: random.nextDouble() * 400.h,
vx: (random.nextDouble() - 0.5) * 2,
vy: (random.nextDouble() - 0.5) * 2,
size: random.nextDouble() * 8 + 4,
color: isWinner
? Colors.green.shade400
: Colors.primaries[random.nextInt(Colors.primaries.length)].shade400,
isWinner: isWinner,
));
}
}
}
void _completeAnimation() {
setState(() {
_isAnimating = false;
_isComplete = true;
});
_winnerController.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(
'Particle Draw',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.indigo.shade700,
),
),
SizedBox(height: 20.h),
// Particle Display
Expanded(
child: Container(
width: 280.w,
height: 300.h,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16.r),
border: Border.all(color: Colors.indigo.shade400, width: 2.w),
),
child: AnimatedBuilder(
animation: _particleAnimation,
builder: (context, child) {
return CustomPaint(
painter: ParticlePainter(
particles: _particles,
animationValue: _particleAnimation.value,
isComplete: _isComplete,
),
size: Size(280.w, 300.h),
);
},
),
),
),
// Winner Display
if (_isComplete)
AnimatedBuilder(
animation: _winnerAnimation,
builder: (context, child) {
return Transform.scale(
scale: _winnerAnimation.value,
child: Container(
margin: EdgeInsets.only(top: 20.h),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green.shade400, Colors.green.shade600],
),
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.green.shade300,
blurRadius: 10.r,
offset: Offset(0, 5.h),
),
],
),
child: Column(
children: [
Icon(
Icons.emoji_events,
color: Colors.white,
size: 32.w,
),
SizedBox(height: 8.h),
Text(
'WINNER!',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
_winnerData?['name'] ?? 'Unknown',
style: TextStyle(
fontSize: 16.sp,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
);
},
)
else if (_isAnimating)
Container(
margin: EdgeInsets.only(top: 20.h),
child: Text(
'Particles swirling...',
style: TextStyle(
fontSize: 16.sp,
color: Colors.indigo.shade600,
),
),
),
],
),
);
}
}
class Particle {
final String id;
final String memberId;
final String memberName;
double x;
double y;
double vx;
double vy;
final double size;
final Color color;
final bool isWinner;
Particle({
required this.id,
required this.memberId,
required this.memberName,
required this.x,
required this.y,
required this.vx,
required this.vy,
required this.size,
required this.color,
required this.isWinner,
});
void update() {
x += vx;
y += vy;
// Bounce off walls
if (x <= 0 || x >= 280) vx = -vx;
if (y <= 0 || y >= 300) vy = -vy;
// Keep particles in bounds
x = x.clamp(0, 280);
y = y.clamp(0, 300);
}
}
class ParticlePainter extends CustomPainter {
final List<Particle> particles;
final double animationValue;
final bool isComplete;
ParticlePainter({
required this.particles,
required this.animationValue,
required this.isComplete,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..style = PaintingStyle.fill;
for (final particle in particles) {
// Update particle position
particle.update();
// Apply animation effects
final alpha = isComplete && particle.isWinner
? 1.0
: (0.3 + 0.7 * animationValue);
paint.color = particle.color.withOpacity(alpha);
// Draw particle
canvas.drawCircle(
Offset(particle.x, particle.y),
particle.size * (isComplete && particle.isWinner ? 1.5 : 1.0),
paint,
);
// Draw member name for winner particles
if (isComplete && particle.isWinner) {
final textPainter = TextPainter(
text: TextSpan(
text: particle.memberName.length > 8
? '${particle.memberName.substring(0, 8)}...'
: particle.memberName,
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
particle.x - textPainter.width / 2,
particle.y + particle.size + 5,
),
);
}
}
// Draw connecting lines for winner particles
if (isComplete) {
final winnerParticles = particles.where((p) => p.isWinner).toList();
if (winnerParticles.isNotEmpty) {
paint.color = Colors.green.withOpacity(0.3);
paint.strokeWidth = 2;
paint.style = PaintingStyle.stroke;
for (int i = 0; i < winnerParticles.length - 1; i++) {
canvas.drawLine(
Offset(winnerParticles[i].x, winnerParticles[i].y),
Offset(winnerParticles[i + 1].x, winnerParticles[i + 1].y),
paint,
);
}
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}