ui: multiple visual upgrades:
- attitude indicator: pitch ladders and little triangle crosshair - accelerometer: G trace and various other stuff - navigator: bigger surprise
This commit is contained in:
@@ -221,8 +221,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
child: AccelGraph(
|
||||
ax: _dynamicAx, // Gravity-compensated lateral
|
||||
ay: _dynamicAy, // Gravity-compensated longitudinal
|
||||
maxG: 1.0,
|
||||
ghostTrackPeriod: const Duration(seconds: 3),
|
||||
maxG: 0.8,
|
||||
ghostTrackPeriod: const Duration(seconds: 4),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -38,55 +37,46 @@ class _AccelGraphState extends State<AccelGraph> {
|
||||
double _ghostAx = 0;
|
||||
double _ghostAy = 0;
|
||||
double _ghostMagnitude = 0;
|
||||
Timer? _ghostResetTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupGhostTimer();
|
||||
}
|
||||
// Timestamped history for sliding window
|
||||
List<({DateTime time, double ax, double ay})> _history = [];
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AccelGraph oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// Update ghost position if current magnitude exceeds previous peak
|
||||
final currentAx = widget.ax ?? 0;
|
||||
final currentAy = widget.ay ?? 0;
|
||||
final currentMag = math.sqrt(currentAx * currentAx + currentAy * currentAy);
|
||||
final now = DateTime.now();
|
||||
|
||||
if (currentMag > _ghostMagnitude) {
|
||||
// Only track history when ghostTrackPeriod is configured
|
||||
if (widget.ghostTrackPeriod != null) {
|
||||
// Add current reading to history
|
||||
_history.add((time: now, ax: currentAx, ay: currentAy));
|
||||
|
||||
// Prune entries outside the window
|
||||
final cutoff = now.subtract(widget.ghostTrackPeriod!);
|
||||
_history.removeWhere((e) => e.time.isBefore(cutoff));
|
||||
|
||||
// Recalculate ghost as max magnitude from current window
|
||||
_ghostAx = currentAx;
|
||||
_ghostAy = currentAy;
|
||||
_ghostMagnitude = currentMag;
|
||||
}
|
||||
_ghostMagnitude = 0;
|
||||
|
||||
// Restart timer if period changed
|
||||
if (oldWidget.ghostTrackPeriod != widget.ghostTrackPeriod) {
|
||||
_setupGhostTimer();
|
||||
for (final entry in _history) {
|
||||
final mag = math.sqrt(entry.ax * entry.ax + entry.ay * entry.ay);
|
||||
if (mag > _ghostMagnitude) {
|
||||
_ghostAx = entry.ax;
|
||||
_ghostAy = entry.ay;
|
||||
_ghostMagnitude = mag;
|
||||
}
|
||||
}
|
||||
|
||||
void _setupGhostTimer() {
|
||||
_ghostResetTimer?.cancel();
|
||||
if (widget.ghostTrackPeriod != null) {
|
||||
_ghostResetTimer = Timer.periodic(widget.ghostTrackPeriod!, (_) {
|
||||
setState(() {
|
||||
// Reset ghost to current position
|
||||
_ghostAx = widget.ax ?? 0;
|
||||
_ghostAy = widget.ay ?? 0;
|
||||
_ghostMagnitude = math.sqrt(_ghostAx * _ghostAx + _ghostAy * _ghostAy);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// No window configured - clear history to save memory
|
||||
_history.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ghostResetTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = AppTheme.of(context);
|
||||
@@ -117,6 +107,7 @@ class _AccelGraphState extends State<AccelGraph> {
|
||||
subdued: theme.subdued,
|
||||
background: theme.background,
|
||||
strokeWeight: strokeSize,
|
||||
traceBuffer: _history.map((e) => Offset(e.ax, e.ay)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -185,6 +176,7 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
final Color subdued;
|
||||
final Color background;
|
||||
final double strokeWeight;
|
||||
final List<Offset> traceBuffer;
|
||||
|
||||
_AccelGraphPainter({
|
||||
required this.ax,
|
||||
@@ -197,6 +189,7 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
required this.subdued,
|
||||
required this.background,
|
||||
required this.strokeWeight,
|
||||
required this.traceBuffer,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -209,13 +202,13 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
|
||||
// No rectangular border
|
||||
|
||||
// Grid lines at 0.5G intervals
|
||||
// Grid lines at 0.25G intervals
|
||||
final gridPaint = Paint()
|
||||
..color = subdued
|
||||
..strokeWidth = strokeWeight * 0.6
|
||||
..strokeWidth = strokeWeight * 0.4
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final gStep = 0.5;
|
||||
final gStep = 0.25;
|
||||
for (double g = gStep; g < maxG; g += gStep) {
|
||||
final offset = (g / maxG) * halfSize;
|
||||
|
||||
@@ -263,17 +256,40 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
axisPaint,
|
||||
);
|
||||
|
||||
// G-ring markers (circles at 1G and 2G for quick reference)
|
||||
// G-ring markers (circles at every 0.5G for quick reference)
|
||||
final ringPaint = Paint()
|
||||
..color = subdued
|
||||
..strokeWidth = strokeWeight
|
||||
..strokeWidth = strokeWeight * 0.5
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
for (double g = 1.0; g <= maxG; g += 1.0) {
|
||||
for (double g = 0.5; g <= maxG; g += 0.5) {
|
||||
final radius = (g / maxG) * halfSize;
|
||||
canvas.drawCircle(center, radius, ringPaint);
|
||||
}
|
||||
|
||||
// Trace line
|
||||
if (traceBuffer.length >= 2) {
|
||||
final tracePaint = Paint()
|
||||
..color = foreground
|
||||
..strokeWidth = strokeWeight * 0.4
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
|
||||
final path = Path();
|
||||
for (int i = 0; i < traceBuffer.length; i++) {
|
||||
final pt = traceBuffer[i];
|
||||
final x = center.dx + (pt.dx.clamp(-maxG, maxG) / maxG) * halfSize;
|
||||
final y = center.dy - (pt.dy.clamp(-maxG, maxG) / maxG) * halfSize;
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, tracePaint);
|
||||
}
|
||||
|
||||
// Ghost dot (if enabled and has data)
|
||||
if (showGhost) {
|
||||
final ghostX = center.dx + (ghostAx / maxG) * halfSize;
|
||||
@@ -281,7 +297,7 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
final ghostRadius = halfSize * 0.08;
|
||||
|
||||
final ghostPaint = Paint()
|
||||
..color = subdued.withValues(alpha: 0.5)
|
||||
..color = subdued
|
||||
..strokeWidth = strokeWeight
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
@@ -311,6 +327,7 @@ class _AccelGraphPainter extends CustomPainter {
|
||||
showGhost != oldDelegate.showGhost ||
|
||||
maxG != oldDelegate.maxG ||
|
||||
foreground != oldDelegate.foreground ||
|
||||
subdued != oldDelegate.subdued;
|
||||
subdued != oldDelegate.subdued ||
|
||||
traceBuffer != oldDelegate.traceBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ class NavigatorWidgetState extends State<NavigatorWidget>
|
||||
animation: _shakeController,
|
||||
child: image,
|
||||
builder: (context, child) {
|
||||
final shake = sin(_shakeController.value * pi * 6) * 10 *
|
||||
(1 - _shakeController.value); // 6 oscillations, 4px amplitude, decay
|
||||
final shake = sin(_shakeController.value * pi * 6) * 25 *
|
||||
(1 - _shakeController.value); // 6 oscillations, 25px amplitude, decay
|
||||
return Transform.translate(
|
||||
offset: Offset(shake, 0),
|
||||
child: child,
|
||||
|
||||
@@ -180,7 +180,7 @@ class _HorizonPainter extends CustomPainter {
|
||||
// Horizon line
|
||||
final linePaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = 2
|
||||
..strokeWidth = borderWeight * 0.1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawLine(
|
||||
@@ -189,12 +189,34 @@ class _HorizonPainter extends CustomPainter {
|
||||
linePaint,
|
||||
);
|
||||
|
||||
// Pitch ladder lines (15° intervals)
|
||||
final ladderPaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = borderWeight * 0.4
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
|
||||
|
||||
for (int deg = -75; deg <= 75; deg += 15) {
|
||||
if (deg == 0) continue; // Skip horizon (already drawn)
|
||||
|
||||
final ladderY = horizonY - (deg / 90) * radius;
|
||||
final double widthMod = (100 - (deg < 0 ? -deg : deg)) / 100;
|
||||
final ladderWidth = radius * 0.7 * widthMod; // longer ladder if close to horizon
|
||||
|
||||
canvas.drawLine(
|
||||
Offset(center.dx - ladderWidth, ladderY),
|
||||
Offset(center.dx + ladderWidth, ladderY),
|
||||
ladderPaint,
|
||||
);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
// Draw circle border
|
||||
final borderPaint = Paint()
|
||||
..color = lineColor.withValues(alpha: 0.5)
|
||||
..strokeWidth = borderWeight
|
||||
..color = lineColor
|
||||
..strokeWidth = borderWeight * 1.1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawCircle(center, radius - 1, borderPaint);
|
||||
@@ -202,7 +224,7 @@ class _HorizonPainter extends CustomPainter {
|
||||
// Draw center reference mark (fixed, doesn't rotate)
|
||||
final refPaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = borderWeight * 0.8
|
||||
..strokeWidth = borderWeight
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
// Small wings
|
||||
@@ -216,12 +238,24 @@ class _HorizonPainter extends CustomPainter {
|
||||
Offset(center.dx + radius * 0.3, center.dy),
|
||||
refPaint,
|
||||
);
|
||||
// Center vertical line
|
||||
|
||||
// Center arrow
|
||||
final refTipPaint = Paint()
|
||||
..color = lineColor
|
||||
..strokeWidth = borderWeight * 0.8
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawLine(
|
||||
Offset(center.dx, center.dy - radius * 0.05),
|
||||
Offset(center.dx, center.dy + radius * 0.1),
|
||||
refPaint,
|
||||
Offset(center.dx, center.dy),
|
||||
Offset(center.dx + radius * 0.07, center.dy + radius * 0.1),
|
||||
refTipPaint,
|
||||
);
|
||||
canvas.drawLine(
|
||||
Offset(center.dx, center.dy),
|
||||
Offset(center.dx - radius * 0.07, center.dy + radius * 0.1),
|
||||
refTipPaint,
|
||||
);
|
||||
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user