Flutter notes

Flutterに関するメモ書き

imageパッケージでアニメーションGIF生成(その2)

前回の記事(↓)の続き。

flutter.tnantoka.com

確かにアニメーション速度が遅くなっている。 それは今後の課題とする。 あと生成時間が長いのでそこもなんとかしたい。

の部分の対応。

前回は色の透明度をアニメーションさせたが、わかりづらいので変えた。 以下の記事を参考に波のアニメーションにした。

qiita.com

painter.dart

色は固定にして、経過時間を受け取るようにしている。

class Painter extends CustomPainter {
  final double time;

  Painter(this.time);

  @override
  void paint(Canvas canvas, Size size) {
    final Paint background = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;
    final Paint foreground = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    final height = 30;

    final Path path = Path();
    path.moveTo(0, size.height);

    Offset prevPoint = Offset.zero;
    for (int i = 0; i <= size.width / 3; i += 1) {
      final step = (i / size.width) - time;
      final x = i.toDouble() * 3;
      final y = sin(step * 2 * pi) * height + height;

      path.lineTo(x, y);
    }

    path.lineTo(size.width, size.height);
    path.close();

    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), background);
    canvas.drawPath(path, foreground);
  }

  @override
  bool shouldRepaint(Painter oldDelegate) => oldDelegate.time != time;
}

main.dart (_MyHomePageState)

前回との変更点

  • Tween無しにした(使わなくても行けたので)
  • 生成速度を改善するため、samplingFactorを100に(デフォルトは10)
  • フレームレートを改善するため、addListenerでフレームを取得するのをやめ、Timer.periodicで間隔をあけて取得。最大フレーム数も100に制限。
    • durationやdelayをいじっても駄目だった(その残骸がコメントアウトとして残ってる)
class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  final List<ui.Image> _images = [];
  final _repaintBoundaryKey = GlobalKey();
  var _generating = false;
  late Timer _timer;

  @override
  void initState() {
    super.initState();

    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )
      // ..addListener(_capture)
      ..repeat();

    _timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
      _capture();
    });
  }

  void _capture() async {
    final RenderRepaintBoundary boundary = _repaintBoundaryKey.currentContext
        ?.findRenderObject() as RenderRepaintBoundary;
    if (boundary.debugNeedsPaint) {
      return;
    }
    final image =
        await boundary.toImage(pixelRatio: ui.window.devicePixelRatio);
    setState(() {
      _images.add(image);
      if (_images.length > 100) {
        _images.removeAt(0);
      }
    });
  }

  @override
  void dispose() {
    _animationController.dispose();
    _timer.cancel();
    super.dispose();
  }

  void _generateGif() async {
    setState(() {
      _generating = true;
    });
    _animationController.stop();
    _timer.cancel();

    final animation = image.Animation();
    final directory = await getApplicationDocumentsDirectory();

    final options = {
      "animation": animation,
      "path": directory.path,
    };

    for (final frameImage in _images.toList()) {
      final imageBytes = await frameImage.toByteData();

      final translatedImage = image.Image.fromBytes(
        frameImage.width,
        frameImage.height,
        imageBytes!.buffer.asUint8List().toList(),
      );
      // translatedImage.duration = 1;

      animation.addFrame(translatedImage);
    }

    compute(_encodeGif, options).then((result) {
      setState(() {
        _generating = false;
      });
    });
  }

  static void _encodeGif(Map<String, Object> option) async {
    Stopwatch stopwatch = Stopwatch();
    stopwatch.start();

    final encoder = image.GifEncoder(samplingFactor: 100 /*, delay: 1 */);

    final animation = option["animation"] as image.Animation;
    final path = option["path"] as String;

    final encoded = encoder.encodeAnimation(animation);

    final file = File('${path}/generated.gif');
    await file.writeAsBytes(encoded!.whereType<int>().toList());

    print('completed ${file.path}');

    stopwatch.stop();
    print('${stopwatch.elapsed}');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
              animation: _animationController,
              builder: (context, child) {
                final Size size = MediaQuery.of(context).size;
                return Stack(
                  children: <Widget>[
                    RepaintBoundary(
                      key: _repaintBoundaryKey,
                      child: Container(
                        color: Colors.transparent,
                        width: size.width,
                        height: 300,
                        child: CustomPaint(
                          painter: Painter(
                            _animationController.value,
                          ),
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
            Container(
              margin: EdgeInsets.only(top: 16),
              child: Visibility(
                visible: _generating,
                maintainSize: true,
                maintainAnimation: true,
                maintainState: true,
                child: CircularProgressIndicator(),
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _generateGif,
        tooltip: 'Generate',
        child: Icon(Icons.save),
      ),
    );
  }
}

生成されたGIFはこれ。45秒程度で生成されるようになったのでまぁ許せる範囲になった。

f:id:tnantoka:20210705213824g:plain