345 lines
11 KiB
Dart
345 lines
11 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(
|
|
'Dock has a milky white body while transmitting a dark backdrop',
|
|
(tester) async {
|
|
final pixels = await _render(
|
|
tester,
|
|
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
|
backdropColor: const Color(0xFF162536),
|
|
preview: true,
|
|
previewName: 'liquid-glass-milky-dark.png',
|
|
);
|
|
// Away from icons and the bevel: white glass lifts the dark background,
|
|
// while its original cool colour remains visible through the material.
|
|
final red = _channel(pixels, 160, 43, 0);
|
|
final green = _channel(pixels, 160, 43, 1);
|
|
expect(red, greaterThan(75));
|
|
expect(red, lessThan(115));
|
|
expect(green - red, closeTo(13, 6));
|
|
},
|
|
);
|
|
testWidgets('resting cursor is neutral grey and clears while held', (
|
|
tester,
|
|
) async {
|
|
final resting = await _render(
|
|
tester,
|
|
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
|
backdropColor: Colors.white,
|
|
preview: true,
|
|
);
|
|
// Sample beside the visible label, away from icon glow and glass rims.
|
|
final selected = _channel(resting, 16, 43, 0);
|
|
final base = _channel(resting, 160, 43, 0);
|
|
// On a white page the white glass must not turn into a grey veil.
|
|
expect(base, greaterThanOrEqualTo(240));
|
|
expect(base - selected, greaterThan(20));
|
|
expect((_channel(resting, 16, 43, 2) - selected).abs(), lessThan(12));
|
|
final held = await _render(
|
|
tester,
|
|
GlassDock(selectedIndex: 0, onSelected: (_) {}),
|
|
backdropColor: Colors.white,
|
|
held: true,
|
|
preview: true,
|
|
);
|
|
expect(_channel(held, 16, 43, 0) - selected, greaterThan(18));
|
|
});
|
|
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
|
|
// this saturated cyan backdrop while applying the new resting grey tint.
|
|
final red = _channel(pixels, 16, 40, 0);
|
|
final green = _channel(pixels, 16, 40, 1);
|
|
final blue = _channel(pixels, 16, 40, 2);
|
|
expect(red, lessThan(125));
|
|
expect(green - red, greaterThanOrEqualTo(65));
|
|
// The thicker milky base still transmits a clear cyan colour difference.
|
|
expect(blue - red, greaterThan(90));
|
|
},
|
|
);
|
|
|
|
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, 50, 0),
|
|
];
|
|
final contrast =
|
|
reds.reduce((a, b) => a > b ? a : b) -
|
|
reds.reduce((a, b) => a < b ? a : b);
|
|
// Fine stripes start at 180 contrast levels; at least two thirds softens,
|
|
// while the stronger curved rim can enlarge individual stripe details.
|
|
expect(contrast, lessThan(60), 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, 50, 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 at least 40%, keeping larger shapes readable.
|
|
expect(contrast, greaterThan(72), 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,
|
|
String? previewName,
|
|
Offset offset = Offset.zero,
|
|
Color backdropColor = const Color(0xFF14B4E6),
|
|
bool held = false,
|
|
}) 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();
|
|
final height = glass is GlassDock ? 58 : 68;
|
|
await tester.pumpWidget(
|
|
RepaintBoundary(
|
|
key: key,
|
|
child: MaterialApp(
|
|
home: Center(
|
|
child: Transform.translate(
|
|
offset: offset,
|
|
child: SizedBox(
|
|
key: glassKey,
|
|
width: 264,
|
|
height: height.toDouble(),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _Backdrop(
|
|
stripes,
|
|
gradient,
|
|
stripeWidth,
|
|
backdropColor,
|
|
),
|
|
),
|
|
),
|
|
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();
|
|
final gesture = held
|
|
? await tester.startGesture(
|
|
tester.getCenter(find.byKey(const Key('dock-cursor'))),
|
|
)
|
|
: null;
|
|
if (held) {
|
|
await tester.pump(const Duration(milliseconds: 110));
|
|
await tester.pump(const Duration(milliseconds: 200));
|
|
}
|
|
if (ui.ImageFilter.isShaderFilterSupported) {
|
|
expect(find.byType(BackdropFilter), findsNothing);
|
|
}
|
|
final result = (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,
|
|
Rect.fromLTWH(0, 0, 792, height * 3.0),
|
|
Paint()..filterQuality = FilterQuality.high,
|
|
);
|
|
final picture = recorder.endRecording();
|
|
final previewImage = await picture.toImage(792, height * 3);
|
|
final png = (await previewImage.toByteData(
|
|
format: ui.ImageByteFormat.png,
|
|
))!;
|
|
final name =
|
|
previewName ??
|
|
(backdropColor == Colors.white
|
|
? held
|
|
? 'liquid-glass-held.png'
|
|
: 'liquid-glass-rest.png'
|
|
: 'liquid-glass-detail.png');
|
|
final output = File('../dist/screenshots/$name');
|
|
await output.parent.create(recursive: true);
|
|
await output.writeAsBytes(png.buffer.asUint8List());
|
|
previewImage.dispose();
|
|
picture.dispose();
|
|
}
|
|
final cropped = ByteData(264 * height * 4);
|
|
for (var y = 0; y < height; 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;
|
|
}))!;
|
|
await gesture?.cancel();
|
|
await tester.pumpAndSettle();
|
|
return result;
|
|
}
|
|
|
|
class _Backdrop extends CustomPainter {
|
|
const _Backdrop(this.stripes, this.gradient, this.stripeWidth, this.colour);
|
|
final bool stripes;
|
|
final bool gradient;
|
|
final double stripeWidth;
|
|
final Color colour;
|
|
|
|
@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 = colour);
|
|
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 ||
|
|
oldDelegate.colour != colour;
|
|
}
|