Rebrand as TPSchedule and refine clear liquid glass dock
This commit is contained in:
@@ -86,12 +86,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final lens = find
|
||||
.descendant(
|
||||
of: find.byType(GlassDock),
|
||||
matching: find.byType(BackdropFilter),
|
||||
)
|
||||
.last;
|
||||
final lens = find.byType(GlassRefraction).last;
|
||||
final resting = tester.getSize(lens);
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.byKey(const Key('dock-cursor'))),
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
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;
|
||||
}
|
||||
@@ -58,4 +58,63 @@ void main() {
|
||||
sourcePicture.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'glass rim reflects nearby backdrop light outside its transmitted ray',
|
||||
() async {
|
||||
const width = 160, height = 100;
|
||||
final program = await ui.FragmentProgram.fromAsset(
|
||||
'shaders/liquid_glass.frag',
|
||||
);
|
||||
Future<ByteData> draw(bool brightEdge) async {
|
||||
final sourceRecorder = ui.PictureRecorder();
|
||||
final sourceCanvas = ui.Canvas(sourceRecorder);
|
||||
sourceCanvas.drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 160, 100),
|
||||
ui.Paint()..color = const ui.Color(0xFF28465A),
|
||||
);
|
||||
if (brightEdge) {
|
||||
sourceCanvas.drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 5, 100),
|
||||
ui.Paint()..color = const ui.Color(0xFF28E65A),
|
||||
);
|
||||
}
|
||||
final sourcePicture = sourceRecorder.endRecording();
|
||||
final source = await sourcePicture.toImage(width, height);
|
||||
final shader = program.fragmentShader()
|
||||
..setFloat(0, width.toDouble())
|
||||
..setFloat(1, height.toDouble())
|
||||
..setFloat(2, 0)
|
||||
..setFloat(3, 0)
|
||||
..setFloat(4, width.toDouble())
|
||||
..setFloat(5, height.toDouble())
|
||||
..setFloat(6, 1)
|
||||
..setImageSampler(0, source);
|
||||
final recorder = ui.PictureRecorder();
|
||||
ui.Canvas(recorder).drawRect(
|
||||
const ui.Rect.fromLTWH(0, 0, 160, 100),
|
||||
ui.Paint()..shader = shader,
|
||||
);
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(width, height);
|
||||
final data = (await image.toByteData())!;
|
||||
image.dispose();
|
||||
picture.dispose();
|
||||
shader.dispose();
|
||||
source.dispose();
|
||||
sourcePicture.dispose();
|
||||
return data;
|
||||
}
|
||||
|
||||
final plain = await draw(false), reflected = await draw(true);
|
||||
final rimGreen = (50 * width + 8) * 4 + 1;
|
||||
final centreGreen = (50 * width + 80) * 4 + 1;
|
||||
expect(
|
||||
reflected.getUint8(rimGreen) - plain.getUint8(rimGreen),
|
||||
greaterThan(2),
|
||||
);
|
||||
expect(reflected.getUint8(centreGreen), plain.getUint8(centreGreen));
|
||||
expect(reflected.getUint8(rimGreen + 2), 255);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ void main() {
|
||||
await tester.tap(find.bySemanticsLabel('个人设置'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('更新学期'), findsOneWidget);
|
||||
expect(find.text('RSchedule'), findsWidgets);
|
||||
expect(find.text('TPSchedule'), findsWidgets);
|
||||
c.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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/app/tp_launch_page.dart';
|
||||
import 'package:hepan_everyday/widgets/tp_logo.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('logo preserves calendar, bindings and smile from supplied art', (
|
||||
tester,
|
||||
) async {
|
||||
final boundaryKey = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
key: boundaryKey,
|
||||
child: const TpLogo(size: 200),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final pixels = await _capture(tester, boundaryKey, 'tpschedule-logo');
|
||||
expect(_pixel(pixels, 200, 40, 100), [255, 210, 42, 255]);
|
||||
expect(_pixel(pixels, 200, 73, 50), [255, 255, 255, 255]);
|
||||
expect(_pixel(pixels, 200, 100, 115), [255, 255, 255, 255]);
|
||||
final background = _pixel(pixels, 200, 10, 100);
|
||||
expect(background[2], closeTo(255, 3));
|
||||
expect(background[0], inInclusiveRange(42, 63));
|
||||
expect(background[1], inInclusiveRange(108, 209));
|
||||
});
|
||||
|
||||
testWidgets('foreground logo leaves its surround transparent', (
|
||||
tester,
|
||||
) async {
|
||||
final boundaryKey = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
key: boundaryKey,
|
||||
child: const TpLogo(size: 200, background: false),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final pixels = await _capture(tester, boundaryKey);
|
||||
expect(_pixel(pixels, 200, 10, 10)[3], 0);
|
||||
expect(_pixel(pixels, 200, 40, 100), [255, 210, 42, 255]);
|
||||
});
|
||||
|
||||
testWidgets('launch fills the screen with brand blue in dark mode', (
|
||||
tester,
|
||||
) async {
|
||||
final boundaryKey = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData.dark(),
|
||||
home: RepaintBoundary(key: boundaryKey, child: const TpLaunchPage()),
|
||||
),
|
||||
);
|
||||
expect(find.text('TPSchedule'), findsOneWidget);
|
||||
final logo = tester.widget<TpLogo>(find.byType(TpLogo));
|
||||
expect(logo.background, isFalse);
|
||||
final pixels = await _capture(tester, boundaryKey, 'tpschedule-launch');
|
||||
for (final point in [const Offset(10, 10), const Offset(790, 590)]) {
|
||||
final pixel = _pixel(pixels, 800, point.dx.toInt(), point.dy.toInt());
|
||||
expect(pixel[2], closeTo(255, 3));
|
||||
expect(pixel[0], inInclusiveRange(42, 63));
|
||||
expect(pixel[1], inInclusiveRange(108, 209));
|
||||
expect(pixel[3], 255);
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Uint8List> _capture(
|
||||
WidgetTester tester,
|
||||
GlobalKey key, [
|
||||
String? previewName,
|
||||
]) async {
|
||||
final boundary =
|
||||
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
return (await tester.runAsync(() async {
|
||||
final image = await boundary.toImage(pixelRatio: 1);
|
||||
final bytes = await image.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
if (const bool.fromEnvironment('TP_BRAND_PREVIEW') && previewName != null) {
|
||||
final png = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
final output = File('../dist/screenshots/$previewName.png');
|
||||
await output.parent.create(recursive: true);
|
||||
await output.writeAsBytes(png!.buffer.asUint8List());
|
||||
}
|
||||
image.dispose();
|
||||
return bytes!.buffer.asUint8List();
|
||||
}))!;
|
||||
}
|
||||
|
||||
List<int> _pixel(Uint8List pixels, int width, int x, int y) =>
|
||||
pixels.sublist((y * width + x) * 4, (y * width + x) * 4 + 4);
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/app/tp_startup.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'startup shows brand while initializing and immediately proceeds when ready',
|
||||
(tester) async {
|
||||
final ready = Completer<void>();
|
||||
await tester.pumpWidget(
|
||||
TpStartup(
|
||||
ready: ready.future,
|
||||
child: const MaterialApp(home: Scaffold(body: Text('已准备好课表'))),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('TPSchedule'), findsOneWidget);
|
||||
expect(find.text('已准备好课表'), findsNothing);
|
||||
ready.complete();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
expect(find.text('已准备好课表'), findsOneWidget);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('TPSchedule'), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user