-
-
Save kherel/5569da4f3bf521ad7b3db2ba25ff7c19 to your computer and use it in GitHub Desktop.
Widget warping and distortion in Flutter
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:typed_data'; | |
import 'dart:ui' hide Image; | |
import 'package:image/image.dart' as img_lib; | |
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
enum ImageFetchState { initial, fetching, fetched } | |
class ImagePlayground extends StatefulWidget { | |
@override | |
_ImagePlaygroundState createState() => _ImagePlaygroundState(); | |
} | |
class _ImagePlaygroundState extends State<ImagePlayground> | |
with SingleTickerProviderStateMixin { | |
GlobalKey _repaintKey = GlobalKey(); | |
AnimationController controller; | |
Animation<double> animation; | |
img_lib.Image image; | |
// Number of images to create | |
static const _frames = 90; | |
// List of fetched images | |
List<MemoryImage> imageCache = []; | |
ImageFetchState fetchState = ImageFetchState.initial; | |
@override | |
void initState() { | |
controller = AnimationController( | |
vsync: this, | |
duration: Duration(milliseconds: 500), | |
); | |
animation = CurvedAnimation(parent: controller, curve: Curves.ease); | |
super.initState(); | |
} | |
// This is extremely unperformant, and can definitely be | |
// handled better, but for demo purposes here it shall stay. | |
void fillImageCache() async { | |
fetchState = ImageFetchState.fetching; | |
final stopwatch = Stopwatch()..start(); | |
if (image == null) { | |
image = await _getImageFromWidget(); | |
} | |
final intensity = 15.0; | |
for (var i = 0; i < _frames; i++) { | |
final f = (i / _frames) * intensity; | |
final result = await multidirectionalImageWaveTransform(f); | |
imageCache.add(MemoryImage(result)); | |
} | |
for (final img in imageCache) { | |
// Cache the image so it shows up during the animation. | |
await precacheImage(img, context); | |
} | |
print(stopwatch.elapsed); | |
fetchState = ImageFetchState.fetched; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
backgroundColor: Colors.black, | |
body: Center( | |
child: MouseRegion( | |
onHover: (_) async { | |
// The widget is cached for the first time when hovered. | |
// WARNING. This will freeze the app. | |
if (fetchState == ImageFetchState.initial) | |
fillImageCache(); | |
else if (fetchState == ImageFetchState.fetching) | |
return; | |
else if (!controller.isAnimating) controller.forward(); | |
}, | |
onExit: (_) { | |
controller.reverse(); | |
}, | |
child: AnimatedBuilder( | |
animation: controller, | |
builder: (context, _) { | |
final curr = (animation.value * (_frames - 1)).toInt(); | |
return ClipRRect( | |
child: RepaintBoundary( | |
key: _repaintKey, | |
child: Transform.scale( | |
scale: 1 + animation.value * 0.2, | |
child: Container( | |
width: 400, | |
height: 400, | |
child: imageCache.isEmpty | |
? Center( | |
child: Text('Hello world!', | |
style: TextStyle( | |
fontSize: 50, | |
fontWeight: FontWeight.w900, | |
color: Colors.white,),), | |
) | |
: null, | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(20), | |
image: imageCache.isEmpty | |
? null | |
: DecorationImage( | |
image: (imageCache[curr]), | |
fit: BoxFit.cover, | |
), | |
), | |
), | |
), | |
), | |
); | |
}), | |
), | |
), | |
); | |
} | |
/// USE THIS FOR HORIZONTAL EFFECT | |
// Future<Uint8List> horizontalImageWaveTransform(double intensity) async { | |
// final image = this.image.clone(); | |
// final imageX = image.clone(); | |
// for (var i = 0; i < image.height; i++) { | |
// final offsetX = intensity * math.sin(2 * 3.14 * i / 180); | |
// for (var j = 0; j < image.width; j++) { | |
// final jx = (j + offsetX.toInt()) % image.height; | |
// if (j + offsetX < image.height) | |
// image.setPixel( | |
// i, | |
// jx, | |
// imageX.getPixel(i, j), | |
// ); | |
// // else | |
// // image.setPixel(i, j, getColor(0, 0, 0, 0)); | |
// } | |
// } | |
// final imageData = img_lib.encodePng(image); | |
// return imageData; | |
// } | |
Future<Uint8List> multidirectionalImageWaveTransform(double intensity) async { | |
final image = this.image.clone(); | |
final imageX = image.clone(); | |
for (var i = 0; i < image.height; i++) { | |
for (var j = 0; j < image.width; j++) { | |
final offsetX = intensity * math.sin(2 * 3.14 * i / 150); | |
final offsetY = intensity * math.cos(2 * 3.14 * j / 150); | |
final jx = (j + offsetX.toInt()) % image.width; | |
final ix = (i + offsetY.toInt()) % image.height; | |
if (j + offsetX < image.width && i + offsetY < image.height) | |
image.setPixel( | |
jx, | |
i, | |
imageX.getPixel(j, ix), | |
); | |
} | |
} | |
final imageData = img_lib.encodePng(image); | |
return imageData; | |
} | |
Future<img_lib.Image> _getImageFromWidget() async { | |
RenderRepaintBoundary boundary = | |
_repaintKey.currentContext.findRenderObject(); | |
final img = await boundary.toImage(pixelRatio: 2); | |
final byteData = await img.toByteData(format: ImageByteFormat.png); | |
final pngBytes = byteData.buffer.asUint8List(); | |
final image = img_lib.decodePng(pngBytes); | |
return image; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment