Last active
April 5, 2023 10:34
-
-
Save rydmike/78cdff7f63515a9ea18f172b8640de55 to your computer and use it in GitHub Desktop.
Flutter: MaskFilter.blur Outer Blur issue on DomCanvas
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'dart:ui'; | |
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(IssueDemoApp()); | |
} | |
// The Maskfilter style we want to demo/test | |
const kMaskFilter = MaskFilter.blur(BlurStyle.outer, 10); | |
class IssueDemoApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
primarySwatch: Colors.red, | |
scaffoldBackgroundColor: Colors.grey[100], | |
buttonTheme: ButtonThemeData( | |
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.red), | |
textTheme: ButtonTextTheme.primary, | |
), | |
), | |
debugShowCheckedModeBanner: false, | |
home: ClipRectIssueDemo(), | |
); | |
} | |
} | |
class ClipRectIssueDemo extends StatefulWidget { | |
@override | |
State<StatefulWidget> createState() { | |
return _ClipRectIssueDemoState(); | |
} | |
} | |
class _ClipRectIssueDemoState extends State<ClipRectIssueDemo> { | |
bool _showHouseWithSun = true; | |
bool _showFrostedGlassLake = true; | |
bool _showWaves = true; | |
bool _useBlurfilter = true; | |
bool _clipRectOn = true; | |
double _width = 301.0; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('MaskFilter Issue Demo'), | |
centerTitle: true, | |
elevation: 0, | |
), | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
// And then some gap space too | |
const SizedBox(height: 20), | |
Text('MaskFilter.blur Outer Blur Issue', | |
style: Theme.of(context).textTheme.headline6), | |
const SizedBox(height: 20), | |
SizedBox( | |
width: 420, | |
height: 420, | |
child: Center( | |
child: SizedBox( | |
height: _width, | |
width: _width, | |
child: _clipRectOn | |
? ClipRect( | |
clipBehavior: Clip.hardEdge, | |
child: BlurMaskDemoWidget( | |
showHouseWithSun: _showHouseWithSun, | |
showFrostedGlassLake: _showFrostedGlassLake, | |
showWaves: _showWaves, | |
useBlurFilter: _useBlurfilter, | |
), | |
) | |
: BlurMaskDemoWidget( | |
showHouseWithSun: _showHouseWithSun, | |
showFrostedGlassLake: _showFrostedGlassLake, | |
showWaves: _showWaves, | |
useBlurFilter: _useBlurfilter, | |
), | |
), | |
), | |
), | |
const SizedBox(height: 10), | |
const Divider(), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: SwitchListTile( | |
title: const Text('MaskFilter.blur outer ON/OFF'), | |
subtitle: const Text( | |
'Turn on too compare the difference between ' | |
'CanvasKit (SKIA) and DomCanvas.\nThe filter causes an ' | |
'expected blur effect outside the paint canvas that we ' | |
'will also cut away with ClipRect.'), | |
value: _useBlurfilter, | |
onChanged: (value) { | |
setState(() { | |
_useBlurfilter = value; | |
}); | |
}, | |
), | |
), | |
), | |
const Divider(), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: SwitchListTile( | |
title: const Text('Show static waves on the lake'), | |
subtitle: const Text( | |
'The above MaskFilter.blur outer is only applied ' | |
'to these CustomPaint wave objects.'), | |
value: _showWaves, | |
onChanged: (value) { | |
setState(() { | |
_showWaves = value; | |
}); | |
}, | |
), | |
), | |
), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: SwitchListTile( | |
title: const Text('Show house over a blood-red sun'), | |
subtitle: const Text('This one is just here to look cool.'), | |
value: _showHouseWithSun, | |
onChanged: (value) { | |
setState(() { | |
_showHouseWithSun = value; | |
}); | |
}, | |
), | |
), | |
), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: SwitchListTile( | |
title: const Text('Make a frosted glass lake'), | |
subtitle: | |
const Text('Just to show and test that this effect is ' | |
'not what caused the issue on DomCanvas.'), | |
value: _showFrostedGlassLake, | |
onChanged: (value) { | |
setState(() { | |
_showFrostedGlassLake = value; | |
}); | |
}, | |
), | |
), | |
), | |
const Divider(), | |
const SizedBox(height: 10), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: SwitchListTile( | |
title: const Text('ClipRect ON/OFF'), | |
subtitle: const Text( | |
'Turn on ClipRect to see edge remnants. \nIf you resize ' | |
'window/media size or change container size, you can ' | |
'observe the edge remnants appearing and dissapearing ' | |
'at different edges.'), | |
value: _clipRectOn, | |
onChanged: (value) { | |
setState(() { | |
_clipRectOn = value; | |
}); | |
}, | |
), | |
), | |
), | |
const SizedBox(height: 10), | |
Center( | |
child: SizedBox( | |
width: 450, | |
child: ListTile( | |
title: const Text('Change Container size'), | |
subtitle: Slider( | |
min: 200.0, | |
max: 400.0, | |
divisions: (400 - 100).floor(), | |
label: _width.floor().toString(), | |
value: _width, | |
onChanged: (value) { | |
setState(() { | |
_width = value; | |
}); | |
}, | |
), | |
trailing: Padding( | |
padding: const EdgeInsets.only(right: 12.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.end, | |
children: <Widget>[ | |
const Text( | |
'Width', | |
style: TextStyle(fontSize: 11), | |
), | |
Text( | |
_width.floor().toString(), | |
style: const TextStyle(fontSize: 15), | |
), | |
], | |
), | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class BlurMaskDemoWidget extends StatelessWidget { | |
const BlurMaskDemoWidget({ | |
Key key, | |
this.showHouseWithSun = false, | |
this.showFrostedGlassLake = false, | |
this.showWaves = false, | |
this.useBlurFilter = false, | |
}) : super(key: key); | |
final bool showHouseWithSun; | |
final bool showFrostedGlassLake; | |
final bool showWaves; | |
final bool useBlurFilter; | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
overflow: Overflow.clip, | |
children: <Widget>[ | |
if (showHouseWithSun) _buildSunWithHouse(), | |
if (showFrostedGlassLake) _buildFrostedGlass(), | |
if (showWaves) _buildWave1(), | |
if (showWaves) _buildWave2(), | |
if (showWaves) _buildWave3(), | |
], | |
); | |
} | |
// The RED ROUND SUN Circle with a HOUSE in it | |
Align _buildSunWithHouse() { | |
return Align( | |
alignment: Alignment.bottomCenter, | |
child: Padding( | |
padding: const EdgeInsets.only(bottom: 80), | |
child: Stack( | |
children: <Widget>[ | |
// Simple way to make a red "sun" circle | |
Container( | |
height: 150, | |
width: 150, | |
decoration: BoxDecoration( | |
shape: BoxShape.circle, | |
color: Colors.red[700], | |
), | |
), | |
// The house in front of the red Sun, just using a home icon | |
Container( | |
height: 89, | |
width: 150, | |
alignment: Alignment.bottomCenter, | |
child: const Icon( | |
Icons.home, | |
size: 85, | |
color: Colors.white, | |
), | |
), | |
// The same house rotated 180 degrees to be upside down as a | |
// reflection the water | |
Container( | |
height: 145, | |
width: 150, | |
alignment: Alignment.bottomCenter, | |
child: const RotatedBox( | |
quarterTurns: 2, | |
child: Icon( | |
Icons.home, | |
size: 85, | |
color: Colors.white, | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
// Put a "frosted glass" effect over half of the sun and the upside down | |
// house to make them look like they are reflected in water | |
Align _buildFrostedGlass() { | |
return Align( | |
alignment: Alignment.bottomCenter, | |
child: ClipRect( | |
child: BackdropFilter( | |
filter: ImageFilter.blur(sigmaX: 6.5, sigmaY: 6.5), | |
child: Container( | |
height: 160, | |
width: double.infinity, | |
decoration: BoxDecoration( | |
color: Colors.grey.shade200.withOpacity(0.3), | |
), | |
), | |
), | |
), | |
); | |
} | |
// Put some waves with red shimmer from the red sun on top of the | |
// "frosted glass" water. The actual creation of the moving animated | |
// waves depends on the pub.dev wave package. | |
Align _buildWave1() { | |
return Align( | |
alignment: Alignment.bottomCenter, | |
child: CustomPaint( | |
painter: _CustomWavePainter( | |
color: Colors.red[800], | |
heightPercentange: 0, | |
waveFrequency: 4, | |
wavePhaseValue: 0, | |
waveAmplitude: 8, | |
blur: useBlurFilter ? kMaskFilter : null, | |
), | |
size: const Size( | |
double.infinity, | |
170.0, | |
), | |
), | |
); | |
} | |
Align _buildWave2() { | |
return Align( | |
alignment: Alignment.bottomCenter, | |
child: CustomPaint( | |
painter: _CustomWavePainter( | |
color: Colors.red[600], | |
heightPercentange: 0.2, | |
waveFrequency: 3.5, | |
wavePhaseValue: 100, | |
waveAmplitude: 10, | |
blur: useBlurFilter ? kMaskFilter : null, | |
), | |
size: const Size( | |
double.infinity, | |
170.0, | |
), | |
), | |
); | |
} | |
Align _buildWave3() { | |
return Align( | |
alignment: Alignment.bottomCenter, | |
child: CustomPaint( | |
painter: _CustomWavePainter( | |
color: Colors.red[500], | |
heightPercentange: 0.4, | |
waveFrequency: 3, | |
wavePhaseValue: 80, | |
waveAmplitude: 9, | |
blur: useBlurFilter ? kMaskFilter : null, | |
), | |
size: const Size( | |
double.infinity, | |
170.0, | |
), | |
), | |
); | |
} | |
} | |
class _CustomWavePainter extends CustomPainter { | |
_CustomWavePainter({ | |
this.color, | |
this.gradient, | |
this.gradientBegin, | |
this.gradientEnd, | |
this.blur, | |
this.heightPercentange, | |
this.waveFrequency, | |
this.wavePhaseValue, | |
this.waveAmplitude, | |
}); | |
final Color color; | |
final List<Color> gradient; | |
final Alignment gradientBegin; | |
final Alignment gradientEnd; | |
final MaskFilter blur; | |
final double waveAmplitude; | |
final double wavePhaseValue; | |
final double waveFrequency; | |
final double heightPercentange; | |
double _tempA = 0.0; | |
double _tempB = 0.0; | |
double viewWidth = 0.0; | |
final Paint _paint = Paint(); | |
void _setPaths(double viewCenterY, Size size, Canvas canvas) { | |
final Layer _layer = Layer( | |
path: Path(), | |
color: color, | |
gradient: gradient, | |
blur: blur, | |
amplitude: (-1.6 + 0.8) * waveAmplitude, | |
phase: wavePhaseValue * 2 + 30, | |
); | |
_layer.path.reset(); | |
_layer.path.moveTo( | |
0.0, | |
viewCenterY + | |
_layer.amplitude * _getSinY(_layer.phase, waveFrequency, -1)); | |
for (int i = 1; i < size.width + 1; i++) { | |
_layer.path.lineTo( | |
i.toDouble(), | |
viewCenterY + | |
_layer.amplitude * _getSinY(_layer.phase, waveFrequency, i)); | |
} | |
_layer.path.lineTo(size.width, size.height); | |
_layer.path.lineTo(0.0, size.height); | |
_layer.path.close(); | |
if (_layer.color != null) { | |
_paint.color = _layer.color; | |
} | |
if (_layer.gradient != null) { | |
final rect = Offset.zero & | |
Size(size.width, size.height - viewCenterY * heightPercentange); | |
_paint.shader = LinearGradient( | |
begin: gradientBegin ?? Alignment.bottomCenter, | |
end: gradientEnd ?? Alignment.topCenter, | |
colors: _layer.gradient) | |
.createShader(rect); | |
} | |
if (_layer.blur != null) { | |
_paint.maskFilter = _layer.blur; | |
} | |
_paint.style = PaintingStyle.fill; | |
canvas.drawPath(_layer.path, _paint); | |
} | |
@override | |
void paint(Canvas canvas, Size size) { | |
final double viewCenterY = size.height * (heightPercentange + 0.1); | |
viewWidth = size.width; | |
_setPaths(viewCenterY, size, canvas); | |
} | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) { | |
return true; | |
} | |
double _getSinY( | |
double startradius, double waveFrequency, int currentposition) { | |
if (_tempA == 0) { | |
_tempA = pi / viewWidth; | |
} | |
if (_tempB == 0) { | |
_tempB = 2 * pi / 360.0; | |
} | |
return sin( | |
_tempA * waveFrequency * (currentposition + 1) + startradius * _tempB); | |
} | |
} | |
/// Meta data of layer | |
class Layer { | |
Layer({ | |
this.color, | |
this.gradient, | |
this.blur, | |
this.path, | |
this.amplitude, | |
this.phase, | |
}); | |
final Color color; | |
final List<Color> gradient; | |
final MaskFilter blur; | |
final Path path; | |
final double amplitude; | |
final double phase; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment