Files
TPSchedule/mobile/test/widgets/glass_material_test.dart
T

271 lines
8.4 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/widgets/glass_dock.dart';
import 'package:hepan_everyday/widgets/glass_refraction.dart';
void main() {
testWidgets(
'glass softens fine background detail without whitening its colour',
(tester) async {
final pixels = await _render(
tester,
const GlassRefraction(strength: 0.32, child: SizedBox.expand()),
stripes: true,
);
final reds = [for (var x = 110; x < 150; x++) _channel(pixels, x, 34, 0)];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
expect(contrast, lessThan(25));
expect(_channel(pixels, 132, 34, 0), closeTo(130, 15));
expect(_channel(pixels, 132, 34, 1), closeTo(120, 15));
expect(_channel(pixels, 132, 34, 2), closeTo(140, 15));
expect(_channel(pixels, 132, 34, 3), 255);
},
);
testWidgets(
'Dock lens preserves the backdrop colour through its neutral tint',
(tester) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
);
// Below the symbol and away from the rim, the selected lens should retain
// most of this saturated cyan backdrop instead of painting a grey pill.
final red = _channel(pixels, 46, 52, 0);
final green = _channel(pixels, 46, 52, 1);
final blue = _channel(pixels, 46, 52, 2);
expect(red, lessThan(50));
expect(green - red, greaterThan(130));
expect(blue - red, greaterThan(165));
},
);
testWidgets('Dock base and selected lens both soften the live backdrop', (
tester,
) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
stripes: true,
preview: true,
);
for (final start in [32, 160]) {
final reds = [
for (var x = start; x < start + 20; x++) _channel(pixels, x, 52, 0),
];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
expect(contrast, lessThan(30), reason: 'background at x=$start');
}
});
testWidgets(
'Dock keeps larger backdrop details recognizable through its glass',
(tester) async {
final pixels = await _render(
tester,
GlassDock(selectedIndex: 0, onSelected: (_) {}),
stripes: true,
stripeWidth: 12,
preview: true,
);
for (final start in [24, 160]) {
final reds = [
for (var x = start; x < start + 40; x++) _channel(pixels, x, 52, 0),
];
final contrast =
reds.reduce((a, b) => a > b ? a : b) -
reds.reduce((a, b) => a < b ? a : b);
// The original stripes have 180 levels of contrast. Both the base and
// selected lens retain over half, keeping these larger shapes readable.
expect(contrast, greaterThan(90), reason: 'background at x=$start');
}
},
);
testWidgets(
'composed diffusion keeps edge refraction aligned when glass moves',
(tester) async {
final plain = await _render(
tester,
const GlassRefraction(strength: 0, child: SizedBox.expand()),
gradient: true,
);
final refracted = await _render(
tester,
const GlassRefraction(strength: 1, child: SizedBox.expand()),
gradient: true,
);
final translated = await _render(
tester,
const GlassRefraction(strength: 1, child: SizedBox.expand()),
gradient: true,
offset: const Offset(23, -37),
);
expect(
(_channel(refracted, 15, 34, 0) - _channel(plain, 15, 34, 0)).abs(),
greaterThan(3),
);
expect(
_channel(refracted, 132, 34, 0),
closeTo(_channel(plain, 132, 34, 0), 2),
);
for (final x in [15, 132, 248]) {
expect(
_channel(translated, x, 34, 0),
closeTo(_channel(refracted, x, 34, 0), 2),
);
}
},
skip: !ui.ImageFilter.isShaderFilterSupported,
);
}
int _channel(ByteData pixels, int x, int y, int channel) =>
pixels.getUint8((y * 264 + x) * 4 + channel);
Future<ByteData> _render(
WidgetTester tester,
Widget glass, {
bool stripes = false,
double stripeWidth = 2,
bool gradient = false,
bool preview = false,
Offset offset = Offset.zero,
}) async {
tester.view.physicalSize = const Size(800, 600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final key = GlobalKey(), glassKey = GlobalKey();
await tester.pumpWidget(
RepaintBoundary(
key: key,
child: MaterialApp(
home: Center(
child: Transform.translate(
offset: offset,
child: SizedBox(
key: glassKey,
width: 264,
height: 68,
child: Stack(
children: [
Positioned.fill(
child: CustomPaint(
painter: _Backdrop(stripes, gradient, stripeWidth),
),
),
Positioned.fill(child: glass),
],
),
),
),
),
),
),
);
if (ui.ImageFilter.isShaderFilterSupported) {
// Native shader loading completes outside the widget test's fake async zone.
await tester.runAsync(
() => ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag'),
);
}
await tester.pumpAndSettle();
if (ui.ImageFilter.isShaderFilterSupported) {
expect(find.byType(BackdropFilter), findsNothing);
}
return (await tester.runAsync(() async {
final boundary =
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 1);
final data = (await image.toByteData(format: ui.ImageByteFormat.rawRgba))!;
final region = tester.getRect(find.byKey(glassKey));
if (preview && const bool.fromEnvironment('TP_BRAND_PREVIEW')) {
final recorder = ui.PictureRecorder();
ui.Canvas(recorder).drawImageRect(
image,
region,
const Rect.fromLTWH(0, 0, 792, 204),
Paint()..filterQuality = FilterQuality.high,
);
final picture = recorder.endRecording();
final previewImage = await picture.toImage(792, 204);
final png = (await previewImage.toByteData(
format: ui.ImageByteFormat.png,
))!;
final output = File('../dist/screenshots/glass-frosted-dock.png');
await output.parent.create(recursive: true);
await output.writeAsBytes(png.buffer.asUint8List());
previewImage.dispose();
picture.dispose();
}
final cropped = ByteData(264 * 68 * 4);
for (var y = 0; y < 68; y++) {
for (var x = 0; x < 264; x++) {
final source =
((region.top.round() + y) * image.width + region.left.round() + x) *
4;
for (var channel = 0; channel < 4; channel++) {
cropped.setUint8(
(y * 264 + x) * 4 + channel,
data.getUint8(source + channel),
);
}
}
}
image.dispose();
return cropped;
}))!;
}
class _Backdrop extends CustomPainter {
const _Backdrop(this.stripes, this.gradient, this.stripeWidth);
final bool stripes;
final bool gradient;
final double stripeWidth;
@override
void paint(Canvas canvas, Size size) {
if (gradient) {
for (var x = 0; x < size.width; x++) {
canvas.drawRect(
Rect.fromLTWH(x.toDouble(), 0, 1, size.height),
Paint()..color = Color.fromARGB(255, 40 + (x * 0.7).round(), 70, 90),
);
}
return;
}
if (!stripes) {
canvas.drawRect(
Offset.zero & size,
Paint()..color = const Color(0xFF14B4E6),
);
return;
}
for (var x = 0.0; x < size.width; x += stripeWidth) {
canvas.drawRect(
Rect.fromLTWH(x, 0, stripeWidth, size.height),
Paint()
..color = (x / stripeWidth).round().isEven
? const Color(0xFFDC3C3C)
: const Color(0xFF28B4DC),
);
}
}
@override
bool shouldRepaint(_Backdrop oldDelegate) =>
oldDelegate.stripes != stripes ||
oldDelegate.gradient != gradient ||
oldDelegate.stripeWidth != stripeWidth;
}