fix(visualizador): add a discrete-bar rendering mode
Audit 1.7/2.5 (t4 lines 66-68, 120-122): the prototype draws 30 discrete bottom-anchored bars (radius 2, vertical gradient); the build's VisualizadorAudio painted a single continuous oscilloscope stroke, so its `barras: 30` parameter never produced bars. Adds an opt-in `barrasDiscretas` mode (default false, byte-identical continuous rendering preserved for any other caller) plus a `gradienteFinAlpha` knob for the two screens' differing gradient end-alpha (.3 vs .45). Wires it into the Escuchar hero and the full player, correcting the player's bar count/height to match the prototype (26->30 bars, 46->40px) at the same time.
This commit is contained in:
@@ -291,6 +291,9 @@ class _EscucharHero extends StatelessWidget {
|
||||
barras: 30,
|
||||
altura: 26,
|
||||
color: context.pluriTokens.liveGreen,
|
||||
// Audit 1.7 (t4 lines 66-68): 30 discrete
|
||||
// bottom-anchored bars, not a continuous stroke.
|
||||
barrasDiscretas: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -152,6 +152,7 @@ class _PantallaReproductorState extends State<PantallaReproductor> {
|
||||
).pluriFadeIn(context, delay: const Duration(milliseconds: 230)),
|
||||
const SizedBox(height: 14),
|
||||
PluriGlassSurface(
|
||||
key: const Key('player-visualizer'),
|
||||
borderRadius: BorderRadius.circular(tokens.radiusLg),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
@@ -161,9 +162,13 @@ class _PantallaReproductorState extends State<PantallaReproductor> {
|
||||
estadoStream: estado.estadoStream,
|
||||
androidAudioSessionIdStream:
|
||||
estado.audio.androidAudioSessionIdStream,
|
||||
barras: 26,
|
||||
// Audit 2.5 (t4 lines 120-122): 30 discrete bars at 40px,
|
||||
// gradient ending at 45% alpha (not the hero's 30%).
|
||||
barras: 30,
|
||||
color: tokens.warmCoral,
|
||||
altura: 46,
|
||||
altura: 40,
|
||||
barrasDiscretas: true,
|
||||
gradienteFinAlpha: 0.45,
|
||||
),
|
||||
).pluriFadeIn(context, delay: const Duration(milliseconds: 270)),
|
||||
const SizedBox(height: 22),
|
||||
|
||||
@@ -20,6 +20,18 @@ class VisualizadorAudio extends StatefulWidget {
|
||||
final double altura;
|
||||
final double anchuraTotal;
|
||||
|
||||
/// Audit 1.7/2.5 (t4 lines 66-68, 120-122): the prototype draws `barras`
|
||||
/// discrete bottom-anchored bars (radius 2, vertical gradient), not a
|
||||
/// continuous oscilloscope stroke. Defaults to `false` so every OTHER
|
||||
/// caller (if any is ever added) keeps the original `_WaveFlowPainter`
|
||||
/// rendering unchanged.
|
||||
final bool barrasDiscretas;
|
||||
|
||||
/// Alpha of the gradient's BOTTOM colour stop in discrete-bar mode. The
|
||||
/// two current consumers disagree (Escuchar hero: `.3`, t4 line 67; full
|
||||
/// player: `.45`, t4 line 121), so this is a parameter, not a constant.
|
||||
final double gradienteFinAlpha;
|
||||
|
||||
const VisualizadorAudio({
|
||||
super.key,
|
||||
required this.estadoStream,
|
||||
@@ -28,6 +40,8 @@ class VisualizadorAudio extends StatefulWidget {
|
||||
this.color,
|
||||
this.altura = 48,
|
||||
this.anchuraTotal = double.infinity,
|
||||
this.barrasDiscretas = false,
|
||||
this.gradienteFinAlpha = 0.3,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -184,23 +198,79 @@ class _VisualizadorAudioState extends State<VisualizadorAudio>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
final t = _controller.value * pi * 2;
|
||||
return SizedBox(
|
||||
height: widget.altura,
|
||||
width: widget.anchuraTotal,
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: _WaveFlowPainter(
|
||||
color: color,
|
||||
phase: t,
|
||||
active: _activo,
|
||||
waveform: _ondaVisual,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
child:
|
||||
widget.barrasDiscretas
|
||||
? _construirBarrasDiscretas(color)
|
||||
: _construirOnda(color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _construirOnda(Color color) {
|
||||
final t = _controller.value * pi * 2;
|
||||
return CustomPaint(
|
||||
painter: _WaveFlowPainter(
|
||||
color: color,
|
||||
phase: t,
|
||||
active: _activo,
|
||||
waveform: _ondaVisual,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Audit 1.7/2.5 (t4 lines 66-68, 120-122): `barras` discrete,
|
||||
/// bottom-anchored bars — radius 2, `gap:2px`, a top-to-bottom gradient
|
||||
/// from [color] to `color@gradienteFinAlpha`. Reuses the SAME
|
||||
/// `_ondaVisual`/`_ondaOrganica` amplitude source the continuous painter
|
||||
/// already computes (real audio-session data when available, an organic
|
||||
/// synthetic fallback otherwise) — only the paint strategy changes.
|
||||
Widget _construirBarrasDiscretas(Color color) {
|
||||
final valores = _valoresBarras();
|
||||
final children = <Widget>[];
|
||||
for (var i = 0; i < valores.length; i++) {
|
||||
if (i > 0) children.add(const SizedBox(width: 2));
|
||||
final valor = valores[i].clamp(0.08, 1.0);
|
||||
children.add(
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
key: ValueKey('visualizador-barra-$i'),
|
||||
height: widget.altura * valor,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
color,
|
||||
color.withValues(alpha: widget.gradienteFinAlpha),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.end, children: children);
|
||||
}
|
||||
|
||||
/// `_ondaVisual` is empty until the animation controller ticks at least
|
||||
/// once (only happens once playback goes active, `_onEstado`'s
|
||||
/// `_controller.repeat()`) — a discrete render before that must still show
|
||||
/// `barras` resting bars, not zero, so it falls back to a flat, low
|
||||
/// baseline of exactly [VisualizadorAudio.barras] values (clamped the same
|
||||
/// way `_ondaOrganica` clamps its own count).
|
||||
List<double> _valoresBarras() {
|
||||
if (_ondaVisual.isNotEmpty) return _ondaVisual;
|
||||
return List<double>.filled(widget.barras.clamp(8, 96), 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
class _WaveFlowPainter extends CustomPainter {
|
||||
|
||||
Reference in New Issue
Block a user