Skip to content

Commit cfe8790

Browse files
committed
feat: add noise and noise overlay shaders with configurations
1 parent 528cdb7 commit cfe8790

10 files changed

+407
-90
lines changed

lib/main.dart

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import 'package:flutter/material.dart';
2-
import 'package:flutter_shaders/flutter_shaders.dart';
32
import 'package:url_launcher/url_launcher.dart';
43

54
import 'crt_shader_config.dart';
5+
import 'noise_overlay_shader_config.dart';
6+
import 'noise_shader_config.dart';
67
import 'ntsc_shader_config.dart';
78
import 'shader_configs.dart';
89
import 'shader_screen.dart';
@@ -43,6 +44,20 @@ final shaders = [
4344
sourceUrl: 'https://www.shadertoy.com/view/lt3yz7',
4445
config: CrtShaderConfig(),
4546
),
47+
const ShaderInfo(
48+
name: 'Noise',
49+
assetKey: 'shaders/noise_shader.frag',
50+
description: 'Animated gradient noise with film grain effect',
51+
sourceUrl: 'https://www.shadertoy.com/view/DdcfzH',
52+
config: NoiseShaderConfig(),
53+
),
54+
const ShaderInfo(
55+
name: 'Noise Overlay',
56+
assetKey: 'shaders/noise_overlay_shader.frag',
57+
description: 'Applies animated noise effect as an overlay on content',
58+
sourceUrl: 'https://www.shadertoy.com/view/DdcfzH',
59+
config: NoiseOverlayShaderConfig(),
60+
),
4661
];
4762

4863
class RgbGlitchDemo extends StatefulWidget {
@@ -69,35 +84,35 @@ class HomeScreen extends StatelessWidget {
6984
@override
7085
Widget build(BuildContext context) {
7186
return Scaffold(
72-
appBar: AppBar(
73-
title: const Text('Shader Gallery'),
74-
centerTitle: true,
87+
appBar: AppBar(title: const Text('Shader Gallery'), centerTitle: true),
88+
body: LayoutBuilder(
89+
builder: (context, constraints) {
90+
return GridView.builder(
91+
padding: const EdgeInsets.all(16),
92+
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
93+
maxCrossAxisExtent: 350,
94+
crossAxisSpacing: 16,
95+
mainAxisSpacing: 16,
96+
childAspectRatio: 4 / 3,
97+
),
98+
itemCount: shaders.length,
99+
itemBuilder: (context, index) {
100+
final shaderInfo = shaders[index];
101+
return _ShaderTile(
102+
shaderInfo: shaderInfo,
103+
onTap: () {
104+
Navigator.of(context).push(
105+
MaterialPageRoute(
106+
builder: (context) =>
107+
ShaderScreen(shaderInfo: shaderInfo),
108+
),
109+
);
110+
},
111+
);
112+
},
113+
);
114+
},
75115
),
76-
body: LayoutBuilder(builder: (context, constraints) {
77-
return GridView.builder(
78-
padding: const EdgeInsets.all(16),
79-
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
80-
maxCrossAxisExtent: 350,
81-
crossAxisSpacing: 16,
82-
mainAxisSpacing: 16,
83-
childAspectRatio: 4/3,
84-
),
85-
itemCount: shaders.length,
86-
itemBuilder: (context, index) {
87-
final shaderInfo = shaders[index];
88-
return _ShaderTile(
89-
shaderInfo: shaderInfo,
90-
onTap: () {
91-
Navigator.of(context).push(
92-
MaterialPageRoute(
93-
builder: (context) => ShaderScreen(shaderInfo: shaderInfo),
94-
),
95-
);
96-
},
97-
);
98-
},
99-
);
100-
}),
101116
);
102117
}
103118
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'dart:ui';
2+
import 'shader_configs.dart';
3+
4+
class NoiseOverlayShaderConfig extends ShaderConfig {
5+
final double filmGrainIntensity;
6+
final double noiseOpacity;
7+
8+
const NoiseOverlayShaderConfig({
9+
this.filmGrainIntensity = 0.1,
10+
this.noiseOpacity = 0.3,
11+
});
12+
13+
@override
14+
bool get requiresImageSampler => true;
15+
16+
@override
17+
Duration? get animationDuration => null; // Unbounded animation
18+
19+
@override
20+
void setUniforms(FragmentShader shader, Size size, double time) {
21+
shader
22+
..setFloat(0, size.width)
23+
..setFloat(1, size.height)
24+
..setFloat(2, time)
25+
..setFloat(3, filmGrainIntensity)
26+
..setFloat(4, noiseOpacity);
27+
}
28+
}

lib/noise_shader_config.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'dart:ui';
2+
import 'shader_configs.dart';
3+
4+
class NoiseShaderConfig extends ShaderConfig {
5+
final double filmGrainIntensity;
6+
7+
const NoiseShaderConfig({this.filmGrainIntensity = 0.1});
8+
9+
@override
10+
bool get requiresImageSampler => false;
11+
12+
@override
13+
Duration? get animationDuration => null; // Unbounded animation
14+
15+
@override
16+
void setUniforms(FragmentShader shader, Size size, double time) {
17+
shader
18+
..setFloat(0, size.width)
19+
..setFloat(1, size.height)
20+
..setFloat(2, time)
21+
..setFloat(3, filmGrainIntensity);
22+
}
23+
}

lib/ntsc_shader_config.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import 'dart:ui';
2-
import 'package:flutter/material.dart';
3-
import 'package:flutter_shaders/flutter_shaders.dart';
42
import 'shader_configs.dart';
53

64
class NtscShaderConfig extends ShaderConfig {

lib/shader_configs.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import 'dart:ui';
22

3-
43
abstract class ShaderConfig {
54
const ShaderConfig();
65

76
void setUniforms(FragmentShader shader, Size size, double time);
7+
8+
// Whether this shader requires an image sampler
9+
bool get requiresImageSampler => true;
10+
11+
// Duration of the animation in seconds (null means unbounded/infinite)
12+
Duration? get animationDuration => const Duration(milliseconds: 1800);
813
}

lib/shader_screen.dart

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import 'package:shaders/tv_test_screen.dart';
55
import 'main.dart';
66

77
class ShaderScreen extends StatefulWidget {
8-
const ShaderScreen({
9-
super.key,
10-
required this.shaderInfo,
11-
});
8+
const ShaderScreen({super.key, required this.shaderInfo});
129

1310
final ShaderInfo shaderInfo;
1411

@@ -23,13 +20,26 @@ class _ShaderScreenState extends State<ShaderScreen>
2320
@override
2421
void initState() {
2522
super.initState();
26-
_controller = AnimationController(
27-
vsync: this,
28-
duration: const Duration(milliseconds: 1800),
29-
);
30-
_controller
31-
..forward()
32-
..repeat(reverse: true);
23+
final duration = widget.shaderInfo.config.animationDuration;
24+
25+
if (duration != null) {
26+
// Bounded animation
27+
_controller = AnimationController(vsync: this, duration: duration);
28+
_controller
29+
..forward()
30+
..repeat(reverse: true);
31+
} else {
32+
// Unbounded animation - use a long duration and repeat without reverse
33+
_controller = AnimationController(
34+
vsync: this,
35+
duration: const Duration(
36+
seconds: 3600,
37+
), // 1 hour cycle for continuous time
38+
);
39+
_controller
40+
..forward()
41+
..repeat();
42+
}
3343
}
3444

3545
@override
@@ -43,47 +53,59 @@ class _ShaderScreenState extends State<ShaderScreen>
4353
final shaderView = AnimatedBuilder(
4454
animation: _controller,
4555
builder: (context, child) {
46-
return ShaderBuilder(
47-
assetKey: widget.shaderInfo.assetKey,
48-
(context, shader, _) {
49-
return AnimatedSampler(
50-
(image, size, canvas) {
51-
final animation = TweenSequence<double>([
52-
TweenSequenceItem(
53-
tween: Tween<double>(begin: 0.0, end: 1.0)
54-
.chain(CurveTween(curve: Curves.easeInOut)),
55-
weight: 50.0,
56-
),
57-
TweenSequenceItem(
58-
tween: Tween<double>(begin: 1.0, end: 0.0)
59-
.chain(CurveTween(curve: Curves.easeInOut)),
60-
weight: 50.0,
61-
),
62-
]).animate(_controller);
63-
widget.shaderInfo.config
64-
.setUniforms(shader, size, animation.value);
65-
shader.setImageSampler(0, image);
56+
return ShaderBuilder(assetKey: widget.shaderInfo.assetKey, (
57+
context,
58+
shader,
59+
_,
60+
) {
61+
return AnimatedSampler((image, size, canvas) {
62+
final duration = widget.shaderInfo.config.animationDuration;
63+
double timeValue;
64+
65+
if (duration != null) {
66+
// Bounded animation - use animated value between 0-1
67+
final animation = TweenSequence<double>([
68+
TweenSequenceItem(
69+
tween: Tween<double>(
70+
begin: 0.0,
71+
end: 1.0,
72+
).chain(CurveTween(curve: Curves.easeInOut)),
73+
weight: 50.0,
74+
),
75+
TweenSequenceItem(
76+
tween: Tween<double>(
77+
begin: 1.0,
78+
end: 0.0,
79+
).chain(CurveTween(curve: Curves.easeInOut)),
80+
weight: 50.0,
81+
),
82+
]).animate(_controller);
83+
timeValue = animation.value;
84+
} else {
85+
// Unbounded animation - use controller value as continuous time
86+
// Scale the 0-1 controller value to actual time in seconds
87+
timeValue = _controller.value * _controller.duration!.inSeconds;
88+
}
89+
90+
widget.shaderInfo.config.setUniforms(shader, size, timeValue);
91+
92+
// Only set image sampler if the shader requires it
93+
if (widget.shaderInfo.config.requiresImageSampler) {
94+
shader.setImageSampler(0, image);
95+
}
6696

67-
canvas.drawRect(
68-
Rect.fromLTWH(0, 0, size.width, size.height),
69-
Paint()..shader = shader,
70-
);
71-
},
72-
child: child!,
97+
canvas.drawRect(
98+
Rect.fromLTWH(0, 0, size.width, size.height),
99+
Paint()..shader = shader,
73100
);
74-
},
75-
);
101+
}, child: child!);
102+
});
76103
},
77-
child: const Center(
78-
child: TvTestScreen(),
79-
),
104+
child: const Center(child: TvTestScreen()),
80105
);
81106

82107
return Scaffold(
83-
appBar: AppBar(
84-
title: Text(widget.shaderInfo.name),
85-
centerTitle: true,
86-
),
108+
appBar: AppBar(title: Text(widget.shaderInfo.name), centerTitle: true),
87109
body: shaderView,
88110
);
89111
}

lib/tv_test_screen.dart

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ class TvTestScreen extends StatelessWidget {
1212
decoration: BoxDecoration(
1313
border: Border.all(color: Colors.black, width: 2),
1414
),
15-
child: const CustomPaint(
16-
painter: TvTestScreenPainter(),
17-
),
15+
child: const CustomPaint(painter: TvTestScreenPainter()),
1816
),
1917
);
2018
}
@@ -103,19 +101,25 @@ class TvTestScreenPainter extends CustomPainter {
103101
..strokeWidth = 1;
104102

105103
final spacing = 8.0;
106-
104+
107105
// Horizontal lines
108106
for (double i = y; i <= y + h; i += spacing) {
109107
canvas.drawLine(Offset(x, i), Offset(x + w, i), paint);
110108
}
111-
109+
112110
// Vertical lines
113111
for (double i = x; i <= x + w; i += spacing) {
114112
canvas.drawLine(Offset(i, y), Offset(i, y + h), paint);
115113
}
116114
}
117115

118-
void _drawVerticalLines(Canvas canvas, double x, double y, double w, double h) {
116+
void _drawVerticalLines(
117+
Canvas canvas,
118+
double x,
119+
double y,
120+
double w,
121+
double h,
122+
) {
119123
final paint = Paint()
120124
..color = Colors.black
121125
..strokeWidth = 1;
@@ -137,10 +141,10 @@ class TvTestScreenPainter extends CustomPainter {
137141
for (int i = 0; i < frequencies.length; i++) {
138142
final freq = frequencies[i];
139143
final paint = Paint()..color = Colors.black;
140-
144+
141145
final x = i * barWidth;
142146
final lineSpacing = barWidth / (freq * 2);
143-
147+
144148
for (double j = 0; j < barWidth; j += lineSpacing * 2) {
145149
canvas.drawRect(
146150
Rect.fromLTWH(x + j, startY, lineSpacing, sectionHeight),
@@ -167,7 +171,7 @@ class TvTestScreenPainter extends CustomPainter {
167171
for (final pos in positions) {
168172
// Outer circle
169173
canvas.drawCircle(pos, radius, paint);
170-
174+
171175
// Inner crosshair
172176
canvas.drawLine(
173177
Offset(pos.dx - radius * 0.7, pos.dy),
@@ -179,7 +183,7 @@ class TvTestScreenPainter extends CustomPainter {
179183
Offset(pos.dx, pos.dy + radius * 0.7),
180184
paint,
181185
);
182-
186+
183187
// Center dot
184188
canvas.drawCircle(pos, 3, Paint()..color = Colors.black);
185189
}
@@ -191,12 +195,12 @@ class TvTestScreenPainter extends CustomPainter {
191195
..strokeWidth = 0.5;
192196

193197
const gridSpacing = 20.0;
194-
198+
195199
// Vertical lines
196200
for (double x = 0; x <= width; x += gridSpacing) {
197201
canvas.drawLine(Offset(x, 0), Offset(x, height), paint);
198202
}
199-
203+
200204
// Horizontal lines
201205
for (double y = 0; y <= height; y += gridSpacing) {
202206
canvas.drawLine(Offset(0, y), Offset(width, y), paint);

0 commit comments

Comments
 (0)