BEMAロゴ

エンジニアの
成長を支援する
技術メディア

【Color Matrix】Flutter画像処理入門:行列計算とBT.709輝度係数で実現するダイナミック色相アニメーション【Hue Rotation】

この記事は「BEMA Lab Advent Calendar 2025Open in new tab」の6日目の記事です。
※本アドベントカレンダーの6日目の投稿となります。

はじめに 

こんにちは、株式会社メンバーズ Cross ApplicationOpen in new tab カンパニーの田原です。

FlutterやWeb開発で画像を扱う際、その色を意のままに操りたいと思ったことはありませんか?単なるフィルターに留まらず、時間をかけて色相(Hue)を一周させるような、虹色に変化し続けるダイナミックなアニメーションを想像してみてください。

添付のgifアニメーションをご覧ください。これは、BEMAのロゴ画像に色相回転のアニメーション効果を適用したものです。画像の明るさを完璧に保ちながら、色相が連続的に変化しているのがお分かりいただけるでしょう。

しかし、このような高度な色相回転を、画像の明るさを変えずに実現するには、RGB空間を超えた行列計算色彩理論が必要です。

本記事は、FlutterのColorMatrixを使いこなし、BT.709輝度係数という国際規格の値を組み込むことで、このダイナミックな色相ローテーションアニメーションを実装する手順を、理論からコードまで徹底的に解説するFlutter画像処理入門です。本記事を通して、あなたもこのような効果をライブラリに頼らずに実装できるようになります。

本記事のゴール

本記事は、読者の皆さんが「理論編」で原理を理解し、「実装編」でコードに落とし込むための、明確な2ステップロードマップを提供します。

1. 理論の理解

  • 輝度分離の核心: RGB空間で色相回転を行う際に、なぜ予期せず明るさまで変わってしまうのか? その本質的な問題を防ぐために「輝度分離」という考え方を理解します。
  • ColorMatrixの基盤: FlutterのColorFilteredウィジェットが内部で使用するColorMatrix(4×5行列)の構造と、BT.709輝度係数がなぜ不可欠なのかを把握します。

2. 実装の実現

  • 数式のコード化: 輝度分離のロジックを統合した色相回転の行列数式を、Flutterのコード(ColorMatrixGenerator)に正確に落とし込みます。
  • アニメーション実装: AnimationControllerと動的に生成されるColorMatrixを組み合わせ、画像の明るさを変えずに色相を360度回転させ続けるアニメーションを完成させます。


    単にライブラリを使うのではなく、「画像処理の核心」に触れることで、あなたのFlutter開発の幅を大きく広げましょう。

理論編:色相回転の「なぜ?」輝度を保つための色彩学と数学

私たちが日常的に扱うデジタル画像はRGB(Red、 Green、 Blue)色空間で表現されていますが、この空間で色相(Hue)を操作しようとすると、しばしば予期せぬ問題に直面します。

画像処理の基本概念:色の三属性 

まず、色の世界で最も基本的な概念である「色の三属性」を確認しておきましょう。すべての色は、以下の3つの独立した性質によって特徴づけられます。

属性名

意味

役割

色相(Hue)

赤、青、緑など、色合いの違い(色み)。

本記事のテーマであり、色相環(360°)で表されます。

明度(Value/Lightness)

色の明るさの度合い。

HSVモデルでは純色が最大値となります。本記事で扱う「輝度(Luma)」は、最大値が白となるR, G, Bに基づく明るさで、白に近づくほど高く、黒に近づくほど低くなります。

彩度(Saturation)

色の鮮やかさの度合い。

鮮やかな純色ほど高く、灰色(無彩色)に近づくほど低くなります。

RGBの課題とHSV/HSLモデル

RGB空間モデルは色を光の三原色で混ぜ合わせる加法混色に基づいています。

しかし、RGB空間の最大の課題は、「色相」「彩度」「明るさ」という色の三属性が分離されていないことです。

例えば、RGB値をシンプルに回転させる処理を施すと、色相だけでなく、その色の明るさ(輝度/Luma)まで変化してしまい、全体的に暗くなったり、白飛びしたりする結果になりがちです。

この問題を解決し、色相や彩度を直感的に操作できるように考案されたのが、HSV(Hue, Saturation, Value/Brightness)やHSL(Hue, Saturation, Lightness)といったモデルです。これらのモデルは、「色相」を角度(0°〜 360°)として扱うため、色相のみを独立して変更することができます。

ColorMatrixを用いて正確に色相回転を実現するためには、HSV/HSLモデルと同様に「明るさ」の情報を分離して保つという原理、すなわち「輝度分離」が必要となります。

輝度分離を実現するYCbCr色空間

輝度分離を実装する上で、その数学的な土台となるのがYCbCr(またはYUV/YIQ)と呼ばれる色空間です。これは、デジタルビデオや画像圧縮(JPEGなど)で広く使用されます。

YCbCrの役割

YCbCrは、色を「明るさ(Y)」「色の違い(色差)」の成分に分離することで、人間の視覚特性に合わせた効率的なデータ処理を可能としています。

成分名

意味

RGBとの違い

Y (Luma / 輝度)

画像の明るさの度合い。

画像のコントラストやディテールを担う、人間の視覚が最も敏感な情報。

C_b (Chroma Blue)

青色と輝度(Y)の色差。

青系の色合いの情報。

C_r (Chroma Red)

赤色と輝度(Y)の色差。

赤系の色合いの情報。

色相回転アニメーションは、このY成分を固定したまま、C_bとC_rの色差成分を回転させることで実現されます。

人間の視覚特性と輝度係数

輝度Yを算出する際、R,  G,  Bの各成分にかけるL_R,  L_G,  L_Bは、人間の目が最も敏感に感じる緑色(G)に最も大きな重みを持たせることが国際規格で定められています。

私たちがFlutterやWebで扱う標準的なデジタル画像(sRGB)では、ITU-R BT.709の係数が使用されます。

規格名

主な適用範囲

LR​ (R係数)

LG​ (G係数)

LB​ (B係数)

ITU-R BT.709

HDTV、sRGB (PC/Web標準)

0.2126

0.7152

0.0722

コードでは、計算の単純化のため、以下の近似値がよく採用されます。

  • L_R = 0.213
  • L_G = 0.715
  • L_B = 0.072

これらの係数こそが、次のColorMatrixによる色相回転の数式を構築する土台となります。

ColorMatrixの基本と輝度係数

FlutterのColorFilteredウィジェットで画像の色を変換するために使用されるColorFilter.matrixは、20個のdouble値のリストを必要とします。これは、画像処理の世界で広く使われている ColorMatrix(色行列) の要素を、コンピューターが扱いやすいように平らに並べたものです。

ColorMatrixの基本構造(4x5行列)

ColorMatrixの役割は、元のピクセル色 (R, G, B, A) を、新しいピクセル色 (R', G', B', A') に変換することです。この変換は、以下の 4x5 の行列(4行5列)を用いて行われます。

R

G

B

A

1 (オフセット)

R'

m11

m12

m13

m14

m15

G'

m21

m22

m23

m24

m25

B'

m31

m32

m33

m34

m35

A'

m41

m42

m43

m44

m45

  • 20個の要素: 表にあるm11からm45までの合計20個の数値がColorFilter.matrixに渡すリストの順番に対応しています。

変換の仕組み:行列乗算と「同次座標系」の利用

色変換は行列乗算によって行われます。この計算を成立させるためには、元のピクセル情報である(R, G, B, A) を、「同次座標系」の考え方に基づき拡張された入力ベクトルに変換する必要があります。

入力ベクトルとは、行列計算にかけられる元のピクセルの色データを格納したリスト(配列)です。このベクトルがColorMatrixと掛け合わされることで、新しい色(出力ベクトル)が生成されます。

定数1の追加(同次座標系)

同次座標系を導入する主な理由は、ColorMatrixが乗算(色の混合やスケール)だけでなく、非線形変換である加算(明るさやコントラストのオフセット調整)も線形な行列乗算として行えるようにするためです。この目的のために、定数1をデータに追加します。

  • 元のデータ: (R, G, B, A)
  • 拡張されたデータ: (R, G, B, A, 1)

この1は、行列の5列目(m15〜m45)と掛け算され、その値が変換後の色にオフセット(加算)として適用されます。

  • 同次座標系とは: 厳密な幾何学用語ですが、ColorMatrixの文脈では「加算(オフセット)を行列の乗算の中に組み込むための仕組み」として理解します。次元を一つ増やすことで、平行移動や加算を線形の行列計算で表現可能にします。

変換計算の定義

この入力ベクトルとColorMatrixを乗算することで、新しい色の出力ベクトル (R', G', B', A') が得られます。

  • R' の計算: R' = m11 x R + m12 x G + m13 x B + m14 x A + m15 x 1

同様に、G', B', A' も行列の対応する行を使って計算されます。

各要素の役割

行列要素

役割

具体例

m11〜m44

線形変換(ミキシング・スケーリング)

画像をセピアやグレースケールにしたり、赤や緑を強調したりします。例えば、m11=2.0にすると、赤 (R') が元の赤 (R) の2倍になります。

m15〜m45

オフセット(加算)

各色成分に定数を加算します。例えば、m15=0.1 にすると、変換後の赤 (R') に明るさがプラスされます。これはコントラストや明るさ調整に不可欠です。

この行列の各要素に、先述の輝度分離と色差回転のロジックを詰め込んだものが、色相回転マトリックスとなります。

色相回転マトリックスの構造と数式

色相回転マトリックスは、画像を特定の色相へ回転させるために使われる特別な変換行列です。FlutterでColorFilter.matrixに渡すList<double>も、この行列を平らに並べたものです。


構造の基本要素

色相を角度θだけ回転させるためのこの行列は、主に以下の2つの要素から構成されます。

  1. 輝度係数 (L_R, L_G, L_B):
    これは、赤 (R)、緑 (G)、青 (B) の各成分が人間の感じる明るさ(輝度)にどれだけ寄与するかを示す重みです。この係数が、色の明るさを変えずに色相だけを正確に回転させるロジックの土台となります。
  2. 三角関数 (C, S):
    回転の角度θ を使ったコサイン (C = cosθ) とサイン (S = sinθ) の値です。これらが実際に色を「回転」させる働きを担います。

マトリックスの役割の分解

この行列の構造は複雑に見えますが、その役割は明確に分かれています。

行列要素

役割

説明

対角要素 (m11, m22, m33)

回転の中心を制御

画像の明るさ(輝度)を維持するように調整する役割を持ちます。これは輝度係数と三角関数の両方を用いて計算されます。

非対角要素 (その他)

色差成分の回転

色相の変化を実際に引き起こす部分です。輝度成分を取り除いた色差成分に対して、サインとコサインの値を用いて回転を適用します。

オフセット (m15, m25, m35, m45)

常にゼロ

色相回転は色の加算(オフセット)を必要としないため、これらの要素は通常すべて0に設定されます。

本質的な仕組み

色相回転マトリックスは、以下の3つのステップのロジックを単一の行列に統合したものです。

  1. RGB 輝度 / 色差: 色を明るさと色の違い(色差)に分離する。
  2. 色差の回転: 色差成分のみをθだけ回転させる。
  3. 輝度 / 色差 → RGB: 回転後の成分を元のRGB形式に戻す。

このように、色相回転マトリックスは、「輝度を維持しつつ、色差だけを回転させる」 という高度なロジックを20個の数値に詰め込んだものです。

実装編:FlutterでのColorMatrix生成とアニメーションの実装

理論を理解したところで、実際にFlutterで色相回転を実装していきます。核となるのは、動的にColorMatrixの20要素のリストを生成するロジックと、AnimationControllerを用いた滑らかなアニメーション制御です。

ColorMatrixの生成:ColorFilterGenerator

色相回転の角度θ(degrees)を受け取り、それに対応するColorMatrix(List<double>)を返す静的関数を定義します。これが最も重要な数式の実装部分です。

この行列は、前述の「RGB → 輝度分離 → 色差回転 → RGB」という一連の変換ロジックを一つの数式に統合したものです。

ColorMatrixの各要素(m_ij)は、輝度係数(L_R, L_G, L_B)と三角関数(C = cosθ, S = sinθ)を使って、以下の数式で定義されます。

R (m_i1)

G (m_i2)

B (m_i3)

R' (R係数)

L_R + C(1-L_R) + S(-L_R)

L_G + C(-L_G) + S(-L_G)

L_B + C(-L_B) + S(1-L_B)

G' (G係数)

L_R + C(-L_R) + S(0.143)

L_G + C(1-L_G) + S(0.140)

L_B + C(-L_B) + S(-0.283)

B' (B係数)

L_R + C(-L_R) + S(-(1-L_R))

L_G + C(-L_G) + S(L_G)

L_B + C(1-L_B) + S(L_B)

この数式を、行列の行優先で20個のdoubleのリストに平坦化して返します。

ColorFilterGenerator.hueAdjustMatrix のクラス/関数の構造

class ColorFilterGenerator {
  static List<double> hueAdjustMatrix(double degrees) {
    // 角度をラジアンに変換
    final value = degrees * pi / 180.0;
    final cosVal = cos(value);
    final sinVal = sin(value);

    // 輝度係数 (BT.709の近似値)
    const double lumR = 0.213;
    const double lumG = 0.715;
    const double lumB = 0.072;

    // YCbCr変換から派生する定数(輝度ベース行列係数)
    const double lumMatrixCoefficient1 = 0.143; 
    const double lumMatrixCoefficient2 = 0.140; 
    const double lumMatrixCoefficient3 = -0.283;

    // 色相回転マトリックスの20要素を定義
    return <double>[
      // R' = R行
      (lumR + cosVal  (1 - lumR) + sinVal  (-lumR)), // m11 (R->R)
      (lumG + cosVal  (-lumG) + sinVal  (-lumG)), // m12 (G->R)
      (lumB + cosVal  (-lumB) + sinVal  (1 - lumB)), // m13 (B->R)
      0, 0, // m14 (A), m15 (Bias)

      // G' = G行
      (lumR + cosVal  (-lumR) + sinVal  lumMatrixCoefficient1), // m21 (R->G)
      (lumG + cosVal  (1 - lumG) + sinVal  lumMatrixCoefficient2), // m22 (G->G)
      (lumB + cosVal  (-lumB) + sinVal  lumMatrixCoefficient3), // m23 (B->G)
      0, 0, // m24 (A), m25 (Bias)

      // B' = B行
      (lumR + cosVal  (-lumR) + sinVal  (-(1 - lumR))), // m31 (R->B)
      (lumG + cosVal  (-lumG) + sinVal  lumG), // m32 (G->B)
      (lumB + cosVal  (1 - lumB) + sinVal  lumB), // m33 (B->B)
      0, 0, // m34 (A), m35 (Bias)

      // A' = A行(不変)
      0, 0, 0, 1, 0, // A' = A
    ];
  }
}

このコードのポイントは以下の2点です。

  1. 三角関数の利用: 角度degreesをラジアンに変換し、cosVal(cosθ)とsinVal(sinθ)を求めます。この値が、行列内の色差成分の回転を制御します。
  2. 輝度係数の埋め込み: 定義した輝度係数lumR, lumG, lumBが、各行・各列の複雑な計算式に組み込まれています。これにより、どの色相に回転しても、元の輝度成分を保持するというロジックが実現されます。
  3. オフセットの0詰め: 行列の末尾にあるオフセット要素(m15, m25, m35, m45)は、色の加算を意味しますが、純粋な色相回転では不要なため、すべて0に設定されています。

コード内のlumMatrixCoefficientについて

コード内のlumMatrixCoefficient1 = 0.143といった固定値は、先述の「RGB → YCbCr → 回転 → RGB」という一連の処理を、単一の行列に統合した結果として現れる係数です。

これらはBT.709の輝度係数から導出される定数であり、これによりColorMatrixは明るさを変えずに正確な色相回転を実現します。

画像への適用:ColorFilteredウィジェット

動的に生成されたColorMatrixを画像に適用するためColorFilteredウィジェットを使用します。

ColorFilteredは、そのchild全体にフィルタを適用します。そのため、アニメーションを画像のみに適用したい場合はImageウィジェットの直前に、画面全体のテーマを動的に変えたい場合は、より上位のウィジェット(例: Scaffoldの直下など)に配置します。

HueRotationImage ウィジェット

// HueRotationImage ウィジェットの build メソッド抜粋
class _HueRotationImageState extends State<HueRotationImage> {
  // ... (initState, didUpdateWidgetの省略)

  @override
  Widget build(BuildContext context) {
    return ColorFiltered(
      // (1) ColorFilter.matrixでColorMatrixを渡す
      colorFilter: ColorFilter.matrix(
        ColorFilterGenerator.hueAdjustMatrix(widget.hueShift),
      ),
      child: Image(
        // (2) フィルターを適用したいImageウィジェット
        image: _imageProvider,
        // ... (サイズ、ローディング処理などの省略)
      ),
    );
  }
}

ColorFiltered は、child に指定したウィジェットの描画結果全体に対して、colorFilterで指定された処理を適用します。

  • (1): ColorFilter.matrixコンストラクタに、先ほど動的に生成した ColorMatrix (20個のdoubleのリスト) を渡します。
  • (2): Imageウィジェットは、受け取った hueShift(色相角度)が変わるたびにリビルドされ、新しいColorMatrixが適用されます。

アニメーション制御:AnimationControllerとaddListener

色相角度を連続的に変化させ、アニメーションを実現します。

_HueRotationScreenStateは、アニメーションを制御するため、SingleTickerProviderStateMixinをミックスインし、AnimationControllerを使用します。

AnimationControllerの準備と設定

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 5), // 5秒で一周(360度)
  )..addListener(_handleAnimationUpdate); // (1)
}
  • durationは 360°回転するのにかかる時間を定義します。addListenerで、アニメーションのフレーム更新ごとに処理を実行します。
  • (1) addListener:コントローラーの値が更新されるたびに、_handleAnimationUpdateメソッドが呼び出され、ウィジェットの描画の更新をトリガーします。

角度の更新処理(_handleAnimationUpdate)

このリスナー内で、AnimationControllerの値を色相角度(0°〜360°)にマッピングし、ウィジェットを再描画します。

void _handleAnimationUpdate() {
    if (_isAutoRotating) {
        setState(() {
            // (1) Controllerの0.0〜1.0の値を0〜360度にマッピング
            currentHue = controller.value * 360;
        });
    }
}
  • (1):_controller.value は0.0から1.0まで連続的に変化します。これを360倍することで、色相角度0°〜360°を得て、状態変数_currentHueに格納します。setStateが呼ばれるたびに、この新しい角度が画像に適用されます。

アニメーションの開始と停止(_toogleAutoRotation)

トグルボタンの操作に応じて、_controller.repeat()でアニメーションを繰り返し実行するか、_controller.stop()で停止させます。

void _toggleAutoRotation() {
    setState(() {
      isAutoRotating = !isAutoRotating;
    });

    if (_isAutoRotating) {
      _controller.repeat(); // アニメーションを繰り返し実行
    } else {
      _controller.stop(); // アニメーションを停止
    }
}

ユーザーが自動回転をオンにすると _controller.repeat()が実行され、ColorMatrix の生成画像への適用が一連の流れとしてフレームレートで繰り返され、鮮やかなローテーションアニメーションが完成します。

統合と完成

最終的に、buildメソッド内で、動的に更新される _currentHueHueRotationImage ウィジェットに渡します。

// build メソッド抜粋
HueRotationImage(
  hueShift: _currentHue, // 連続的に変化する角度を渡す
  imagePath: 'assets/bema.png',
  // ...
),

このシンプルな連携により、以下のサイクルが完成し、画像の明るさを変えずに色相のみを変化させ続けるアニメーションが実現されます。

controller値変化 → addListener発火 → currentHue更新 → setStatebuild再実行 → 新しいColorMatrix生成と適用 → 画像更新

完成した色相回転アニメーション

画像に色相回転のアニメーション効果が適用されているのが確認できます。

完全な実装コードcolor_filter_generator.dart

省略。

hue_rotation_image.dart

import 'package:flutter/material.dart';
import 'package:hue_rotation_sample/utils/color_filter_generator.dart';

class HueRotationImage extends StatefulWidget {
  final double hueShift;
  final String imagePath;
  final double width;
  final double height;
  final BoxFit fit;

  const HueRotationImage({
    super.key,
    required this.hueShift,
    required this.imagePath,
    this.width = 250,
    this.height = 250,
    this.fit = BoxFit.contain,
  });

  @override
  State<HueRotationImage> createState() => _HueRotationImageState();
}

class _HueRotationImageState extends State<HueRotationImage> {
  late ImageProvider _imageProvider;

  @override
  void initState() {
    super.initState();
    _imageProvider = AssetImage(widget.imagePath);
  }

  @override
  void didUpdateWidget(covariant HueRotationImage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.imagePath != widget.imagePath) {
      _imageProvider = AssetImage(widget.imagePath);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ColorFiltered(
      colorFilter: ColorFilter.matrix(
        ColorFilterGenerator.hueAdjustMatrix(widget.hueShift),
      ),
      child: Image(
        image: _imageProvider,
        width: widget.width,
        height: widget.height,
        fit: widget.fit,
        errorBuilder:
            (context, error, stackTrace) => const Text('Error loading image'),
        loadingBuilder:
            (context, child, loadingProgress) =>
                loadingProgress == null
                    ? child
                    : const SizedBox(
                      width: 50,
                      height: 50,
                      child: Center(child: CircularProgressIndicator()),
                    ),
      ),
    );
  }
}

color_wheel.dart
省略。

auto_rotation_toggle.dart

import 'package:flutter/material.dart';

class AutoRotationToggle extends StatelessWidget {
  final bool isAutoRotating;
  final VoidCallback onToggle;

  const AutoRotationToggle({
    super.key,
    required this.isAutoRotating,
    required this.onToggle,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: DecoratedBox(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.grey),
          borderRadius: BorderRadius.circular(8),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Auto Hue Rotation'),
            const SizedBox(width: 8),
            Switch(
              value: isAutoRotating,
              onChanged: (value) {
                onToggle();
              },
            ),
          ],
        ),
      ),
    );
  }
}

hue_rotation_screen.dart


import 'package:flutter/material.dart';
import 'package:hue_rotation_sample/widgets/color_wheel.dart';
import 'package:hue_rotation_sample/widgets/auto_rotation_toggle.dart';
import 'package:hue_rotation_sample/widgets/hue_rotation_image.dart';
class HueRotationScreen extends StatefulWidget {
  const HueRotationScreen({super.key});

  @override
  State<HueRotationScreen> createState() => _HueRotationScreenState();
}

class _HueRotationScreenState extends State<HueRotationScreen>
    with SingleTickerProviderStateMixin, WidgetsBindingObserver {
  late AnimationController _controller;
  double _currentHue = 0.0;
  bool _isAutoRotating = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 5),
    )..addListener(_handleAnimationUpdate);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleAnimationUpdate() {
    if (_isAutoRotating) {
      setState(() {
        currentHue = controller.value * 360;
      });
    }
  }

  void _toggleAutoRotation() {
    setState(() {
      isAutoRotating = !isAutoRotating;
    });

    if (_isAutoRotating) {
      _controller.repeat();
    } else {
      _controller.stop();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hue Rotation with Color Matrix')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            HueRotationImage(
              hueShift: _currentHue,
              imagePath: 'assets/bema.png',
              width: 250,
              height: 250,
            ),
            const SizedBox(height: 40),
            Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Stack(
                  alignment: Alignment.center,
                  children: [
                    ColorWheelWidget(
                      selectedHue: _currentHue,
                      isAutoRotating: _isAutoRotating,
                    ),
                    Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Text('Hue', style: TextStyle(fontSize: 16)),
                        const SizedBox(width: 8),
                        SizedBox(
                          width: 48,
                          child: Text(
                            '${_currentHue.toStringAsFixed(0)}°',
                            textAlign: TextAlign.center,
                            style: const TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                AutoRotationToggle(
                  isAutoRotating: _isAutoRotating,
                  onToggle: _toggleAutoRotation,
                ),
                const SizedBox(height: 16),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

まとめ

本記事では、FlutterのColorFilteredと ColorMatrixを使用し、数学的な背景に基づいた色相ローテーションアニメーションの実装を解説しました。

  • 輝度分離の原則: RGB空間の課題を解決し、画像の明るさ(輝度/Luma)を完全に保ったまま色相を変化させる鍵が、この輝度分離の原理であることを確認しました。
  • ColorMatrixの統合: BT.709輝度係数と三角関数を組み合わせることで、YCbCr空間での回転ロジックを単一のColorMatrix(色行列)に統合する高度な手法をコードに落とし込みました。
  • アニメーションの実現: AnimationControllerを用いて、この動的なColorMatrixの生成と適用を連続的に行うことで、滑らかな360度の色相アニメーションを完成させました。

この技術は、単なる画像フィルターに留まらず、UI全体のテーマカラーの動的な切り替えや、モード切り替えの視覚化など、幅広く応用可能です。

この低レベルの画像処理の知識は、あなたのFlutter開発における表現の幅を大きく広げる強力な武器となるはずです。ぜひ、ご自身のプロジェクトで、色彩を意のままに操る楽しさを体験してください。

最後までお読みいただき、ありがとうございました!

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

田原 葉
田原 葉
2024年にメンバーズに中途で入社。前職はiOSエンジニア。現在はCross ApplicationカンパニーでFlutter技術をメインにモバイルアプリ開発支援を担当。
詳しく見る
ページトップへ戻る