Flutterで2つの画像を合成する

Flutterで2枚の画像をこんな感じで合成したかったのですが、試行錯誤することになったので備忘録として残します。 f:id:chari_ngo:20200410225618p:plain

画像表示おさらい

Flutterはこんな感じにするだけで簡単に画像を表示することができるのでした。

Image.asset("assets/image.png")

f:id:chari_ngo:20200410230436p:plain
オリジナル画像

また画像に対して色を合成することもできます。 BlendModeに関しては一般的なものなので今回は詳しく触れませんが、公式ドキュメント が詳しいです。

Image.asset(
  "assets/gear.png",
  color: Colors.green,
  colorBlendMode: BlendMode.srcIn,
)

f:id:chari_ngo:20200410230553p:plain
色を合成した画像

正直ここまではAndoirdStudioのサジェストに従っていけば難なくこなせるでしょう。

画像どうしを合成するには

UIに凝ってくるとある画像に対して単色ではなくテクスチャを合成したい要求が出てくるかもしれません。先の例でいう歯車に対して緑色を合成するように以下のテクスチャの合成方法を考えます。

f:id:chari_ngo:20200410230945p:plain
合成したいテクスチャ

画像と色の合成に関しはFlutterのWidgetクラスの内部で行われているため、単純に考えると自前でCustomPainterなどを実装しCanvasに対する描画するロジックを書かねばなりません。 これははっきり言って面倒で運用コストが掛かるので他の方法を考える必要があります。

そこでFlutterが公式に提供しているShaderMaskというクラスを利用します。このクラスではシェーダを使ってマスク処理を行ってくれます。以下の動画の例ではカラーグラデーションやアニメーションなどを定義し、シェーダに変換することでWidgetに対してマスク処理を適用しています。 www.youtube.com

つまり画像データもカラーグラデーションなどのベクタデータ同様に、シェーダに変換できればShaderMaskクラスを利用して2つの画像を合成することができます。

画像を合成方法と結果

まず画像をbyteデータとして読み込み、画像としてデコードします。ここではassets配下にあるpng画像と取り込んでいます。

Future<ui.Image> _loadPngImage(String assetPath) async {
  final byteData = await rootBundle.load(assetPath);
  return decodeImageFromList(byteData.buffer.asUint8List());
}

画像がSVGの場合はflutter_svg を利用し以下のように画像を読み込むことができます。

Future<ui.Image> _loadSvgImage(String assetPath) async {
  final rawSvg = await rootBundle.loadString(assetPath);
  final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, rawSvg);
  final Picture picture = svgRoot.toPicture();
  // 元画像のサイズに合わせる
  return picture.toImage(32, 32);
}

画像をFutureクラスで受けとり、画像の読み込みに成功した場合はシェーダに変換し、ShaderMaskに設定します。ShaderMaskのblendModeは単色合成のときと同様に扱えます。シェーダに変換されたテクスチャが単色に相当します。

FutureBuilder(
    future: _loadPngImage("assets/texture.png"),
    builder: (context, AsyncSnapshot<ui.Image> image) {
      return (image.hasData)
          ? ShaderMask(
              child: Image.asset("assets/image.png"),
              shaderCallback: (bounds) => ImageShader(
                image.data,
                TileMode.repeated,
                TileMode.repeated,
                Matrix4.identity().storage,
              ),
              blendMode: BlendMode.srcIn,
            )
          : Container();
    }),

このように画像をシェーダに変換することで2つの画像を合成することが可能です。

f:id:chari_ngo:20200410232448p:plain
テクスチャ合成後の画像

全体のサンプルプロジェクトはこちらに置いていますので、参考にしていただければと思います。

github.com

おわりに

Android(Java)時代のつらさしか知らなかったのですが、Flutterはけっこう楽しいですね