Skip to content

Instantly share code, notes, and snippets.

@CoMatu
Created November 20, 2023 05:47
Show Gist options
  • Select an option

  • Save CoMatu/ed1e851145ec2b279d72ff46b7a2f3d4 to your computer and use it in GitHub Desktop.

Select an option

Save CoMatu/ed1e851145ec2b279d72ff46b7a2f3d4 to your computer and use it in GitHub Desktop.
Shader + Text
import 'dart:math';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
const Color background = Colors.white;
const text = 'TEXT\nSHADER';
class MainWidget extends StatelessWidget {
const MainWidget({super.key});
@override
Widget build(BuildContext context) {
// 1. Create text child
const textChild = TextChild(text: text);
// 2. Create ImageProvider
const imageProvider = NetworkImage(
'https://media.giphy.com/media/5VKbvrjxpVJCM/giphy.gif',
// 'https://picsum.photos/1000',
);
// 3. Apply shader to text child
const textWithShader = ImageShaderBuilder(
imageProvider: imageProvider,
child: textChild,
);
// 4. Add background
return LayoutBuilder(
builder: (context, constraints) {
return Container(
height: constraints.maxHeight / 2,
width: constraints.maxWidth * 0.8,
color: background,
padding: const EdgeInsets.all(16),
child: textWithShader,
);
},
);
}
}
// IMAGE STREAM BUILDER
// See MyImage class from Flutter docs
// https://api.flutter.dev/flutter/painting/ImageProvider-class.html
class ImageShaderBuilder extends StatefulWidget {
const ImageShaderBuilder({
super.key,
required this.imageProvider,
// Add child widget
required this.child,
});
// Add child widget
final Widget child;
final ImageProvider imageProvider;
@override
State<ImageShaderBuilder> createState() => _ImageShaderBuilderState();
}
// IMAGE STREAM BUILDER STATE
// See MyImage class from Flutter docs
// https://api.flutter.dev/flutter/painting/ImageProvider-class.html
class _ImageShaderBuilderState extends State<ImageShaderBuilder> {
ImageStream? _imageStream;
ImageInfo? _imageInfo;
// Create image shader for given Rect size
ShaderCallback createImageShader(ui.Image image) {
shaderCalback(Rect bounds) {
// Calculate scale for X and Y sides
final scaleX = bounds.width / image.width;
final scaleY = bounds.height / image.height;
final scale = max(scaleX, scaleY);
// Calculate offset to center resized image
final scaledImageWidth = image.width * scale;
final sacledImageHeight = image.height * scale;
final offset = Offset(
(scaledImageWidth - bounds.width) / 2,
(sacledImageHeight - bounds.height) / 2,
);
final matrix = Matrix4.identity()
// Scale image horizontally and vertically
..scale(scale, scale)
// Center horizontally and vertically
..leftTranslate(
-offset.dx,
-offset.dy,
);
// Image shader
return ImageShader(
image,
TileMode.decal,
TileMode.decal,
matrix.storage,
);
}
return shaderCalback;
}
// Change Build function
@override
Widget build(BuildContext context) {
final image = _imageInfo?.image;
// No image for shader -> show child
if (image == null) {
return widget.child;
}
final shaderCallback = createImageShader(image);
// Apply shader to the child
return ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: shaderCallback,
child: widget.child,
);
}
// Keep source code
@override
void didChangeDependencies() {
super.didChangeDependencies();
// We call _getImage here because createLocalImageConfiguration() needs to
// be called again if the dependencies changed, in case the changes relate
// to the DefaultAssetBundle, MediaQuery, etc, which that method uses.
_getImage();
}
@override
void didUpdateWidget(ImageShaderBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.imageProvider != oldWidget.imageProvider) {
_getImage();
}
}
void _getImage() {
final ImageStream? oldImageStream = _imageStream;
_imageStream =
widget.imageProvider.resolve(createLocalImageConfiguration(context));
if (_imageStream!.key != oldImageStream?.key) {
// If the keys are the same, then we got the same image back, and so we don't
// need to update the listeners. If the key changed, though, we must make sure
// to switch our listeners to the new image stream.
final ImageStreamListener listener = ImageStreamListener(_updateImage);
oldImageStream?.removeListener(listener);
_imageStream!.addListener(listener);
}
}
void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
setState(() {
// Trigger a build whenever the image changes.
_imageInfo?.dispose();
_imageInfo = imageInfo;
});
}
@override
void dispose() {
_imageStream?.removeListener(ImageStreamListener(_updateImage));
_imageInfo?.dispose();
_imageInfo = null;
super.dispose();
}
}
// TEXT CHILD
class TextChild extends StatelessWidget {
final String text;
const TextChild({
super.key,
required this.text,
});
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.contain,
child: Text(
text,
textAlign: TextAlign.center,
softWrap: false,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 1000,
),
),
);
}
}
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: const Scaffold(
body: Center(
child: MainWidget(),
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment