Chrome拡張でサカバンバスピス

これは[クソアプリ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/kuso-app)の21日目の記事です。

2023年の顔と言えばサカバンバスピス。
というわけで、Chrome拡張開発の練習ついでにサイドバーにサカバンバスピスを召喚してみました。

それではいってみましょう。

# サイドパネルって何？

Chromeにはサイドパネルというものがあります。
サイドパネルなんて使ったことがありませんでした。
そんな私みたいにサイドパネルって何？という人は、右上にあるこれをクリックしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/79875b01-611b-c80f-216d-ad6d0b98fd37.png)

開きましたね。そうです、それがサイドパネルです。

サイドパネルの特徴は以下の通りです。

- 開閉できる。
- 表示内容を変えられる。
- ページを移動しても表示が変わらない。
- Chrome拡張アプリが表示できる。

何か閃きませんか？
そうです、デスクトップマスコットにちょうどいいのです。

というわけで作ってみましょう。

# Chrome拡張

サイドパネルを使うには、Chrome拡張を作る必要があります。
プラグインっぽいのかなー、面倒だなー、と身構えていましたが、実体は単なるReactアプリでした。（Reactである必要はありません）

乱暴な話、Reactアプリに```manifest.json```を追加するだけでChrome拡張の完成です。
というわけで、まずはReactアプリを作りましょう。

# Reactアプリ

さくっとつくりましょう。

つくったら、public/にmanifest.jsonを置きます。
manifest.jsonの作り方は以下の通りです。
[マニフェスト ファイル形式](https://developer.chrome.com/docs/extensions/reference/manifest?hl=ja)

これでビルドして、Chrome拡張機能のページ( chrome://extensions/ )から、「パッケージ化されていない拡張機能を読み込む」でbuild/フォルダーを選択すればロードされて完成です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/f655d2d3-9b66-19dc-b122-f5bee8940e42.png)

# VRM

デスクトップマスコットといえばVRMです。
というわけで、VRMを表示できるようにします。

WebブラウザでVRMといえばthree-vrm( https://github.com/pixiv/three-vrm )ですが、もう少し簡単に使えるChatVRMを使います。
https://github.com/pixiv/ChatVRM

ChatGPTとか余計なものは剥ぎ取って、VRMロード部分と表示部分だけを残します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/cc2cfecd-ac88-d3b1-7ed3-9189ac19e7b4.png)

これくらいあれば大丈夫でしょう。
TypeScriptの人はそのまま使えますが、私はJavaScripterなので、jsにわざわざ変換して使っています。

表示部分はこれだけです。

```html
<VrmViewer />
```

はい、簡単ですね。
表示できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/a3965504-3e71-fb10-8c4d-befd78bf4e0a.png)

# サカバンバスピス

サカバンバスピスのオリジナルデザインはエルガ・マルク＝クリク博士が作成され、ヘルシンキ自然史博物館に展示されている模型です。

その愛らしい見た目に虜となった人も多いのではないでしょうか。
私はシーマンぽさを感じました。

というわけで、デスクトップマスコットである今回のアプリにもってこいですね。
採用です。

サカバンバスピスは、にゃこまるすてっぷ＋様がかわいいモデルを作成されていますので、利用させていただきました。ありがとうございます。
https://booth.pm/ja/items/4852701

せっかくかわいい女の子が表示されていますが、サカバンバスピスに差し替えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/527baab4-199a-de4d-a078-f299a418297b.png)

ああ無常。

# 餌を与える

サカバンバスピスは約四億年前のオルドビス期の生物です。
顎がないため、海底の泥の中の微生物を口ですくって食べていたおとなしい生物だったそうです。

そんなサカバンバスピスに餌を与えましょう。
何を餌にするかですが、せっかくChrome拡張を使っているので、見ているページを餌にしてみましょう。

### コンテンツスクリプト

見ているページの情報を得るには、コンテンツスクリプトという仕組みを使います。
見ているページにJavaScriptを差し込み、実行させるというウイルスのような機能です。

こんな風にしてサービスワーカーにメッセージを送ります。

```js:content-script.js
(function () {
    const pageTitle = document.title;
    const pageUrl = window.location.href;
    chrome.runtime.sendMessage({ title: pageTitle, url: pageUrl });
})();
```

コンテンツスクリプトを使うには、manifestでの宣言が必要です。

```json:manifest.json
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content-script.js"]
    }
  ]
```

### サービスワーカー

コンテンツスクリプトからのイベントをバッググラウンドで受け取るには、サービスワーカーを使います。

サービスワーカーで、コンテンツスクリプトから送信されたメッセージを受け取り、そのままReactアプリに送信します。

```js:service-worker.js
if (chrome.runtime) {
  chrome.runtime.onMessage.addListener(async function (message, sender, sendResponse) {  
      const data = { title: message.title, url: message.url };
      chrome.runtime.sendMessage({
        name: 'current-page',
        data: data
      });
  });
}
```

Reactアプリではこんな風にしてメッセージを受け取ります。
```js
  useEffect(() => {
    const messageListener = (({ name, data }) => {
      switch (name) {
        case 'current-page':
          setData(data);
          break;
      }
    });
    if (window.chrome.runtime) {
      window.chrome.runtime.onMessage.addListener(messageListener);
      return () => {
        window.chrome.runtime.onMessage.removeListener(messageListener);
      };
    }
  }, []);
```

これで見ているページの情報をReactアプリで使えるようになりました。

# 餌を撒く

これで餌を撒く準備ができましたので、実際に餌を撒きましょう。

具体的には、見ているURLの文字が上からバラバラと降ってくるようにします。
cssでアニメーションさせます。

```js
const FallingLetters = ({ text }) => {
    const [letters, setLetters] = useState([]);
  
    useEffect(() => {
      setLetters(text.split('').map((char, index) => ({
        char,
        id: `${char}-${index}-${Math.random()}`,
        style: {
          left: `${Math.random() * 100}vw`,
          animationDuration: `${2 + Math.random() * 5}s`,
          animationDelay: `${Math.random() * 2}s`,
          opacity: Math.random(),
          animationTimingFunction: 'linear',
          animationName: `fall, rotate`    
        }
      })));
    }, [text]);
  
    return (
      <div className="falling-letters-container">
        {letters.map(letter => (
          <span key={letter.id} className="falling-letter" style={letter.style}>
            {letter.char}
          </span>
        ))}
      </div>
    );
};
```

```css
.falling-letters-container {
    position: relative;
    height: 100vh;
    overflow: hidden;
}

.falling-letter {
    position: absolute;
    top: -100px;
    font-size: 2rem;
    animation-name: fall, rotate;
    animation-timing-function: linear;
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
}

@keyframes fall {
    0% {
        top: -100px;
    }

    100% {
        top: calc(100vh - 40px);
    }
}

@keyframes rotate {
    from {
        transform: rotate(0deg);
    }

    to {
        transform: rotate(calc(360deg * 4));
    }
}
```

これでURLがバラバラと降ってくるようになります。

![favs.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/dbd40223-7876-ec6a-145b-49374525763f.gif)

サカバンバスピスには胸ビレなどがないため、安定して水中を泳ぐことができなかったそうです。
というわけで、せっかく餌が降ってきても、サカバンバスピスが動くことはありません。
残念ですね。

# 実際のアプリ

ちなみに今のアプリはいろいろ拡張されてただのクソアプリではなくなってしまっていますが、上記の機能はきっちり搭載しています。よければどうぞお使いください。
https://chromewebstore.google.com/detail/hplledhjehdflkofifngbljgjekfacnd?hl=ja

# ちなみにカメラで拡大もできます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44912/d91db2cc-0b03-994f-9e4e-3efc32e47afb.png)

ずっと表示していると、サカバンバスピスが何かを語りかけているかのような錯覚に陥ります。
これがAIか……。

May the Sacabambaspis be with you.

おしまい。