import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:forward_chaining_man_app/app/views/page_login.dart'; import 'package:forward_chaining_man_app/app/views/splash_screen/page/page_splash_screen.dart'; import 'package:forward_chaining_man_app/app/views/teacher_page.dart'; import 'package:get/get.dart'; import 'package:lottie/lottie.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:math' as math; class IntroPage extends StatefulWidget { const IntroPage({Key? key}) : super(key: key); @override State createState() => _IntroPageState(); } class _IntroPageState extends State with TickerProviderStateMixin { final PageController _pageController = PageController(); late AnimationController _animationController; late AnimationController _networkController; int _currentPage = 0; // Network visualization data final List _nodes = []; final List _connections = []; final List> _onboardingPages = [ { 'title': 'Selamat Datang', 'description': 'Forward Chaining adalah aplikasi rekomendasi karir dan jurusan kuliah yang menggunakan sistem pakar berbasis aturan untuk memberikan hasil yang akurat.', 'icon': Icons.psychology, }, { 'title': 'Cara Kerja Aplikasi', 'description': 'Aplikasi ini menggunakan metode Forward Chaining, yaitu teknik inferensi sistem pakar yang mengevaluasi fakta awal (jawaban Anda) dan mencocokkannya dengan aturan secara berurutan untuk mencapai kesimpulan terbaik.', 'icon': Icons.sync_alt, }, { 'title': 'Alur Penggunaan', 'description': 'Anda akan menjawab pertanyaan seputar minat, bakat, dan kepribadian. Sistem kemudian menganalisis jawaban tersebut, menerapkan aturan Forward Chaining, dan menghasilkan rekomendasi karir atau jurusan kuliah yang paling sesuai.', 'icon': Icons.question_answer, }, { 'title': 'Siap Memulai', 'description': 'Silakan pilih peran Anda sebagai Siswa untuk mendapatkan rekomendasi karir/kuliah atau sebagai Guru untuk memantau hasil rekomendasi siswa Anda.', 'icon': Icons.rocket_launch, }, ]; @override void initState() { super.initState(); // Create network nodes and connections _setupNetworkGraph(); // Main controller for logo and elements animation _animationController = AnimationController( duration: const Duration(seconds: 10), vsync: this, )..repeat(); // Network animation controller _networkController = AnimationController( duration: const Duration(milliseconds: 15000), vsync: this, )..repeat(); } // Setup network graph with nodes and connections void _setupNetworkGraph() { final random = math.Random(42); // Fixed seed for consistent layout // Create nodes - spreading them beyond screen boundaries for (int i = 0; i < 25; i++) { _nodes.add( NetworkNode( x: -0.3 + random.nextDouble() * 1.6, y: -0.3 + random.nextDouble() * 1.6, size: 3.0 + random.nextDouble() * 5.0, ), ); } // Create connections between nodes for (int i = 0; i < _nodes.length; i++) { final connectionCount = 3 + random.nextInt(4); final connectedIndices = {}; for (int j = 0; j < connectionCount; j++) { int attempts = 0; while (attempts < 10) { final targetIndex = random.nextInt(_nodes.length); if (targetIndex != i && !connectedIndices.contains(targetIndex)) { connectedIndices.add(targetIndex); _connections.add( NetworkConnection( sourceIndex: i, targetIndex: targetIndex, pulseOffset: random.nextDouble(), pulseSpeed: 0.2 + random.nextDouble() * 0.3, ), ); break; } attempts++; } } } } @override void dispose() { _pageController.dispose(); _animationController.dispose(); _networkController.dispose(); super.dispose(); } void _goToNextPage() { if (_currentPage < _onboardingPages.length - 1) { _pageController.nextPage( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, ); } } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final screenWidth = MediaQuery.of(context).size.width; return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.blue.shade800, Colors.indigo.shade900, ], ), ), child: Stack( children: [ // Grid background pattern AnimatedBuilder( animation: _networkController, builder: (context, child) { return CustomPaint( painter: GridPatternPainter( progress: _networkController.value, ), size: Size.infinite, ); }, ), // // Subtle wave pattern in background AnimatedBuilder( animation: _networkController, builder: (context, child) { return CustomPaint( painter: WavePatternPainter( progress: _networkController.value, ), size: Size.infinite, ); }, ), // Main content SafeArea( child: Column( children: [ Expanded( child: PageView.builder( controller: _pageController, itemCount: _onboardingPages.length, onPageChanged: (index) { setState(() { _currentPage = index; }); }, itemBuilder: (context, index) { return _buildPageContent( index, screenHeight, screenWidth); }, ), ), // Bottom navigation and indicators Container( padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 20), child: Column( children: [ // Page indicator dots Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_onboardingPages.length, (index) { return Container( width: 8, height: 8, margin: const EdgeInsets.symmetric(horizontal: 5), decoration: BoxDecoration( shape: BoxShape.circle, color: _currentPage == index ? Colors.white : Colors.white.withOpacity(0.3), ), ); }), ), const SizedBox(height: 20), // Next/Start button _currentPage < _onboardingPages.length - 1 ? _buildButton( label: 'Lanjut', icon: Icons.arrow_forward, onTap: _goToNextPage, ) : _buildRoleSelectionSection(), ], ), ), ], ), ), ], ), ), ); } Widget _buildPageContent(int index, double screenHeight, double screenWidth) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 20), // App Logo with Animation Hero( tag: 'app_logo', child: Container( width: 100, height: 100, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 15, offset: const Offset(0, 8), ), ], ), child: Stack( alignment: Alignment.center, children: [ // Animated logo with glow AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.rotate( angle: _animationController.value * 2 * math.pi, child: Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.blue.withOpacity(0.3), width: 2, ), ), ), ); }, ), // Main icon with scale animation AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: 0.9 + 0.1 * math.sin( _animationController.value * 2 * math.pi), child: Icon( Icons.psychology, size: 50, color: Colors.indigo, ), ); }, ), // Glowing effect AnimatedBuilder( animation: _networkController, builder: (context, child) { return CustomPaint( painter: GlowingCirclePainter( progress: _networkController.value, color: Colors.blue.shade500, ), size: const Size(100, 100), ); }, ), ], ), ), ), const SizedBox(height: 16), // App Title const Text( 'EduGuide', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, letterSpacing: 1.2, ), ), const SizedBox(height: 8), // App Subtitle Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: const Text( 'Sistem Rekomendasi Karir & Kuliah', style: TextStyle( fontSize: 14, color: Colors.white, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 40), const SizedBox(height: 24), // Page Title Text( _onboardingPages[index]['title'], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 16), // Page Description - Limited height and no scroll Container( constraints: BoxConstraints(maxHeight: screenHeight * 0.15), child: Text( _onboardingPages[index]['description'], textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, maxLines: 5, style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 16, height: 1.5, ), ), ), // Visual content based on page SizedBox( height: screenHeight * 0.15, child: _buildPageVisualization(index), ), ], ), ); } Widget _buildButton({ required String label, required IconData icon, required VoidCallback onTap, }) { return ElevatedButton( onPressed: onTap, style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.blue.shade800, padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), elevation: 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( label, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), Icon(icon, size: 20), ], ), ); } Widget _buildPageVisualization(int pageIndex) { switch (pageIndex) { case 0: return _buildWelcomeVisualization(); case 1: return _buildForwardChainingVisualization(); case 2: return _buildWorkflowVisualization(); case 3: return _buildGetStartedVisualization(); default: return const SizedBox.shrink(); } } Widget _buildWelcomeVisualization() { return AnimatedBuilder( animation: _animationController, builder: (context, child) { return CustomPaint( painter: BrainNetworkPainter( animation: _animationController.value, color: Colors.white.withOpacity(0.6), ), size: const Size(300, 150), ); }, ); } Widget _buildForwardChainingVisualization() { return AnimatedBuilder( animation: _animationController, builder: (context, child) { // Animation cycle position (0-4) final cyclePosition = (_animationController.value * 4) % 4; final currentStep = cyclePosition.floor(); final progress = cyclePosition - currentStep; // Define forward chaining steps final List> steps = [ { 'icon': Icons.help_outline, 'label': 'Input', 'color': Colors.blue.shade400, }, { 'icon': Icons.psychology, 'label': 'Proses', 'color': Colors.purple.shade400, }, { 'icon': Icons.lightbulb_outline, 'label': 'Analisis', 'color': Colors.deepPurple.shade400, }, { 'icon': Icons.check_circle_outline, 'label': 'Hasil', 'color': Colors.green.shade400, }, ]; return LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; return Container( height: 100, child: Stack( alignment: Alignment.center, fit: StackFit.expand, children: [ // Custom painted curved line CustomPaint( painter: CurvedWorkflowPainter( progress: currentStep + progress, maxProgress: steps.length - 1, stepColors: steps.map((s) => s['color'] as Color).toList(), ), size: Size(width, 100), ), // Step nodes ...List.generate(4, (i) { // Position calculation final stepPosition = width * 0.1 + (width * 0.8 * (i / (steps.length - 1))); // Animation states bool isActive = currentStep == i; bool isPrevious = currentStep > i; bool isNext = (currentStep + 1) % 4 == i; double scale = 1.0; if (isActive) { scale = 1.0 + 0.1 * math.sin(progress * math.pi * 2); } else if (isNext && progress > 0.5) { scale = 0.8 + (0.2 * ((progress - 0.5) * 2)); // Start scaling up at progress 0.5 } else { scale = isPrevious ? 0.9 : 0.8; } Color nodeColor = isActive ? steps[i]['color'] : isPrevious ? steps[i]['color'].withOpacity(0.7) : Colors.white.withOpacity(0.3); // Calculate Y position with slight wave effect final nodeY = i % 2 == 0 ? 10.0 : 40.0; return Positioned( top: nodeY, left: stepPosition - 20, // Center node on position child: Column( mainAxisSize: MainAxisSize.min, children: [ Transform.scale( scale: scale, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), shape: BoxShape.circle, border: Border.all( color: nodeColor, width: 2, ), boxShadow: isActive ? [ BoxShadow( color: steps[i]['color'] .withOpacity(0.4), blurRadius: 8, spreadRadius: 1, ) ] : [], ), child: Center( child: Icon( steps[i]['icon'], color: isActive || isPrevious ? Colors.white : Colors.white.withOpacity(0.5), size: 20, ), ), ), ), const SizedBox(height: 6), Text( steps[i]['label'], style: TextStyle( color: isActive || isPrevious ? Colors.white : Colors.white.withOpacity(0.5), fontSize: 11, fontWeight: isActive ? FontWeight.bold : FontWeight.normal, ), ), ], ), ); }), // Animated pulse point on the curve if (progress > 0 && progress < 1 && currentStep < 3) AnimatedPulsePoint( progress: currentStep + progress, maxProgress: steps.length - 1, width: width, color: steps[(currentStep + 1) % 4]['color'], ), ], ), ); }, ); }, ); } // WORKFLOW VISUALIZATION - Untuk halaman "Alur Penggunaan" Widget _buildWorkflowVisualization() { return AnimatedBuilder( animation: _animationController, builder: (context, child) { // Animation cycle position (0-4) final cyclePosition = (_animationController.value * 4) % 4; final currentStep = cyclePosition.floor(); final progress = cyclePosition - currentStep; // Define workflow steps final List> steps = [ { 'icon': Icons.question_answer_outlined, 'label': 'Jawab', 'color': Colors.orange.shade400, }, { 'icon': Icons.psychology, 'label': 'Proses', 'color': Colors.blue.shade400, }, { 'icon': Icons.integration_instructions_outlined, 'label': 'Analisis', 'color': Colors.purple.shade400, }, { 'icon': Icons.emoji_events_outlined, 'label': 'Hasil', 'color': Colors.green.shade400, }, ]; return LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; return Container( height: 100, child: Stack( alignment: Alignment.center, fit: StackFit.expand, children: [ // Custom painted curved line CustomPaint( painter: CurvedWorkflowPainter( progress: currentStep + progress, maxProgress: steps.length - 1, stepColors: steps.map((s) => s['color'] as Color).toList(), ), size: Size(width, 100), ), // Step nodes ...List.generate(4, (i) { // Position calculation final stepPosition = width * 0.1 + (width * 0.8 * (i / (steps.length - 1))); // Animation states bool isActive = currentStep == i; bool isPrevious = currentStep > i; bool isNext = (currentStep + 1) % 4 == i; double scale = 1.0; if (isActive) { scale = 1.0 + 0.1 * math.sin(progress * math.pi * 2); } else if (isNext && progress > 0.5) { scale = 0.8 + (0.2 * ((progress - 0.5) * 2)); // Start scaling up at progress 0.5 } else { scale = isPrevious ? 0.9 : 0.8; } Color nodeColor = isActive ? steps[i]['color'] : isPrevious ? steps[i]['color'].withOpacity(0.7) : Colors.white.withOpacity(0.3); // Calculate Y position with slight wave effect final nodeY = i % 2 == 0 ? 10.0 : 40.0; return Positioned( top: nodeY, left: stepPosition - 20, // Center node on position child: Column( mainAxisSize: MainAxisSize.min, children: [ Transform.scale( scale: scale, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), shape: BoxShape.circle, border: Border.all( color: nodeColor, width: 2, ), boxShadow: isActive ? [ BoxShadow( color: steps[i]['color'] .withOpacity(0.4), blurRadius: 8, spreadRadius: 1, ) ] : [], ), child: Center( child: Icon( steps[i]['icon'], color: isActive || isPrevious ? Colors.white : Colors.white.withOpacity(0.5), size: 20, ), ), ), ), const SizedBox(height: 6), Text( steps[i]['label'], style: TextStyle( color: isActive || isPrevious ? Colors.white : Colors.white.withOpacity(0.5), fontSize: 11, fontWeight: isActive ? FontWeight.bold : FontWeight.normal, ), ), ], ), ); }), // Animated pulse point on the curve if (progress > 0 && progress < 1 && currentStep < 3) AnimatedPulsePoint( progress: currentStep + progress, maxProgress: steps.length - 1, width: width, color: steps[(currentStep + 1) % 4]['color'], ), ], ), ); }, ); }, ); } Widget _buildStepBox(String text, IconData icon, double delay) { final animValue = (_animationController.value + delay) % 1.0; final pulseScale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); return Container( padding: const EdgeInsets.all(6), margin: const EdgeInsets.all(3), width: 85, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10), border: Border.all( color: Colors.white.withOpacity(0.3), width: 1, ), ), child: Column( children: [ Transform.scale( scale: pulseScale, child: Icon(icon, color: Colors.white, size: 18), ), const SizedBox(height: 4), Text( text, style: const TextStyle( color: Colors.white, fontSize: 10, ), ), ], ), ); } Widget _buildGetStartedVisualization() { return AnimatedBuilder( animation: _animationController, builder: (context, child) { final animValue = _animationController.value; return Container( height: 120, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildAnimatedRoleCard( title: 'Siswa', icon: Icons.school, color: Colors.blue.shade500, offset: 0, ), const SizedBox(width: 40), _buildAnimatedRoleCard( title: 'Guru', icon: Icons.person_pin, color: Colors.purple.shade500, offset: 0.5, ), ], ), ); }, ); } Widget _buildAnimatedRoleCard({ required String title, required IconData icon, required Color color, required double offset, }) { return AnimatedBuilder( animation: _animationController, builder: (context, child) { final animValue = (_animationController.value + offset) % 1.0; final scale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); final bounce = math.sin(animValue * 2 * math.pi) * 5; return Transform.translate( offset: Offset(0, bounce), child: Transform.scale( scale: scale, child: Container( width: 80, height: 80, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all( color: color.withOpacity(0.5), width: 2, ), boxShadow: [ BoxShadow( color: color.withOpacity(0.3), blurRadius: 10, spreadRadius: 1, ) ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, color: Colors.white, size: 32, ), const SizedBox(height: 8), Text( title, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ), ); }, ); } Widget _buildRoleIcon(IconData icon, String label, double delay) { return AnimatedBuilder( animation: _animationController, builder: (context, child) { final animValue = (_animationController.value + delay) % 1.0; final scale = 0.9 + 0.1 * math.sin(animValue * 2 * math.pi); return Column( children: [ Transform.scale( scale: scale, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( icon, color: Colors.white, size: 30, ), ), ), const SizedBox(height: 8), Text( label, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ); }, ); } Widget _buildRoleSelectionSection() { return Column( children: [ // Student Role Button _buildRoleButton( title: 'Siswa', subtitle: 'Dapatkan rekomendasi karir dan kuliah', icon: Icons.school, color: Colors.blue.shade600, onTap: () { _saveRole('student'); Get.to( () => StudentLoginPage(), transition: Transition.rightToLeft, duration: const Duration(milliseconds: 400), ); }, ), const SizedBox(height: 16), // Teacher Role Button _buildRoleButton( title: 'Guru', subtitle: 'Kelola data dan pantau perkembangan siswa', icon: Icons.person_pin, color: Colors.deepPurple, onTap: () { _saveRole('teacher'); Get.to( () => const TeacherLoginPage(), transition: Transition.rightToLeft, duration: const Duration(milliseconds: 400), ); }, ), ], ); } // Helper method to save user role preference Future _saveRole(String role) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('user_role', role); } // Helper method to build role selection button Widget _buildRoleButton({ required String title, required String subtitle, required IconData icon, required Color color, required VoidCallback onTap, }) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white), color: Colors.white, boxShadow: [ BoxShadow( color: Colors.blue.withOpacity(0.1), spreadRadius: 1, blurRadius: 10, offset: const Offset(0, 3), ), ], ), child: Row( children: [ Container( width: 45, height: 45, decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(15), ), child: Icon( icon, color: Colors.blue.shade600, size: 28, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: color, ), ), const SizedBox(height: 4), Text( subtitle, style: const TextStyle( fontSize: 12, color: Colors.black54, ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: color, size: 14, ), ], ), ), ); } } class WorkflowPathPainter extends CustomPainter { final double progress; final int currentStep; final List stepColors; WorkflowPathPainter({ required this.progress, required this.currentStep, required this.stepColors, }); @override void paint(Canvas canvas, Size size) { final Paint pathPaint = Paint() ..color = Colors.white.withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; final Paint activePaint = Paint() ..color = stepColors[currentStep] ..style = PaintingStyle.stroke ..strokeWidth = 3.0; // We'll draw a curved path connecting all points final Path path = Path(); // Calculate arc path final width = size.width; final height = size.height * 0.5; final centerY = size.height * 0.5; // Start at left path.moveTo(width * 0.25, centerY); // Draw curve through points path.cubicTo( width * 0.4, centerY - height, // control point 1 width * 0.6, centerY + height, // control point 2 width * 0.75, centerY, // end point ); // Draw the inactive path canvas.drawPath(path, pathPaint); // Draw the active progress path if (currentStep < 3) { // Determine path metrics for precise length calculations PathMetrics metrics = path.computeMetrics(); PathMetric pathMetric = metrics.first; // Calculate the percentage of path to draw based on current step and progress double totalPathPercent = (currentStep + progress) / 3; // Extract the portion of the path that should be drawn Path extractPath = Path(); extractPath.addPath( pathMetric.extractPath(0, pathMetric.length * totalPathPercent), Offset.zero, ); // Draw the active portion of the path canvas.drawPath(extractPath, activePaint); // Add animated dots/pulse if (progress > 0 && progress < 1) { // Calculate the exact position for the animating dot double animatingPointPercent = (currentStep + progress) / 3; Tangent? tangent = pathMetric .getTangentForOffset(pathMetric.length * animatingPointPercent); if (tangent != null) { // Draw pulsing dot at the current animation point canvas.drawCircle( tangent.position, 4 + 2 * math.sin(progress * math.pi), // Pulse effect Paint()..color = stepColors[(currentStep + 1) % 4]); } } } else { // If we're at the last step, animate the whole path canvas.drawPath(path, activePaint); } } @override bool shouldRepaint(WorkflowPathPainter oldDelegate) { return oldDelegate.progress != progress || oldDelegate.currentStep != currentStep; } } // Node in the network class NetworkNode { final double x; // Position (0-1) final double y; final double size; NetworkNode({ required this.x, required this.y, required this.size, }); } // Connection between nodes class NetworkConnection { final int sourceIndex; final int targetIndex; final double pulseOffset; // Random offset for animation final double pulseSpeed; // Speed of pulse animation NetworkConnection({ required this.sourceIndex, required this.targetIndex, required this.pulseOffset, required this.pulseSpeed, }); } // Neural network visualization class NetworkPainter extends CustomPainter { final double progress; final List nodes; final List connections; NetworkPainter({ required this.progress, required this.nodes, required this.connections, }); @override void paint(Canvas canvas, Size size) { // Draw connections first (lines between nodes) for (final connection in connections) { final source = nodes[connection.sourceIndex]; final target = nodes[connection.targetIndex]; final sourcePos = Offset(source.x * size.width, source.y * size.height); final targetPos = Offset(target.x * size.width, target.y * size.height); // Calculate distance for line dashing final dx = targetPos.dx - sourcePos.dx; final dy = targetPos.dy - sourcePos.dy; final distance = math.sqrt(dx * dx + dy * dy); // Create a normalized direction vector final dirX = dx / distance; final dirY = dy / distance; // Calculate pulse position based on progress final pulseProgress = (progress * connection.pulseSpeed + connection.pulseOffset) % 1.0; final pulsePos = Offset( sourcePos.dx + dx * pulseProgress, sourcePos.dy + dy * pulseProgress, ); // Draw the connection line final linePaint = Paint() ..color = Colors.white.withOpacity(0.15) ..style = PaintingStyle.stroke ..strokeWidth = 1.2; canvas.drawLine(sourcePos, targetPos, linePaint); // Draw pulse traveling along the connection final pulsePaint = Paint() ..color = Colors.blue.shade100.withOpacity(0.5) ..style = PaintingStyle.fill; canvas.drawCircle(pulsePos, 1.5, pulsePaint); // Add a subtle glow around the pulse final glowPaint = Paint() ..color = Colors.blue.shade100.withOpacity(0.2) ..style = PaintingStyle.fill; canvas.drawCircle(pulsePos, 5.0, glowPaint); } // Draw nodes for (final node in nodes) { final nodePos = Offset(node.x * size.width, node.y * size.height); // Node glow (outer circle) final glowPaint = Paint() ..color = Colors.blue.shade200.withOpacity(0.2) ..style = PaintingStyle.fill; canvas.drawCircle(nodePos, node.size * 3.8, glowPaint); // Node main circle final nodePaint = Paint() ..color = Colors.white.withOpacity(0.7) ..style = PaintingStyle.fill; canvas.drawCircle(nodePos, node.size, nodePaint); // Node inner circle final innerPaint = Paint() ..color = Colors.blue.shade100 ..style = PaintingStyle.fill; canvas.drawCircle(nodePos, node.size * 0.6, innerPaint); } } @override bool shouldRepaint(NetworkPainter oldDelegate) { return oldDelegate.progress != progress; } } // Glowing circle around the icon class GlowingCirclePainter extends CustomPainter { final double progress; final Color color; GlowingCirclePainter({required this.progress, required this.color}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); // Pulsing circular glow - very subtle final double pulseSize = 1.0 + math.sin(progress * math.pi) * 0.08; // Draw multiple circles with diminishing opacity for (int i = 0; i < 3; i++) { final paint = Paint() ..color = color.withOpacity(0.2 - (i * 0.05)) ..style = PaintingStyle.stroke ..strokeWidth = 2.0 - (i * 0.5); // Each circle is larger than the previous canvas.drawCircle( center, (40 + i * 5) * pulseSize, paint, ); } // Draw spinning arc - very slow final spinnerPaint = Paint() ..color = color.withOpacity(0.7) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; // Rotation angle changes very slowly with progress final startAngle = progress * math.pi; const arcLength = math.pi * 1.2; // Longer arc canvas.drawArc( Rect.fromCircle(center: center, radius: 45), startAngle, arcLength, false, spinnerPaint, ); // Second arc in opposite direction - even slower canvas.drawArc( Rect.fromCircle(center: center, radius: 35), -startAngle * 0.7, -arcLength * 0.8, false, spinnerPaint..strokeWidth = 1.5, ); } @override bool shouldRepaint(GlowingCirclePainter oldDelegate) { return oldDelegate.progress != progress || oldDelegate.color != color; } } // Custom painter for brain network visualization class BrainNetworkPainter extends CustomPainter { final double animation; final Color color; BrainNetworkPainter({required this.animation, required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; final dotPaint = Paint() ..color = color ..style = PaintingStyle.fill; // Create nodes final nodes = []; final random = math.Random(42); // Fixed seed for consistent points for (int i = 0; i < 7; i++) { nodes.add(Offset( random.nextDouble() * size.width, random.nextDouble() * size.height, )); } // Draw connections between nodes for (int i = 0; i < nodes.length; i++) { for (int j = i + 1; j < nodes.length; j++) { final distance = (nodes[i] - nodes[j]).distance; if (distance < size.width * 0.4) { // Only connect nearby nodes final path = Path() ..moveTo(nodes[i].dx, nodes[i].dy) ..lineTo(nodes[j].dx, nodes[j].dy); canvas.drawPath(path, paint); // Draw pulse along the line final progress = (animation + i * 0.1) % 1.0; final pointOffset = Offset.lerp(nodes[i], nodes[j], progress)!; canvas.drawCircle(pointOffset, 2.5, dotPaint); } } } // Draw nodes for (final node in nodes) { final animatedRadius = 3.0 + 1.5 * math.sin(animation * 2 * math.pi + nodes.indexOf(node)); canvas.drawCircle(node, animatedRadius, dotPaint); } } @override bool shouldRepaint(covariant BrainNetworkPainter oldDelegate) { return true; } } class CurvedWorkflowPainter extends CustomPainter { final double progress; final double maxProgress; final List stepColors; CurvedWorkflowPainter({ required this.progress, required this.maxProgress, required this.stepColors, }); @override void paint(Canvas canvas, Size size) { final width = size.width; final height = size.height; // Create base path for the full curve final path = Path(); path.moveTo( width * 0.1, height * 0.25); // Start at 10% from left, at 25% height // Control points for the curve List points = []; // Calculate points for the S curve for (int i = 0; i < 4; i++) { final x = width * 0.1 + (width * 0.8 * (i / (maxProgress))); final y = i % 2 == 0 ? height * 0.25 : height * 0.55; // Alternate Y positions points.add(Offset(x, y)); } // Draw curved path through points for (int i = 0; i < points.length - 1; i++) { final current = points[i]; final next = points[i + 1]; // Middle point for control final midX = (current.dx + next.dx) / 2; // Add curve segment if (i == 0) { path.quadraticBezierTo( midX, current.dy, // Control point next.dx, next.dy, // End point ); } else if (i == points.length - 2) { path.quadraticBezierTo( midX, current.dy, // Control point next.dx, next.dy, // End point ); } else { path.cubicTo( midX, current.dy, // First control point midX, next.dy, // Second control point next.dx, next.dy, // End point ); } } // Draw inactive path final inactivePaint = Paint() ..color = Colors.white.withOpacity(0.2) ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round; canvas.drawPath(path, inactivePaint); // Draw active progress path if (progress > 0) { // Calculate total length final PathMetrics pathMetrics = path.computeMetrics(); final PathMetric pathMetric = pathMetrics.first; // Extract the active portion of the path final double pathLength = pathMetric.length; final double activePortion = (progress / maxProgress) * pathLength; final Path activePath = Path(); activePath.addPath( pathMetric.extractPath(0, activePortion), Offset.zero, ); // Create gradient for active path final activeGradient = LinearGradient( colors: [ stepColors[0], stepColors[math.min(progress.floor(), stepColors.length - 1)], ], ).createShader(Rect.fromLTWH(0, 0, width, height)); final activePaint = Paint() ..shader = activeGradient ..style = PaintingStyle.stroke ..strokeWidth = 3.0 ..strokeCap = StrokeCap.round; canvas.drawPath(activePath, activePaint); } } @override bool shouldRepaint(CurvedWorkflowPainter oldDelegate) { return oldDelegate.progress != progress; } } // Animated pulse point that follows the curve class AnimatedPulsePoint extends StatelessWidget { final double progress; final double maxProgress; final double width; final Color color; const AnimatedPulsePoint({ required this.progress, required this.maxProgress, required this.width, required this.color, }); @override Widget build(BuildContext context) { // Calculate position along the curve final double x = width * 0.1 + (width * 0.8 * (progress / maxProgress)); // Calculate y position (alternating wave) final int floorPos = progress.floor(); final double fraction = progress - floorPos; double y; if (floorPos % 2 == 0) { // Moving from top to bottom y = 30.0 + (40.0 * fraction); } else { // Moving from bottom to top y = 70.0 - (40.0 * fraction); } return Positioned( left: x - 4, top: y - 4, child: Container( width: 8, height: 8, decoration: BoxDecoration( color: color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: color.withOpacity(0.6), blurRadius: 8, spreadRadius: 2, ), ], ), ), ); } }