Skip to content

Instantly share code, notes, and snippets.

@Rahiche
Created October 21, 2023 23:18
Show Gist options
  • Save Rahiche/11285b14a9b953dee9e1d7ee3a6396be to your computer and use it in GitHub Desktop.
Save Rahiche/11285b14a9b953dee9e1d7ee3a6396be to your computer and use it in GitHub Desktop.
iOS 17 Faded Gradual Blur
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_shaders/flutter_shaders.dart';
void main() {
runApp(BlurGallery());
}
class BlurGallery extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark(
useMaterial3: true,
),
home: Builder(builder: (context) {
return Scaffold(
appBar: AppBar(title: Text('Blur Effect Demo')),
body: GridView.count(
crossAxisCount: 2,
children: <Widget>[
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GradualBackgroundBlur()),
);
},
child: buildContent("Gradual Background Blur"),
),
],
),
);
}),
);
}
Widget buildContent(String text) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(child: Text(text)),
),
);
}
}
class GradualBackgroundBlur extends StatefulWidget {
@override
_GradualBackgroundBlurState createState() => _GradualBackgroundBlurState();
}
class _GradualBackgroundBlurState extends State<GradualBackgroundBlur> {
bool isClipped = true;
ScrollController _scrollController = ScrollController();
double blurSigma = 1;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
const maxBlur = 8.0;
const scrollThreshold = 200.0;
if (_scrollController.offset < scrollThreshold) {
// Calculate blurSigma based on scroll position
setState(() {
blurSigma = maxBlur * (_scrollController.offset / scrollThreshold);
if (blurSigma == maxBlur) {}
});
print("blurSigma ${blurSigma}");
}
}
@override
void dispose() {
_scrollController.removeListener(_onScroll); // Remove listener
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final child = Scaffold(
body: Stack(
children: [
Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ListView.builder(
controller: _scrollController,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return SizedBox(
height: 100,
);
}
if (index == 1) {
return Container(
child: Image.asset(
'assets/rg.jpg',
fit: BoxFit.cover,
),
);
}
return ListTile(
title: Text(index.toString()),
);
},
),
),
],
),
if (isClipped)
Align(
alignment: Alignment.topCenter,
child: ClipRect(
child: Container(
height: 120,
width: double.infinity,
child: BackdropFilter(
filter:
ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma),
child: Container(
color: Colors.transparent,
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(36.0),
child: Switch.adaptive(
value: isClipped,
onChanged: (value) {
setState(() {
isClipped = value;
});
},
),
),
),
],
),
);
if (isClipped) {
return child;
} else {
return TiltShift(
child: child,
progress: blurSigma,
);
}
}
}
class TiltShift extends StatelessWidget {
const TiltShift({Key? key, required this.child, required this.progress})
: super(key: key);
final Widget child;
final double progress;
@override
Widget build(BuildContext context) {
return ShaderBuilder(
(context, shader, _) {
return AnimatedSampler(
(image, size, canvas) {
shader.setFloat(0, size.width);
shader.setFloat(1, size.height);
shader.setFloat(2, progress);
shader.setImageSampler(0, image);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..shader = shader,
);
},
child: child,
);
},
assetKey: 'shaders/tilt_shift_gaussian.frag',
);
}
}
#version 460 core
precision mediump float;
#include <flutter/runtime_effect.glsl>
uniform vec2 uViewSize;
uniform float sigma;
uniform sampler2D uTexture;
out vec4 FragColor;
float normpdf(in float x, in float sigma)
{
return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma;
}
void main() {
vec2 fragCoord = FlutterFragCoord().xy;
vec2 uv = fragCoord / uViewSize;
vec4 color = vec4(texture(uTexture, uv).rgb, 1.0);
if(fragCoord.y < uViewSize.y * 0.2) { // Compute the effect for the top 30% of the image.
// How much the shift is visible.
const float shiftPower = 4.0;
//declare stuff
//The bigger the value the slower the effect
const int mSize = 35;
const int kSize = (mSize-1)/2;
float kernel[mSize];
vec3 final_colour = vec3(0.0);
//create the 1-D kernel
float Z = 0.00;
for (int j = 0; j <= kSize; ++j)
kernel[kSize+j] = kernel[kSize-j] = normpdf(float(j), sigma );
//get the normalization factor (as the gaussian has been clamped)
for (int j = 0; j < mSize; ++j)
Z += kernel[j];
//read out the texels
for (int i=-kSize; i <= kSize; ++i)
{
for (int j=-kSize; j <= kSize; ++j)
final_colour += kernel[kSize+j]*kernel[kSize+i]*texture(uTexture, (fragCoord.xy+vec2(float(i),float(j))) / uViewSize).rgb;
}
// Blend factor for the effect
float val = clamp(shiftPower * abs(uViewSize.y * 0.2 - fragCoord.y) / (uViewSize.y * 0.3), 0.0, 1.0);
FragColor = vec4(final_colour/(Z*Z), 1.0) * val + color * (1.0 - val);
} else {
FragColor = color;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment