[Dd]enzow(ill)? with DB and Python

DBとか資格とかPythonとかの話をつらつらと

Chrome Extensionのアイコンを動かしたい(chrome.browserAction.setIcon)

最近ちょこちょこChrome Extensionのコードを書いています。メインの処理自体はAPIを叩いて結果を取り出すだけのシンプルなものですが、細部に凝りだすと際限がないですね。

Chrome ExtensionのbrowserActionを使っている場合、常にブラウザの右上にアイコンが表示されますが、これを処理中はアニメーションさせたかったので色々試しました。 ※↓のような動きをしたかったのです。

f:id:denzow:20180218205838g:plain

browserAction

Chrome Extensionでの機能は特定ページで動作するPageActionと常に起動するBrowserActionがあります。以前は両者のアイコンの表示位置はタイミングが異なりましたが、現在はどちらであっても右上にアイコンが表示されます。 ※PageActionは有効化しないとグレーアウトしますが。。。

こんな感じにmanifest.jsonに書いておくとBrowserActionが有効化されます。

  "browser_action": {
    "default_icon" : "images/icon16.png",
    "default_title": "mouse over title",
    "default_popup": "popup.html"
  },

このようにdefault_iconでアイコンの画像を指定しますが、他にもchrome.browserAction.setIconを使用してコード内から動的にアイコンを設定することも可能です。

chrome.browserAction.setIcon({
    imageData: 'new image'
});

canvasによるアニメーション

通常のWEBアプリであれば、ロード中だけgifに差し替えるといったアプローチが簡単ですがChrome Extension のiconはどうやらgifは指定できないようなので、自分でアニメーションを作成する必要があります。

HTML5のcanvasでは読み込んだ画像の回転等の加工を行うことが出来ます。また、context.getImageDataで画像データを生成できるため、これを利用します。

以下は、100msごとに指定した画像を10度ずつ回転させるコード例です。

See the Pen ぐるぐる by denzow (@denzow) on CodePen.

回転の中心点はデフォルトではキャンバスの左上になっているため、回転前に軸をずらす必要があります。上述のCodePenのうち関連部分の抜粋です。

function rotate(){
    // 画像のクリア
    context.clearRect(0, 0, canvas.width, canvas.height);
    // キャンバスの半分の位置までずらす
    context.translate(
        parseInt(canvas.width / 2), 
        parseInt(canvas.height / 2)
    );
    // 回転させる
    context.rotate((degree * Math.PI) / 180);
    // ずらしたキャンバスを元に戻す
    context.translate(
        -parseInt(canvas.width / 2), 
        -parseInt(canvas.height / 2)
    );
    // 画像を描画する
    context.drawImage(
        image, 
        0, 0, image.width, image.height,   // src
        0, 0, canvas.width, canvas.height  // dest
    );
}

流れとしては

  1. clearRectで前回の描画をクリア
  2. translateで中心をずらす
  3. rotateで回転させる(ラジアン)
  4. translateでずらした中心を戻す
  5. 描画する

です。あとはsetInterval等で定期的に実行することで回転するアニメーションが作成できます。

Chrome Extensionへの反映

アニメーションは上述のコードがほぼ全てです。後は描画したイメージを定期的にchrome.browserAction.setIconに渡せばブラウザ上のアイコンをくるくるすることが可能になります。一応汎用的に使えるようにクラス化したものが以下になります。

export class LoadingBrowserIcon{

    constructor(srcIcon, iconWidth=19, iconHeight=19){
        // 元になるアイコンパス
        this.srcIcon = srcIcon;
        this.image = new Image();
        this.canvas = document.createElement("canvas");
        // chrome iconは19 x 19 まで。
        this.canvas.width = iconWidth;
        this.canvas.height = iconHeight;
        this.context = this.canvas.getContext('2d');

        // アイコン回転角
        this.degree = 0;
        // ローディングフラグ
        this.isLoading = false;
        // 画像が準備済か
        this.isReady = false;
        this.image.onload = ()=>{
            this.isReady = true;
        };
        this.image.src = srcIcon;
        this.timer = null;
        
    }
    _setIcon(){
        // あまりないが画像準備ができてなければ何もしない
        if(!this.isReady){
            return;
        }

        // ローディングが終了し、degree=0になったらtimerを止める
        if(!this.isLoading && this.degree == 0){
            if(this.timer){
                clearInterval(this.timer);
            }
        }
        // 既存画像のクリア
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
        this.context.save();
        //回転 軸をずらして回して軸を戻す
        this.context.translate(parseInt(this.canvas.width / 2), parseInt(this.canvas.height / 2)); 
        this.context.rotate((this.degree * Math.PI) / 180);
        this.context.translate(-parseInt(this.canvas.width / 2), -parseInt(this.canvas.height / 2));

        // 画像をキャンパスサイズにリサイズ
        this.context.drawImage(this.image, 0, 0, this.image.width, this.image.height, 0, 0, this.canvas.width, this.canvas.height);
        // iconにセット
        chrome.browserAction.setIcon({
            imageData: this.context.getImageData(0, 0, this.canvas.width, this.canvas.height)
        });
        // キャンバスの状態を初期化
        this.context.restore();
    }
    reset(){
        // 直接ここでtimerを止めず、フラグだけ立てておくことでdegree=0までは回転させる
        this.isLoading = false;
    }
    loading(){
        this.isLoading = true;
        this.timer = setInterval(()=>{
            this.degree = (this.degree + 5) % 360;
            this._setIcon();
        }, 10)
        
    }
}

回転を直ちに停止させるのではなく、きれいに元の角度になってから止めたいがために少し込み入ってますが以下の様にシンプルに使えます。なお、描画時のアイコンサイズは指定できますがChrome ExtensionのBrowserIconは19x19しか受け付けないためそれ以外の指定はあまり意味がないです。

// import
import { LoadingBrowserIcon } from './lib/icon';
// 初期化
const iconControl = new LoadingBrowserIcon('../images/profile.png');

// くるくる開始
iconControl.loading();

// do something

// くるくる停止
iconControl.reset();

ES6なのでBabelやらBrowserifyやら必要ですが、割りと使い勝手がいい感じになったと思います。