Skip to content

Instantly share code, notes, and snippets.

@kherel
Forked from wilsonowilson/distortion.dart
Created June 2, 2021 06:28
Show Gist options
  • Select an option

  • Save kherel/5569da4f3bf521ad7b3db2ba25ff7c19 to your computer and use it in GitHub Desktop.

Select an option

Save kherel/5569da4f3bf521ad7b3db2ba25ff7c19 to your computer and use it in GitHub Desktop.

Revisions

  1. @wilsonowilson wilsonowilson created this gist Mar 17, 2021.
    173 changes: 173 additions & 0 deletions distortion.dart
    Original file line number Diff line number Diff line change
    @@ -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;
    }
    }