diff --git a/app/lib/pages/shell_page.dart b/app/lib/pages/shell_page.dart index 2f7b51a..01aa278 100644 --- a/app/lib/pages/shell_page.dart +++ b/app/lib/pages/shell_page.dart @@ -50,7 +50,11 @@ class _ShellPageState extends State { ), child: Column( children: [ - const DeltaMark(size: 36), + // Static in the persistent nav rail: a 60fps pulse + // here repaints every scroll frame and stutters web + // scrolling. The breathing mark stays on the + // landing page (which isn't scrolled). + const DeltaMark(size: 36, animated: false), const SizedBox(height: ReclaimSpace.sm), const BrandWordmark(size: 18), Text( diff --git a/app/lib/widgets/delta_mark.dart b/app/lib/widgets/delta_mark.dart index fd921cb..6ed7398 100644 --- a/app/lib/widgets/delta_mark.dart +++ b/app/lib/widgets/delta_mark.dart @@ -46,17 +46,30 @@ class _DeltaMarkState extends State @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller, - builder: (context, _) { - final t = _controller.value; - // Breathe: scale 1 → 1.28 → 1 over the cycle. - final pulse = 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); - return CustomPaint( - size: Size.square(widget.size), - painter: _DeltaPainter(pulse: widget.animated ? pulse : 1.0), - ); - }, + // Static mark: no controller listening, paint once. + if (!widget.animated) { + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: 1.0), + ); + } + // Animated: isolate the per-frame repaint in its own layer so + // the breathing pulse never invalidates surrounding content + // (web scroll stays smooth). + return RepaintBoundary( + child: AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final t = _controller.value; + // Breathe: scale 1 → 1.28 → 1 over the cycle. + final pulse = + 1.0 + 0.28 * (1 - (math.cos(t * 2 * math.pi).abs())); + return CustomPaint( + size: Size.square(widget.size), + painter: _DeltaPainter(pulse: pulse), + ); + }, + ), ); } }