import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../rides/ride_state_machine.dart'; class LeanVisualizer extends StatelessWidget { final double lean; final RideSession session; const LeanVisualizer({super.key, required this.lean, required this.session}); @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'LEAN ANGLE', style: TextStyle( color: Colors.white54, fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 1.5, ), ), const SizedBox(height: 6), SizedBox( width: 110, height: 110, child: Stack( alignment: Alignment.center, children: [ CustomPaint( size: const Size(110, 110), painter: _LeanDialPainter(leanAngle: lean), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( '${lean.abs().toStringAsFixed(0)}°', style: const TextStyle( fontSize: 26, fontWeight: FontWeight.w900, color: Colors.white, fontFamily: 'JetBrains Mono', ), ), Text( lean > 1.0 ? 'RIGHT' : (lean < -1.0 ? 'LEFT' : 'UPRIGHT'), style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: lean.abs() > 1.0 ? Theme.of(context).colorScheme.primary : Colors.white38, ), ), ], ), ], ), ), const SizedBox(height: 6), FittedBox( fit: BoxFit.scaleDown, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'L: ${session.maxLeanLeft.toStringAsFixed(0)}°', style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontSize: 11, fontWeight: FontWeight.w700, fontFamily: 'JetBrains Mono', ), ), const SizedBox(width: 12), Text( 'R: ${session.maxLeanRight.toStringAsFixed(0)}°', style: TextStyle( color: Theme.of(context).colorScheme.secondary, fontSize: 11, fontWeight: FontWeight.w700, fontFamily: 'JetBrains Mono', ), ), ], ), ), ], ); } } class _LeanDialPainter extends CustomPainter { final double leanAngle; _LeanDialPainter({required this.leanAngle}); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = size.width * 0.44; // Track arc final trackPaint = Paint() ..color = const Color(0xFF222222) ..style = PaintingStyle.stroke ..strokeWidth = 3.5 ..strokeCap = StrokeCap.round; canvas.drawArc( Rect.fromCircle(center: center, radius: radius), math.pi, math.pi, false, trackPaint, ); // Ticks final tickPaint = Paint() ..color = Colors.white24 ..strokeWidth = 1.0; final centerTickPaint = Paint() ..color = Colors.white70 ..strokeWidth = 1.5; for (final deg in [-60, -45, -30, -15, 0, 15, 30, 45, 60]) { final rad = (270 + deg) * (math.pi / 180.0); final isZero = deg == 0; final inner = radius - (isZero ? 5.0 : 3.0); final outer = radius + (isZero ? 3.0 : 1.5); final p1 = Offset(center.dx + inner * math.cos(rad), center.dy + inner * math.sin(rad)); final p2 = Offset(center.dx + outer * math.cos(rad), center.dy + outer * math.sin(rad)); canvas.drawLine(p1, p2, isZero ? centerTickPaint : tickPaint); } // Active sweep arc final clampedLean = leanAngle.clamp(-65.0, 65.0); if (clampedLean.abs() > 0.5) { final isExtreme = clampedLean.abs() > 45; final activeColor = isExtreme ? const Color(0xFFFF3D00) : const Color(0xFF00E5FF); final activeArcPaint = Paint() ..color = activeColor ..style = PaintingStyle.stroke ..strokeWidth = 4.0 ..strokeCap = StrokeCap.round; final startRad = 270 * (math.pi / 180.0); final sweepRad = clampedLean * (math.pi / 180.0); canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startRad, sweepRad, false, activeArcPaint, ); } // Pointer needle on perimeter final angleRad = (270 + clampedLean) * (math.pi / 180); final innerNeedle = Offset(center.dx + (radius * 0.74) * math.cos(angleRad), center.dy + (radius * 0.74) * math.sin(angleRad)); final outerNeedle = Offset(center.dx + (radius + 2.0) * math.cos(angleRad), center.dy + (radius + 2.0) * math.sin(angleRad)); final needlePaint = Paint() ..color = const Color(0xFF00E5FF) ..strokeWidth = 2.5 ..strokeCap = StrokeCap.round; canvas.drawLine(innerNeedle, outerNeedle, needlePaint); // Glowing tip dot canvas.drawCircle(outerNeedle, 3.0, Paint()..color = const Color(0xFF00E5FF)); } @override bool shouldRepaint(covariant _LeanDialPainter oldDelegate) => oldDelegate.leanAngle != leanAngle; }