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

July 5, 2021

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

https://flutter.tnantoka.com/entry/2021/06/30/230206

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

の部分の対応。

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

https://qiita.com/derodero24/items/590f97ecec2900a947f0

painter.dart

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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をいじっても駄目だった(その残骸がコメントアウトとして残ってる)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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秒程度で生成されるようになったのでまぁ許せる範囲になった。