import 'dart:math' as math; import 'package:flutter/material.dart'; import '../theme/app_theme.dart'; /// Primitive attitude indicator (whiskey mark) displaying roll/pitch. /// /// Visual: tilting horizon line based on roll angle /// Hard left (-45°+): ╲ /// Left (-15°): ╲─ /// Level (0°): ─ /// Right (+15°): ─╱ /// Hard right (+45°+): ╱ /// /// Below the horizon: numeric readout "R: -12° P: 5°" class WhiskeyMark extends StatelessWidget { /// Roll angle in degrees. Negative = left bank, positive = right bank. final double? roll; /// Pitch angle in degrees. Negative = nose down, positive = nose up. final double? pitch; const WhiskeyMark({ super.key, this.roll, this.pitch, }); @override Widget build(BuildContext context) { final theme = AppTheme.of(context); return LayoutBuilder( builder: (context, constraints) { final size = math.min(constraints.maxWidth, constraints.maxHeight); final horizonSize = size * 0.6; final fontSize = size * 0.12; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Horizon indicator SizedBox( width: horizonSize, height: horizonSize, child: CustomPaint( painter: _HorizonPainter( roll: roll ?? 0, pitch: pitch ?? 0, lineColor: theme.foreground, skyColor: theme.subdued.withValues(alpha: 0.2), groundColor: theme.highlight.withValues(alpha: 0.3), ), ), ), SizedBox(height: size * 0.05), // Numeric readout Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'R: ${_formatAngle(roll)}', style: TextStyle( fontSize: fontSize, fontWeight: FontWeight.w400, fontFeatures: const [FontFeature.tabularFigures()], color: theme.foreground, ), ), SizedBox(width: size * 0.1), Text( 'P: ${_formatAngle(pitch)}', style: TextStyle( fontSize: fontSize, fontWeight: FontWeight.w400, fontFeatures: const [FontFeature.tabularFigures()], color: theme.subdued, ), ), ], ), SizedBox(height: size * 0.02), // Label Text( 'ATTITUDE', style: TextStyle( fontSize: fontSize * 0.8, fontWeight: FontWeight.w400, color: theme.subdued, letterSpacing: 1, ), ), ], ); }, ); } String _formatAngle(double? angle) { if (angle == null) return '—°'; return '${angle.round()}°'; } } /// Custom painter for the tilting horizon line class _HorizonPainter extends CustomPainter { final double roll; final double pitch; final Color lineColor; final Color skyColor; final Color groundColor; _HorizonPainter({ required this.roll, required this.pitch, required this.lineColor, required this.skyColor, required this.groundColor, }); @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final radius = math.min(size.width, size.height) / 2; // Clip to circle canvas.save(); canvas.clipPath(Path()..addOval(Rect.fromCircle(center: center, radius: radius))); // Convert roll to radians (negate so positive roll tilts right visually) final rollRad = -roll * math.pi / 180; // Pitch offset (positive pitch moves horizon down, showing more sky) // Scale: 90° pitch = full radius displacement final pitchOffset = (pitch / 90) * radius; // Calculate horizon line endpoints // The horizon is a horizontal line that we rotate by roll and offset by pitch final horizonY = center.dy + pitchOffset; // Paint sky (above horizon) final skyPaint = Paint()..color = skyColor; final groundPaint = Paint()..color = groundColor; // Create rotated horizon path canvas.save(); canvas.translate(center.dx, center.dy); canvas.rotate(rollRad); canvas.translate(-center.dx, -center.dy); // Sky rectangle (above horizon) canvas.drawRect( Rect.fromLTRB( center.dx - radius * 2, center.dy - radius * 2, center.dx + radius * 2, horizonY, ), skyPaint, ); // Ground rectangle (below horizon) canvas.drawRect( Rect.fromLTRB( center.dx - radius * 2, horizonY, center.dx + radius * 2, center.dy + radius * 2, ), groundPaint, ); // Horizon line final linePaint = Paint() ..color = lineColor ..strokeWidth = 2 ..style = PaintingStyle.stroke; canvas.drawLine( Offset(center.dx - radius, horizonY), Offset(center.dx + radius, horizonY), linePaint, ); canvas.restore(); // Draw circle border final borderPaint = Paint() ..color = lineColor.withValues(alpha: 0.5) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; canvas.drawCircle(center, radius - 1, borderPaint); // Draw center reference mark (fixed, doesn't rotate) final refPaint = Paint() ..color = lineColor ..strokeWidth = 2 ..style = PaintingStyle.stroke; // Small wings canvas.drawLine( Offset(center.dx - radius * 0.3, center.dy), Offset(center.dx - radius * 0.1, center.dy), refPaint, ); canvas.drawLine( Offset(center.dx + radius * 0.1, center.dy), Offset(center.dx + radius * 0.3, center.dy), refPaint, ); // Center dot canvas.drawCircle(center, 3, Paint()..color = lineColor); canvas.restore(); } @override bool shouldRepaint(_HorizonPainter oldDelegate) { return roll != oldDelegate.roll || pitch != oldDelegate.pitch || lineColor != oldDelegate.lineColor; } }