Created
January 15, 2024 16:40
-
-
Save PlugFox/be3bb8873da4433c14e21201a4581653 to your computer and use it in GitHub Desktop.
Flutter Card with shader
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:ui' as ui show FragmentProgram, FragmentShader; | |
import 'package:agora_app/theme_audo/theme.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/scheduler.dart'; | |
/// {@template card} | |
/// A Card widget. | |
/// {@endtemplate} | |
/// {@category shader} | |
/// {@category decoration} | |
class Card extends StatefulWidget { | |
/// {@macro card} | |
const Card({ | |
this.background = Colors.transparent, | |
this.color = const Color.fromRGBO(43, 45, 39, 0.24), | |
this.shadow = const BoxShadow( | |
color: Color.fromRGBO(100, 100, 100, 1), | |
blurRadius: 8, | |
offset: Offset(4, 6), | |
), | |
this.radius = const BorderRadius.all(Radius.circular(8)), | |
this.gap = 3, | |
this.stroke = 1, | |
this.border = 1, | |
this.speed = 0, | |
this.offset = 8, | |
this.child, | |
super.key, | |
}) : assert(gap >= 0 && stroke >= 0 && border >= 0 && offset >= 0); | |
/// The rectangle fill color. | |
final Color background; | |
/// The color used for the stroke. | |
final Color color; | |
/// Shadow | |
final BoxShadow shadow; | |
/// The radius of the corners of the widget in logical pixels. | |
/// Defaults to 8 logical pixels for all corners. | |
final BorderRadius radius; | |
/// Gaps between dashes. | |
final double gap; | |
/// The length of the dashes. | |
final double stroke; | |
/// The width of the border. | |
final double border; | |
/// Animation speed. | |
/// Zero means no animation. | |
/// Defaults to 0. | |
final double speed; | |
/// Offset | |
final double offset; | |
/// The widget below this widget in the tree. | |
final Widget? child; | |
@override | |
State<Card> createState() => _CardState(); | |
} | |
class _CardState extends State<Card> with SingleTickerProviderStateMixin { | |
/// Init shader. | |
static final Future<ui.FragmentShader?> _shaderFuture = | |
ui.FragmentProgram.fromAsset('assets/shaders/card.frag') | |
.then<ui.FragmentShader?>((program) => program.fragmentShader(), onError: (_, __) => null); | |
/// Seed value notifier for shader mutation. | |
late final ValueNotifier<double> _seed = ValueNotifier<double>(.0); | |
/// Animated ticker. | |
late final Ticker _ticker; | |
void _updateSeed(Duration elapsed) => _seed.value = elapsed.inMilliseconds * widget.speed / 8000; | |
@override | |
void initState() { | |
super.initState(); | |
_ticker = createTicker(_updateSeed); | |
if (widget.speed > 0) _ticker.start(); | |
} | |
@override | |
void didUpdateWidget(covariant Card oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (oldWidget.speed > 0 && widget.speed <= 0) { | |
_ticker.stop(); | |
} else if (oldWidget.speed <= 0 && widget.speed > 0) { | |
_ticker.start(); | |
} | |
} | |
@override | |
void dispose() { | |
_ticker.dispose(); | |
_seed.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) => AnimatedThemeBuilder( | |
builder: (context, colorScheme, child) => RepaintBoundary( | |
child: FutureBuilder<ui.FragmentShader?>( | |
initialData: null, | |
future: _shaderFuture, | |
builder: (context, snapshot) => CustomPaint( | |
painter: _CardPainter( | |
shader: snapshot.data, | |
seed: _seed, | |
background: widget.background, | |
shadow: widget.shadow, | |
color: widget.color, | |
gap: widget.gap, | |
stroke: widget.stroke, | |
radius: widget.radius, | |
border: widget.border, | |
offset: widget.offset, | |
), | |
child: widget.child, | |
), | |
), | |
), | |
); | |
} | |
class _CardPainter extends CustomPainter { | |
_CardPainter({ | |
required this.seed, | |
required this.background, | |
required this.radius, | |
required this.color, | |
required this.shadow, | |
required this.gap, | |
required this.stroke, | |
required this.border, | |
required this.offset, | |
required this.shader, | |
}) : super(repaint: seed); | |
/// The color used for the stroke. | |
final ValueListenable<double> seed; | |
/// The rectangle fill color. | |
final Color background; | |
/// The color used for the stroke. | |
final Color color; | |
/// Shadow | |
final BoxShadow shadow; | |
/// The radius of the corners of the widget in logical pixels. | |
final BorderRadius radius; | |
/// Gaps between dashes. | |
final double gap; | |
/// The length of the dashes. | |
final double stroke; | |
/// The width of the border. | |
final double border; | |
/// Offset | |
final double offset; | |
/// Shader. | |
final ui.FragmentShader? shader; | |
@override | |
void paint(Canvas canvas, Size size) { | |
final rect = Offset.zero & size; | |
if (shader == null) { | |
// Fallback to canvas drawing. | |
final rrect = RRect.fromRectAndCorners( | |
rect, | |
topLeft: radius.topLeft, | |
topRight: radius.topRight, | |
bottomLeft: radius.bottomLeft, | |
bottomRight: radius.bottomRight, | |
); | |
if (background.alpha > 0) { | |
canvas.drawRRect( | |
rrect, | |
Paint() | |
..style = PaintingStyle.fill | |
..color = background, | |
); | |
} | |
canvas.drawRRect( | |
rrect, | |
Paint() | |
..style = PaintingStyle.stroke | |
..strokeWidth = border | |
..color = color, | |
); | |
return; | |
} | |
final paint = Paint() | |
..shader = (shader! | |
..setFloat(0, size.width) | |
..setFloat(1, size.height) | |
..setFloat(2, seed.value) | |
..setFloat(3, background.red / 255) | |
..setFloat(4, background.green / 255) | |
..setFloat(5, background.blue / 255) | |
..setFloat(6, background.alpha / 255) | |
..setFloat(7, color.red / 255) | |
..setFloat(8, color.green / 255) | |
..setFloat(9, color.blue / 255) | |
..setFloat(10, color.alpha / 255) | |
..setFloat(11, radius.topRight.x) | |
..setFloat(12, radius.bottomRight.x) | |
..setFloat(13, radius.topLeft.x) | |
..setFloat(14, radius.bottomLeft.x) | |
..setFloat(15, gap) | |
..setFloat(16, stroke) | |
..setFloat(17, border) | |
..setFloat(18, shadow.color.red / 255) | |
..setFloat(19, shadow.color.green / 255) | |
..setFloat(20, shadow.color.blue / 255) | |
..setFloat(21, shadow.color.alpha / 255) | |
..setFloat(22, -shadow.offset.dx) | |
..setFloat(23, -shadow.offset.dy) | |
..setFloat(24, shadow.blurRadius) | |
..setFloat(25, shadow.spreadRadius) | |
..setFloat(26, offset)); | |
canvas | |
/* ..clipRRect(rrect) */ | |
.drawRect(rect, paint); | |
} | |
@override | |
bool shouldRepaint(covariant _CardPainter oldDelegate) => | |
shader != oldDelegate.shader || | |
color != oldDelegate.color || | |
background != oldDelegate.background || | |
shadow != oldDelegate.shadow || | |
radius != oldDelegate.radius || | |
gap != oldDelegate.gap || | |
stroke != oldDelegate.stroke || | |
border != oldDelegate.border || | |
offset != oldDelegate.offset; | |
} |
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
// Based at Rounded rect + border & shadow by @inobelar | |
// https://www.shadertoy.com/view/fsdyzB | |
#version 460 core | |
#define SHOW_GRID | |
#include <flutter/runtime_effect.glsl> | |
uniform vec2 uSize; // size of the shape (iResolution) | |
uniform float uSeed; // shader playback time (in seconds) | |
uniform vec4 uBackground; // the color of rectangle | |
uniform vec4 uColor; // the color of (internal) border | |
uniform vec4 uRadius; // the radiuses of the corners in pixels (topRight, bottomRight, topLeft, bottomLeft) | |
uniform float uGap; // gap between the lines | |
uniform float uStroke; // stroke length of the lines | |
uniform float uBorder; // The border thicness/size/width (in pixels) of the shape | |
uniform vec4 uShadowColor; // shadow color of the shape | |
uniform vec2 uShadowOffset; // shadow offset of the shape | |
uniform float uShadowBlur; // shadow blur of the shape | |
uniform float uShadowSpread; // shadow spread of the shape | |
uniform float uOffset; // offset | |
// uSize - size of the shape (iResolution) | |
// gl_FragCoord.xy - pixel coordinate of the fragment (fragCoord) | |
// Calculate normalized coordinates: | |
// vec2 normalizedCoords = gl_FragCoord.xy / uSize; | |
// fragColor - output color of the fragment | |
out vec4 fragColor; | |
// from https://iquilezles.org/articles/distfunctions | |
// additional thanks to iq for optimizing conditional block for individual | |
// corner radii! | |
float roundedBoxSDF(vec2 CenterPosition, vec2 Size, vec4 Radius) { | |
Radius.xy = (CenterPosition.x > 0.0) ? Radius.xy : Radius.zw; | |
Radius.x = (CenterPosition.y > 0.0) ? Radius.x : Radius.y; | |
vec2 q = abs(CenterPosition) - Size + Radius.x; | |
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - Radius.x; | |
} | |
void main() { | |
// ========================================================================= | |
// Inputs (uniforms) | |
vec2 u_rectSize = | |
uSize - vec2(uOffset * 2.0); // The pixel-space scale of the rectangle. | |
vec2 u_rectCenter = | |
(uSize.xy / 2.0); // The pixel-space rectangle center location | |
float u_edgeSoftness = | |
0.5; // How soft the edges should be (in pixels). Higher values could be | |
// used to simulate a drop shadow. | |
// Border | |
float u_borderSoftness = | |
2.0; // How soft the (internal) border should be (in pixels) | |
// Shadow | |
float u_shadowSoftness = uShadowBlur; // The (half) shadow radius (in pixels) | |
vec2 u_shadowOffset = | |
uShadowOffset; // The pixel-space shadow offset from rectangle center | |
// Colors | |
vec4 u_colorBgFillColor = vec4(0, 0, 0, 0.0); // The color of background | |
vec4 u_colorShadow = uShadowColor; // The color of shadow | |
// ========================================================================= | |
vec2 halfSize = (u_rectSize / 2.0); // Rectangle extents (half of the size) | |
vec4 radius = | |
max(uRadius * 0.75, | |
vec4((sin(uSeed) + 0.5)) * uRadius); // Animated corners radiuses | |
// ------------------------------------------------------------------------- | |
// Calculate distance to edge. | |
float distance = | |
roundedBoxSDF(gl_FragCoord.xy - u_rectCenter, halfSize, radius); | |
// Smooth the result (free antialiasing). | |
float smoothedAlpha = 1.0 - smoothstep(0.0, u_edgeSoftness, distance); | |
// ------------------------------------------------------------------------- | |
// Border. | |
float borderAlpha = | |
1.0 - smoothstep(uBorder - u_borderSoftness, uBorder, abs(distance)); | |
// ------------------------------------------------------------------------- | |
// Apply a drop shadow effect. | |
float shadowDistance = roundedBoxSDF( | |
gl_FragCoord.xy - u_rectCenter + u_shadowOffset, halfSize, radius); | |
float shadowAlpha = | |
max(0, uShadowColor.a - smoothstep(-u_shadowSoftness, u_shadowSoftness, | |
shadowDistance)); | |
// Apply colors layer-by-layer: background <- shadow <- rect <- border. | |
// Blend background with shadow | |
vec4 res_shadow_color = mix( | |
u_colorBgFillColor, vec4(u_colorShadow.rgb, shadowAlpha), shadowAlpha); | |
// Blend (background+shadow) with rect | |
// Note: | |
// - Used 'min(uBackground.a, smoothedAlpha)' instead of 'smoothedAlpha' | |
// to enable rectangle color transparency | |
vec4 res_shadow_with_rect_color = | |
mix(res_shadow_color, uBackground, min(uBackground.a, smoothedAlpha)); | |
// Blend (background+shadow+rect) with border | |
// Note: | |
// - Used 'min(borderAlpha, smoothedAlpha)' instead of 'borderAlpha' | |
// to make border 'internal' | |
// - Used 'min(uColor.a, alpha)' instead of 'alpha' to enable | |
// border color transparency | |
vec4 res_shadow_with_rect_with_border = | |
mix(res_shadow_with_rect_color, uColor, | |
min(uColor.a, min(borderAlpha, smoothedAlpha))); | |
// ------------------------------------------------------------------------- | |
// Set output color | |
//fragColor = res_shadow_with_rect_with_border; | |
// ... | |
// Если borderAlpha не отрицательная, то пустить лучь из центра | |
// Just for debug purposes | |
/* if (borderAlpha > 0 && gl_FragCoord.x > gl_FragCoord.y) { | |
fragColor = vec4(1.0, 0.0, 0.0, 1.0); | |
return; | |
} */ | |
// Set output color | |
fragColor = res_shadow_with_rect_with_border; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment