Skip to content

Instantly share code, notes, and snippets.

@wilsonowilson
Created March 17, 2021 19:05
Show Gist options
  • Save wilsonowilson/6d0b56cab01fb5f00e110eb3b413dd79 to your computer and use it in GitHub Desktop.
Save wilsonowilson/6d0b56cab01fb5f00e110eb3b413dd79 to your computer and use it in GitHub Desktop.
Widget warping and distortion in Flutter
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;
}
}
@backandy
Copy link

Dear Wilson. Can it be used with a video to undistort a cylindrical view?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment