Udemy撮影用フォーカスモードの設計と実装
動画撮影で画面をキャプチャすると、Miller Columnsの4カラム全てが映り込む。ナビゲーション用のColumn 2/3は視聴者にとってノイズでしかない。撮影時だけカラムを消してコンテンツを全幅表示するモードを作ることにした。
フォーカスモードの状態管理
なぜ composable + useState か
最初はレイアウトの <slot /> 経由でpropsを渡す案を検討したが、Nuxtの <slot /> はpropsを受け取れない。レイアウト→ページ→子コンポーネントとpropsをバケツリレーする設計は、階層が深くなるたびに接続コードが増える。
useFocusMode() composableを作り、useState でアプリ全体の状態を共有する方式に切り替えた。どのコンポーネントからでも useFocusMode() を呼ぶだけで状態を読み書きできる。
// useFocusMode.ts の核心部分
const focusMode = useState<boolean>('focusMode', () => false)
const toggle = () => { focusMode.value = !focusMode.value }
Codexレビューで「SSR時にuseStateの初期値がクライアントと不一致になる可能性」を指摘されたが、フォーカスモードは常にfalse起動なので問題なし。
MillerViewerのフォーカスモード対応
Column 2/3を v-if="!focusMode" で非表示にし、Column 4(ステージ)が全幅を占めるようにした。フォーカスモード時はColumn 1も非表示になり、スライドコンテンツだけが画面に残る。
ナビゲーション情報が消えると「今どこにいるか」がわからなくなる。Column 4のヘッダーにパンくずリストを表示して、現在位置を補った。
パンくずリスト統合進捗バー
パンくずリストに4段階の進捗バーを組み込んだ。
| レベル | 表示内容 | 例 |
|---|---|---|
| 全体 | 全コースの進捗 | 28/1306 |
| 大カテゴリー | セクション内の進捗 | 3/12 |
| 中カテゴリー | チャプター内の進捗 | 2/5 |
| 小カテゴリー | トピック内の進捗 | 1/3 |
各レベルをチップ化し、背景色のフィルで進捗を表現した。チップの左端から現在位置の割合だけ色が埋まっていく。テキストで「28/1306」と表示しつつ、背景色が約2%分だけ塗られている。数字と色の二重チャネルで進捗が伝わる。
全コース累計位置の算出
全1306スライドの中で「今何枚目か」を出すために、セクション・チャプター・トピックの階層をフラットに展開し、各スライドに累計インデックスを振った。カテゴリーを選択するたびに、そのカテゴリー配下の先頭スライドの累計インデックスを引いて相対位置を計算する。
シーンカードトランジション
セクションやチャプターの境界を越えたときに、映画のシーンカードのようなトランジションを入れた。
- 中カテゴリー間: タイトルカードがフェードインし、1秒後にフェードアウトしてスライドが現れる
- 大カテゴリー間: 画面全体が暗転し、新しいセクション名が浮かび上がってから次のスライドへ遷移する
大カテゴリー間の暗転演出は、視聴者に「章が変わった」と体で感じさせるための仕掛け。スライドがパッと切り替わるだけだと、セクション境界を見逃す。
セクション境界の自動ナビゲーション
大カテゴリーの最後のスライドで右矢印を押すと、次の大カテゴリーの先頭スライドに自動遷移するようにした。カテゴリー階層の状態(Column 1/2/3の選択状態)も連動して更新される。
逆方向も同様で、先頭スライドで左矢印を押すと前の大カテゴリーの末尾に戻る。1306枚を矢印キーだけで端から端まで移動できる。
localStorageによるスライド位置復帰
ブラウザをリロードしたとき、最後に見ていたスライドに戻りたい。ページパスごとにglobalIdx(累計インデックス)をlocalStorageに保存し、ページ読み込み時に復帰する。
// 保存: スライド遷移のたびに更新
localStorage.setItem(`slide-pos:${route.path}`, String(globalIdx))
// 復帰: onMounted時に読み出し
const saved = localStorage.getItem(`slide-pos:${route.path}`)
if (saved) navigateToSlide(Number(saved))
撮影中にブラウザが落ちても、同じスライドから再開できる。1306枚の中から手動で探し直す手間が消えた。
振り返り
フォーカスモードの設計で一番時間を使ったのは、状態管理の方式選定だった。<slot />にpropsを渡せないとわかった瞬間に手が止まり、composable + useStateの案に切り替えるまで30分ほど設計図を書き直した。結果的に、どのコンポーネントからでもワンライナーで状態にアクセスできる構成に落ち着いた。
進捗バーのチップ化は、パンくずリストと進捗表示を1行に収めるための工夫だった。背景フィルで進捗を表現してみたら、数字を読まなくても色の面積だけで「まだ序盤だな」と目が判断していた。
シーンカードトランジションは、動画撮影という用途を考えると必須だった。テキストベースの学習プラットフォームなら不要だが、動画では視聴者が画面から目を離す瞬間がある。暗転が入ることで「今から新しい話が始まる」と注意を引き戻せる。