デザインの味方！Electronで画面の色を瞬時にキャッチするツールの作り方

# イントロダクション
この記事では、Electronを使って画面上のカーソル位置にある色をリアルタイムで取得するアプリの作成方法を紹介します。

https://github.com/kazuma-naka/ironuki-gui

# 使用した Library
- [robotjs](https://www.npmjs.com/package/robotjs)
- [screenshot-desktop](https://www.npmjs.com/package/screenshot-desktop)
- [pngjs](https://www.npmjs.com/package/pngjs)

# 作成した理由
**デザイン作業中に、スクリーン上の色を簡単に取得できたら便利だと思ったことはありませんか？**  
個人でアプリを開発している際に、色を16進数で取得したい場合があります。これまではスクリーンショットを撮り、それを `Affinity Designer` に読み込み、色の16進数を取得していました。しかし、毎回スクリーンショットを撮るのは手間で、リアルタイムに色を取得できる方法がないかと考え、このアプリの作成に取り掛かりました。

## 課題：`robotjs` の限界
`robotjs` にはマウスカーソル下の色を取得する機能が既に備わっています。しかし、私が使っている Windows 環境では、この機能がうまく機能せず、期待する色を取得できませんでした。

```js
// Get pixel color under the mouse.
var robot = require("robotjs");
 
// Get mouse position.
var mouse = robot.getMousePos();
 
// Get pixel color in hex format.
var hex = robot.getPixelColor(mouse.x, mouse.y);
console.log("#" + hex + " at x:" + mouse.x + " y:" + mouse.y);
```

この問題を解決するために、別のアプローチを試みました。それが、スクリーンショットを使って画面全体をキャプチャし、その中からマウスカーソルの位置の色を計算するという方法です。これにより、正確にカーソル下の色をリアルタイムで取得できる仕組みを実現しました。

# 課題をどう克服したか

### スクリーン全体のキャプチャと座標のスケーリング
この問題を解決するため、`screenshot-desktop` と `pngjs` を使用して、画面全体のスクリーンショットを取得し、その中からマウスカーソルの位置のピクセル情報を解析するというアプローチを取りました。これにより、どの環境でも動作する汎用的な色取得アプリが完成しました。

以下のコードは、その過程を表しています。

```javascript
import screenshot from "screenshot-desktop";
import { PNG } from "pngjs";

class Screenshot {
  async captureScreen() {
    // スクリーン全体をキャプチャして、PNG形式でバッファを返す
    return await screenshot({ format: "png", screen: "" });
  }

  parseImageBuffer(imgBuffer) {
    // PNGバッファを解析し、ピクセル情報を取得する
    return PNG.sync.read(imgBuffer);
  }
}

export default Screenshot;
```

ここでスクリーンショットとピクセル情報を取得する関数を定義します。

---

### マウス位置のスケーリングと色の抽出
スクリーンショットが取得できたら、次のステップはマウスの位置に対応するピクセルを正確に見つけることです。マウスの位置とスクリーンショットのサイズを比較し、スケールに基づいて位置を調整します。

```javascript
class ColorProcessor {
  mapMousePosition(
    mouse,
    screenResolution,
    capturedImageWidth,
    capturedImageHeight
  ) {
    // マウスの位置をスクリーンショットのスケールに合わせて変換する
    const scaleX = capturedImageWidth / screenResolution.width;
    const scaleY = capturedImageHeight / screenResolution.height;
    return {
      x: Math.floor(mouse.x * scaleX),
      y: Math.floor(mouse.y * scaleY),
    };
  }

  extractColor(png, x, y) {
    // スクリーンショット内の指定されたピクセル位置から色を取得する
    const idx = (y * png.width + x) * 4;
    return {
      red: png.data[idx],
      green: png.data[idx + 1],
      blue: png.data[idx + 2],
    };
  }
  
  rgbToHex(r, g, b) {
    // RGBから16進数の色形式に変換
    return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
  }
}
export default ColorProcessor;
```

この部分のコードでは、マウスの座標を正確にスケールすることで、実際のスクリーンとキャプチャ画像との間で一貫性を保つことができます。

---

# 最後のステップ：色の取得

最後に、取得した座標から色を抽出し、RGB値を16進数に変換するステップに進みます。

```javascript
class ColorCapture {
  async getColorUnderMouse() {
    const mousePos = robot.getMousePos();
    const screenSize = robot.getScreenSize();
    try {
      const imgBuffer = await screenshot({ format: "png", screen: "" });
      const png = PNG.sync.read(imgBuffer);
      const scaledMouseX = Math.floor(
        mousePos.x * (png.width / screenSize.width)
      );
      const scaledMouseY = Math.floor(
        mousePos.y * (png.height / screenSize.height)
      );
      const idx = (scaledMouseY * png.width + scaledMouseX) * 4;
      const red = png.data[idx];
      const green = png.data[idx + 1];
      const blue = png.data[idx + 2];
      const hex = this.rgbToHex(red, green, blue);
      return { hex, x: mousePos.x, y: mousePos.y };
    } catch (error) {
      console.error("Error capturing screenshot or processing image:", error);
    }
  }

  rgbToHex(r, g, b) {
    return [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
  }
}
```

### 100 ms ごとに色を取得する

```javascript
function startColorCapture() {
  const colorCapture = new ColorCapture();
  colorInterval = setInterval(async () => {
    if (!isPaused) {
      const colorData = await colorCapture.getColorUnderMouse();
      if (colorData) {
        colorWindow.webContents.send("color-data", colorData);
      }
    }
  }, 100);
}

```

![1000011650.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569408/d39a41f1-dc35-2aac-30e2-aa68c472fe07.gif)