TIF_NGANJUK_E41210753/lib/app/views/page_intro.dart

1605 lines
49 KiB
Dart

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<IntroPage> createState() => _IntroPageState();
}
class _IntroPageState extends State<IntroPage> with TickerProviderStateMixin {
final PageController _pageController = PageController();
late AnimationController _animationController;
late AnimationController _networkController;
int _currentPage = 0;
// Network visualization data
final List<NetworkNode> _nodes = [];
final List<NetworkConnection> _connections = [];
final List<Map<String, dynamic>> _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 = <int>{};
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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<void> _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<Color> 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<NetworkNode> nodes;
final List<NetworkConnection> 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 = <Offset>[];
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<Color> 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<Offset> 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,
),
],
),
),
);
}
}