diff --git a/lib/ui/widgets/map_layout.dart b/lib/ui/widgets/map_layout.dart new file mode 100644 index 0000000..0db87c7 --- /dev/null +++ b/lib/ui/widgets/map_layout.dart @@ -0,0 +1,29 @@ +import 'dart:math' as math; +import 'dart:ui'; + +/// Deterministic serpentine layout for the journey map. Stage 0 is at the +/// bottom; the path snakes upward. Works for any stage count. +class MapLayout { + const MapLayout({ + required this.width, + this.nodeSpacing = 108, + this.topPadding = 140, + this.bottomPadding = 150, + }); + + final double width; + final double nodeSpacing; + final double topPadding; + final double bottomPadding; + + double get amplitude => width * 0.26; + + double heightFor(int count) => + topPadding + bottomPadding + (count - 1) * nodeSpacing; + + Offset nodeCenter(int index, int count) { + final y = heightFor(count) - bottomPadding - index * nodeSpacing; + final x = width / 2 + amplitude * math.sin(index * 1.05); + return Offset(x, y); + } +} diff --git a/test/ui/map_layout_test.dart b/test/ui/map_layout_test.dart new file mode 100644 index 0000000..61cb9fe --- /dev/null +++ b/test/ui/map_layout_test.dart @@ -0,0 +1,32 @@ +import 'package:block_seasons/ui/widgets/map_layout.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const layout = MapLayout(width: 400); + + test('node 0 sits near the bottom, later nodes climb', () { + final h = layout.heightFor(60); + final first = layout.nodeCenter(0, 60); + final last = layout.nodeCenter(59, 60); + expect(first.dy, greaterThan(h - 200)); + expect(last.dy, lessThan(200)); + for (var i = 1; i < 60; i++) { + expect(layout.nodeCenter(i, 60).dy, + lessThan(layout.nodeCenter(i - 1, 60).dy)); + } + }); + + test('x stays within horizontal margins', () { + for (var i = 0; i < 60; i++) { + final x = layout.nodeCenter(i, 60).dx; + expect(x, greaterThanOrEqualTo(400 * 0.12)); + expect(x, lessThanOrEqualTo(400 * 0.88)); + } + }); + + test('vertical spacing is uniform', () { + final a = layout.nodeCenter(3, 60).dy - layout.nodeCenter(4, 60).dy; + final b = layout.nodeCenter(40, 60).dy - layout.nodeCenter(41, 60).dy; + expect(a, closeTo(b, 0.001)); + }); +}