unicorn.studioで作ったものをサイトに載せる

## この記事の概要

[unicorn.studio](https://www.unicorn.studio/)というツールがあります。

ノーコードでWebGLを扱えて、インタラクティブなエフェクトやモーションを簡単に作ることができます。

適当に触っていても面白い表現が作れて、非常に魅力的なツールです。

ただ、作ったものを実際にサイト上に載せるのにはやや苦労しました（特にReactでは）。
というわけでコピー＆ペーストにて動かせるように記事にします。

## unicorn.studioの特徴

いきなり実装の仕方から入るのも不親切なので、少しだけunicorn.studioの特徴を記載します。

### エフェクトが豊富

- 標準エフェクト
  - ディストーション、ブラー、ライトなど、様々な種類のエフェクトが搭載されている
  - 公式サイトでは「35+ effects」という表記ながら、実際に数えたら50あった
    - 徐々に増えているのかもしれない
- 自分で書くカスタムエフェクト
  - シェーダーが書ける場合、自分で書くことも可能

### モーションやインタラクションをつけるのが簡単

- 登場、スクロール、マウスオーバーをトリガーにしたモーション
- マウスの動きに追従した変化

### この手のライブラリの中では軽い

- gzipped時で36kB
- 役割や機能の豊富さが違うので単純比較すべきではないものの、参考程度にthree.jsは167.9kB、p5.jsは244.6kB

### FramerとWebflowなどでの利用が簡単

- ノーコードな技術スタック（？）に焦点を当てていそう
- それもあってか自前で実装するサイトへのサポートはやや手薄な印象

### 画像や動画としての書き出しも可能

- サイズや拡張子、フレームレートなどが選択できる
- インタラクティブ性はなくなってしまうものの、手軽に使えるという意味では嬉しい機能

## 下準備

エディタ画面右上の`Export`から`Embed`を開くと`Project ID`があります。
コードの中でこれを使うのでコピーしておきます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/364bdcd7-dbee-4717-9bcb-5d4a0cc74cfc.png)


## 実装

素のHTML上で使用する場合と、Reactで使用する場合の両方を記載します。

### HTML

最低限ならこの3ファイルで良いです。
（`main.js`の中身も1行しかないので直接HTMLに書いても良いですが、さすがにそうはしないだろう、ということで3ファイル用意しています。）

```sh:ファイル構成
.
├── index.html
├── main.js
└── style.css
```

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="style.css" />
    <script
      src="https://cdn.unicorn.studio/v1.3.2/unicornStudio.umd.js"
      defer
    ></script>
    <script src="main.js" defer></script>
  </head>
  <body>
    <div
      data-us-project="YOUR_PROJECT_ID"
      style="width: 100%; height: 100%"
    ></div>
    <!-- お好きな内容 -->
  </body>
</html>
```

```javascript:main.js
UnicornStudio.init();
```

CSSはお好きに書いてもらえれば大丈夫です。

重要なのは以下の3点です。

1. CDNにあるunicron.studioのコードを使う
1. `data-us-project`にProject IDを記載する
1. `init()`する

### React

Viteの`create-vite`でReactプロジェクトを作ったという前提での記載です。

```sh:ファイル構成
.
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── public
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── components
│   │   └── UnicornEmbed.tsx
│   ├── hooks
│   │   └── useUnicornStudio.ts
│   ├── index.css
│   ├── main.tsx
│   └── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
```

`projectId`を渡して描画できるコンポーネントを作成します。

```tsx:src/components/UnicornEmbed.tsx
import { useRef } from "react";

interface UnicornEmbedProps {
  projectId: string;
}

export const UnicornEmbed: React.FC<UnicornEmbedProps> = ({ projectId }) => {
  const containerRef = useRef<HTMLDivElement>(null);


  return (
    <div
      ref={containerRef}
      data-us-project={projectId}
      style={{ width: "100%", height: "100%" }}
    />
  );
};
```

1ページで複数回`UnicornEmbed`を使うかもしれないと考えて、hookを作成します。

```tsx:src/hooks/useUnicornStudio.ts
import { useEffect } from "react";

declare global {
  interface Window {
    UnicornStudio: {
      init(): Promise<void>;
      destroy(): void;
    };
  }
}

let isInitialized = false;

export const useUnicornStudio = () => {
  useEffect(() => {
    if (isInitialized) return;

    if (!window.UnicornStudio) {
      window.UnicornStudio = {
        init: () => Promise.resolve(),
        destroy: () => {},
      };

      const script = document.createElement("script");
      script.src = "https://cdn.unicorn.studio/v1.3.2/unicornStudio.umd.js";
      script.onload = () => {
        window.UnicornStudio.init();
        isInitialized = true;
      };
      document.head.appendChild(script);
    }

    return () => {
      if (isInitialized) {
        window.UnicornStudio.destroy();
        isInitialized = false;
      }
    };
  }, []);
};
```

あとは、今作った2つの機構を使えば大丈夫です。

```tsx:src/App.tsx
import "./App.css";
import { UnicornEmbed } from "./components/UnicornEmbed";
import { useUnicornStudio } from "./hooks/useUnicornStudio";

function App() {
  useUnicornStudio();
  return (
    <>
      <UnicornEmbed projectId="YOUR_PROJECT_ID" />
      {/* お好きな内容 */}
    </>
  );
}

export default App;
```