import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'dart:math'; class TuberculosePage extends StatefulWidget { const TuberculosePage({super.key}); @override State createState() => _TuberculosePageState(); } class _TuberculosePageState extends State with TickerProviderStateMixin { late AnimationController _fadeController; late AnimationController _slideController; late AnimationController _scaleController; late AnimationController _pulseController; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _scaleAnimation; late Animation _pulseAnimation; final FirebaseFirestore _firestore = FirebaseFirestore.instance; Map? tuberculoseData; bool isLoading = true; @override void initState() { super.initState(); _fadeController = AnimationController( duration: const Duration(milliseconds: 1200), vsync: this, ); _slideController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _scaleController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _pulseController = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this, ); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut), ); _slideAnimation = Tween( begin: const Offset(0, 0.5), end: Offset.zero, ).animate( CurvedAnimation(parent: _slideController, curve: Curves.elasticOut)); _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate( CurvedAnimation(parent: _scaleController, curve: Curves.elasticOut), ); _pulseAnimation = Tween(begin: 1.0, end: 1.05).animate( CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), ); _loadTuberculoseData(); } Future _loadTuberculoseData() async { try { final doc = await _firestore .collection('maladies') .where('nom', isEqualTo: 'Tuberculose pulmonaire') .get(); if (doc.docs.isNotEmpty) { setState(() { tuberculoseData = doc.docs.first.data(); isLoading = false; }); // Animations en cascade _fadeController.forward(); await Future.delayed(const Duration(milliseconds: 200)); _slideController.forward(); await Future.delayed(const Duration(milliseconds: 300)); _scaleController.forward(); _pulseController.repeat(reverse: true); } } catch (e) { setState(() { isLoading = false; }); _showErrorSnackBar('Erreur lors du chargement: $e'); } } void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.error_outline, color: Colors.white), const SizedBox(width: 12), Expanded(child: Text(message)), ], ), backgroundColor: const Color(0xFFE57373), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), margin: const EdgeInsets.all(16), ), ); } void _goBack() { Navigator.of(context).pop(); } @override void dispose() { _fadeController.dispose(); _slideController.dispose(); _scaleController.dispose(); _pulseController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8FFFE), body: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // App Bar avec design glassmorphism SliverAppBar( expandedHeight: 140, floating: false, pinned: true, elevation: 0, backgroundColor: Colors.transparent, flexibleSpace: FlexibleSpaceBar( title: const Text( 'Tuberculose Pulmonaire', style: TextStyle( fontWeight: FontWeight.w700, color: Colors.white, fontSize: 18, letterSpacing: 0.5, shadows: [ Shadow( offset: Offset(0, 2), blurRadius: 8, color: Colors.black38, ), ], ), ), background: Stack( fit: StackFit.expand, children: [ // Gradient de fond Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF1B5E20), Color(0xFF2E7D32), Color(0xFF43A047), Color(0xFF66BB6A), ], stops: [0.0, 0.3, 0.7, 1.0], ), ), ), // Effet de particules Positioned.fill( child: CustomPaint( painter: ParticlesPainter(), ), ), // Overlay gradient Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.1), Colors.transparent, ], ), ), ), ], ), ), leading: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white.withOpacity(0.3)), ), child: IconButton( icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 20), onPressed: _goBack, ), ), ), // Contenu principal SliverToBoxAdapter( child: isLoading ? _buildLoadingWidget() : _buildMainContent(), ), ], ), // Bouton de retour flottant floatingActionButton: _buildFloatingBackButton(), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } Widget _buildFloatingBackButton() { return Container( margin: const EdgeInsets.only(bottom: 20), child: FloatingActionButton.extended( onPressed: _goBack, backgroundColor: const Color(0xFF4CAF50), foregroundColor: Colors.white, elevation: 8, highlightElevation: 12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), label: const Text( 'Retour', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, letterSpacing: 0.5, ), ), icon: const Icon( Icons.arrow_back_rounded, size: 22, ), ), ); } Widget _buildLoadingWidget() { return Container( height: MediaQuery.of(context).size.height * 0.7, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Loading animation améliorée Stack( alignment: Alignment.center, children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( valueColor: const AlwaysStoppedAnimation(Color(0xFF4CAF50)), strokeWidth: 4, backgroundColor: Colors.grey.withOpacity(0.2), ), ), Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFF4CAF50).withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: const Icon( Icons.medical_services, color: Color(0xFF4CAF50), size: 24, ), ), ], ), const SizedBox(height: 32), const Text( 'Chargement des informations...', style: TextStyle( fontSize: 18, color: Color(0xFF666666), fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), const SizedBox(height: 8), Text( 'Préparation des données médicales', style: TextStyle( fontSize: 14, color: Colors.grey[500], fontWeight: FontWeight.w400, ), ), ], ), ), ); } Widget _buildMainContent() { if (tuberculoseData == null) { return Container( height: MediaQuery.of(context).size.height * 0.7, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 120, decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(60), ), child: const Icon( Icons.error_outline_rounded, size: 64, color: Color(0xFFE57373), ), ), const SizedBox(height: 24), const Text( 'Données non trouvées', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Color(0xFFE57373), ), ), const SizedBox(height: 8), Text( 'Vérifiez votre connexion et réessayez', style: TextStyle( color: Colors.grey[600], fontSize: 16, ), ), const SizedBox(height: 24), // Bouton de retour dans l'état d'erreur ElevatedButton.icon( onPressed: _goBack, icon: const Icon(Icons.arrow_back_rounded), label: const Text('Retour'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF4CAF50), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ], ), ), ); } return FadeTransition( opacity: _fadeAnimation, child: SlideTransition( position: _slideAnimation, child: ScaleTransition( scale: _scaleAnimation, child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Carte principale avec effet de pulsation AnimatedBuilder( animation: _pulseAnimation, builder: (context, child) { return Transform.scale( scale: _pulseAnimation.value, child: _buildDiseaseCard(), ); }, ), const SizedBox(height: 32), // Section Causes avec animation décalée _buildAnimatedSection( delay: 0, child: Column( children: [ _buildSectionHeader( '🦠 Causes', Icons.coronavirus_outlined), const SizedBox(height: 16), _buildCausesSection(), ], ), ), const SizedBox(height: 32), // Section Conséquences _buildAnimatedSection( delay: 200, child: Column( children: [ _buildSectionHeader( '⚠️ Conséquences', Icons.warning_amber_outlined), const SizedBox(height: 16), _buildConsequencesSection(), ], ), ), const SizedBox(height: 32), // Section Préventions _buildAnimatedSection( delay: 400, child: Column( children: [ _buildSectionHeader( '🛡️ Préventions', Icons.shield_outlined), const SizedBox(height: 16), _buildPreventionsSection(), ], ), ), const SizedBox(height: 100), // Espace pour le bouton flottant ], ), ), ), ), ); } Widget _buildAnimatedSection({required int delay, required Widget child}) { return TweenAnimationBuilder( duration: Duration(milliseconds: 800 + delay), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOutBack, builder: (context, value, _) { return Transform.translate( offset: Offset(0, 30 * (1 - value)), child: Opacity( opacity: value, child: child, ), ); }, ); } Widget _buildDiseaseCard() { return Container( width: double.infinity, decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white, Color(0xFFF8FFFE), Color(0xFFE8F5E8), ], ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: const Color(0xFF4CAF50).withOpacity(0.15), spreadRadius: 0, blurRadius: 20, offset: const Offset(0, 8), ), BoxShadow( color: Colors.white.withOpacity(0.7), spreadRadius: 1, blurRadius: 10, offset: const Offset(-5, -5), ), ], ), child: Stack( children: [ // Motif de fond subtil Positioned.fill( child: CustomPaint( painter: BackgroundPatternPainter(), ), ), Padding( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Image avec effet glassmorphism Container( width: 90, height: 90, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white.withOpacity(0.4), Colors.white.withOpacity(0.1), ], ), border: Border.all( color: Colors.white.withOpacity(0.3), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), spreadRadius: 1, blurRadius: 15, offset: const Offset(0, 5), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(18), child: tuberculoseData!['image'] != null ? CachedNetworkImage( imageUrl: tuberculoseData!['image'], fit: BoxFit.cover, placeholder: (context, url) => Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.grey[200]!, Colors.grey[100]!, ], ), ), child: const Center( child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Color(0xFF4CAF50), ), ), ), ), errorWidget: (context, url, error) => Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.grey[200]!, Colors.grey[100]!, ], ), ), child: const Icon( Icons.medical_services_outlined, color: Colors.grey, size: 45, ), ), ) : Container( decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFF4CAF50), Color(0xFF66BB6A), ], ), ), child: const Icon( Icons.medical_services_outlined, color: Colors.white, size: 45, ), ), ), ), const SizedBox(width: 20), // Informations principales Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tuberculoseData!['nom'] ?? 'Tuberculose pulmonaire', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1B5E20), letterSpacing: -0.5, height: 1.2, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( gradient: LinearGradient( colors: [ _getStageColor(tuberculoseData!['stade']), _getStageColor(tuberculoseData!['stade']) .withOpacity(0.8), ], ), borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( color: _getStageColor(tuberculoseData!['stade']) .withOpacity(0.3), spreadRadius: 0, blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Text( 'Stade: ${tuberculoseData!['stade'] ?? 'Non spécifié'}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 14, letterSpacing: 0.3, ), ), ), ], ), ), ], ), ], ), ), ], ), ); } Widget _buildSectionHeader(String title, IconData icon) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ const Color(0xFF4CAF50).withOpacity(0.05), Colors.transparent, ], ), borderRadius: BorderRadius.circular(16), border: Border.all( color: const Color(0xFF4CAF50).withOpacity(0.1), ), ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ const Color(0xFF4CAF50).withOpacity(0.15), const Color(0xFF4CAF50).withOpacity(0.05), ], ), borderRadius: BorderRadius.circular(14), border: Border.all( color: const Color(0xFF4CAF50).withOpacity(0.2), ), ), child: Icon(icon, color: const Color(0xFF2E7D32), size: 26), ), const SizedBox(width: 16), Text( title, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w800, color: Color(0xFF1B5E20), letterSpacing: -0.3, ), ), ], ), ); } Widget _buildCausesSection() { final causes = [ { 'titre': 'Bactérie Mycobacterium tuberculosis', 'description': 'Agent pathogène principal responsable de la tuberculose pulmonaire', 'icon': Icons.coronavirus_outlined, 'gradient': [const Color(0xFFE57373), const Color(0xFFEF5350)], }, { 'titre': 'Transmission aérienne', 'description': 'Propagation par gouttelettes lors de toux, éternuements ou parole', 'icon': Icons.air, 'gradient': [const Color(0xFF81C784), const Color(0xFF66BB6A)], }, { 'titre': 'Système immunitaire affaibli', 'description': 'VIH, malnutrition, diabète augmentent les risques d\'infection', 'icon': Icons.shield_outlined, 'gradient': [const Color(0xFF64B5F6), const Color(0xFF42A5F5)], }, { 'titre': 'Conditions de vie précaires', 'description': 'Surpeuplement, mauvaise ventilation favorisent la transmission', 'icon': Icons.home_outlined, 'gradient': [const Color(0xFFFFB74D), const Color(0xFFFF9800)], }, ]; return Column( children: causes.asMap().entries.map((entry) { int index = entry.key; Map cause = entry.value; return _buildAnimatedInfoCard( delay: index * 100, titre: cause['titre'] as String, description: cause['description'] as String, icon: cause['icon'] as IconData, gradient: cause['gradient'] as List, ); }).toList(), ); } Widget _buildConsequencesSection() { final consequences = [ { 'titre': 'Destruction du tissu pulmonaire', 'description': 'Formation de cavités et cicatrices dans les poumons', 'icon': Icons.healing_outlined, 'gradient': [const Color(0xFFE57373), const Color(0xFFEF5350)], }, { 'titre': 'Insuffisance respiratoire', 'description': 'Difficulté à respirer et diminution de la capacité pulmonaire', 'icon': Icons.air, 'gradient': [const Color(0xFF9575CD), const Color(0xFF7E57C2)], }, { 'titre': 'Propagation systémique', 'description': 'Extension possible vers d\'autres organes (os, reins, cerveau)', 'icon': Icons.scatter_plot_outlined, 'gradient': [const Color(0xFFFF8A65), const Color(0xFFFF7043)], }, { 'titre': 'Complications cardiovasculaires', 'description': 'Risque d\'hypertension pulmonaire et de cœur pulmonaire', 'icon': Icons.favorite_outline, 'gradient': [const Color(0xFFE91E63), const Color(0xFFAD1457)], }, ]; return Column( children: consequences.asMap().entries.map((entry) { int index = entry.key; Map consequence = entry.value; return _buildAnimatedInfoCard( delay: index * 100, titre: consequence['titre'] as String, description: consequence['description'] as String, icon: consequence['icon'] as IconData, gradient: consequence['gradient'] as List, ); }).toList(), ); } Widget _buildPreventionsSection() { final preventions = [ { 'titre': 'Vaccination BCG', 'description': 'Vaccination recommandée chez les nourrissons et personnes à risque', 'icon': Icons.vaccines_outlined, 'gradient': [const Color(0xFF4CAF50), const Color(0xFF43A047)], }, { 'titre': 'Dépistage précoce', 'description': 'Tests réguliers pour les personnes à risque et contacts', 'icon': Icons.search_outlined, 'gradient': [const Color(0xFF2196F3), const Color(0xFF1976D2)], }, { 'titre': 'Hygiène respiratoire', 'description': 'Port du masque, couverture de la bouche lors de toux', 'icon': Icons.masks, 'gradient': [const Color(0xFF9C27B0), const Color(0xFF7B1FA2)], }, { 'titre': 'Amélioration des conditions de vie', 'description': 'Ventilation adéquate, réduction du surpeuplement', 'icon': Icons.home_outlined, 'gradient': [const Color(0xFFFF9800), const Color(0xFFE65100)], }, { 'titre': 'Renforcement immunitaire', 'description': 'Nutrition équilibrée, traitement des maladies associées', 'icon': Icons.fitness_center_outlined, 'gradient': [const Color(0xFF795548), const Color(0xFF5D4037)], }, ]; return Column( children: preventions.asMap().entries.map((entry) { int index = entry.key; Map prevention = entry.value; return _buildAnimatedInfoCard( delay: index * 100, titre: prevention['titre'] as String, description: prevention['description'] as String, icon: prevention['icon'] as IconData, gradient: prevention['gradient'] as List, ); }).toList(), ); } Widget _buildAnimatedInfoCard({ required int delay, required String titre, required String description, required IconData icon, required List gradient, }) { return TweenAnimationBuilder( duration: Duration(milliseconds: 600 + delay), tween: Tween(begin: 0.0, end: 1.0), curve: Curves.easeOutBack, builder: (context, value, _) { return Transform.translate( offset: Offset(30 * (1 - value), 0), child: Opacity( opacity: value, child: _buildInfoCard( titre: titre, description: description, icon: icon, gradient: gradient, ), ), ); }, ); } Widget _buildInfoCard({ required String titre, required String description, required IconData icon, required List gradient, }) { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.white, Colors.white.withOpacity(0.9), ], ), borderRadius: BorderRadius.circular(20), border: Border.all( color: gradient[0].withOpacity(0.1), width: 1, ), boxShadow: [ BoxShadow( color: gradient[0].withOpacity(0.1), spreadRadius: 0, blurRadius: 20, offset: const Offset(0, 8), ), BoxShadow( color: Colors.white.withOpacity(0.7), spreadRadius: 1, blurRadius: 10, offset: const Offset(-2, -2), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () { HapticFeedback.lightImpact(); // Animation de tap _showCardDetails(titre, description); }, child: Padding( padding: const EdgeInsets.all(20), child: Row( children: [ // Icône avec gradient Container( width: 60, height: 60, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: gradient, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: gradient[0].withOpacity(0.3), spreadRadius: 0, blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Icon( icon, color: Colors.white, size: 28, ), ), const SizedBox(width: 20), // Contenu texte Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titre, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: Color(0xFF1B5E20), letterSpacing: -0.2, ), ), const SizedBox(height: 8), Text( description, style: TextStyle( fontSize: 14, color: Colors.grey[600], fontWeight: FontWeight.w400, height: 1.4, letterSpacing: 0.1, ), ), ], ), ), // Flèche indicatrice Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: gradient[0].withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon( Icons.arrow_forward_ios, color: gradient[0], size: 16, ), ), ], ), ), ), ), ); } void _showCardDetails(String titre, String description) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (context) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle bar Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 24), Text( titre, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w800, color: Color(0xFF1B5E20), ), ), const SizedBox(height: 16), Text( description, style: TextStyle( fontSize: 16, color: Colors.grey[700], height: 1.6, ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.pop(context), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF4CAF50), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text( 'Fermer', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), ), ), ], ), ), ); } Color _getStageColor(String? stage) { switch (stage?.toLowerCase()) { case 'précoce': case 'precoce': case 'leger': case 'léger': return const Color(0xFF4CAF50); case 'modéré': case 'modere': case 'moyen': return const Color(0xFFFF9800); case 'avancé': case 'avance': case 'sévère': case 'severe': case 'grave': return const Color(0xFFE57373); case 'critique': case 'terminal': return const Color(0xFFF44336); default: return const Color(0xFF9E9E9E); } } } // Painter pour les particules dans l'AppBar class ParticlesPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.1) ..style = PaintingStyle.fill; final random = Random(42); // Seed fixe pour la cohérence for (int i = 0; i < 50; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = random.nextDouble() * 3 + 1; canvas.drawCircle(Offset(x, y), radius, paint); } // Ajout de quelques particules plus grandes paint.color = Colors.white.withOpacity(0.05); for (int i = 0; i < 20; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = random.nextDouble() * 8 + 3; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } // Painter pour le motif de fond de la carte principale class BackgroundPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = const Color(0xFF4CAF50).withOpacity(0.02) ..style = PaintingStyle.stroke ..strokeWidth = 1; final spacing = 40.0; // Lignes diagonales for (double i = -size.height; i < size.width + size.height; i += spacing) { canvas.drawLine( Offset(i, 0), Offset(i + size.height, size.height), paint, ); } // Cercles décoratifs paint.style = PaintingStyle.fill; paint.color = const Color(0xFF4CAF50).withOpacity(0.01); final random = Random(123); for (int i = 0; i < 15; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = random.nextDouble() * 20 + 5; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }