Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## 0.2.5

* Improve `WaveWidget` paint performance for long-duration, high-amplitude
wave configurations like GitHub issue #53.
* Add a manual benchmark for the issue #53 scenario and regression coverage for
runtime amplitude updates.

## 0.2.4

* Expand Dart SDK compatibility to allow Dart 3.
Expand Down
152 changes: 103 additions & 49 deletions lib/src/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,20 @@ class _WaveWidgetState extends State<WaveWidget> with TickerProviderStateMixin {
throw FlutterError('Unsupported or missing `ColorMode` in `config`.');
}

List<int> _animationDurations(Config config) {
if (config.colorMode == ColorMode.custom) {
return (config as CustomConfig).durations!;
}
if (config.colorMode == ColorMode.random) {
return (config as RandomConfig).durations;
}
if (config.colorMode == ColorMode.single) {
return (config as SingleConfig).durations;
}

throw FlutterError('Unsupported or missing `ColorMode` in `config`.');
}

Color _colorWithOpacity(Color color, double opacity) {
return color.withAlpha((opacity * 255).round());
}
Expand Down Expand Up @@ -396,11 +410,15 @@ class _WaveWidgetState extends State<WaveWidget> with TickerProviderStateMixin {
}

bool _shouldRecreateAnimations(WaveWidget oldWidget) {
return oldWidget.config != widget.config ||
oldWidget.waveAmplitude != widget.waveAmplitude ||
oldWidget.wavePhase != widget.wavePhase ||
if (oldWidget.wavePhase != widget.wavePhase ||
oldWidget.isLoop != widget.isLoop ||
oldWidget.duration != widget.duration;
oldWidget.duration != widget.duration) {
return true;
}

final oldDurations = _animationDurations(oldWidget.config);
final durations = _animationDurations(widget.config);
return !_listEquals(oldDurations, durations);
}

@override
Expand All @@ -426,16 +444,26 @@ class _WaveWidgetState extends State<WaveWidget> with TickerProviderStateMixin {

@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: widget.backgroundColor,
image: widget.backgroundImage,
),
child: Stack(
children: _buildPaints(),
return RepaintBoundary(
child: Container(
decoration: BoxDecoration(
color: widget.backgroundColor,
image: widget.backgroundImage,
),
child: Stack(
children: _buildPaints(),
),
),
);
}

bool _listEquals<T>(List<T> first, List<T> second) {
if (first.length != second.length) return false;
for (int i = 0; i < first.length; i++) {
if (first[i] != second[i]) return false;
}
return true;
}
}

class _WaveLayerSpec {
Expand Down Expand Up @@ -490,6 +518,10 @@ class Layer {
}

class _CustomWavePainter extends CustomPainter {
static const int _minWaveSamples = 96;
static const int _samplesPerFrequencyUnit = 48;
static const double _targetWaveSampleWidth = 4.0;

final Color? color;
final List<Color>? gradient;
final Alignment? gradientBegin;
Expand All @@ -501,6 +533,10 @@ class _CustomWavePainter extends CustomPainter {
final double heightPercentage;

final Paint _paint = Paint();
final Path _path = Path();
Shader? _cachedGradientShader;
Size? _cachedGradientSize;
double? _cachedGradientCenterY;

_CustomWavePainter({
this.color,
Expand All @@ -515,49 +551,38 @@ class _CustomWavePainter extends CustomPainter {
Listenable? repaint,
}) : super(repaint: repaint);

void _setPaths(double viewCenterY, Size size, Canvas canvas) {
final layer = Layer(
path: Path(),
color: color,
gradient: gradient,
blur: blur,
amplitude: -0.8 * waveAmplitude,
phase: wavePhaseValue.value * 2 + 30,
);

layer.path!.reset();
layer.path!.moveTo(
void _paintWave(double viewCenterY, Size size, Canvas canvas) {
final amplitude = -0.8 * waveAmplitude;
final phase = wavePhaseValue.value * 2 + 30;
final path = _path..reset();
path.moveTo(
0.0,
viewCenterY +
layer.amplitude! * _getSinY(layer.phase!, waveFrequency, -1, size),
viewCenterY + amplitude * _getSinY(phase, -1, size),
);
for (int i = 1; i < size.width + 1; i++) {
layer.path!.lineTo(
i.toDouble(),
viewCenterY +
layer.amplitude! * _getSinY(layer.phase!, waveFrequency, i, size),

final sampleCount = _sampleCountForWidth(size.width);
final sampleWidth = size.width / sampleCount;
for (int i = 1; i <= sampleCount; i++) {
final x = i == sampleCount ? size.width : sampleWidth * i;
path.lineTo(
x,
viewCenterY + amplitude * _getSinY(phase, x, size),
);
}

layer.path!.lineTo(size.width, size.height);
layer.path!.lineTo(0.0, size.height);
layer.path!.close();
if (layer.color != null) {
_paint.color = layer.color!;
path.lineTo(size.width, size.height);
path.lineTo(0.0, size.height);
path.close();

if (color != null) {
_paint.color = color!;
_paint.shader = null;
} else if (gradient != null) {
_paint.shader = _gradientShader(size, viewCenterY);
}
if (layer.gradient != null) {
final rect = Offset.zero &
Size(size.width, size.height - viewCenterY * heightPercentage);
_paint.shader = LinearGradient(
begin: gradientBegin ?? Alignment.bottomCenter,
end: gradientEnd ?? Alignment.topCenter,
colors: layer.gradient!,
).createShader(rect);
}
_paint.maskFilter = layer.blur;
_paint.maskFilter = blur;
_paint.style = PaintingStyle.fill;
canvas.drawPath(layer.path!, _paint);
canvas.drawPath(path, _paint);
}

@override
Expand All @@ -566,7 +591,7 @@ class _CustomWavePainter extends CustomPainter {
return;
}
final viewCenterY = size.height * (heightPercentage + 0.1);
_setPaths(viewCenterY, size, canvas);
_paintWave(viewCenterY, size, canvas);
}

@override
Expand All @@ -577,14 +602,14 @@ class _CustomWavePainter extends CustomPainter {
oldDelegate.gradientEnd != gradientEnd ||
oldDelegate.blur != blur ||
oldDelegate.waveAmplitude != waveAmplitude ||
oldDelegate.wavePhaseValue != wavePhaseValue ||
oldDelegate.waveFrequency != waveFrequency ||
oldDelegate.heightPercentage != heightPercentage;
}

double _getSinY(
double startRadius,
double waveFrequency,
int currentPosition,
double currentPosition,
Size size,
) {
final scale = pi / size.width;
Expand All @@ -595,6 +620,35 @@ class _CustomWavePainter extends CustomPainter {
);
}

int _sampleCountForWidth(double width) {
final maxSamples = max(1, width.ceil());
final widthSamples = (width / _targetWaveSampleWidth).ceil();
final frequencySamples =
(waveFrequency.abs() * _samplesPerFrequencyUnit).ceil();
final requestedSamples =
max(_minWaveSamples, max(widthSamples, frequencySamples));
return min(maxSamples, requestedSamples);
}

Shader _gradientShader(Size size, double viewCenterY) {
if (_cachedGradientShader != null &&
_cachedGradientSize == size &&
_cachedGradientCenterY == viewCenterY) {
return _cachedGradientShader!;
}

final shaderHeight = max(0.0, size.height - viewCenterY * heightPercentage);
final rect = Offset.zero & Size(size.width, shaderHeight);
_cachedGradientShader = LinearGradient(
begin: gradientBegin ?? Alignment.bottomCenter,
end: gradientEnd ?? Alignment.topCenter,
colors: gradient!,
).createShader(rect);
_cachedGradientSize = size;
_cachedGradientCenterY = viewCenterY;
return _cachedGradientShader!;
}

bool _listEquals<T>(List<T>? first, List<T>? second) {
if (first == null) return second == null;
if (second == null || first.length != second.length) return false;
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: wave
description: Widget for displaying waves with custom color, duration, floating and blur effects.
version: 0.2.4
version: 0.2.5
homepage: https://github.com/glorylab/wave

environment:
Expand Down
56 changes: 54 additions & 2 deletions test/wave_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,52 @@ void main() {

expect(tester.takeException(), isNull);
});

testWidgets('renders issue 53 long-duration high-amplitude waves',
(WidgetTester tester) async {
await tester.pumpWidget(getWaveWidget(
config: issue53Config(),
waveAmplitude: 180,
waveFrequency: 1,
));

for (int i = 0; i < 10; i++) {
await tester.pump(const Duration(milliseconds: 16));
}

expect(tester.takeException(), isNull);
});

testWidgets('updates wave amplitude at runtime',
(WidgetTester tester) async {
await tester.pumpWidget(getWaveWidget(
config: SingleConfig(),
waveAmplitude: 8,
));
await tester.pump(const Duration(milliseconds: 16));

await tester.pumpWidget(getWaveWidget(
config: SingleConfig(),
waveAmplitude: 24,
));
await tester.pump(const Duration(milliseconds: 16));

expect(tester.takeException(), isNull);
});
});
}

Widget getWaveWidget({
Config? config,
int? duration,
bool isLoop = true,
double waveAmplitude = 5.0,
double waveFrequency = 1.6,
}) {
return MaterialApp(
home: Container(
home: SizedBox(
width: 1440,
height: 300,
child: WaveWidget(
backgroundColor: Colors.white,
config: config ??
Expand All @@ -102,8 +138,24 @@ Widget getWaveWidget({
double.infinity,
double.infinity,
),
waveAmplitude: 5.0,
waveAmplitude: waveAmplitude,
waveFrequency: waveFrequency,
),
),
);
}

Config issue53Config() {
return CustomConfig(
gradients: [
[Colors.white, Colors.white, Colors.white],
const [Color(0xFF3EA894), Color(0xFF00BAB9), Color(0xFF42B58D)],
const [Color(0xFFBEFED2), Color(0xFF39DBB1), Color(0xFF00CDA3)],
const [Color(0xFF3EA894), Color(0xFF00BAB9), Color(0xFF42B58D)],
],
durations: [43000, 43000, 45000, 45000],
heightPercentages: [0.55, 0.552, 0.90, 0.91],
gradientBegin: Alignment.centerRight,
gradientEnd: Alignment.centerLeft,
);
}
Loading