酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

WriteableBitmapを使ってBackground.pngの背景色を変更する

Baseball Japanがお陰様で相変わらずのご好評を賜っております。新機能の実装のためにBaseball JapanをWindows Phone OS 7.1以降対応アプリとして、バージョンを2.0に上げました*1

バージョン2.0では、ホーム画面にセカンダリタイルを追加して、いきなりチーム情報画面へジャンプ出来るようにしました。

今回は、このホーム画面に表示されているタイル画像を動的に作成する方法についてご紹介したいと思います。

分離ストレージの画像をタイルに表示する方法は、以前「分離ストレージの画像をタイルに表示する」にてご紹介しましたので、そちらの方をご覧ください。

やりたい事

今回紹介したい事としては、「各球団にはチームカラーが存在しており、どのタイルがどのチームの画面へジャンプ出来るのかをより判りやすくするため、タイルの背景をチームカラーにする」なのですが、どうやってタイル画像を作りましょうか?

チームカラー1色の画像と、その上から描画するのバッターの画像(タイルアイコンそのままです)を合成して、1枚のタイル画像を動的に作成します。イメージとしては下図の通りです。

アプリ内に画像を格納するのが一番簡単ですが、12球団分用意するとなると、アプリ自体のサイズが大きくなってしまうので、ここは動的に生成する方向で行きましょう!

何も考えずに合成してみる

Windows Phone 7でもPCでも同じですが、ディスプレイに表示されている画面はピクセルという最小単位で構成されています。各ピクセルはピクセルの不透明度を示すアルファ値を持っています。

上位8ビットにはアルファ値が格納されています。ARGB32におけるアルファ値は0〜255で表現されます。このアルファ値は不透明度を表した値で、アルファ値が最大(255)の場合は不透明に、最小(0)の場合は透明となります。

下位24ビットにはRGB値が格納されています。ARGB32におけるRGB値とは三原色(R成分、G成分、B色成分)の値が、それぞれ8ビットずつ格納されています。このRGBはRed,Green,Blueの頭文字を取ったものです。

殆どの場合0(透明)か255(不透明)なので全てのピクセルにおいてアルファ値の計算を行うのは無駄です。

描画したいピクセルのアルファ値だけを見て、描画するかどうかを判定し、手っ取り早く実装してしまいましょう。

  • アルファ値が0だったら、チームカラーを描画
  • アルファ値がそれ以外だったら、バッターのイラストを描画

コードを書くと以下のようになります。

    // チームカラーを取り出す
    Color c = getTeamColor();
    int intColor = c.A << 24 | c.R << 16 | c.G << 8 | c.B;

    // 合成元の画像(バッターのイラスト)をアプリリソースから取り出す
    var info = App.GetResourceStream(new Uri("Background.png", UriKind.Relative));
    WriteableBitmap overlapBmp = new WriteableBitmap(173, 173);
    overlapBmp.SetSource(info.Stream);

    // セカンダリタイルの背景用画像を作成
    WriteableBitmap bmp = new WriteableBitmap(173, 173);
    var pixes = bmp.PixelWidth * bmp.PixelHeight;
    for (int i = 0; i < pixes; i++) {
        int oPx = overlapBmp.Pixels[i];

        byte a2 = Convert.ToByte((oPx >> 24) & 0xFF);
        double ax2 = (double)a2 / 255;

        // 合成元の画像のピクセルのアルファ値をチェック
        if (a2 == 0) {
            // 透明なのでチームカラーのARGB値をそのまま代入
            bmp.Pixels[i] = intColor;
        } else {
            // 不透明なので合成元画像のARGB値をそのまま代入
            bmp.Pixels[i] = oPx;
        }
    }
    bmp.Invalidate();

上記のコードにて作成した画像をタイルに設定してみました。

バッターの影の部分のアルファ値が、想定していた0と255が設定されているようです。こうなると単純に合成元の画像のピクセルを描画する or 描画しないという判断が出来ません。

アルファ値を考慮して合成を行う

合成元の画像に中途半端(?)なアルファ値が設定されている事がわかりました。アルファ値を考慮して合成を行う必要があります。

先ほど書いた通り、殆どのピクセルには0と255が設定されていることには変わりはないので、0と255の場合は前述したコードと同じ動作にしましょう。

    // チームカラーを取り出す
    Color c = getTeamColor();
    int intColor = c.A << 24 | c.R << 16 | c.G << 8 | c.B;

    // 合成元の画像(バッターのイラスト)をアプリリソースから取り出す
    var info = App.GetResourceStream(new Uri("Background.png", UriKind.Relative));
    WriteableBitmap overlapBmp = new WriteableBitmap(173, 173);
    overlapBmp.SetSource(info.Stream);

    // セカンダリタイルの背景用画像を作成
    WriteableBitmap bmp = new WriteableBitmap(173, 173);
    var pixes = bmp.PixelWidth * bmp.PixelHeight;
    for (int i = 0; i < pixes; i++) {
        int oPx = overlapBmp.Pixels[i];

        byte a2 = Convert.ToByte((oPx >> 24) & 0xFF);
        double ax2 = (double)a2 / 255;

        // 合成元の画像のピクセルのアルファ値をチェック
        if (a2 == 0) {
            // 透明なのでチームカラーのARGB値をそのまま代入
            bmp.Pixels[i] = intColor;
        } else if (a2 == 255) {
            // 不透明なので合成元画像のARGB値をそのまま代入
            bmp.Pixels[i] = oPx;
        } else {
            // 合成元画像のRGB値を取り出す
            byte r2 = Convert.ToByte((oPx >> 16) & 0xff);
            byte g2 = Convert.ToByte((oPx >> 8) & 0xff);
            byte b2 = Convert.ToByte(oPx & 0xff);

            // アルファ値を元に合成後のRGB値を算出
            byte r = Convert.ToByte((c.R * (1.0 - ax2) + r2 * ax2));
            byte g = Convert.ToByte((c.G * (1.0 - ax2) + g2 * ax2));
            byte b = Convert.ToByte((c.B * (1.0 - ax2) + b2 * ax2));
            bmp.Pixels[i] = 255 << 24 | r << 16 | g << 8 | b;
        }
    }
    bmp.Invalidate();

上記のコードにて作成した画像をタイルに設定してみました。

どうでしょう?アルファ値を考慮した綺麗な画像になったのではないでしょうか。

チームカラーのRGB値はループの中で変更されることが無いので、一回一回計算しておりません((………というより、途中でgetTeamColorメソッドで取得済みのColorオブジェクトのRGB値がそのまま使えることがわかりました))。

173x173程度の画像なのであまり違いが判らないですが、ループ内から少しだけ計算処理を取り除いて、少しだけ高速化させようとしています。

*1:9/28現在、App Hubへバージョンアップ申請中。近日公開予定です