【Flutter】BlendModeで画像の加工をマスター

対象者

  • ユーザーインターフェースを美しく、またはユニークにデザインしたい中級者
  • BlendModeの理解を深め、より高度な画像処理やアニメーションに挑戦したい上級者

はじめに

BlendModeはUIデザインの視覚的な側面をよりリッチにするための強力なツールです。しかし、それらのBlendModeが何を行い、どのように使用すればいいのかを理解するのは初心者にとっては難しいかもしれません。そのため、この記事ではさまざまなBlendModeを具体的な例とともに紹介し、それぞれの効果と使い道を説明します。これにより、初心者でもBlendModeの基本を理解し、自分のアプリ開発に役立てることができるようになるでしょう。

BlendModeの各モード

アプリを作成し、以下の点を確認できるようにしています。

  • 上部で写真に対しての影響
  • 中段は白背景に対しての影響
  • 下段は白背景に対しての影響
  • 中段、下段は、白、黒、青の丸に対しての影響

元のソース画像として、赤から青のグラデーションを使用しています。

    ShaderMask(
      shaderCallback: (Rect bounds) {
            return LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [Colors.red, Colors.blue],
              stops: [0.0, 1.0],
            ).createShader(bounds);
      },
      blendMode: blendMode,
      child: Image.network(imageUrl),
    ),

clear

全てを透明にします。ソース画像と背景画像、両方を消すのに使えます。

src

元のソース画像のみを表示します。背景画像を消すのに使えます。

dst

背景画像のみを表示します。ソース画像を消すのに使えます。

srcOver

元のソース画像を背景画像の上に表示します。(ソース画像が大きいため、srcと変わらなくなっている、、)

dstOver

背景画像を元のソース画像の上に表示します。

srcIn

元のソース画像と背景画像が交差する部分のみを表示します。ソース画像の色を使います。

dstIn

背景画像と元のソース画像が交差する部分のみを表示します。背景画像の色を使います

srcOut

元のソース画像と背景画像が交差しない部分のみを表示します。ソース画像の色を使います。

dstOut

背景画像と元のソース画像が交差しない部分のみを表示します。背景画像の色を使います

srcATop

元のソース画像と背景画像が交差する部分と、背景画像と交差しない部分を表示します。つまり、元のソース画像は背景画像と重なっている部分にのみ表示され、その部分の色は元のソース画像の色が使用されます。一方、背景画像と重ならない部分は、元の背景画像がそのまま表示されます。

dstATop

背景画像と元のソース画像が交差する部分と、元のソース画像と交差しない部分を表示します。
重なる部分の色は背景画像の色が使用されます。重ならない部分では、元のソース画像が表示されます。

xor

元のソース画像と背景画像が交差しない部分のみを表示します。
2つの画像の重なり合う部分を除外する際などにマスキングに使用されます。

plus

元のソース画像と背景画像の色を加算します。
色の強度を増加させますので、明るさを強調する際などに使用されます。

modulate

元のソース画像と背景画像の色を乗算します。色調を調整する際などに使用されます。

screen

元のソース画像と背景画像の色を反転させたものを乗算し、結果を反転させます。

overlay

背景画像の明るさに基づいて元のソース画像を調整します。

darken

元のソース画像と背景画像の色の中で、より暗い方を選びます。

lighten

元のソース画像と背景画像の色の中で、より明るい方を選びます。

colorDodge

元のソース画像の色を背景画像の色の逆数で除算します。

colorBurn

背景画像の色を元のソース画像の色の逆数で除算します。

hardLight

元のソース画像の色に基づいて背景画像を調整します。

softLight

元のソース画像の色に基づいて背景画像をより柔らかく調整します。

difference

元のソース画像と背景画像の色の差分を計算します。

exclusion

元のソース画像と背景画像の色の差分をより少ないコントラストで計算します。

multiply

元のソース画像と背景画像の色を乗算します。

hue

背景画像の彩度と明度を保持したまま、元のソース画像の色相を適用します。

saturation

背景画像の色相と明度を保持したまま、元のソース画像の彩度を適用します。

color

背景画像の明度を保持したまま、元のソース画像の色相と彩度を適用します。

luminosity

元のソース画像の色相と彩度を保持したまま、背景画像の明度を適用します。

Q&A

Q: BlendModeを使用する主な利点は何ですか?
A: BlendModeは二つ以上の画像や色を重ねる際に、その重ねた結果の見た目をコントロールするための手段です。これにより、複雑なエフェクトや美しいグラデーションなどを生成でき、アプリのビジュアル体験を豊かにします。

Q: より高度なBlendModeの効果を生成するためにはどうすればよいですか?
A: 高度な効果を生成するためには、複数のBlendModeを組み合わせて使用することがあります。また、元のソース画像や背景画像の色調を微調整することで、微妙な効果を実現することも可能です。

Q: BlendModeの使用にあたって注意する点はありますか?
A: BlendModeは画像や色の重ね合わせに基づく効果を生成するため、重ね合わせる元の画像や色によっては意図した結果が得られない場合があります。そのため、BlendModeの選択と使用は、期待する効果に基づいて慎重に行う必要があります。

まとめ

本記事では、BlendModeが持つポテンシャルとその応用方法を解説しました。まず、BlendModeとは何かを紹介し、その重要性を理解していただきました。さらに、28種類のBlendModeが存在することを把握し、それぞれの特性と使い方を学びました。これにより、Flutterを用いてより表現豊かなデザインを作る力を身につけました。

さらに、具体的なアプリケーションを通じてBlendModeの使用法を実践し、美しい色彩表現やダイナミックな画像表現が可能であることを学びました。これにより、Flutterの使用範囲が大きく広がったと実感していただけたことでしょう。

これからのFlutterでの開発が、より一層楽しく、創造的になることを願っています。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BlendMode Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String imageUrl =
      'https://flutter.salon/wp-content/uploads/2021/11/IMGP7872.jpg'; //default image URL
  BlendMode blendMode = BlendMode.srcOver; //default BlendMode
  final urlController = TextEditingController();
  final List<BlendMode> blendModes = BlendMode.values;

  @override
  void initState() {
    super.initState();
    urlController.text = imageUrl;
  }

  @override
  Widget build(BuildContext context) {
    print(BlendMode.values.toList().toString());
    return Scaffold(
      appBar: AppBar(
        title: Text('BlendMode Demo'),
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: TextField(
                controller: urlController,
                decoration: InputDecoration(
                  border: OutlineInputBorder(),
                  labelText: 'Image URL',
                ),
              ),
            ),
            FilledButton(
              onPressed: () {
                setState(() {
                  imageUrl = urlController.text;
                });
              },
              child: Text('Apply'),
            ),
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: ToggleButtons(
                  isSelected:
                      BlendMode.values.map((e) => e == blendMode).toList(),
                  children: BlendMode.values
                      .map(
                        (e) => TextButton(
                          onPressed: () {
                            setState(() {
                              blendMode = e;
                            });
                          },
                          child: Text(
                            e.name,
                            style: TextStyle(
                                color: e == blendMode
                                    ? Colors.blue
                                    : Colors.black),
                          ),
                        ),
                      )
                      .toList()),
            ),
            ShaderMask(
              shaderCallback: (Rect bounds) {
                return LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  colors: [Colors.red, Colors.blue],
                  stops: [0.0, 1.0],
                ).createShader(bounds);
              },
              blendMode: blendMode,
              child: Image.network(imageUrl),
            ),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                ShaderMaskCircle(blendMode: blendMode, color: Colors.white),
                ShaderMaskCircle(blendMode: blendMode, color: Colors.black),
                ShaderMaskCircle(blendMode: blendMode, color: Colors.blue),
              ],
            ),
            ColoredBox(
              color: Colors.black,
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  ShaderMaskCircle(blendMode: blendMode, color: Colors.white),
                  ShaderMaskCircle(blendMode: blendMode, color: Colors.black),
                  ShaderMaskCircle(blendMode: blendMode, color: Colors.blue),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ShaderMaskCircle extends StatelessWidget {
  const ShaderMaskCircle({
    super.key,
    required this.blendMode,
    required this.color,
  });

  final BlendMode blendMode;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return ShaderMask(
      shaderCallback: (Rect bounds) {
        return LinearGradient(
          begin: Alignment.topCenter,
          end: Alignment.bottomCenter,
          colors: [Colors.red, Colors.blue],
          stops: [0.0, 1.0],
        ).createShader(bounds);
      },
      blendMode: blendMode,
      child: Container(
        height: 100,
        width: 100,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: color,
        ),
      ),
    );
  }
}