React-Konvaで作成したcanvasをTwitterに画像投稿する

Reactで画像を作成してTwitterAPIを使って画像投稿する。

# ReactでCanvasを使う
探したところReact-Kanvaが便利そうだった。使い方は公式や他の方の記事を参照。
公式：https://konvajs.org/docs/react/
使い方：https://qiita.com/YSRKEN/items/bb8b34510d70ec90eb50

# 画像をDataURLとして取得
作成したCanvasの画像データを取得する。
useRefのHookを使ってCanvasの要素を取得し、.toDataURL()を呼び出せばOK

（https://qiita.com/YSRKEN/items/bb8b34510d70ec90eb50 から引用）

```react
import React, { useRef } from "react";
import { Stage as StageType } from 'konva/types/Stage';

const stageRef = useRef<StageType>(null);

const process = () => {
  const temp = stageRef.current;
  // stageRefの中身(temp )がnullな可能性を考慮してチェック
  if (temp !== null) {
    // dataUrlに、画像データがdata URL(MIME Type + base64文字列)形式で書き込まれる。
    // toDataURLの引数を変更すれば、PNG以外の画像形式への変換も可能
    const dataUrl = temp.toDataURL();
    // これ以降、dataUrlを使った操作(画像保存、画像加工等)を行う
  }
};

return <Stage ref={stageRef} width={500} height={500}>
  <Layer>
    <Rect stroke='black' strokeWidth={4} x={5} y={5} width={490} height={490} />
    <Rect fill='red' x={100} y={100} width={300} height={200} />
  </Layer>
</Stage>;
```

余談だが、画像をダウンロードしたい場合は\<a>タグにDataURLを入れてダウンロードする。

```react
  const downloadCanvas = () => {
      // dataUrlを取得
      const url = stageRef.current.toDataURL();
      // <a>タグを作成してdataUrlを入れる
      const a = document.createElement("a");
      a.href = url;
      a.download = "image.png";
      // <a>タグをクリックする処理
      a.click();
   }
```

## ブラウザの幅に合わせてCanvasの拡大率を変える

ブラウザのサイズに合わせてCanvasの拡大率を変更したい場合は、\<div>要素などでブラウザのフレームサイズを取得して、拡大率を算出する。

```react
  // Stageの要素
  const stageRef = useRef(null);
  // 画像を表示するフレームの要素
  const frameRef = useRef(null);
  // Canvasで生成する画像のサイズ
  const sceneWidth = 960;
  const sceneHight = 1280;
  // ブラウザ表示時の拡大率
  const [scale, setScale] = useState(1);

  useEffect(() => {
    // ブラウザ表示時の拡大率を設定
    // ここでは横幅(offsetWidth)に合わせる
    setScale(frameRef.current.offsetWidth / sceneWidth);
  }, []);

  return (
    <div ref={frameRef}>
        <Stage
         width={sceneWidth * scale}
         height={sceneHight * scale}
         scale={{ x: scale, y: scale }}
         ref={stageRef}
        >
           ...
        </Stage>
    </div>
  )

```

ただ、これを行うと初期表示の一瞬だけブラウザに拡大率１の画像が表示されてしまう。ひとまずCSSのpositionを使って画面外に表示することで対応した。
display:hiddenにしてもいいのだが、画像取得の際にhiddenだと取れなくなってしまう。

```react
      {/* 拡大率が１のときは画面外へ飛ばす */}
      <style jsx>
        {`
          .canvas {
            ${scale == 1 ? " position: absolute; right: -10000px;" : ""}
          }
        `}
      </style>
```

## 画像取得時に拡大率を戻す

ブラウザのサイズに合わせて拡大率を変更したままDataURLを取得すると、そのサイズの画像が取得されてしまう。
画像取得前に拡大率を１に変更し、取得後にブラウザの拡大率に戻す。
ボタン押下時にツイートフラグ（isTweetCanvas）をtrueにして、useEffectで拡大率(scale)を変更。scaleが１になった次のレンダリングで画像を取得する。

```react
  // ブラウザ表示時の拡大率
  const [scale, setScale] = useState(1);
  // ボタン押下のフラグ
  const [isTweetCanvas, setIsTweetCanvas] = useState(false);

  useEffect(() => {
    if (isTweetCanvas) {
      // 画像を取得する前に画像の拡大率を１にする
      if (scale != 1) {
        setScale(1);
      } else {
        // 画像取得、ツイート処理（後述）
        getDataUrlAndTweet();
        // 拡大率、ツイートフラグを元に戻す
        setScale(frameRef.current.offsetWidth / sceneWidth);
        setisTweetCanvas(false);
      }
    }
  }, [isTweetCanvas, scale]);

  return (<>
    <div ref={frameRef}>
        <Stage
         width={sceneWidth * scale}
         height={sceneHight * scale}
         scale={{ x: scale, y: scale }}
         ref={stageRef}
        >
           ...
        </Stage>
    </div>
    <button onClick={setIsTweetCanvas(true)}>画像をツイート</button>
  </>)
```

# TwitterAPIの呼び出し

TwitterAPIのライブラリは見た感じスターが一番多かった[Twit](https://github.com/ttezel/twit)を使用。
※現時点（2021/12/19）でTwitter公式がオススメしている[node-twitter-api-v2](https://github.com/PLhery/node-twitter-api-v2)は、DataURLを使った画像アップロードはできないので注意。

ブラウザで呼び出すとCORSエラーやfsが開けませんといったエラーになるので、BFFなどnode.jsが動く環境で呼び出す。Next.jsならmiddlewareで良さそう。

**DataURLをそのまま入れるとエラーになるので冒頭の「data:image/png;base64」を削除する。**

### ブラウザ側

```react:browser.tsx
  async function getDataUrlAndTweet() {
    // axiosでミドルウェアを呼び出し
    await axios.post("/api/tweet-canvas", {
      dataUrl: stageRef.current
        .toDataURL()
        // 「data:image/png;base64」を削除
        .replace(/^data:image\/png;base64,/, ""),
    });
  }
```

### バックエンド側
大体Twitのサンプルをコピー

```react:pages/api/tweet-canvas.ts
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // ブラウザ側から取得したデータ
  const body = req.body;

  // トークンを設定
  // トークンの取得方法は「TwitterAPI　トークン取得方法」などでググってください
  var T = new Twit({
    consumer_key: process.env.TWITTER_APP_KEY,
    consumer_secret: process.env.TWITTER_APP_SECRET,
    access_token: process.env.TWITTER_TOKEN,
    access_token_secret: process.env.TWITTER_TOKEN_SECRET,
    timeout_ms: 60 * 1000,
    strictSSL: true,
  });

  // 画像をツイッターにアップロード
  T.post(
    "media/upload",
    // ここにDataURLを入れる。
    // 「media」ではなく「media_data」を使用する。
    { media_data: body.dataUrl },
    function (err, data, response) {
      var mediaIdStr = data.media_id_string;
      var altText =
      var meta_params = { media_id: mediaIdStr, alt_text: { text: altText } };

      T.post(
        "media/metadata/create",
        meta_params,
        function (err, data, response) {
          if (!err) {
            var params = {
              // ツイート文
              status: "loving life #nofilter",
              media_ids: [mediaIdStr],
            };

            T.post("statuses/update", params, function (err, data, response) {
              console.log(data);
            });
          }
        }
      );
    }
  );

```

# おわり

これで画像付きツイートがツイートされているはず。
最初にnode-twitter-api-v2を使ったせいで上手くいかず色々悩んでしまった。
DataURLから「data:image/png;base64」を削除するのも忘れずに。