Skip to content

Commit c1a0ff0

Browse files
committed
feat: add AI Assistant shader and integrate into shader list with customizable properties
1 parent d3b21d2 commit c1a0ff0

File tree

7 files changed

+225
-46
lines changed

7 files changed

+225
-46
lines changed
174 KB
Loading
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import 'package:flutter/material.dart';
2+
import 'dart:ui';
3+
import 'shader_builder.dart';
4+
5+
class AiAssistantShaderBuilder extends CustomShaderBuilder {
6+
const AiAssistantShaderBuilder();
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+
}
21+
22+
@override
23+
Widget buildShader(
24+
ShaderMetadata metadata,
25+
FragmentShader shader,
26+
Size size,
27+
double time,
28+
Widget? child,
29+
) {
30+
return ClipRect(
31+
child: CustomPaint(
32+
size: Size.infinite,
33+
painter: _AiAssistantShaderPainter(shader),
34+
),
35+
);
36+
}
37+
38+
@override
39+
Widget? childBuilder(BuildContext context) {
40+
return null;
41+
}
42+
}
43+
44+
class _AiAssistantShaderPainter extends CustomPainter {
45+
final FragmentShader shader;
46+
47+
_AiAssistantShaderPainter(this.shader);
48+
49+
@override
50+
void paint(Canvas canvas, Size size) {
51+
canvas.drawRect(
52+
Rect.fromLTWH(0, 0, size.width, size.height),
53+
Paint()..shader = shader,
54+
);
55+
}
56+
57+
@override
58+
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
59+
}

lib/main.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'noise_shader_builder.dart';
1616
import 'ntsc_shader_builder.dart';
1717
import 'rings_shader_builder.dart';
1818
import 'shader_screen.dart';
19+
import 'ai_assistant_shader_builder.dart';
1920

2021
void main() {
2122
usePathUrlStrategy();
@@ -32,6 +33,8 @@ class ShaderInfo {
3233
final DateTime dateAdded;
3334
final CustomShaderBuilder builder;
3435
final String path;
36+
final EdgeInsetsGeometry? padding;
37+
final Color? backgroundColor;
3538

3639
const ShaderInfo({
3740
required this.name,
@@ -42,6 +45,8 @@ class ShaderInfo {
4245
required this.dateAdded,
4346
required this.builder,
4447
required this.path,
48+
this.padding,
49+
this.backgroundColor,
4550
});
4651

4752
ShaderMetadata get metadata => ShaderMetadata(
@@ -114,6 +119,18 @@ final shaders = [
114119
builder: const ClearlyBugShaderBuilder(),
115120
path: 'clearly-bug-shader',
116121
),
122+
ShaderInfo(
123+
name: 'AI Assistant',
124+
assetKey: 'shaders/ai_assistant.frag',
125+
description: 'A rotating effect resembling an AI assistant.',
126+
sourceUrl: 'https://www.shadertoy.com/view/MXsyzl',
127+
author: 'Saphirah',
128+
dateAdded: DateTime(2025, 8, 21),
129+
builder: const AiAssistantShaderBuilder(),
130+
path: 'ai-assistant',
131+
padding: EdgeInsets.all(32),
132+
backgroundColor: Colors.black,
133+
),
117134
];
118135

119136
// Helper function to create URL-safe shader names

lib/widgets/shader_animation_view.dart

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -52,56 +52,62 @@ class _ShaderAnimationViewState extends State<ShaderAnimationView> with SingleTi
5252

5353
@override
5454
Widget build(BuildContext context) {
55-
return LayoutBuilder(
56-
builder: (context, size) {
57-
return AnimatedBuilder(
58-
animation: _controller,
59-
builder: (context, child) {
60-
return ShaderBuilder(
61-
assetKey: widget.shaderInfo.assetKey,
62-
(context, shader, _) {
63-
final duration = widget.shaderInfo.builder.animationDuration;
64-
double timeValue;
55+
return ColoredBox(
56+
color: widget.shaderInfo.backgroundColor ?? Colors.transparent,
57+
child: Padding(
58+
padding: widget.shaderInfo.padding ?? EdgeInsets.zero,
59+
child: LayoutBuilder(
60+
builder: (context, size) {
61+
return AnimatedBuilder(
62+
animation: _controller,
63+
builder: (context, child) {
64+
return ShaderBuilder(
65+
assetKey: widget.shaderInfo.assetKey,
66+
(context, shader, _) {
67+
final duration = widget.shaderInfo.builder.animationDuration;
68+
double timeValue;
6569

66-
if (duration != null) {
67-
// Bounded animation - use animated value between 0-1
68-
final animation = TweenSequence<double>([
69-
TweenSequenceItem(
70-
tween: Tween<double>(
71-
begin: 0.0,
72-
end: 1.0,
73-
).chain(CurveTween(curve: Curves.easeInOut)),
74-
weight: 50.0,
75-
),
76-
TweenSequenceItem(
77-
tween: Tween<double>(
78-
begin: 1.0,
79-
end: 0.0,
80-
).chain(CurveTween(curve: Curves.easeInOut)),
81-
weight: 50.0,
82-
),
83-
]).animate(_controller);
84-
timeValue = animation.value;
85-
} else {
86-
// Unbounded animation - use controller value as continuous time
87-
// Scale the 0-1 controller value to actual time in seconds
88-
timeValue = _controller.value * _controller.duration!.inSeconds;
89-
}
70+
if (duration != null) {
71+
// Bounded animation - use animated value between 0-1
72+
final animation = TweenSequence<double>([
73+
TweenSequenceItem(
74+
tween: Tween<double>(
75+
begin: 0.0,
76+
end: 1.0,
77+
).chain(CurveTween(curve: Curves.easeInOut)),
78+
weight: 50.0,
79+
),
80+
TweenSequenceItem(
81+
tween: Tween<double>(
82+
begin: 1.0,
83+
end: 0.0,
84+
).chain(CurveTween(curve: Curves.easeInOut)),
85+
weight: 50.0,
86+
),
87+
]).animate(_controller);
88+
timeValue = animation.value;
89+
} else {
90+
// Unbounded animation - use controller value as continuous time
91+
// Scale the 0-1 controller value to actual time in seconds
92+
timeValue = _controller.value * _controller.duration!.inSeconds;
93+
}
9094

91-
widget.shaderInfo.builder.setUniforms(shader, size.biggest, timeValue);
95+
widget.shaderInfo.builder.setUniforms(shader, size.biggest, timeValue);
9296

93-
return widget.shaderInfo.builder.buildShader(
94-
widget.shaderInfo.metadata,
95-
shader,
96-
size.biggest,
97-
timeValue,
98-
widget.shaderInfo.builder.childBuilder(context),
97+
return widget.shaderInfo.builder.buildShader(
98+
widget.shaderInfo.metadata,
99+
shader,
100+
size.biggest,
101+
timeValue,
102+
widget.shaderInfo.builder.childBuilder(context),
103+
);
104+
},
99105
);
100106
},
101107
);
102108
},
103-
);
104-
},
109+
),
110+
),
105111
);
106112
}
107113
}

lib/widgets/shader_card.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ class ShaderCard extends StatelessWidget {
1919
child: Column(
2020
crossAxisAlignment: CrossAxisAlignment.start,
2121
children: [
22-
Image.asset(
23-
'assets/screenshots/${shaderInfo.path}.png',
24-
fit: BoxFit.cover,
22+
AspectRatio(
23+
aspectRatio: 16 / 9,
24+
child: Image.asset(
25+
'assets/screenshots/${shaderInfo.path}.png',
26+
fit: BoxFit.cover,
27+
),
2528
),
2629
Padding(
2730
padding: const EdgeInsets.all(8.0),

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ flutter:
4949
- shaders/ntsc_shader.frag
5050
- shaders/rings_shader.frag
5151
- shaders/clearly_bug_shader.frag
52+
- shaders/ai_assistant.frag

shaders/ai_assistant.frag

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#version 460 core
2+
precision mediump float;
3+
4+
#include <flutter/runtime_effect.glsl>
5+
6+
uniform vec2 iResolution;
7+
uniform float iTime;
8+
9+
out vec4 fragColor;
10+
11+
// CONFIGURATION
12+
13+
//Layers
14+
float layerCount = 2.0;
15+
float numberOfBlobsPerLayer = 3.0; // One side is hidden to make it more irregular
16+
17+
// Size
18+
float circleSize = 0.9; // 0-1
19+
float borderSize = 0.015; // 0-1
20+
21+
// Animations
22+
float pulseAnimationStrength = 0.02; // 0-1
23+
float pulseAnimationSpeed = 1.0;
24+
25+
#define PI 3.14159265358979323846264338327950288
26+
#define positiveSin(number) (sin(number) + 1.0) / 2.0
27+
#define remap(number, oldMin, oldMax, newMin, newMax) (number - oldMin) / (oldMax - oldMin) * (newMax - newMin) + newMin
28+
29+
float saturate(float number){
30+
return clamp(number, 0.0, 1.0);
31+
}
32+
33+
float radialGradient(vec2 uv){
34+
float distanceToCenter = distance(vec2(0.5, 0.5), uv);
35+
vec2 uvRadial = uv - vec2(0.5, 0.5);
36+
float angle = atan(uvRadial.y, uvRadial.x);
37+
float radialGradient = (angle + PI) / (2.0 * PI);
38+
return radialGradient;
39+
}
40+
41+
42+
float getNumberOfLayers(){
43+
return layerCount * positiveSin(iTime * PI);
44+
}
45+
46+
float getCircleSize(){
47+
float circleAnimation01 = positiveSin(iTime * pulseAnimationSpeed); //This can be replaced with volume input
48+
float circleSizeWithoutBorder = circleSize - borderSize * 2.0;
49+
float pulseAnimationStrengthMapped = remap(pulseAnimationStrength, 0.0, 1.0, 0.0, circleSizeWithoutBorder);
50+
return remap(circleAnimation01, 0.0, 1.0, circleSizeWithoutBorder - pulseAnimationStrengthMapped, circleSizeWithoutBorder);
51+
}
52+
53+
float circleMask(float distance){
54+
float circleSizeConfigured = 1.0 / (getCircleSize() * 0.5);
55+
return pow(saturate(distance * circleSizeConfigured), 50.0);
56+
}
57+
58+
void main()
59+
{
60+
vec2 fragCoord = FlutterFragCoord().xy;
61+
// UV Map
62+
float aspectRatio = iResolution.x / iResolution.y;
63+
vec2 uv = fragCoord / iResolution.xy * vec2(aspectRatio, 1.0) + vec2(1.0 - aspectRatio, 0.0) * 0.5;
64+
65+
// Color Gradient
66+
vec3 color = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0,2,4));
67+
vec4 initialColor = vec4(color,1.0);
68+
69+
// Radial UV Map
70+
float distanceToCenter = distance(vec2(0.5, 0.5), uv);
71+
float radial = radialGradient(uv);
72+
73+
// Generate Circle Mask
74+
float innerCircleMask = circleMask(distanceToCenter);
75+
float outerCircleMask = 0.0;
76+
float layers = getNumberOfLayers();
77+
78+
// Generate Moving Blobs
79+
for(int i = 0; i <= 3; i++){
80+
float x = float(i);
81+
float layerWeight = positiveSin(iTime * x * 0.572); //
82+
// float layerWeight = saturate(layers - x);
83+
84+
float rotationSpeed = sin((x + 1.0) * 4.62843) * 0.4;
85+
float distance = (1.0 - getCircleSize()) / 2.0 * (layerWeight - borderSize);
86+
float distanceNoise = saturate(sin(mod(radial + iTime * rotationSpeed, 1.0) * PI * numberOfBlobsPerLayer) * distance);
87+
outerCircleMask += circleMask(distanceToCenter - distanceNoise - borderSize) / 3.0;
88+
}
89+
90+
//Combine Masks
91+
outerCircleMask = saturate(outerCircleMask);
92+
fragColor = initialColor * (innerCircleMask - outerCircleMask) ;
93+
}

0 commit comments

Comments
 (0)