26
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

『スライド』をやめた ─ ページ遷移するたびにポケモンが鳴いてシロナ戦が流れるローカルアプリを爆速で作った話

26
Last updated at Posted at 2026-03-13

monthly_wrapped_demo.gif

2026年2月27日、ポケモンは誕生30周年を迎えました。
おめでとう。そしてありがとう、PokéAPI。この記事はその感謝を込めて書いています。

はじめに

みなさん、月次定例ってどうやって発表してますか?

PowerPoint? Googleスライド? Notion?

僕もそうでした。毎月、淡々とスライドを作って、淡々と発表して、淡々と終わる。
内容はちゃんとしてるはずなのに、なぜか聞いてる側の目が死んでいる

「いや、俺の2ヶ月の成果、もっと盛り上がってよくない?」

そんなワガママな欲求から、月次定例の発表スライドをやめて、Spotify Wrapped風のWebアプリを作りました。

BGMが流れ、ポケモンが鳴き、紙吹雪が舞う。
月次定例が、ライブになりました。

この記事でわかること

  • Spotify Wrapped風プレゼンアプリの設計思想と技術構成
  • Web Audio APIで効果音をコードだけで生成する方法
  • PokéAPIでポケモンの鳴き声を再生する実装
  • Claude Codeとの並行開発で爆速でアプリを完成させた話

完成したもの

Monthly Wrapped ─ Spotify Wrappedのストーリー形式で、月次の成果を発表するWebアプリです。

monthly_wrapped_demo.gif

機能一覧

機能 説明
📱 ストーリーUI Instagram Stories風プログレスバーで進行表示
🎨 10種のカラーテーマ スライドごとにグラデーションが変わる
✨ パーティクル&紙吹雪 35個のキラキラ + 最後に紙吹雪が降る
🎵 BGM ループ再生で会場の空気を支配
🔊 スライド遷移SE Web Audio APIでリアルタイム生成した効果音
🐾 ポケモン 各スライドに伝説ポケモンが登場し、鳴く
📊 カウントアップ 数字が0からイージング付きで増加
📸 メディア埋め込み 画像・動画をスライド内に表示

全部入り。 やりすぎました。

全体アーキテクチャ


なぜ作ったのか

きっかけは単純で、Spotify Wrappedが好きすぎた

年末に「あなたの今年のトップアーティストは〜」ってやるあれです。
あのワクワク感、テンポの良さ、次のスライドが気になる感じ。

「これ、月次定例でやったら最高では?」

と思ったのが運の尽きでした。

Before / After

Before 😐 After 🎉
形式 PowerPoint Spotify Wrapped風Webアプリ
無音 BGM + 効果音 + ポケモンの鳴き声
視覚 静的なスライド パーティクル + カウントアップ + 紙吹雪
反応 「ふーん」 拍手
更新コスト 毎月スライド作り直し JSON差し替え 5分
ポケモン いない 鳴く

技術スタック

React 18.3 + Vite 5.4

以上。

フレームワークはReactだけ。状態管理ライブラリなし。CSSフレームワークなし。 バニラに近い構成です。

技術 用途 一言
React 18 UIコンポーネント これだけで十分
Vite 5 ビルド&開発サーバー 爆速HMR
Web Audio API 効果音のリアルタイム生成 音をコードで作る狂気
PokéAPI ポケモンのスプライト&鳴き声 神API。感謝
CSS Keyframes パーティクル・紙吹雪・アニメーション GPU加速で60fps
JSON スライドデータ管理 差し替えるだけで内容変更

ソースファイルはたった5つ。合計 約600行。 これで全機能入りのプレゼンアプリが動きます。

src/
├── App.jsx          # 110行 - ナビゲーション & プログレスバー
├── Slide.jsx        # 330行 - 6種類のスライド描画エンジン
├── Confetti.jsx     # 紙吹雪パーティクル(物理演算付き)
├── useAudio.js      # 107行 - BGM + SE + ポケモン鳴き声
└── useParallax.js   # パララックス効果

おもしろ実装ポイント 6選

1. 会議でBGMが流れる ─ 正気か?

まず一番ヤバいのはこれ。月次定例にBGMを付けた。

実装自体はシンプルで、Web Audio APIの AudioContext でループ再生しているだけ:

const startBGM = () => {
  const ctx = getCtx();
  fetch('/assets/bgm.mp3')
    .then(r => r.arrayBuffer())
    .then(buf => ctx.decodeAudioData(buf))
    .then(audioBuffer => {
      const source = ctx.createBufferSource();
      source.buffer = audioBuffer;
      source.loop = true;  // ← これだけでずっと流れる
      const gain = ctx.createGain();
      gain.gain.value = 0.08;  // 主張しすぎない音量
      source.connect(gain).connect(ctx.destination);
      source.start();
    });
};

技術的には大したことない。でも「会議でBGMを流すという発想がそもそもおかしい。

しかも曲はダイヤモンド・パールのシロナ戦

ポケモン史上最強のチャンピオン戦BGMが、月次定例の会議で流れている。冷静に考えてほしい。報告資料の発表中にシロナ戦のイントロが鳴ってる。 なぜか緊張感が出る。なぜか本気になる。

gain.gain.value = 0.08 ─ この音量調整に全てが詰まっている。大きすぎると発表の邪魔(シロナ戦は主張が強い)、小さすぎると存在感がない。0.08、これがベスト。会議のBGMの最適音量を追い求めた男がここにいる。


2. 効果音はダウンロードしない、その場で合成する

スライド遷移時の「シュッ」という効果音、音声ファイルを使っていません

Web Audio APIでホワイトノイズを生成→バンドパスフィルター→250msで減衰。すべてリアルタイムで合成しています。

const playSlideSE = () => {
  const ctx = getCtx();
  const duration = 0.25;
  const buffer = ctx.createBuffer(1, ctx.sampleRate * duration, ctx.sampleRate);
  const data = buffer.getChannelData(0);

  // ホワイトノイズ × エンベロープ
  for (let i = 0; i < data.length; i++) {
    const t = i / data.length;
    const envelope = Math.sin(t * Math.PI) * (1 - t * 0.5);
    data[i] = (Math.random() * 2 - 1) * envelope * 0.15;
  }

  const source = ctx.createBufferSource();
  source.buffer = buffer;

  // バンドパスフィルターで「シュッ」感を演出
  const filter = ctx.createBiquadFilter();
  filter.type = 'bandpass';
  filter.frequency.setValueAtTime(2000, ctx.currentTime);
  filter.frequency.exponentialRampToValueAtTime(800, ctx.currentTime + duration);
  filter.Q.value = 1.5;

  // ゲイン減衰
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(0.3, ctx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);

  source.connect(filter).connect(gain).connect(ctx.destination);
  source.start();
  source.stop(ctx.currentTime + duration);
};

音をコードで作る。 冷静に考えるとヤバいですが、これが Web Audio APIの本気です。

音声ファイルの管理が不要になるし、パラメータを変えるだけで音色を調整できる。月次定例のためにここまでやる必要があったかは…まぁ、あった。

Web Audio API のポイント
ブラウザの自動再生ポリシーにより、ユーザーインタラクション後にしかAudioContextを初期化できません。今回は「画面をクリックして開始」のオーバーレイで解決しています。


3. ポケモンが毎スライド鳴く 🎉 祝30周年

2026年2月27日、ポケモンは30周年を迎えました
このアプリで盛大にお祝いさせてもらいました。

各スライドにはポケモンIDが設定されており、PokéAPIからスプライト画像と鳴き声をリアルタイムで取得しています。

const playPokemonCry = (pokemonId) => {
  const ctx = getCtx();
  const url = `https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/${pokemonId}.ogg`;

  fetch(url)
    .then(r => r.arrayBuffer())
    .then(buf => ctx.decodeAudioData(buf))
    .then(audioBuffer => {
      const source = ctx.createBufferSource();
      source.buffer = audioBuffer;
      const gain = ctx.createGain();
      gain.gain.value = 0.1;  // 控えめに鳴く(会議中なので)
      source.connect(gain).connect(ctx.destination);
      source.start();
    });
};

スライド遷移時、400msの間を置いてからポケモンの鳴き声を再生しています。この「間」が大事で、遷移SEとポケモンの鳴き声が被らないようにしている。

01_intro_dialga.png

こだわり:全員が歴代パッケージの伝説ポケモン

このアプリに登場するポケモン、実は全員が歴代シリーズのパッケージを飾った伝説ポケモンです。

最初のスライドはディアルガ。ダイヤモンド・パールの顔で、時間を司るポケモン。「月次」の始まりにこれ以上の選択肢はない。

スライド ポケモン パッケージ 選んだ理由
intro ディアルガ 💎 ダイヤモンド 時間を司る → 「月次」の始まり
成長 ホウオウ 🥇 金 復活と再生 → チームの成長
横断 カイオーガ 🔵 サファイア 海の王 → oceanテーマ
開発① レックウザ 💚 エメラルド 天空の支配者 → emeraldテーマ
開発② グラードン 🔴 ルビー 大地の創造 → sunsetテーマ
AI分析 レシラム ⬜ ホワイト 真実の白炎 → 情熱
広告 パルキア 💜 パール 空間を司る → violetテーマ
管理 ゼルネアス 🦌 X 生命の力 → forestテーマ
文化 ルギア 🥈 銀 海の守り神 → チームの結束
発見 ゼクロム ⬛ ブラック 理想の黒雷 → 課題発見
視点 ソルガレオ ☀️ サン 太陽の使者 → 新しい視点
計画 ギラティナ 🌀 プラチナ 反転世界 → 未来への布石
まとめ アルセウス ⭐ LEGENDS 創造神 → すべてを締めくくる

ディアルガで始まり、アルセウスで終わる。

パッケージの伝説ポケモンだけで13枚のスライドを構成する。やりすぎ? いいえ、愛です。 30周年だし。


4. 紙吹雪は物理演算している

最後のサマリースライドで降る紙吹雪、ちゃんと重力加速度と回転を計算しています。

// 毎フレーム物理演算
p.y += p.vy;
p.vy += 0.15;                   // 重力加速度
p.x += p.vx;
p.rotation += p.rotationSpeed;  // 回転

// 画面外に出たら上から再投入(オブジェクトプール)
if (p.y > height + 20) {
  p.y = -20;
  p.vy = Math.random() * 2 + 1;
}

月次定例に物理演算は要るのか?

要る。

紙吹雪がヒラヒラと自然に落ちてくるだけで、会場の「おお〜」が全然違います。

02_stat_team_growth.png


5. カウントアップが気持ちいい

数字の表示は0からスタートして、ease-out cubicのイージングでカウントアップ。

const eased = 1 - Math.pow(1 - progress, 3);  // ease-out cubic
setText(String(Math.round(eased * target)));

03_stat_projects.png

「7プロジェクト」→「0...1...3...5...7プロジェクト!」と加速しながら増える。

最初にグッと増えて、目標値に近づくにつれてゆっくりになる。「到達した感」が気持ちいい。

ただの数字なのにテンションが上がる。これがWrapped力です。


6. プログレスバーがInstagram Stories

画面上部にInstagram Storiesと同じプログレスバーを実装しました。

[████████][████░░░░][░░░░░░░░][░░░░░░░░][░░░░░░░░]
  完了      現在      未読      未読      未読

現在のスライドがアニメーションで埋まっていき、次に進むと次のバーが動き始める。

「あと何枚あるんだろう」がわかるのが地味に大事。 聞いてる人の集中力が持続します。

PowerPointの 12/30 とは情報量が違う


Claude Codeとの並行開発がヤバかった

今回の開発で一番語りたいのは、実はClaude Codeとの共同開発体験です。

開発フロー

直列 vs 並行:開発のガントチャート

普通、一人で開発すると直列でしか進まない:

Claude Codeとの並行開発だと:

圧倒的に速い。

しかも、仕様書を先に書いておいたことで、Claude Codeが仕様に沿って自律的に実装してくれた。

「ここどうしますか?」じゃなくて「仕様通りに作りました、確認してください」が返ってくる。

Claude Codeに任せたこと

タスク 詳細 感想
🎨 10種のカラーテーマ グラデーション・アクセント・グロウを全パターン一発生成 色彩感覚がある
🔊 Web Audio API ノイズ生成→フィルター→減衰のパイプラインを一発実装 音響工学もいける
🐾 PokéAPI連携 fetch→decode→再生の非同期処理、エラーハンドリング込み 非同期処理完璧
✨ CSSアニメーション パーティクル35個分のランダムパラメータ、8種のkeyframes 職人芸
📝 発表原稿 8分間のタイムスケジュール付きセリフを生成 ライターもできる
🐾 ポケモン選定 各スライドの内容に合わせた伝説ポケモンを提案 ポケモンの解像度が高い

Claude Code活用のコツ

一番のポイントは「仕様書を先に書く」こと。
Claude Codeに丸投げするんじゃなくて、設計と仕様は自分で決める。実装を任せる。
自分 = プロデューサー、Claude Code = エンジニアチーム。この役割分担がハマった。

仕様書に書いた内容はこんな感じ:

仕様書の抜粋(クリックで展開)
## スライドタイプ一覧
- intro: タイトル画面(bigText, subText)
- stat: 数字を大きく見せる(bigText, unit, subText)
- project: 成果紹介(title, description, tags, media)
- highlight: ハイライトエピソード(bigText, subText)
- summary: 締めくくり(stats配列, subText)

## ルール
- 1スライド1メッセージ。情報を詰め込みすぎない
- 連続するスライドで同じthemeを使わない
- bigTextは短く。長い文章はsubTextに

## カラーテーマ
midnight, coral, emerald, violet, ocean, flame, forest, sunset, rose, gold

スライドのデータ構造

JSONを書き換えるだけで誰でも自分の月次発表が作れます。

{
  "meta": {
    "name": "あなたの名前",
    "team": "チーム名",
    "month": "2026年3月",
    "monthShort": "Mar 2026"
  },
  "slides": [
    {
      "type": "intro",
      "theme": "midnight",
      "topLabel": "MONTHLY WRAPPED",
      "bigText": "2026年3月",
      "subText": "あなたの1ヶ月を振り返ろう",
      "pokemon": 483
    },
    {
      "type": "stat",
      "theme": "coral",
      "topLabel": "PROJECTS",
      "bigText": "5",
      "unit": "プロジェクト",
      "subText": "今月関わったプロジェクト数",
      "pokemon": 250
    }
  ]
}

来月の発表? slides.json を差し替えるだけ。5分で終わる。

毎月の運用も自動化

仕様書にヒアリングフローを組み込んであるので、Claude Codeに以下を送るだけ:

あなたは「Monthly Wrapped」のスライドデータを作成するアシスタントです。
Spotify Wrapped風のストーリー形式で、私の今月の成果を魅力的に伝えるスライドを作ってください。

Claude Codeが対話形式で今月の成果をヒアリングして、slides.json自動生成してくれます。

つまり毎月の運用コストがほぼゼロ。最高の仕組みができてしまった。


10種のカラーテーマ

スライドごとに雰囲気がガラッと変わるのが、Wrapped体験の肝です。

テーマ 色味 おすすめ用途
🌙 midnight 紺→紫→暗灰 intro、締めくくり
🪸 coral 暗赤→赤→オレンジ 数字インパクト
🌿 emerald 暗緑→エメラルド→ミント プロジェクト紹介
💜 violet 暗紫→紫→薄紫 インパクト数値
🌊 ocean 暗青→青→スカイブルー プロジェクト紹介
🔥 flame 暗赤→赤→オレンジ 熱いトピック
🌲 forest 暗緑→緑→ライトグリーン プロジェクト紹介
🌅 sunset 暗橙→オレンジ→黄 数字・成果
🌹 rose 暗ピンク→ローズ→桃色 エモいハイライト
✨ gold 暗金→ゴールド→黄 summary(ラスト)

ルール: 連続するスライドで同じテーマを使わない。 色の変化で「次に進んだ」感を出す。


発表当日 ─ 会議がライブになった

会議室でプロジェクターに映して、npm run dev で起動。

画面に「MONTHLY WRAPPED — 画面をクリックして開始」のオーバーレイが表示される。

参加者が全員着席したのを確認して、クリック。

BGMが流れ始める。

一瞬の沈黙。そして笑い。

「え、音楽流れるの?」

そう、月次定例で音楽が流れるんです。 普通に考えたらおかしい。会議ですよ。アジェンダがあって、議事録があって、次のアクションを決める場で、BGMがループ再生されてる。

でもこれが効く。

BGMが流れた瞬間、聞く姿勢が「会議モード」から「コンテンツを楽しむモード」に切り替わる。Spotify Wrappedを見るときの、あのリラックスした集中力。月次定例なのに、みんなの目がちゃんと画面を見てる

スライドが切り替わるたびに「シュッ」と効果音。
数字がカウントアップして、ポケモンが鳴く。
パーティクルがキラキラ舞う。
BGMはずっとループしてる。

いつもの月次定例が、完全にライブになった。

13_summary_arceus.png

最後のサマリースライドでアルセウスが鳴き、紙吹雪が降った時、
普通に拍手が起きました。

月次定例で拍手って何?

BGMの威力、マジで侮れない。 同じ内容を無音で発表するのと、BGM付きで発表するのでは聞いてる人の反応が全然違う。エンタメの基本はやっぱり音楽。映画もゲームもそう。月次定例だって同じだった。

操作方法

操作 アクション
画面右側クリック / →キー / Space 次のスライド
画面左側クリック / ←キー 前のスライド
スワイプ左右(モバイル) 前後移動

おまけ:技術の学びメモ

Web Audio API — ブラウザで音を"作る"

ブラウザ上で音をプログラムで生成できるのは本当に面白い。効果音を外部ファイルに依存せず、コードだけで作れるのはWebアプリならではの強み。

注意点:

  • AudioContextユーザーインタラクション後にしか初期化できない(ブラウザの自動再生ポリシー)
  • audioCtx.state === 'suspended' のチェックを忘れずに
  • 今回は「クリックして始める」オーバーレイでこれを解決
PokéAPI — 30年分のポケモンデータが無料
  • Official Artworkのスプライトが高解像度でおすすめ
  • 鳴き声は GitHub上の .ogg ファイルから直接取得可能
  • レスポンスが速い。個人開発の遊び心を加えるのに最高のAPI
  • 完全無料。感謝しかない
CSS clamp() — メディアクエリ不要のレスポンシブ
font-size: clamp(2.5rem, 8vw, 6rem);

min, preferred, max を一行で指定できるので、メディアクエリなしでレスポンシブな文字サイズが実現できます。プロジェクター(大画面)でもスマホでも読みやすいサイズになるので、プレゼンアプリに特に効きました。

Reactカスタムフック — ロジックの分離

useAudio フックで音声ロジックを完全に分離したことで、UIコンポーネント側は関数を呼ぶだけ:

const { playSlideSE, playPokemonCry, startBGM } = useAudio();

// スライド遷移時
playSlideSE();
setTimeout(() => playPokemonCry(slide.pokemon), 400);

音の実装を知らなくていい。フックによるロジック分離は、コンポーネントの見通しを良くするだけでなく、テストや差し替えも容易にしてくれます。


まとめ

月次定例は、もっと楽しくていい。

技術の無駄遣いだと思いますか?
僕は技術の正しい使い方だと思っています。

だって、自分たちの成果を伝える場なんだから、
最高の体験で届けたいじゃないですか。

ポケモン30周年、Spotify Wrapped、Claude Code。
2026年のいいとこ取りをして、月次定例をライブにした話でした。

来月はどのポケモンを鳴かせようかな。


この記事が面白かったらLGTMお願いします!
過去、Chromeの新規タブをドラクエ5風に改造した記事を書いたので、そちらもぜひ。

26
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?