import 'dart:async'; import 'dart:math' as math; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:lba/gen/assets.gen.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:flutter_compass/flutter_compass.dart'; import 'package:lba/res/colors.dart'; import '../services/location_service.dart'; import 'package:flutter_map/flutter_map.dart'; class HintCameraWidget extends StatefulWidget { final double targetLatitude; final double targetLongitude; final String hintDescription; final VoidCallback onHintFound; final VoidCallback onClose; final Duration? remainingTime; final String? cardTitle; const HintCameraWidget({ super.key, required this.targetLatitude, required this.targetLongitude, required this.hintDescription, required this.onHintFound, required this.onClose, this.remainingTime, this.cardTitle, }); @override State createState() => _HintCameraWidgetState(); } class _HintCameraWidgetState extends State with TickerProviderStateMixin { MobileScannerController? _controller; StreamSubscription? _compassSubscription; StreamSubscription? _locationSubscription; Timer? _countdownTimer; final MapController _miniMapController = MapController(); double _deviceHeading = 0.0; double _bearingToTarget = 0.0; double _distanceToTarget = 0.0; Position? _currentPosition; bool _isNearTarget = false; bool _wasNearTarget = false; bool _isMapVisible = false; Duration _currentRemainingTime = Duration.zero; late AnimationController _rotationController; late AnimationController _scanController; late AnimationController _entranceController; late Animation _rotationAnimation; late Animation _scanAnimation; late Animation _entranceOpacityAnimation; late Animation _entranceScaleAnimation; late Animation _entranceSlideAnimation; late AnimationController _transformController; late Animation _transformAnimation; final double _cameraHorizontalFov = 60.0; @override void initState() { super.initState(); _initializeCamera(); _setupAnimations(); _startCompassListening(); _startLocationUpdates(); _initializeTimer(); } void _initializeTimer() { if (widget.remainingTime != null) { _currentRemainingTime = widget.remainingTime!.inSeconds == 0 ? const Duration(hours: 12) : widget.remainingTime!; _startCountdownTimer(); } } void _startCountdownTimer() { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (mounted) { setState(() { if (_currentRemainingTime.inSeconds > 0) { _currentRemainingTime = Duration(seconds: _currentRemainingTime.inSeconds - 1); } else { timer.cancel(); } }); } }); } void _initializeCamera() { _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, ); } void _setupAnimations() { _rotationController = AnimationController( duration: const Duration(seconds: 8), vsync: this, )..repeat(); _scanController = AnimationController( duration: const Duration(milliseconds: 3500), vsync: this, )..repeat(); _entranceController = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, ); _rotationAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _rotationController, curve: Curves.linear), ); _scanAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _scanController, curve: Curves.easeInOutSine), ); _entranceOpacityAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _entranceController, curve: const Interval(0.0, 0.8, curve: Curves.easeOut), ), ); _entranceScaleAnimation = Tween(begin: 0.3, end: 1.0).animate( CurvedAnimation( parent: _entranceController, curve: const Interval(0.0, 0.8, curve: Curves.elasticOut), ), ); _entranceSlideAnimation = Tween( begin: const Offset(0.0, 0.5), end: Offset.zero, ).animate( CurvedAnimation( parent: _entranceController, curve: const Interval(0.2, 1.0, curve: Curves.elasticOut), ), ); _transformController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, ); _transformAnimation = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _transformController, curve: Curves.elasticOut, ), ); _entranceController.forward(); } void _startCompassListening() { _compassSubscription = FlutterCompass.events?.listen((CompassEvent event) { if (!mounted) return; setState(() { _deviceHeading = event.heading ?? 0.0; }); }); } void _startLocationUpdates() { _locationSubscription = LocationService.getPositionStream()?.listen((Position? position) { if (position != null && mounted) { final bearing = LocationService.getBearing( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, ); final distance = LocationService.calculateDistance( position.latitude, position.longitude, widget.targetLatitude, widget.targetLongitude, ); final isNear = distance <= 10.0; setState(() { _currentPosition = position; _bearingToTarget = bearing; _distanceToTarget = distance; if (_isNearTarget != isNear) { _wasNearTarget = _isNearTarget; _isNearTarget = isNear; if (isNear && !_wasNearTarget) { _transformController.forward(); } else if (!isNear && _wasNearTarget) { _transformController.reverse(); } } else { _isNearTarget = isNear; } }); if (_isMapVisible) { _updateMiniMapCamera(); } if (isNear) { widget.onHintFound(); } } }); } void _updateMiniMapCamera() { if (_currentPosition != null) { final userLocation = LatLng(_currentPosition!.latitude, _currentPosition!.longitude); final targetLocation = LatLng(widget.targetLatitude, widget.targetLongitude); _miniMapController.fitCamera( CameraFit.bounds( bounds: LatLngBounds(userLocation, targetLocation), padding: const EdgeInsets.all(60.0), ), ); } } String _formatDistance(double distance) { if (distance >= 100) { return '${distance.round()}m'; } else { return '${distance.toStringAsFixed(1)}m'; } } double _getDistanceFontSize(double distance) { if (distance >= 10000) { return 20; } else if (distance >= 1000) { return 26; } else { return 32; } } void _toggleMapVisibility() { setState(() { _isMapVisible = !_isMapVisible; if (_isMapVisible) { Future.delayed(const Duration(milliseconds: 100), () { _updateMiniMapCamera(); }); } }); } @override void dispose() { _compassSubscription?.cancel(); _locationSubscription?.cancel(); _countdownTimer?.cancel(); _controller?.dispose(); _rotationController.dispose(); _scanController.dispose(); _entranceController.dispose(); _transformController.dispose(); _miniMapController.dispose(); super.dispose(); } void _showARHintInfoDialog() { showDialog( context: context, barrierDismissible: true, barrierColor: Colors.black.withOpacity(0.7), builder: (BuildContext context) { return const _ARHintInfoDialog(); }, ); } Widget _buildMiniMap() { if (_currentPosition == null) return const SizedBox.shrink(); final userLocation = LatLng(_currentPosition!.latitude, _currentPosition!.longitude); final targetLocation = LatLng(widget.targetLatitude, widget.targetLongitude); return AnimatedPositioned( duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic, bottom: _isMapVisible ? 100 : -220, left: 20, right: 20, child: ClipRRect( borderRadius: BorderRadius.circular(25), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Container( height: 200, decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: BorderRadius.circular(25), border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: ClipRRect( borderRadius: BorderRadius.circular(24), child: FlutterMap( mapController: _miniMapController, options: MapOptions( initialCenter: userLocation, initialZoom: 15, interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( urlTemplate: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', subdomains: const ['a', 'b', 'c', 'd'], ), MarkerLayer( markers: [ Marker( point: userLocation, width: 40, height: 40, child: const PulsingMarker(isUser: true), ), Marker( point: targetLocation, width: 40, height: 40, child: const PulsingMarker(isUser: false), ), ], ), ], ), ), ), ), ), ); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _entranceController, builder: (context, child) { return FadeTransition( opacity: _entranceOpacityAnimation, child: SlideTransition( position: _entranceSlideAnimation, child: ScaleTransition( scale: _entranceScaleAnimation, child: Scaffold( backgroundColor: Colors.black, extendBodyBehindAppBar: true, appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight + 10), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.8), Colors.black.withOpacity(0.4), Colors.transparent, ], ), ), child: ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: AppBar( backgroundColor: Colors.transparent, elevation: 0, toolbarHeight: kToolbarHeight + 10, leading: Container( margin: const EdgeInsets.all(8), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.white.withOpacity(0.08), Colors.white.withOpacity(0.05), ], ), shape: BoxShape.circle, border: Border.all( color: Colors.white.withOpacity(0.3), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, spreadRadius: 1, ), ], ), child: IconButton( onPressed: widget.onClose, icon: SvgPicture.asset(Assets.icons.back.path, colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn)), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ), title: AnimatedBuilder( animation: _entranceController, builder: (context, child) { return Transform.scale( scale: 0.8 + (0.2 * _entranceScaleAnimation.value), child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.primary.withOpacity(0.3 * _entranceOpacityAnimation.value), AppColors.primary.withOpacity(0.1 * _entranceOpacityAnimation.value), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(25), border: Border.all( color: AppColors.primary.withOpacity(0.4 * _entranceOpacityAnimation.value), width: 1.5, ), boxShadow: [ BoxShadow( color: AppColors.primary.withOpacity(0.2 * _entranceOpacityAnimation.value), blurRadius: 15, spreadRadius: 2, ), BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Text( 'AR Hint', style: TextStyle( color: Colors.white.withOpacity(_entranceOpacityAnimation.value), fontSize: 15, fontWeight: FontWeight.w700, letterSpacing: 1.2, fontFamily: 'monospace'), ), ), ); }, ), centerTitle: true, actions: [ Container( margin: const EdgeInsets.all(10), decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.white.withOpacity(0.10), Colors.white.withOpacity(0.05), ], ), shape: BoxShape.circle, border: Border.all( color: Colors.white.withOpacity(0.3), width: 1.5, ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, spreadRadius: 1, ), ], ), child: IconButton( onPressed: _showARHintInfoDialog, icon: SvgPicture.asset(Assets.icons.infoCircle.path, colorFilter: const ColorFilter.mode( Colors.white, BlendMode.srcIn)), ), ), ], ), ), ), ), ), body: Stack( children: [ if (_controller != null) MobileScanner(controller: _controller!) else Container( color: Colors.black, child: Center( child: CircularProgressIndicator(color: AppColors.primary), ), ), BackdropFilter( filter: ImageFilter.blur(sigmaX: 0.5, sigmaY: 0.5), child: Container(color: Colors.black.withOpacity(0.1)), ), if (widget.remainingTime != null) _buildTimerWidget(), _buildAROverlay(), _buildMiniMap(), ], ), floatingActionButton: AnimatedBuilder( animation: _entranceController, builder: (context, child) { return Transform.translate( offset: Offset(0, 100 * (1 - _entranceScaleAnimation.value)), child: Opacity( opacity: _entranceOpacityAnimation.value, child: Transform.scale( scale: _entranceScaleAnimation.value, child: Padding( padding: const EdgeInsets.only(right: 8.0), child: _buildMapFAB(), ), ), ), ); }, ), ), ), ), ); }, ); } Widget _buildTimerWidget() { if (widget.remainingTime == null) return const SizedBox.shrink(); final Duration timeToShow = _currentRemainingTime.inSeconds <= 0 ? (widget.remainingTime!.inSeconds == 0 ? const Duration(hours: 12) : Duration.zero) : _currentRemainingTime; final hours = timeToShow.inHours; final minutes = timeToShow.inMinutes % 60; final seconds = timeToShow.inSeconds % 60; final isExpired = _currentRemainingTime.inSeconds <= 0 && widget.remainingTime!.inSeconds > 0; return AnimatedBuilder( animation: _entranceController, builder: (context, child) { return Positioned( top: kToolbarHeight + 60, right: 16 - (100 * (1 - _entranceOpacityAnimation.value)), child: Opacity( opacity: _entranceOpacityAnimation.value, child: Transform.scale( scale: _entranceScaleAnimation.value, child: ClipRRect( borderRadius: BorderRadius.circular(20), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.transparent, borderRadius: BorderRadius.circular(20), border: Border.all( color: Colors.white.withOpacity(0.2 * _entranceOpacityAnimation.value), width: 0.5, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( isExpired ? 'Expired' : hours > 0 ? '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}' : '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}', style: TextStyle( color: (isExpired ? const Color(0xFFFF3B30) : Colors.white) .withOpacity(_entranceOpacityAnimation.value), fontSize: 15, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ], ), ), ), ), ), ), ); }, ); } Widget _buildMapFAB() { return ClipRRect( borderRadius: BorderRadius.circular(18), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: AnimatedContainer( duration: const Duration(milliseconds: 300), decoration: BoxDecoration( color: _isMapVisible ? AppColors.primary.withOpacity(0.5) : Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(18), border: Border.all(color: Colors.white.withOpacity(0.2)), ), child: FloatingActionButton( onPressed: _toggleMapVisibility, backgroundColor: Colors.transparent, elevation: 0, highlightElevation: 0, child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), transitionBuilder: (child, animation) { return ScaleTransition(scale: animation, child: child); }, child: Icon( _isMapVisible ? Icons.map_outlined : Icons.map, key: ValueKey(_isMapVisible), color: Colors.white, ), ), ), ), ), ); } Widget _buildAROverlay() { if (_currentPosition == null) { return AnimatedBuilder( animation: _entranceController, builder: (context, child) { return Center( child: FadeTransition( opacity: _entranceOpacityAnimation, child: SlideTransition( position: Tween( begin: const Offset(0.0, -0.3), end: Offset.zero, ).animate(CurvedAnimation( parent: _entranceController, curve: const Interval(0.4, 1.0, curve: Curves.elasticOut), )), child: const Text( 'ACQUIRING GPS SIGNAL...', style: TextStyle( color: Colors.white70, fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 1.5, ), ), ), ), ); }, ); } double normalizeAngle(double angle) => (angle + 180) % 360 - 180; double deviceHeadingNormalized = normalizeAngle(_deviceHeading); double bearingToTargetNormalized = normalizeAngle(_bearingToTarget); double angleDifference = bearingToTargetNormalized - deviceHeadingNormalized; if (angleDifference > 180) angleDifference -= 360; if (angleDifference < -180) angleDifference += 360; bool isVisible = angleDifference.abs() < (_cameraHorizontalFov / 2); return AnimatedBuilder( animation: _entranceController, builder: (context, child) { return Opacity( opacity: _entranceOpacityAnimation.value, child: Stack( children: [ Transform.scale( scale: _entranceScaleAnimation.value, child: CustomPaint( painter: HudPainter(animationValue: _entranceOpacityAnimation.value), size: Size.infinite, ), ), if (!isVisible) _buildDirectionalOverlay(angleDifference), if (!isVisible) _buildOffScreenIndicator(angleDifference) else _buildOnScreenHint(angleDifference), ], ), ); }, ); } Widget _buildDirectionalOverlay(double angleDifference) { final isLeft = angleDifference < 0; final overlayColor = _isNearTarget ? const Color(0xFF00E676) : AppColors.primary; return Align( alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight, child: Container( width: MediaQuery.of(context).size.width * 0.13, height: double.infinity, decoration: BoxDecoration( gradient: LinearGradient( begin: isLeft ? Alignment.centerLeft : Alignment.centerRight, end: isLeft ? Alignment.centerRight : Alignment.centerLeft, stops: const [0.0, 0.2, 0.4, 0.7, 1.0], colors: [ overlayColor.withOpacity(0.45), overlayColor.withOpacity(0.30), overlayColor.withOpacity(0.15), overlayColor.withOpacity(0.05), Colors.transparent, ], ), ), ), ); } Widget _buildOffScreenIndicator(double angleDifference) { final isLeft = angleDifference < 0; final arrowColor = _isNearTarget ? const Color(0xFF00E676) : AppColors.primary; return Align( alignment: isLeft ? Alignment.centerLeft : Alignment.centerRight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24.0), child: Icon( isLeft ? Icons.arrow_back_ios_new_rounded : Icons.arrow_forward_ios_rounded, color: arrowColor.withOpacity(0.8), size: 48, shadows: [ Shadow(color: arrowColor.withOpacity(0.5), blurRadius: 15), ], ), ), ); } Widget _buildOnScreenHint(double angleDifference) { final double screenX = angleDifference / (_cameraHorizontalFov / 2); final double distance = _distanceToTarget; return AnimatedPositioned( duration: const Duration(milliseconds: 100), curve: Curves.easeOut, top: 0, bottom: 0, left: (1 + screenX) * (MediaQuery.of(context).size.width / 2) - 100, child: AnimatedBuilder( animation: Listenable.merge([_rotationAnimation, _scanAnimation, _transformAnimation]), builder: (context, child) { return SizedBox( width: 200, height: 200, child: CustomPaint( painter: AdvancedTargetPainter( isNear: _isNearTarget, distance: distance, pulseValue: 1.0, rotationValue: _rotationAnimation.value, scanValue: _scanAnimation.value, transformValue: _transformAnimation.value, ), child: Center( child: ClipRRect( borderRadius: BorderRadius.circular(100), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( width: 140, height: 140, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.05), shape: BoxShape.circle, border: Border.all( color: (_isNearTarget ? const Color(0xFF00E676) : const Color(0xFF2196F3)) .withOpacity(0.4), width: 1, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Flexible( child: Container( padding: const EdgeInsets.symmetric(horizontal: 4), child: FittedBox( fit: BoxFit.scaleDown, child: Text( _isNearTarget ? "TARGET\nACQUIRED" : "SCANNING", textAlign: TextAlign.center, style: TextStyle( color: _isNearTarget ? const Color(0xFF00E676) : const Color(0xFF2196F3), fontWeight: FontWeight.w600, fontSize: _isNearTarget ? 9 : 12, letterSpacing: _isNearTarget ? 0.5 : 1.5, height: _isNearTarget ? 1.1 : 1.0, fontFamily: 'monospace'), ), ), ), ), const SizedBox(height: 12), Text( _formatDistance(distance), style: TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: _getDistanceFontSize(distance), letterSpacing: 0.5, fontFamily: 'monospace'), ), ], ), ), ), ), ), ), ); }, ), ); } } class PulsingMarker extends StatefulWidget { final bool isUser; const PulsingMarker({super.key, required this.isUser}); @override _PulsingMarkerState createState() => _PulsingMarkerState(); } class _PulsingMarkerState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final color = widget.isUser ? Colors.blue.shade400 : Colors.yellow.shade700; return FadeTransition( opacity: Tween(begin: 1.0, end: 0.3).animate(_animationController), child: ScaleTransition( scale: Tween(begin: 0.5, end: 1.0).animate(_animationController), child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: color.withOpacity(0.5), border: Border.all(color: color, width: 2), ), child: Padding( padding: const EdgeInsets.all(8.0), child: SvgPicture.asset( widget.isUser ? Assets.icons.gps.path : Assets.icons.locationCross.path, colorFilter: const ColorFilter.mode(Colors.black, BlendMode.srcIn), width: 10, ), ), ), ), ); } } class HudPainter extends CustomPainter { final double animationValue; HudPainter({required this.animationValue}); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = AppColors.primary.withOpacity(0.3 + (animationValue * 0.2)) ..style = PaintingStyle.stroke ..strokeWidth = 2.0; const bracketSize = 30.0; const margin = 20.0; canvas.drawLine( Offset(margin, margin), Offset(margin + bracketSize, margin), paint); canvas.drawLine( Offset(margin, margin), Offset(margin, margin + bracketSize), paint); canvas.drawLine(Offset(size.width - margin, margin), Offset(size.width - margin - bracketSize, margin), paint); canvas.drawLine(Offset(size.width - margin, margin), Offset(size.width - margin, margin + bracketSize), paint); canvas.drawLine(Offset(margin, size.height - margin), Offset(margin + bracketSize, size.height - margin), paint); canvas.drawLine(Offset(margin, size.height - margin), Offset(margin, size.height - margin - bracketSize), paint); canvas.drawLine( Offset(size.width - margin, size.height - margin), Offset(size.width - margin - bracketSize, size.height - margin), paint); canvas.drawLine( Offset(size.width - margin, size.height - margin), Offset(size.width - margin, size.height - margin - bracketSize), paint); } @override bool shouldRepaint(covariant HudPainter oldDelegate) => oldDelegate.animationValue != animationValue; } class AdvancedTargetPainter extends CustomPainter { final bool isNear; final double distance; final double pulseValue; final double rotationValue; final double scanValue; final double transformValue; AdvancedTargetPainter({ required this.isNear, required this.distance, required this.pulseValue, required this.rotationValue, required this.scanValue, required this.transformValue, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final paint = Paint()..style = PaintingStyle.stroke; final scannerColor = const Color(0xFF2196F3); final targetColor = const Color(0xFF00E676); final primaryColor = Color.lerp(scannerColor, targetColor, transformValue)!; final baseRadius = math.max(60.0, math.min(85.0, 100 - (distance / 2))); paint ..color = primaryColor.withOpacity(0.1 * pulseValue) ..strokeWidth = 20.0 ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10); canvas.drawCircle(center, baseRadius, paint); paint ..color = primaryColor.withOpacity(0.8) ..strokeWidth = 2.5 ..maskFilter = null; canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius), rotationValue * 2 * math.pi, 2.5, false, paint); canvas.drawArc(Rect.fromCircle(center: center, radius: baseRadius), rotationValue * 2 * math.pi + 3, 2.5, false, paint); paint ..color = primaryColor.withOpacity(0.4) ..strokeWidth = 1.5; canvas.drawCircle(center, baseRadius - 15, paint); if (!isNear || transformValue < 1.0) { final sweepAngle = scanValue * 2 * math.pi; final scannerOpacity = isNear ? (1.0 - transformValue) : 1.0; paint ..color = scannerColor.withOpacity(scannerOpacity) ..strokeWidth = 4.0; canvas.drawArc( Rect.fromCircle(center: center, radius: baseRadius - 8), sweepAngle, math.pi / 4, false, paint, ); } if (isNear || transformValue > 0.0) { final targetOpacity = isNear ? transformValue : (1.0 - transformValue); paint ..color = targetColor.withOpacity((1.0 - pulseValue) * targetOpacity) ..strokeWidth = 4.0 ..style = PaintingStyle.stroke; canvas.drawCircle(center, baseRadius + (pulseValue * 15), paint); final crosshairScale = transformValue; final crosshairSize = 20.0 * crosshairScale; paint ..color = targetColor.withOpacity(targetOpacity) ..strokeWidth = 3.0; canvas.drawLine( Offset(center.dx - crosshairSize, center.dy), Offset(center.dx + crosshairSize, center.dy), paint, ); canvas.drawLine( Offset(center.dx, center.dy - crosshairSize), Offset(center.dx, center.dy + crosshairSize), paint, ); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } class _ARHintInfoDialog extends StatefulWidget { const _ARHintInfoDialog({Key? key}) : super(key: key); @override State<_ARHintInfoDialog> createState() => _ARHintInfoDialogState(); } class _ARHintInfoDialogState extends State<_ARHintInfoDialog> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; late Animation _fadeAnimation; late Animation _iconScaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _scaleAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.elasticOut, )); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.8, curve: Curves.easeOut), )); _iconScaleAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _controller, curve: const Interval(0.2, 1.0, curve: Curves.bounceOut), )); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _fadeAnimation, child: ScaleTransition( scale: _scaleAnimation, child: Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), elevation: 10, backgroundColor: AppColors.surface, child: Stack( clipBehavior: Clip.none, alignment: Alignment.topCenter, children: [ Padding( padding: const EdgeInsets.only( top: 50, left: 20, right: 20, bottom: 20, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( "AR Hint Guide", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 15), Text( "• Point camera around to locate target\n• Blue circle shows target direction\n• Arrows guide you when target is off-screen\n• Circle turns green when target found", style: TextStyle( color: AppColors.popupText, fontSize: 15, height: 1.5, fontWeight: FontWeight.w500, ), textAlign: TextAlign.left, ), const SizedBox(height: 25), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: AppColors.textPrimary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: () async { await _controller.reverse(); if (mounted) Navigator.of(context).pop(); }, child: const Text("Got it"), ), ), ], ), ), Positioned( top: -40, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.surface, boxShadow: [ BoxShadow( color: AppColors.shadowColor, blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: CircleAvatar( backgroundColor: AppColors.surface, radius: 40, child: ScaleTransition( scale: _iconScaleAnimation, child: SvgPicture.asset( Assets.icons.ar.path, colorFilter: ColorFilter.mode( AppColors.primary, BlendMode.srcIn, ), width: 35, ), ), ), ), ), ], ), ), ), ); } }