Skip to content

Commit 2f52d18

Browse files
committed
feat: add interactive rings shader and implement responsive UI components
1 parent 0a21aff commit 2f52d18

File tree

8 files changed

+562
-97
lines changed

8 files changed

+562
-97
lines changed

lib/main.dart

Lines changed: 20 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import 'package:flutter/material.dart';
33
import 'package:flutter_web_plugins/url_strategy.dart';
44
import 'package:go_router/go_router.dart';
55
import 'package:shadcn_ui/shadcn_ui.dart';
6+
import 'package:shaders/widgets/widgets.dart';
67

78
import 'shader_builder.dart';
89
import 'crt_shader_builder.dart';
910
import 'noise_overlay_shader_builder.dart';
1011
import 'noise_shader_builder.dart';
1112
import 'ntsc_shader_builder.dart';
13+
import 'rings_shader_builder.dart';
1214
import 'shader_screen.dart';
1315

1416
void main() {
@@ -88,6 +90,16 @@ final shaders = [
8890
dateAdded: DateTime(2025, 7, 28),
8991
builder: const NoiseOverlayShaderBuilder(),
9092
),
93+
ShaderInfo(
94+
name: 'Interactive Rings',
95+
assetKey: 'shaders/rings_shader.frag',
96+
description: 'Animated rings that respond to touch and mouse interaction. Tap and move around quickly.',
97+
sourceUrl: 'https://www.shadertoy.com/view/Xtj3DW',
98+
author: 'Pol Jeremias',
99+
dateAdded: DateTime(2025, 7, 29),
100+
builder: const RingsShaderBuilder(),
101+
path: 'rings-shader',
102+
),
91103
];
92104

93105
// Helper function to create URL-safe shader names
@@ -195,11 +207,14 @@ class HomeScreen extends StatelessWidget {
195207
@override
196208
Widget build(BuildContext context) {
197209
return Scaffold(
198-
body: Column(
199-
children: [
200-
const TopMenu(),
201-
Expanded(child: ContentGrid()),
202-
],
210+
body: SafeArea(
211+
bottom: false,
212+
child: Column(
213+
children: [
214+
const TopMenu(),
215+
Expanded(child: ContentGrid()),
216+
],
217+
),
203218
),
204219
);
205220
}
@@ -248,42 +263,6 @@ class ContentGrid extends StatelessWidget {
248263
}
249264
}
250265

251-
class TopMenu extends StatelessWidget {
252-
const TopMenu({super.key});
253-
254-
@override
255-
Widget build(BuildContext context) {
256-
return Container(
257-
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
258-
decoration: BoxDecoration(
259-
border: Border(
260-
bottom: BorderSide(
261-
color: ShadTheme.of(context).colorScheme.border,
262-
width: 1,
263-
),
264-
),
265-
),
266-
child: Row(
267-
children: [
268-
// Home button
269-
ShadButton(
270-
onPressed: () => context.go('/'),
271-
child: const Row(
272-
mainAxisSize: MainAxisSize.min,
273-
children: [
274-
Icon(Icons.home, size: 20),
275-
SizedBox(width: 8),
276-
Text('Shader Gallery'),
277-
],
278-
),
279-
),
280-
const Spacer(),
281-
],
282-
),
283-
);
284-
}
285-
}
286-
287266
class _ShaderCard extends StatelessWidget {
288267
const _ShaderCard({required this.shaderInfo, required this.onTap});
289268

lib/rings_shader_builder.dart

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import 'package:flutter/material.dart';
2+
import 'dart:ui';
3+
import 'shader_builder.dart';
4+
5+
class RingsShaderBuilder extends CustomShaderBuilder {
6+
const RingsShaderBuilder();
7+
8+
@override
9+
bool get requiresImageSampler => false;
10+
11+
@override
12+
Duration? get animationDuration => null; // Unbounded animation
13+
14+
@override
15+
void setUniforms(FragmentShader shader, Size size, double time) {
16+
shader
17+
..setFloat(0, size.width)
18+
..setFloat(1, size.height)
19+
..setFloat(2, time)
20+
..setFloat(3, 0.5);
21+
// ..setFloat(4, size.width * 0.5) // Default interaction point X (center)
22+
// ..setFloat(5, size.height * 0.5); // Default interaction point Y (center)
23+
}
24+
25+
@override
26+
Widget buildShader(
27+
ShaderMetadata metadata,
28+
FragmentShader shader,
29+
Size size,
30+
double time,
31+
Widget? child,
32+
) {
33+
return _InteractiveRingsWidget(shader: shader);
34+
}
35+
36+
@override
37+
Widget? childBuilder(BuildContext context) {
38+
return null;
39+
}
40+
}
41+
42+
class _InteractiveRingsWidget extends StatefulWidget {
43+
final FragmentShader shader;
44+
45+
const _InteractiveRingsWidget({required this.shader});
46+
47+
@override
48+
State<_InteractiveRingsWidget> createState() => _InteractiveRingsWidgetState();
49+
}
50+
51+
class _InteractiveRingsWidgetState extends State<_InteractiveRingsWidget> {
52+
double _mouseX = 0.0;
53+
double _mouseY = 0.0;
54+
double _interactionStrength = 2.0;
55+
56+
void _updateInteraction(double x, double y, double strength) {
57+
setState(() {
58+
_mouseX = x;
59+
_mouseY = y;
60+
_interactionStrength = strength;
61+
});
62+
widget.shader
63+
..setFloat(3, strength)
64+
..setFloat(4, x)
65+
..setFloat(5, y);
66+
}
67+
68+
@override
69+
Widget build(BuildContext context) {
70+
return Stack(
71+
children: [
72+
GestureDetector(
73+
onTapDown: (details) {
74+
_updateInteraction(details.localPosition.dx, details.localPosition.dy, 1.0);
75+
},
76+
onTapUp: (_) {
77+
_updateInteraction(_mouseX, _mouseY, 0.5);
78+
},
79+
onPanUpdate: (details) {
80+
_updateInteraction(details.localPosition.dx, details.localPosition.dy, 0.8);
81+
},
82+
onPanEnd: (_) {
83+
_updateInteraction(_mouseX, _mouseY, 0.5);
84+
},
85+
child: CustomPaint(
86+
size: Size.infinite,
87+
painter: _RingsShaderPainter(widget.shader),
88+
),
89+
),
90+
// Debug text overlay
91+
// Positioned(
92+
// left: 16,
93+
// bottom: 16,
94+
// child: Container(
95+
// padding: const EdgeInsets.all(8),
96+
// decoration: BoxDecoration(
97+
// color: Colors.black54,
98+
// borderRadius: BorderRadius.circular(4),
99+
// ),
100+
// child: Text(
101+
// 'Mouse: (${_mouseX.toStringAsFixed(1)}, ${_mouseY.toStringAsFixed(1)})\n'
102+
// 'Strength: ${_interactionStrength.toStringAsFixed(2)}',
103+
// style: const TextStyle(
104+
// color: Colors.white,
105+
// fontSize: 12,
106+
// fontFamily: 'monospace',
107+
// ),
108+
// ),
109+
// ),
110+
// ),
111+
],
112+
);
113+
}
114+
}
115+
116+
class _RingsShaderPainter extends CustomPainter {
117+
final FragmentShader shader;
118+
119+
_RingsShaderPainter(this.shader);
120+
121+
@override
122+
void paint(Canvas canvas, Size size) {
123+
final paint = Paint()..shader = shader;
124+
canvas.drawRect(Offset.zero & size, paint);
125+
}
126+
127+
@override
128+
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
129+
}

lib/shader_screen.dart

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:shadcn_ui/shadcn_ui.dart';
33
import 'package:go_router/go_router.dart';
4+
import 'package:shaders/widgets/details_top_menu.dart';
45
import 'package:url_launcher/url_launcher.dart';
56
import 'widgets/widgets.dart';
67
import 'main.dart';
@@ -18,13 +19,16 @@ class ShaderScreen extends StatelessWidget {
1819
@override
1920
Widget build(BuildContext context) {
2021
return Scaffold(
21-
body: Column(
22-
children: [
23-
// Top menu (same as home screen)
24-
_buildTopMenu(context),
25-
// Main content
26-
Expanded(child: _buildMainContent(context)),
27-
],
22+
body: SafeArea(
23+
bottom: false,
24+
child: Column(
25+
children: [
26+
// Top menu (same as home screen)
27+
_buildTopMenu(context),
28+
// Main content
29+
Expanded(child: _buildMainContent(context)),
30+
],
31+
),
2832
),
2933
);
3034
}
@@ -40,51 +44,7 @@ class ShaderScreen extends StatelessWidget {
4044
),
4145
),
4246
),
43-
child: Row(
44-
children: [
45-
// Back to home button
46-
ShadButton(
47-
onPressed: () => context.go('/'),
48-
child: const Row(
49-
mainAxisSize: MainAxisSize.min,
50-
children: [
51-
Icon(Icons.arrow_back, size: 20),
52-
SizedBox(width: 8),
53-
Text('Back to Gallery'),
54-
],
55-
),
56-
),
57-
const SizedBox(width: 16),
58-
// Shader title
59-
Expanded(
60-
child: Text(
61-
shaderInfo.name,
62-
style: ShadTheme.of(context).textTheme.h3,
63-
),
64-
),
65-
// Actions
66-
Row(
67-
children: [
68-
ShadButton.outline(
69-
onPressed: () async {
70-
final url = Uri.parse(shaderInfo.sourceUrl);
71-
if (await canLaunchUrl(url)) {
72-
await launchUrl(url);
73-
}
74-
},
75-
child: const Row(
76-
mainAxisSize: MainAxisSize.min,
77-
children: [
78-
Icon(Icons.link, size: 16),
79-
SizedBox(width: 8),
80-
Text('View Source'),
81-
],
82-
),
83-
),
84-
],
85-
),
86-
],
87-
),
47+
child: DetailsTopMenu(shaderInfo: shaderInfo),
8848
);
8949
}
9050

@@ -108,17 +68,26 @@ class ShaderScreen extends StatelessWidget {
10868
],
10969
);
11070
} else {
111-
return Column(
71+
return ListView(
11272
children: [
113-
// Shader view (top half)
114-
Expanded(child: ShaderAnimationView(shaderInfo: shaderInfo)),
73+
ConstrainedBox(
74+
constraints: BoxConstraints(
75+
maxHeight: constraints.maxHeight * 0.6,
76+
),
77+
child: ShaderAnimationView(shaderInfo: shaderInfo),
78+
),
11579
// Horizontal divider
11680
Container(
11781
height: 1,
11882
color: ShadTheme.of(context).colorScheme.border,
11983
),
12084
// Source code view (bottom half)
121-
Expanded(child: RightColumn(shaderInfo: shaderInfo)),
85+
ConstrainedBox(
86+
constraints: BoxConstraints(
87+
maxHeight: 1200,
88+
),
89+
child: RightColumn(shaderInfo: shaderInfo),
90+
),
12291
],
12392
);
12493
}

0 commit comments

Comments
 (0)