# HTMLプレゼンテーションテンプレート
スライドプレゼンテーション生成のリファレンスアーキテクチャ。すべてのプレゼンテーションはこの構造に従います。
## ベースHTML構造
```html
Presentation Title
Presentation Title
Subtitle or author
```
## 必須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
```
```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(``、`