Skip to content

Commit d6f7773

Browse files
committed
feat: add fullscreen shader widget and integration tests for shader screenshots
1 parent 5bd413e commit d6f7773

File tree

15 files changed

+307
-34
lines changed

15 files changed

+307
-34
lines changed

.vscode/launch.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
"name": "shaders",
99
"request": "launch",
1010
"type": "dart",
11+
"program": "lib/main.dart",
1112
"preLaunchTask": "Copy Shaders to Assets"
1213
},
1314
{
1415
"name": "shaders (profile mode)",
1516
"request": "launch",
17+
"program": "lib/main.dart",
1618
"type": "dart",
1719
"flutterMode": "profile",
1820
"preLaunchTask": "Copy Shaders to Assets"
1921
},
2022
{
2123
"name": "shaders (release mode)",
24+
"program": "lib/main.dart",
2225
"request": "launch",
2326
"type": "dart",
2427
"flutterMode": "release",
738 KB
Loading

assets/screenshots/crt-shader.png

211 KB
Loading
724 KB
Loading
1.07 MB
Loading

assets/screenshots/ntsc-shader.png

816 KB
Loading
191 KB
Loading
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:integration_test/integration_test.dart';
3+
import 'package:shaders/fullscreen_shader_widget.dart';
4+
import 'package:shaders/main.dart';
5+
import 'package:flutter/material.dart';
6+
7+
void main() async {
8+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
9+
10+
group('Shader Screenshot Tests', () {
11+
for (final shaderInfo in shaders) {
12+
testWidgets('Screenshot for ${shaderInfo.name}', (WidgetTester tester) async {
13+
// Build the fullscreen shader widget
14+
// set tester size to 16:9
15+
16+
await tester.pumpWidget(
17+
MaterialApp(
18+
debugShowCheckedModeBanner: false,
19+
home: Scaffold(
20+
body: AspectRatio(
21+
aspectRatio: 16 / 9,
22+
child: FullscreenShaderWidget(
23+
shaderInfo: shaderInfo,
24+
),
25+
),
26+
),
27+
),
28+
);
29+
await binding.convertFlutterSurfaceToImage();
30+
31+
// Wait for the shader to initialize and animate a bit
32+
await tester.pump(const Duration(milliseconds: 1000));
33+
await tester.pump(const Duration(milliseconds: 1000));
34+
35+
// Take a screenshot
36+
await tester.binding.delayed(const Duration(milliseconds: 100));
37+
38+
// Save the screenshot with a specific filename
39+
final screenshotData = await binding.takeScreenshot(shaderInfo.path);
40+
41+
print('Screenshot taken for ${shaderInfo.name} of size: ${screenshotData.length} bytes');
42+
});
43+
}
44+
});
45+
}

lib/fullscreen_shader_widget.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_shaders/flutter_shaders.dart';
3+
import 'main.dart';
4+
5+
/// A widget that displays a shader in full screen for screenshot purposes
6+
class FullscreenShaderWidget extends StatefulWidget {
7+
final ShaderInfo shaderInfo;
8+
9+
const FullscreenShaderWidget({
10+
super.key,
11+
required this.shaderInfo,
12+
});
13+
14+
@override
15+
State<FullscreenShaderWidget> createState() => _FullscreenShaderWidgetState();
16+
}
17+
18+
class _FullscreenShaderWidgetState extends State<FullscreenShaderWidget> with SingleTickerProviderStateMixin {
19+
late final AnimationController _controller;
20+
21+
@override
22+
void initState() {
23+
super.initState();
24+
final duration = widget.shaderInfo.builder.animationDuration;
25+
26+
if (duration != null) {
27+
// Bounded animation
28+
_controller = AnimationController(vsync: this, duration: duration);
29+
_controller.forward();
30+
} else {
31+
// Unbounded animation - use a long duration
32+
_controller = AnimationController(
33+
vsync: this,
34+
duration: const Duration(seconds: 3600),
35+
);
36+
_controller.forward();
37+
}
38+
}
39+
40+
@override
41+
void dispose() {
42+
_controller.dispose();
43+
super.dispose();
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return LayoutBuilder(
49+
builder: (context, constraints) {
50+
return AnimatedBuilder(
51+
animation: _controller,
52+
builder: (context, child) {
53+
return ShaderBuilder(
54+
assetKey: widget.shaderInfo.assetKey,
55+
(context, shader, _) {
56+
final duration = widget.shaderInfo.builder.animationDuration;
57+
double timeValue;
58+
59+
if (duration != null) {
60+
// Bounded animation - use a simple normalized value
61+
timeValue = _controller.value;
62+
} else {
63+
// Unbounded animation - use controller value as continuous time
64+
timeValue = _controller.value * _controller.duration!.inSeconds;
65+
}
66+
67+
widget.shaderInfo.builder.setUniforms(shader, constraints.biggest, timeValue);
68+
69+
return SizedBox.expand(
70+
child: widget.shaderInfo.builder.buildShader(
71+
widget.shaderInfo.metadata,
72+
shader,
73+
constraints.biggest,
74+
timeValue,
75+
widget.shaderInfo.builder.childBuilder(context),
76+
),
77+
);
78+
},
79+
);
80+
},
81+
);
82+
},
83+
);
84+
}
85+
}

lib/main.dart

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_web_plugins/url_strategy.dart';
44
import 'package:go_router/go_router.dart';
55
import 'package:google_fonts/google_fonts.dart';
66
import 'package:shadcn_ui/shadcn_ui.dart';
7+
import 'package:shaders/widgets/shader_card.dart';
78
import 'package:shaders/widgets/widgets.dart';
89

910
import 'shader_builder.dart';
@@ -262,12 +263,12 @@ class ContentGrid extends StatelessWidget {
262263
crossAxisCount: crossAxisCount,
263264
crossAxisSpacing: 16,
264265
mainAxisSpacing: 16,
265-
childAspectRatio: 4 / 3,
266+
childAspectRatio: 1,
266267
),
267268
itemCount: shaders.length,
268269
itemBuilder: (context, index) {
269270
final shaderInfo = shaders[index];
270-
return _ShaderCard(
271+
return ShaderCard(
271272
shaderInfo: shaderInfo,
272273
onTap: () {
273274
context.go('/shader/${shaderInfo.path}');
@@ -280,37 +281,6 @@ class ContentGrid extends StatelessWidget {
280281
}
281282
}
282283

283-
class _ShaderCard extends StatelessWidget {
284-
const _ShaderCard({required this.shaderInfo, required this.onTap});
285-
286-
final ShaderInfo shaderInfo;
287-
final VoidCallback onTap;
288-
289-
@override
290-
Widget build(BuildContext context) {
291-
return InkWell(
292-
onTap: onTap,
293-
borderRadius: BorderRadius.circular(8),
294-
child: ShadCard(
295-
title: Text(shaderInfo.name),
296-
footer: Text.rich(
297-
TextSpan(
298-
children: [
299-
TextSpan(text: 'by ${shaderInfo.author}'),
300-
],
301-
),
302-
style: ShadTheme.of(context).textTheme.small,
303-
),
304-
description: Text(
305-
shaderInfo.description,
306-
maxLines: 2,
307-
overflow: TextOverflow.ellipsis,
308-
),
309-
),
310-
);
311-
}
312-
}
313-
314284
extension on TargetPlatform {
315285
bool get isDesktop {
316286
return this == TargetPlatform.macOS || this == TargetPlatform.windows || this == TargetPlatform.linux;

0 commit comments

Comments
 (0)