# HTMLプレゼンテーションテンプレート スライドプレゼンテーション生成のリファレンスアーキテクチャ。すべてのプレゼンテーションはこの構造に従います。 ## ベースHTML構造 ```html Presentation Title

Presentation Title

Subtitle or author

Slide Title

Content...

``` ## 必須JavaScriptの機能 すべてのプレゼンテーションに以下が必要です: 1. **SlidePresentationクラス** — メインコントローラー(以下を含む): - キーボードナビゲーション(矢印キー、スペース、Page Up/Down) - タッチ/スワイプサポート - マウスホイールナビゲーション - プログレスバーの更新 - ナビゲーションドット 2. **Intersection Observer** — スクロールトリガーアニメーション用: - スライドがビューポートに入ったときに `.visible` クラスを追加 - CSSトランジションを効率的にトリガー 3. **オプションの拡張機能**(選択したスタイルに合わせる): - カスタムカーソルとトレイル - パーティクルシステム背景(canvas) - パララックスエフェクト - ホバー時の3Dチルト - マグネティックボタン - カウンターアニメーション 4. **インライン編集**(フェーズ1でユーザーが選択した場合のみ — 「いいえ」の場合は完全にスキップ): - 編集トグルボタン(デフォルト非表示、ホバーホットゾーンまたはEキーで表示) - localStorageへの自動保存 - ファイルのエクスポート/保存機能 - 以下の「インライン編集の実装」セクションを参照 ## インライン編集の実装(オプトインのみ) **フェーズ1でユーザーがインライン編集に「いいえ」を選択した場合、編集関連のHTML、CSS、JSを一切生成しないでください。** **CSS `~` 兄弟セレクターをホバーベースの表示/非表示に使用しないでください。** CSS単体のアプローチ(`edit-hotzone:hover ~ .edit-toggle`)は、トグルボタンの `pointer-events: none` がホバーチェーンを壊すため失敗します:ユーザーがホットゾーンにホバー → ボタンが表示 → マウスがボタンに向かって移動 → ホットゾーンを離れる → クリック前にボタンが消える。 **必須アプローチ:400msの遅延タイムアウトを持つJSベースのホバー。** HTML: ```html
``` CSS(表示/非表示はJSクラスのみで制御): ```css /* ここではCSS ~兄弟セレクターを使用しないでください! pointer-events: noneがホバーチェーンを壊します。 遅延タイムアウトを持つJSを使用する必要があります。 */ .edit-hotzone { position: fixed; top: 0; left: 0; width: 80px; height: 80px; z-index: 10000; cursor: pointer; } .edit-toggle { opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 10001; } .edit-toggle.show, .edit-toggle.active { opacity: 1; pointer-events: auto; } ``` JS(3つのインタラクション方法): ```javascript // 1. トグルボタンのクリックハンドラー document.getElementById("editToggle").addEventListener("click", () => { editor.toggleEditMode(); }); // 2. 400msのグレース期間を持つホットゾーンのホバー const hotzone = document.querySelector(".edit-hotzone"); const editToggle = document.getElementById("editToggle"); let hideTimeout = null; hotzone.addEventListener("mouseenter", () => { clearTimeout(hideTimeout); editToggle.classList.add("show"); }); hotzone.addEventListener("mouseleave", () => { hideTimeout = setTimeout(() => { if (!editor.isActive) editToggle.classList.remove("show"); }, 400); }); editToggle.addEventListener("mouseenter", () => { clearTimeout(hideTimeout); }); editToggle.addEventListener("mouseleave", () => { hideTimeout = setTimeout(() => { if (!editor.isActive) editToggle.classList.remove("show"); }, 400); }); // 3. ホットゾーンの直接クリック hotzone.addEventListener("click", () => { editor.toggleEditMode(); }); // 4. キーボードショートカット(Eキー、テキスト編集中はスキップ) document.addEventListener("keydown", (e) => { if ( (e.key === "e" || e.key === "E") && !e.target.getAttribute("contenteditable") ) { editor.toggleEditMode(); } }); ``` **重要: `exportFile()` はouterHTMLをキャプチャする前に編集状態を除去する必要があります。** ユーザーが編集モードでCtrl+Sを押すと、`document.documentElement.outerHTML` がライブDOM(`body.edit-active`、すべてのテキスト要素の `contenteditable="true"`、トグルボタンとバナーの `.active`/`.show` クラスを含む)をキャプチャします。保存されたファイルを開く人は、点線のアウトライン、チェックマークボタン、編集バナーが表示され、永久に編集モードのように見えます。 `exportFile()` は常にこのように実装してください: ```javascript exportFile() { // 保存されたファイルがクリーンに開くよう、一時的に編集状態を除去 const editableEls = Array.from(document.querySelectorAll('[contenteditable]')); editableEls.forEach(el => el.removeAttribute('contenteditable')); document.body.classList.remove('edit-active'); // トグルボタンとバナーからUIクラスも除去 const editToggle = document.getElementById('editToggle'); const editBanner = document.querySelector('.edit-banner'); editToggle?.classList.remove('active', 'show'); editBanner?.classList.remove('active', 'show'); const html = '\n' + document.documentElement.outerHTML; // ユーザーが編集を続けられるよう編集状態を復元 document.body.classList.add('edit-active'); editableEls.forEach(el => el.setAttribute('contenteditable', 'true')); editToggle?.classList.add('active'); editBanner?.classList.add('active'); const blob = new Blob([html], { type: 'text/html' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'presentation.html'; a.click(); URL.revokeObjectURL(a.href); } ``` ## 画像パイプライン(画像がない場合はスキップ) フェーズ1でユーザーが「画像なし」を選択した場合は完全にスキップします。画像が提供された場合は、HTML生成前に処理します。 **依存関係:** `pip install Pillow` ### 画像処理 ```python from PIL import Image, ImageDraw # 円形クロップ(モダン/クリーンなスタイルのロゴ用) def crop_circle(input_path, output_path): img = Image.open(input_path).convert('RGBA') w, h = img.size size = min(w, h) left, top = (w - size) // 2, (h - size) // 2 img = img.crop((left, top, left + size, top + size)) mask = Image.new('L', (size, size), 0) ImageDraw.Draw(mask).ellipse([0, 0, size, size], fill=255) img.putalpha(mask) img.save(output_path, 'PNG') # リサイズ(HTMLを肥大化させる大きすぎる画像用) def resize_max(input_path, output_path, max_dim=1200): img = Image.open(input_path) img.thumbnail((max_dim, max_dim), Image.LANCZOS) img.save(output_path, quality=85) ``` | 状況 | 操作 | | -------------------------------- | ----------------------------- | | 角丸デザインの正方形ロゴ | `crop_circle()` | | 1MB超の画像 | `resize_max(max_dim=1200)` | | アスペクト比が不正 | `img.crop()` で手動クロップ | 処理済み画像は `_processed` サフィックスで保存します。元ファイルは上書きしないでください。 ### 画像の配置 **ローカルで表示されるため直接ファイルパスを使用**(base64は不可): ```html Screenshot ``` ```css .slide-image { max-width: 100%; max-height: min(50vh, 400px); object-fit: contain; border-radius: 8px; } .slide-image.screenshot { border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } .slide-image.logo { max-height: min(30vh, 200px); } ``` **ボーダー/シャドウカラーは選択したスタイルのアクセントカラーに合わせてください。** 複数のスライドで同じ画像を繰り返さないでください(タイトルとクロージングのロゴは例外)。 **配置パターン:** タイトルスライドでロゴを中央に。スクリーンショットはテキストとの2カラムレイアウトで。フルブリード画像はテキストオーバーレイのあるスライド背景として(控えめに)。 --- ## コード品質 **コメント:** 各セクションに何をするのか、どう変更するのかを説明する明確なコメントを付けてください。 **アクセシビリティ:** - セマンティックHTML(`
`、`