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(
|
child: AccelGraph(
|
||||||
ax: _dynamicAx, // Gravity-compensated lateral
|
ax: _dynamicAx, // Gravity-compensated lateral
|
||||||
ay: _dynamicAy, // Gravity-compensated longitudinal
|
ay: _dynamicAy, // Gravity-compensated longitudinal
|
||||||
maxG: 1.0,
|
maxG: 0.8,
|
||||||
ghostTrackPeriod: const Duration(seconds: 3),
|
ghostTrackPeriod: const Duration(seconds: 4),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -38,55 +37,46 @@ class _AccelGraphState extends State<AccelGraph> {
|
|||||||
double _ghostAx = 0;
|
double _ghostAx = 0;
|
||||||
double _ghostAy = 0;
|
double _ghostAy = 0;
|
||||||
double _ghostMagnitude = 0;
|
double _ghostMagnitude = 0;
|
||||||
Timer? _ghostResetTimer;
|
|
||||||
|
|
||||||
@override
|
// Timestamped history for sliding window
|
||||||
void initState() {
|
List<({DateTime time, double ax, double ay})> _history = [];
|
||||||
super.initState();
|
|
||||||
_setupGhostTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(AccelGraph oldWidget) {
|
void didUpdateWidget(AccelGraph oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
// Update ghost position if current magnitude exceeds previous peak
|
|
||||||
final currentAx = widget.ax ?? 0;
|
final currentAx = widget.ax ?? 0;
|
||||||
final currentAy = widget.ay ?? 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;
|
_ghostAx = currentAx;
|
||||||
_ghostAy = currentAy;
|
_ghostAy = currentAy;
|
||||||
_ghostMagnitude = currentMag;
|
_ghostMagnitude = 0;
|
||||||
}
|
|
||||||
|
|
||||||
// Restart timer if period changed
|
for (final entry in _history) {
|
||||||
if (oldWidget.ghostTrackPeriod != widget.ghostTrackPeriod) {
|
final mag = math.sqrt(entry.ax * entry.ax + entry.ay * entry.ay);
|
||||||
_setupGhostTimer();
|
if (mag > _ghostMagnitude) {
|
||||||
|
_ghostAx = entry.ax;
|
||||||
|
_ghostAy = entry.ay;
|
||||||
|
_ghostMagnitude = mag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No window configured - clear history to save memory
|
||||||
|
_history.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_ghostResetTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = AppTheme.of(context);
|
final theme = AppTheme.of(context);
|
||||||
@@ -117,6 +107,7 @@ class _AccelGraphState extends State<AccelGraph> {
|
|||||||
subdued: theme.subdued,
|
subdued: theme.subdued,
|
||||||
background: theme.background,
|
background: theme.background,
|
||||||
strokeWeight: strokeSize,
|
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 subdued;
|
||||||
final Color background;
|
final Color background;
|
||||||
final double strokeWeight;
|
final double strokeWeight;
|
||||||
|
final List<Offset> traceBuffer;
|
||||||
|
|
||||||
_AccelGraphPainter({
|
_AccelGraphPainter({
|
||||||
required this.ax,
|
required this.ax,
|
||||||
@@ -197,6 +189,7 @@ class _AccelGraphPainter extends CustomPainter {
|
|||||||
required this.subdued,
|
required this.subdued,
|
||||||
required this.background,
|
required this.background,
|
||||||
required this.strokeWeight,
|
required this.strokeWeight,
|
||||||
|
required this.traceBuffer,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -209,13 +202,13 @@ class _AccelGraphPainter extends CustomPainter {
|
|||||||
|
|
||||||
// No rectangular border
|
// No rectangular border
|
||||||
|
|
||||||
// Grid lines at 0.5G intervals
|
// Grid lines at 0.25G intervals
|
||||||
final gridPaint = Paint()
|
final gridPaint = Paint()
|
||||||
..color = subdued
|
..color = subdued
|
||||||
..strokeWidth = strokeWeight * 0.6
|
..strokeWidth = strokeWeight * 0.4
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
final gStep = 0.5;
|
final gStep = 0.25;
|
||||||
for (double g = gStep; g < maxG; g += gStep) {
|
for (double g = gStep; g < maxG; g += gStep) {
|
||||||
final offset = (g / maxG) * halfSize;
|
final offset = (g / maxG) * halfSize;
|
||||||
|
|
||||||
@@ -263,17 +256,40 @@ class _AccelGraphPainter extends CustomPainter {
|
|||||||
axisPaint,
|
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()
|
final ringPaint = Paint()
|
||||||
..color = subdued
|
..color = subdued
|
||||||
..strokeWidth = strokeWeight
|
..strokeWidth = strokeWeight * 0.5
|
||||||
..style = PaintingStyle.stroke;
|
..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;
|
final radius = (g / maxG) * halfSize;
|
||||||
canvas.drawCircle(center, radius, ringPaint);
|
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)
|
// Ghost dot (if enabled and has data)
|
||||||
if (showGhost) {
|
if (showGhost) {
|
||||||
final ghostX = center.dx + (ghostAx / maxG) * halfSize;
|
final ghostX = center.dx + (ghostAx / maxG) * halfSize;
|
||||||
@@ -281,7 +297,7 @@ class _AccelGraphPainter extends CustomPainter {
|
|||||||
final ghostRadius = halfSize * 0.08;
|
final ghostRadius = halfSize * 0.08;
|
||||||
|
|
||||||
final ghostPaint = Paint()
|
final ghostPaint = Paint()
|
||||||
..color = subdued.withValues(alpha: 0.5)
|
..color = subdued
|
||||||
..strokeWidth = strokeWeight
|
..strokeWidth = strokeWeight
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
@@ -311,6 +327,7 @@ class _AccelGraphPainter extends CustomPainter {
|
|||||||
showGhost != oldDelegate.showGhost ||
|
showGhost != oldDelegate.showGhost ||
|
||||||
maxG != oldDelegate.maxG ||
|
maxG != oldDelegate.maxG ||
|
||||||
foreground != oldDelegate.foreground ||
|
foreground != oldDelegate.foreground ||
|
||||||
subdued != oldDelegate.subdued;
|
subdued != oldDelegate.subdued ||
|
||||||
|
traceBuffer != oldDelegate.traceBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ class NavigatorWidgetState extends State<NavigatorWidget>
|
|||||||
animation: _shakeController,
|
animation: _shakeController,
|
||||||
child: image,
|
child: image,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final shake = sin(_shakeController.value * pi * 6) * 10 *
|
final shake = sin(_shakeController.value * pi * 6) * 25 *
|
||||||
(1 - _shakeController.value); // 6 oscillations, 4px amplitude, decay
|
(1 - _shakeController.value); // 6 oscillations, 25px amplitude, decay
|
||||||
return Transform.translate(
|
return Transform.translate(
|
||||||
offset: Offset(shake, 0),
|
offset: Offset(shake, 0),
|
||||||
child: child,
|
child: child,
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class _HorizonPainter extends CustomPainter {
|
|||||||
// Horizon line
|
// Horizon line
|
||||||
final linePaint = Paint()
|
final linePaint = Paint()
|
||||||
..color = lineColor
|
..color = lineColor
|
||||||
..strokeWidth = 2
|
..strokeWidth = borderWeight * 0.1
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
canvas.drawLine(
|
canvas.drawLine(
|
||||||
@@ -189,12 +189,34 @@ class _HorizonPainter extends CustomPainter {
|
|||||||
linePaint,
|
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();
|
canvas.restore();
|
||||||
|
|
||||||
// Draw circle border
|
// Draw circle border
|
||||||
final borderPaint = Paint()
|
final borderPaint = Paint()
|
||||||
..color = lineColor.withValues(alpha: 0.5)
|
..color = lineColor
|
||||||
..strokeWidth = borderWeight
|
..strokeWidth = borderWeight * 1.1
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
canvas.drawCircle(center, radius - 1, borderPaint);
|
canvas.drawCircle(center, radius - 1, borderPaint);
|
||||||
@@ -202,7 +224,7 @@ class _HorizonPainter extends CustomPainter {
|
|||||||
// Draw center reference mark (fixed, doesn't rotate)
|
// Draw center reference mark (fixed, doesn't rotate)
|
||||||
final refPaint = Paint()
|
final refPaint = Paint()
|
||||||
..color = lineColor
|
..color = lineColor
|
||||||
..strokeWidth = borderWeight * 0.8
|
..strokeWidth = borderWeight
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
// Small wings
|
// Small wings
|
||||||
@@ -216,12 +238,24 @@ class _HorizonPainter extends CustomPainter {
|
|||||||
Offset(center.dx + radius * 0.3, center.dy),
|
Offset(center.dx + radius * 0.3, center.dy),
|
||||||
refPaint,
|
refPaint,
|
||||||
);
|
);
|
||||||
// Center vertical line
|
|
||||||
|
// Center arrow
|
||||||
|
final refTipPaint = Paint()
|
||||||
|
..color = lineColor
|
||||||
|
..strokeWidth = borderWeight * 0.8
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
canvas.drawLine(
|
canvas.drawLine(
|
||||||
Offset(center.dx, center.dy - radius * 0.05),
|
Offset(center.dx, center.dy),
|
||||||
Offset(center.dx, center.dy + radius * 0.1),
|
Offset(center.dx + radius * 0.07, center.dy + radius * 0.1),
|
||||||
refPaint,
|
refTipPaint,
|
||||||
);
|
);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(center.dx, center.dy),
|
||||||
|
Offset(center.dx - radius * 0.07, center.dy + radius * 0.1),
|
||||||
|
refTipPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user