Files
SpotiFLAC-Mobile/lib/screens/equalizer_curve.dart
T
zarzet e9834f060c refactor(equalizer): split the equalizer screen into widgets and curve parts
The screen file mixed state, a dozen private widgets and the curve painter.
Move the widgets and the painter with its helpers into part files; code is
unchanged.
2026-10-18 22:42:37 +07:00

207 lines
6.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
part of 'equalizer_screen.dart';
double _snap(double value, double step) => (value / step).round() * step;
/// Signed gain with a true minus sign, e.g. `+3.5`, `0.0`, `−2.0`.
String _signed(double value) {
final text = value.abs().toStringAsFixed(1);
if (text == '0.0') return text;
return value > 0 ? '+$text' : '−$text';
}
String _frequency(double hz) => hz >= 1000
? '${(hz / 1000).toStringAsFixed(hz >= 10000 ? 0 : 1)}k'
: hz.toStringAsFixed(0);
class _EqualizerCurve extends CustomPainter {
_EqualizerCurve({
required this.settings,
required this.color,
required this.grid,
required this.label,
required this.labelStyle,
required this.textScaler,
});
final EqualizerSettings settings;
final Color color, grid, label;
final TextStyle labelStyle;
final TextScaler textScaler;
static const _range = 18.0;
static const _minHz = 20.0, _maxHz = 20000.0;
TextPainter _text(String text) => TextPainter(
text: TextSpan(
text: text,
style: labelStyle.copyWith(color: label),
),
textDirection: TextDirection.ltr,
textScaler: textScaler.clamp(maxScaleFactor: 1.3),
)..layout();
@override
void paint(Canvas canvas, Size size) {
final freqLabels = [
for (final hz in const [100.0, 1000.0, 10000.0])
(hz, _text(_frequency(hz))),
];
final bottom = size.height - freqLabels.first.$2.height - 6;
final gutter = _text('+12').width + 8;
final plot = Rect.fromLTRB(gutter, 4, size.width, bottom);
double x(double hz) =>
plot.left +
math.log(hz / _minHz) / math.log(_maxHz / _minHz) * plot.width;
double y(double gain) =>
plot.center.dy - gain.clamp(-_range, _range) / _range * plot.height / 2;
final guide = Paint()
..color = grid.withValues(alpha: 0.5)
..strokeWidth = 1;
for (final gain in const [12.0, -12.0]) {
canvas.drawLine(
Offset(plot.left, y(gain)),
Offset(plot.right, y(gain)),
guide,
);
}
for (final (hz, _) in freqLabels) {
canvas.drawLine(
Offset(x(hz), plot.top),
Offset(x(hz), plot.bottom),
guide,
);
}
// Dashed 0 dB baseline.
final baseline = Paint()
..color = grid
..strokeWidth = 1.5;
for (var dx = plot.left; dx < plot.right; dx += 8) {
canvas.drawLine(
Offset(dx, plot.center.dy),
Offset(math.min(dx + 4, plot.right), plot.center.dy),
baseline,
);
}
for (final (gain, text) in [(12.0, '+12'), (0.0, '0'), (-12.0, '−12')]) {
final painter = _text(text);
painter.paint(
canvas,
Offset(gutter - 8 - painter.width, y(gain) - painter.height / 2),
);
}
for (final (hz, painter) in freqLabels) {
painter.paint(canvas, Offset(x(hz) - painter.width / 2, bottom + 6));
}
final curve = settings.copyWith(gains: settings.effectiveGains);
final points = <Offset>[Offset(plot.left, y(curve.gains.first))];
for (var i = 0; i < curve.gains.length; i++) {
final point = Offset(x(settings.frequencies[i]), y(curve.gains[i]));
if (point.dx > points.last.dx + 0.5 && point.dx < plot.right - 0.5) {
points.add(point);
}
}
points.add(Offset(plot.right, y(curve.gains.last)));
final line = _monotoneSpline(points);
final area = Path.from(line)
..lineTo(plot.right, plot.center.dy)
..lineTo(plot.left, plot.center.dy)
..close();
canvas.save();
canvas.clipRect(plot);
canvas.drawPath(
area,
Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
color.withValues(alpha: 0.42),
color.withValues(alpha: 0.12),
color.withValues(alpha: 0.42),
],
).createShader(plot),
);
canvas.restore();
canvas.drawPath(
line,
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 3
..strokeJoin = StrokeJoin.round
..strokeCap = StrokeCap.round,
);
final dot = Paint()..color = color;
final effective = curve.gains;
for (var i = 0; i < effective.length; i++) {
final hz = settings.frequencies[i];
if (hz < _minHz || hz > _maxHz) continue;
canvas.drawCircle(
Offset(x(hz), y(effective[i])),
effective.length > 15 ? 2.5 : 4,
dot,
);
}
}
/// Smooth curve through [points] that never overshoots between bands
/// (Fritsch–Carlson monotone cubic interpolation).
static Path _monotoneSpline(List<Offset> points) {
final n = points.length;
final path = Path()..moveTo(points.first.dx, points.first.dy);
if (n < 3) {
for (final point in points.skip(1)) {
path.lineTo(point.dx, point.dy);
}
return path;
}
final slopes = [
for (var i = 0; i < n - 1; i++)
(points[i + 1].dy - points[i].dy) / (points[i + 1].dx - points[i].dx),
];
final tangents = [
slopes.first,
for (var i = 1; i < n - 1; i++)
slopes[i - 1] * slopes[i] <= 0 ? 0.0 : (slopes[i - 1] + slopes[i]) / 2,
slopes.last,
];
for (var i = 0; i < n - 1; i++) {
if (slopes[i] == 0) {
tangents[i] = tangents[i + 1] = 0;
continue;
}
final a = tangents[i] / slopes[i], b = tangents[i + 1] / slopes[i];
final s = a * a + b * b;
if (s > 9) {
final t = 3 / math.sqrt(s);
tangents[i] = t * a * slopes[i];
tangents[i + 1] = t * b * slopes[i];
}
}
for (var i = 0; i < n - 1; i++) {
final p0 = points[i], p1 = points[i + 1];
final h = (p1.dx - p0.dx) / 3;
path.cubicTo(
p0.dx + h,
p0.dy + tangents[i] * h,
p1.dx - h,
p1.dy - tangents[i + 1] * h,
p1.dx,
p1.dy,
);
}
return path;
}
@override
bool shouldRepaint(_EqualizerCurve oldDelegate) =>
oldDelegate.settings != settings ||
oldDelegate.color != color ||
oldDelegate.grid != grid ||
oldDelegate.label != label ||
oldDelegate.labelStyle != labelStyle ||
oldDelegate.textScaler != textScaler;
}