ElectronとTypeScriptベースのVRChatの写真を使ったフレンド関係Viewerを作ってみた


![main.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3682031/3ee69199-93f5-11f6-e647-22b5d4f32aa0.png)


Electronを活用したデスクトップアプリケーション、JavaScriptベースのフロントエンド、そしてTypeScriptベースのバックエンドを組み合わせ、VRChatの写真を解析・メタデータを抽出した上で友達ネットワークを可視化するフルスタックアプリケーションを作ってみました。

ダウンロードやSource Codeに関しては下記のリンクを確認してください。

https://github.com/refiaa/VRChatFriendShipVisualizer

---

## 1. 使用技術スタックと開発環境

### バックエンド
- **言語/フレームワーク**: TypeScript, Node.js, Express  
  → バックエンドAPIサーバーはExpress/RESTful APIをベースに構築され、TypeScriptの強力な型検査とモジュールシステムにより、安定したコード構造が保たれています。
- **ファイルシステム操作**: Node.js組み込みモジュール（fs, path）および [fs-extra](https://www.npmjs.com/package/fs-extra)  
  → 画像ディレクトリの探索、メタデータファイルの読み書き作業を行います。
- **HTTPリクエスト処理および外部連携**: [node-fetch](https://www.npmjs.com/package/node-fetch), form-data  
  → 画像アップロード時に外部一時ファイルホスティングサービス（tmpfiles.org）と連携します。
- **リアルタイム進捗状況更新**: Server-Sent Events (SSE)  
  → メタデータ生成作業の進捗状況をリアルタイムでクライアントにストリーミングします。

### フロントエンド
- **言語/ライブラリ**: JavaScript, HTML, CSS, d3.js  
  → VRChatの友達ネットワークの可視化は、d3.jsのforce simulationを活用して実装され、ユーザーとのインタラクション（検索、スライダー、ズーム、ドラッグなど）をサポートします。
- **UIフレームワークとスタイリング**: CSS
- **通信**: fetch API, EventSource (SSE)  
  → バックエンドとのデータ通信や、リアルタイムの進捗状況更新に使用されます。
- **Electronレンダラープロセス**  
  → Electron内でHTML/CSS/JavaScriptで作成されたフロントエンドが、デスクトップアプリケーションとして動作します。

---

## 2. バックエンド：画像メタデータのパースとAPIサーバー

### 2-1. 画像メタデータのパース
バックエンドの主要機能のひとつは、VRChatの写真（PNGファイル）内に含まれるメタデータを抽出することです。写真には、PNG標準のiTXtチャンクを利用してJSON形式のメタデータが埋め込まれています。  
**主要な実装ロジックは `src/utils/pngParser.ts` に実装**され、以下の手順でメタデータを抽出します。

1. **PNG形式の検証**: PNGファイルのシグネチャ（137,80,78,71,13,10,26,10）を確認します。
2. **チャンクのパース**: ファイルデータを順次読み込み、各チャンクの長さ、タイプ、データ、CRC値をパースします。
3. **iTXtチャンクの抽出とパース**: iTXtチャンクが存在する場合、そのチャンクからテキストデータを抽出し、JSONとしてパースします。  
   もしiTXtチャンクが存在しない場合は、デフォルトで現在の時刻とファイル名を含む空のメタデータオブジェクトを返します。

> **pngParser.ts 主要コードスニペット**
> ```typescript
> private extractMetadata(data: Buffer): any {
>   if (!this.isPNG(data)) {
>     throw new Error("Invalid PNG format");
>   }
>
>   const chunks = this.parseChunks(data);
>   const iTXtChunk = chunks.find(chunk => chunk.type === "iTXt");
>
>   if (!iTXtChunk) {
>     return {
>       timestamp: new Date().toISOString(),
>       filename: path.basename(this.filePath),
>       metadata: {}
>     };
>   }
>
>   try {
>     const metadata = this.parseITXtChunk(iTXtChunk.data);
>     return {
>       ...JSON.parse(metadata),
>       timestamp: new Date().toISOString(),
>       filename: path.basename(this.filePath)
>     };
>   } catch (error) {
>     throw new Error(`Failed to parse metadata: ${error instanceof Error ? error.message : "Unknown error"}`);
>   }
> }
> ```
  
### 2-2. メタデータの構造と含まれる情報
メタデータはPNGファイルのiTXtチャンク内にJSON文字列として保存されます。一般的に含まれる情報は以下の通りです:
- **timestamp**: メタデータ生成時刻（バックエンドでデフォルト値として追加）
- **filename**: 元のPNGファイル名
- **metadata**: ユーザーまたはシステムによって挿入された追加情報  
  → 例として、VRChat内での友達関係を表現するためのfriend ID、友達間の相互作用回数、タグ情報などが含まれる可能性があります。

この構造により、バックエンドは各JSONファイルを読み込み、VRChatの友達ネットワークデータに変換し、d3.jsによる可視化に必要なノードとリンクのデータを生成できます。

### 2-3. APIサーバーとSSEによる進捗状況更新
バックエンドのExpressサーバーは、さまざまなAPIエンドポイントを提供しています。代表的なエンドポイントは以下の通りです:
- **/api/metadata/generate**: PNGファイルをスキャンし、メタデータを生成する作業を開始し、SSEを通じて "start"、"progress"、"complete" などのイベントをストリーミングします。
- **/api/metadata/date-range, /api/metadata/filter**: メタデータファイルの日付範囲を計算したり、特定期間内のファイルのみをフィルタリングします。
- **/api/upload/image**: クライアントから送信されたBase64画像データを外部ホスティングサービスにアップロードします。

SSEを使用したリアルタイム更新により、ユーザーはメタデータ生成作業の進捗状況を確認し、必要に応じて作業を中断することが可能です。

---

## 3. フロントエンド：VRChat友達ネットワークの可視化

### 3-1. データの可視化とd3.js
フロントエンドでは、d3.jsを利用して友達ネットワークを可視化します。主要な実装は `public/js/network-visualization.js` にあり、force simulationを通じてノードとリンクの物理的な配置を計算します。  
例えば、以下のコードでは各ノードの大きさと色は出現回数（友達関係の頻度など）に基づいて決定され、リンクは関係強度により線の太さと透明度が調整されます。

> **ネットワーク可視化コードスニペット**
> ```javascript
> const simulation = d3.forceSimulation(filteredNodes)
>   .force('link', d3.forceLink(currentLinks)
>     .id(d => d.id)
>     .distance(d => 200 / (d.strength || 1)))
>   .force('charge', d3.forceManyBody().strength(d => -500 * Math.sqrt(d.count)))
>   .force('collide', d3.forceCollide().radius(d => Math.sqrt(d.count) * 10 + 20))
>   .force('x', d3.forceX(width / 2).strength(0.03))
>   .force('y', d3.forceY(height / 2).strength(0.03));
> ```
  
### 3-2. UIとインタラクション機能
フロントエンドはElectronのレンダラープロセス内で動作し、ユーザーとの多様なインタラクションをサポートします。
- **検索機能**: 特定の友達名でノードをハイライトし、関連するリンクのみを強調表示します。
- **日付スライダー**: メタデータの日付範囲に基づいてフィルタリングし、特定期間のデータのみを可視化します。
- **共有およびエクスポート**: SVGエクスポートやPNG変換を通じて生成されたネットワークグラフを外部に共有可能にします。
- **ローディングオーバーレイと進捗状況表示**: メタデータ生成作業中は、ローディングスピナーとともに現在の進捗状況をリアルタイムでユーザーに表示します。