diff --git a/lib/pages/player/widgets/player_double_tap_hearts.dart b/lib/pages/player/widgets/player_double_tap_hearts.dart index 3146699..e0f05a5 100644 --- a/lib/pages/player/widgets/player_double_tap_hearts.dart +++ b/lib/pages/player/widgets/player_double_tap_hearts.dart @@ -123,47 +123,43 @@ class _PlayerDoubleTapHeartBurstState extends State @override Widget build(BuildContext context) { return IgnorePointer( - child: SizedBox.shrink( - child: AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final elapsed = - _controller.value * _controller.duration!.inMilliseconds; - return Stack( - fit: StackFit.expand, - clipBehavior: Clip.none, - children: [ - for (var index = 0; index < _hearts.length; index++) - _heart(_hearts[index], elapsed, index), - if (kDebugMode) - Positioned( - left: widget.origin.dx + 8, - top: widget.origin.dy, - child: IgnorePointer( - child: Text( - '${elapsed.round()}ms', - key: const Key('heart-burst-elapsed-debug'), - style: const TextStyle( - color: Colors.white60, - fontSize: 11, - ), + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final elapsed = + _controller.value * _controller.duration!.inMilliseconds; + return Stack( + fit: StackFit.expand, + clipBehavior: Clip.none, + children: [ + for (var index = 0; index < _hearts.length; index++) + _heart(_hearts[index], elapsed, index), + if (kDebugMode) + Positioned( + left: widget.origin.dx + 8, + top: widget.origin.dy + 10, + child: IgnorePointer( + child: Text( + '${elapsed.round()}ms', + key: const Key('heart-burst-elapsed-debug'), + style: const TextStyle( + color: Colors.white60, + fontSize: 11, ), ), ), - ], - ); - }, - ), + ), + ], + ); + }, ), ); } Widget _heart(_HeartParticle heart, double elapsed, int index) { - final progress = ((elapsed - heart.delay) / _heartDurationMs).clamp( - 0.0, - 1.0, - ); - if (progress <= 0) return const SizedBox.shrink(); + final rawProgress = (elapsed - heart.delay) / _heartDurationMs; + if (rawProgress < 0) return const SizedBox.shrink(); + final progress = rawProgress.clamp(0.0, 1.0); final move = Curves.easeOutCubic.transform(progress); final scale = progress < _heartPopPortion ? _heartStartScale + diff --git a/test/player/player_double_tap_hearts_test.dart b/test/player/player_double_tap_hearts_test.dart index 8ae4781..b799123 100644 --- a/test/player/player_double_tap_hearts_test.dart +++ b/test/player/player_double_tap_hearts_test.dart @@ -57,6 +57,48 @@ void main() { expect(_opacity(tester, 0), lessThan(0.5)); }); + testWidgets('heart position is anchored to the overlay origin', ( + tester, + ) async { + const origin = Offset(300, 546); + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: PlayerDoubleTapHeartBurst( + burstId: 4, + origin: origin, + onCompleted: _noop, + ), + ), + ], + ), + ), + ), + ); + + final firstHeart = find.byKey(const Key('double-tap-heart-0')); + expect( + (tester.getCenter(firstHeart) - origin).distance, + lessThanOrEqualTo(1), + ); + final debugTopLeft = tester.getTopLeft( + find.byKey(const Key('heart-burst-elapsed-debug')), + ); + expect( + (debugTopLeft - const Offset(308, 556)).distance, + lessThanOrEqualTo(1), + ); + + await tester.pump(const Duration(milliseconds: 750)); + final midpoint = tester.getCenter(firstHeart); + expect(midpoint.dy, lessThan(origin.dy)); + expect((midpoint.dx - origin.dx).abs(), lessThanOrEqualTo(1)); + }); + testWidgets('develop heart burst completes after about 1590ms', ( tester, ) async {