Nuxt/VueのUIライブラリのInspira UIを利用し紙吹雪を簡単に実装する

複雑なUIを簡単に実装できるInspira UIを利用し、紙吹雪のUIを実装してみました。

最終的な画面は以下の通り。

![confetti.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/68d50c6b-fc3f-d596-c7ff-0676c3f4113c.gif)

他にもInspira UIを利用することで、アニメーションリストや3Dカードなど、nxutでリッチなUI作ろうとしたときに簡単に実装ができそうでした。

https://inspira-ui.com/

インストールの際に、公式の手順とは異なる箇所がいくつかあったため、備忘録としてインストール手順から残しておきます。

最終的なコードは以下の通り

https://github.com/sey323/inspira-ui-confetti-sample

## はじめかた

nuxtのプロジェクトを新規作成する。

```sh
npx nuxi@latest init inspira-ui-sample
```

### Tailwind CSSの設定

Inspira UIの利用に必要なTailwindCSSの設定を行う。公式の手順を参考に進める。

https://tailwindcss.com/docs/guides/nuxtjs

必要ライブラリのインストール。

```shell
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
```

インストールが完了したら、`nuxt.config.ts`を編集する。

```typescript:nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },
  css: ['~/assets/css/main.css'],
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    },
  },
})
```

その後、`tailwind.config.js`を以下の値で更新する

```typescript:tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./plugins/**/*.{js,ts}",
    "./app.vue",
    "./error.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

最後に`./assets/css/main.css`を作成する

```typescript:./assets/css/main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

上記で設定は完了となる。

`app.vue`を更新し、TailwindCSSが正しく設定されているか、動作確認を行う。

```vue:app.vue
<template>
  <h1 class="text-3xl font-bold underline">
    Hello world!
  </h1>
</template>
```

サーバーを起動し、Hello Worldの文字にアンダーラインが記載されていれば、OKとなる

```sh
npm run dev
```

![スクリーンショット 2024-10-27 17.41.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/c35467dd-bdab-8b7b-4984-3ff9ba7a1627.png)

### Inspira UIの設定

Inspira UIの設定を行う。公式の手順を参考に進める。

https://inspira-ui.com/getting-started/installation

必要をインストール


```sh
npm install -D @inspira-ui/plugins clsx tailwind-merge tailwindcss-animate class-variance-authority
npm install @vueuse/core @vueuse/motion
```

インストール完了後、各種設定ファイルの更新を行う。

まず`tailwind.config.js`を以下の内容に更新する

```typescript:tailwind.config.js
/** @type {import('tailwindcss').Config} */
import animate from "tailwindcss-animate";
import { setupInspiraUI } from "@inspira-ui/plugins";
export default {
  darkMode: "selector",
  safelist: ["dark"],
  prefix: "",
  content: [
    "./components/**/*.{js,vue,ts}",
    "./layouts/**/*.vue",
    "./pages/**/*.vue",
    "./plugins/**/*.{js,ts}",
    "./app.vue",
    "./error.vue",
    "./node_modules/flowbite/**/*.{js,ts}",
  ],
  theme: {
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        xl: "calc(var(--radius) + 4px)",
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [animate, setupInspiraUI],
};
```

`assets/css/main.css`も、以下の内容に更新する

```css:assets/css/main.css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;

    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;

    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;

    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;

    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 47.4% 11.2%;

    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;

    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;

    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;

    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;

    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 48%;
  }
}
```

`lib/utils.ts`を新規作成する。

```typescript:lib/utils.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

export type ObjectValues<T> = T[keyof T];
```

最後に`nuxt.config.ts`に`@vueuse/motion/nuxt`のモジュール定義を追加する。

```typescript:nuxt.config.ts
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ["@vueuse/motion/nuxt"], // 追加
  devtools: { enabled: true },
  css: ["~/assets/css/main.css"],
  postcss: {
    plugins: {
      tailwindcss: {},
      autoprefixer: {},
    },
  },
});
```

こちらでIsnpira UIの設定は完了。

### 紙吹雪のUIの実装

紙吹雪(Confetti)を利用する場合は、以下のライブラリを追加でインストールする必要がある。

```sh
npm install canvas-confetti
npm install -D @types/canvas-confetti
```

その後`app.vue`を更新する。


```vue:app.vue
<template>
  <div
    class="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"
  >
    <span
      class="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"
    >
      Confetti
    </span>

    <!-- Confetti component with ref -->
    <Confetti
      ref="confettiRef"
      class="absolute top-0 left-0 z-0 size-full"
      @mouseenter="fireConfetti"
    />
  </div>
</template>

<script setup lang="ts">
const confettiRef = ref(null);

// Function to trigger confetti
const fireConfetti = () => {
  confettiRef.value?.fire({});
};
</script>
```

Confettiコンポーネントを作成し花吹雪の処理を実装する。

```vue:components/Confetti.vue
<template>
  <div>
    <canvas ref="canvasRef" :class="$props.class"></canvas>
    <slot />
  </div>
</template>

<script setup lang="ts">
import confetti, {
  GlobalOptions as ConfettiGlobalOptions,
  Options as ConfettiOptions,
  CreateTypes as ConfettiInstance,
} from "canvas-confetti";

type Api = {
  fire: (options?: ConfettiOptions) => void;
};

type ConfettiProps = {
  options?: ConfettiOptions;
  globalOptions?: ConfettiGlobalOptions;
  manualstart?: boolean;
  class?: string;
};

const props = defineProps<ConfettiProps>();

const instanceRef = ref<ConfettiInstance | null>(null);
const canvasRef = ref<HTMLCanvasElement | null>(null);

// Confetti API
function fire(opts: ConfettiOptions = {}) {
  instanceRef.value?.({ ...props.options, ...opts });
}

const api: Api = { fire };

provide("ConfettiContext", api);

// Initialize confetti when mounted
onMounted(() => {
  if (canvasRef.value) {
    instanceRef.value = confetti.create(canvasRef.value, {
      ...props.globalOptions,
      resize: true,
    });

    if (!props.manualstart) {
      fire();
    }
  }
});

// Cleanup when unmounted
onUnmounted(() => {
  if (instanceRef.value) {
    instanceRef.value.reset();
    instanceRef.value = null;
  }
});

defineExpose({
  fire,
});
</script>
```

こちらで実装は完了。数秒おきに紙吹雪が表示されるようになる。

![confetti.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163680/68d50c6b-fc3f-d596-c7ff-0676c3f4113c.gif)


紙吹雪の出し方を調整したい場合、以下の公式ドキュメントを参照

https://inspira-ui.com/components/confetti

## おわりに

他に利用可能なコンポーネントは、以下のURLから確認できる。

https://inspira-ui.com/components