Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Created January 15, 2024 16:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save PlugFox/be3bb8873da4433c14e21201a4581653 to your computer and use it in GitHub Desktop.
Save PlugFox/be3bb8873da4433c14e21201a4581653 to your computer and use it in GitHub Desktop.
Flutter Card with shader
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;
}
// 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