Created
November 20, 2023 05:47
-
-
Save CoMatu/ed1e851145ec2b279d72ff46b7a2f3d4 to your computer and use it in GitHub Desktop.
Shader + Text
This file contains hidden or 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 '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