diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 718de8b..dd094df 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -1359,17 +1359,31 @@ class _VoiceInformationField extends StatelessWidget { ), LayoutBuilder( builder: (context, constraints) { - return CustomPaint( - size: Size( - constraints.maxWidth, - constraints.maxHeight.clamp(300.0, 520.0), - ), - painter: _VoiceInformationPainter( - samples: samples, - active: active, - progress: progress, - liveLevel: liveLevel, - ), + final paintSize = Size( + constraints.maxWidth, + constraints.maxHeight.clamp(300.0, 520.0), + ); + return Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + size: paintSize, + painter: _VoiceWaveUnderlayPainter( + samples: samples, + active: active, + liveLevel: liveLevel, + ), + ), + CustomPaint( + size: paintSize, + painter: _VoiceInformationPainter( + samples: samples, + active: active, + progress: progress, + liveLevel: liveLevel, + ), + ), + ], ); }, ), @@ -1378,6 +1392,92 @@ class _VoiceInformationField extends StatelessWidget { } } +class _VoiceWaveUnderlayPainter extends CustomPainter { + const _VoiceWaveUnderlayPainter({ + required this.samples, + required this.active, + required this.liveLevel, + }); + + final List samples; + final bool active; + final double liveLevel; + + @override + void paint(Canvas canvas, Size size) { + if (samples.isEmpty) { + return; + } + + final centerY = size.height * 0.52; + final width = math.min(size.width, 420.0); + final startX = (size.width - width) / 2; + final endX = startX + width; + final visibleSamples = samples.length.clamp(24, 96); + final sampleStart = math.max(0, samples.length - visibleSamples); + final baseline = 12.0 + liveLevel * 34.0; + final topPath = Path(); + final bottomPath = Path(); + + for (var index = 0; index < visibleSamples; index++) { + final sample = samples[sampleStart + index].clamp(0.0, 1.0); + final x = startX + width * (index / math.max(1, visibleSamples - 1)); + final phase = index * 0.52; + final lift = baseline + sample * 112.0; + final softWave = math.sin(phase) * (8.0 + liveLevel * 14.0); + final y = centerY - lift * 0.5 + softWave; + final mirrorY = centerY + lift * 0.5 - softWave; + if (index == 0) { + topPath.moveTo(x, y); + bottomPath.moveTo(x, mirrorY); + } else { + topPath.lineTo(x, y); + bottomPath.lineTo(x, mirrorY); + } + } + + final fillPath = Path.from(topPath) + ..lineTo(endX, centerY) + ..lineTo(endX, centerY) + ..addPath(bottomPath, Offset.zero) + ..lineTo(startX, centerY) + ..close(); + final alpha = active ? 0.10 + liveLevel * 0.16 : 0.05; + final fillPaint = Paint() + ..shader = LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + const Color(0xFF38F5D3).withValues(alpha: alpha * 0.35), + const Color(0xFFFF2D75).withValues(alpha: alpha), + const Color(0xFFFF7A90).withValues(alpha: alpha * 0.65), + ], + ).createShader(Rect.fromLTWH(startX, 0, width, size.height)) + ..style = PaintingStyle.fill + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 18); + canvas.drawPath(fillPath, fillPaint); + + final strokePaint = Paint() + ..color = const Color( + 0xFFFF6B8A, + ).withValues(alpha: active ? 0.22 + liveLevel * 0.28 : 0.08) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.4 + liveLevel * 3.0 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5); + canvas.drawPath(topPath, strokePaint); + canvas.drawPath(bottomPath, strokePaint); + } + + @override + bool shouldRepaint(covariant _VoiceWaveUnderlayPainter oldDelegate) { + return oldDelegate.samples != samples || + oldDelegate.active != active || + oldDelegate.liveLevel != liveLevel; + } +} + class _VoiceInformationPainter extends CustomPainter { const _VoiceInformationPainter({ required this.samples,