|
|
@@ -0,0 +1,173 @@ |
|
|
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; |
|
|
} |
|
|
} |