keynot について
Keynot は、ブラウザーで表示する単一 HTML ファイルのプレゼンテーションを作成するスキルです。素早く編集したい場合やブランドに沿ったスライドが必要な場合に適し、実行時の外部依存なしでキーボード、タッチ、全画面操作に対応します。
主な機能
- 01自己完結型 HTML プレゼンテーションの作成
- 02キーボードとタッチ操作のナビゲーション
- 03全画面表示とアニメーション演出
- 04画像の埋め込みとブランドスタイルの適用
- 05印刷または PDF 出力
- 06複数のスライドレイアウトパターン
活用シーン
- 01ブラウザーで開けるプレゼンテーションの作成
- 02ブランドガイドに沿ったスライドの設計
- 03スライドの印刷または PDF 保存
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ shawnzam/ keynot --skill keynotインストール後、Claude Code で/keynotを入力して読み込まれたか確認します。
詳細
Keynot
ブラウザーで表示するスライドプレゼンテーションを、自己完結した単一の HTML ファイルとして作成するスキルです。キーボード操作、スワイプ、全画面表示、段階的なアニメーション表示、画像の埋め込み、ブランドに忠実なデザインに対応します。実行時に外部依存関係は必要ありません。
このスキルを使う場面
- ユーザーが「プレゼンテーション」「デッキ」「スライド」「発表用の 1 ページ資料」を求めている
- ユーザーがブランドスタイルガイドを持っており、それに合わせたスライドを求めている
- ユーザーがブラウザーですばやくスライドを反復編集したい
- ユーザーがソフトウェアなしでどこでも開けるファイルを求めている
次の場合を除き、PPTX より HTML を優先します。
- ユーザーが明示的に
.pptxを求めている - ユーザーが PowerPoint でスライドを編集する必要がある
- ユーザーが Office で編集する必要のある相手と共有する
ステップ 1:ブランドガイドを抽出する
ユーザーからブランドの PDF、URL、またはブランドについての説明が提供された場合は、コードを書く前に以下を抽出します。明確かつ正確に記述してください。
抽出する項目
色
- プライマリ(主となる背景色/テキスト色)
- アクセント(行動喚起、罫線、強調表示)
- ニュートラル(背景、クリーム色、グレー)
- 正確な HEX コードを記録します。決して近似値を使わないでください
タイポグラフィ
- セリフ体フォント(大見出し、引用文)
- サンセリフ体フォント(本文、ラベル、UI)
- 独自フォントが指定されている場合(例:ITC Stone Serif、Acumin)は、最も近い Google Fonts のフォントを探します
- フォントの組み合わせを明記します
レイアウトの特徴
- 主となる色の使用比率(例:「青を常に主体とし、赤はアクセントとしてのみ使う」)
- ガイドに見られる典型的なレイアウトパターン(分割パネル、罫線、区切り線、余白)
- トーン:フォーマル/エディトリアル、温かみがある/親しみやすい、ミニマル/高密度
ロゴとマークのルール
- ロゴ/ワードマークを再現してよいか
- クリアスペースの要件
- 承認済みの色のバリエーション(フルカラー、白、黒)
- 注意:大学や企業などの機関ブランドでは、商標登録されたロゴを再現せず、テキストベースのワードマークを使用します
ブランド記録の例
抽出したら、確認や差し替えが簡単にできるよう、ブランドのパラメーターをデッキの <style> 冒頭に簡潔に記録します。
Primary Colors:
Primary: #HEX (dominant — backgrounds, headings)
Accent: #HEX (rules, CTAs, status — used sparingly)
Cream: #HEX (light slide backgrounds)
Usage Rule: One color dominates. Accent appears sparingly —
rules, call-to-action bars, status indicators.
Never use the accent color as a full background.
Typography:
Serif: <Brand serif> → <closest Google Font>
Sans-serif: <Brand sans-serif> → <closest Google Font>
Layout Language:
- Which colors own which regions (dark panels vs. light panels)
- Any spine bars, rules, or dividers the brand uses
- Layout motifs (split panels, stat columns, card grids)
- Tone: editorial / warm / minimal / dense
Logo Rule: For institutional or trademarked brands, do NOT
reproduce the logo. Use a text-based wordmark instead:
<div class="wordmark">
<div class="w-line-1">Organization</div>
<div class="w-line-2">Subtitle or parent entity</div>
</div>
ステップ 2:デッキの構成
各デッキは単一の HTML ファイルです。すべてのアセット(CDN 経由のフォント、base64 形式の画像)を埋め込みます。
シェル構造
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="deck" id="deck">
<div class="slide" id="s1">...</div>
<div class="slide" id="s2">...</div>
</div>
<div class="nav">
<button id="prev">←</button>
<div class="dots" id="dots"></div>
<button id="next">→</button>
<span class="counter" id="counter">1 / N</span>
<button id="fsBtn" onclick="toggleFS()">⛶</button>
</div>
<div class="rotate-overlay" aria-hidden="true">
<svg class="rotate-icon" width="56" height="56" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2.5" />
<line x1="12" y1="18.5" x2="12.01" y2="18.5" />
</svg>
<h2>Rotate your device</h2>
<p>This deck is designed for landscape viewing. Turn your phone sideways to continue.</p>
</div>
<script></script>
</body>
</html>
CSS の基本パターン
:root {
/* Always use CSS variables for brand colors — swap these per brand */
--primary: #1a1a2e; /* dominant: dark panels, headings */
--accent: #e94560; /* sparingly: rules, CTAs, status */
--white: #ffffff;
--cream: #f7f4ee; /* light slide backgrounds */
--serif: 'Cormorant Garamond', Georgia, serif;
--sans: 'DM Sans', system-ui, sans-serif;
}
/* Deck shell */
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
.deck { width: 100vw; height: 100vh; position: relative; }
/* Slides: opacity transition, stacked absolutely */
.slide {
position: absolute; inset: 0;
opacity: 0; pointer-events: none;
transition: opacity .55s cubic-bezier(.4,0,.2,1);
}
.slide.active { opacity: 1; pointer-events: all; }
/* Staggered reveal animation on active slide children */
.slide.active .reveal { animation: fadeUp .6s both; }
.slide.active .reveal:nth-child(2) { animation-delay: .1s; }
.slide.active .reveal:nth-child(3) { animation-delay: .2s; }
/* ...continue for as many children as needed */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
ナビゲーション CSS
.nav {
position: fixed; bottom: 28px; left: 50%;
transform: translateX(-50%);
z-index: 999;
display: flex; align-items: center; gap: 14px;
background: rgba(26,26,46,.75); /* tint from --primary */
backdrop-filter: blur(10px);
padding: 9px 20px; border-radius: 40px;
border: 1px solid rgba(255,255,255,.14);
opacity: 1;
transition: opacity .4s ease; /* required for auto-hide */
}
.nav-hidden { opacity: 0; pointer-events: none; } /* auto-hide state */
.nav button {
background: none; border: none; cursor: pointer;
color: rgba(255,255,255,.65); font-size: 16px;
width: 30px; height: 30px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
transition: color .2s, background .2s;
}
.nav button:hover { color: #fff; background: rgba(255,255,255,.12); }
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(255,255,255,.3); cursor: pointer;
transition: background .2s, transform .2s;
}
.dot.active { background: var(--accent); transform: scale(1.35); }
JavaScript(完全版 — 算術処理を変更しないでください)
// Fullscreen
function toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
document.getElementById('fsBtn').innerHTML = '✕';
} else {
document.exitFullscreen();
document.getElementById('fsBtn').innerHTML = '⛶';
}
}
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement)
document.getElementById('fsBtn').innerHTML = '⛶';
});
// Deep-link & session persistence helpers
function parseHash() {
const m = location.hash.match(/^#slide-(\d+)$/);
if (!m) return -1;
const idx = parseInt(m[1], 10) - 1; // hash is 1-based, array is 0-based
return (idx >= 0 && idx < document.querySelectorAll('.slide').length) ? idx : -1;
}
function initialSlide() {
const fromHash = parseHash();
if (fromHash >= 0) return fromHash;
const stored = sessionStorage.getItem('keynot-slide');
if (stored !== null) {
const idx = parseInt(stored, 10);
if (idx >= 0 && idx < document.querySelectorAll('.slide').length) return idx;
}
return 0;
}
function persistSlide(idx) {
sessionStorage.setItem('keynot-slide', idx);
history.replaceState(null, '', '#slide-' + (idx + 1));
}
// Navigation
const slides = document.querySelectorAll('.slide'); // REQUIRED — do not omit
const dotsEl = document.getElementById('dots');
const counter = document.getElementById('counter');
let cur = initialSlide();
// Remove default 'active' class from slide 1 if restoring a different slide
slides.forEach(s => s.classList.remove('active'));
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot';
d.addEventListener('click', () => go(i));
dotsEl.appendChild(d);
});
// Activate the initial slide
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
function go(n) {
slides[cur].classList.remove('active');
dotsEl.children[cur].classList.remove('active');
cur = (n + slides.length) % slides.length;
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
}
// Deep-link: respond to manual hash changes or pasted URLs
window.addEventListener('hashchange', () => {
const idx = parseHash();
if (idx >= 0 && idx !== cur) go(idx);
});
document.getElementById('next').addEventListener('click', () => go(cur + 1));
document.getElementById('prev').addEventListener('click', () => go(cur - 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') go(cur + 1);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') go(cur - 1);
if (e.key === 'f' || e.key === 'F') toggleFS();
});
// Auto-hide nav after 1.8s of inactivity; reappear on any interaction
const nav = document.querySelector('.nav');
let hideTimer;
function showNav() {
nav.classList.remove('nav-hidden');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => nav.classList.add('nav-hidden'), 1800);
}
document.addEventListener('mousemove', showNav);
document.addEventListener('touchstart', showNav, { passive: true });
document.addEventListener('keydown', showNav);
showNav(); // start timer on load
// Swipe
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx;
if (Math.abs(dx) > 50) go(dx < 0 ? cur + 1 : cur - 1);
}, { passive: true });
縦向き/モバイルへの対応
スライドデッキは横向きのメディアです。分割パネルのレイアウトを読みにくいモバイル向けの縦積みに再配置するのではなく、ビューポートが縦向きかつ狭い場合は「デバイスを回転してください」というオーバーレイを表示します。これにより、縦向きに持った iPhone に対応しつつ、レイアウトパターンごとに CSS を二重に用意する必要がなくなります。横向きのスマートフォンでの表示は、フォントサイズが clamp() ですでに縮小されるため、引き続き機能します。
この CSS を各デッキの <style> ブロックの末尾付近に追加します。ほかのスタイルを上書きしません。
/* Rotate-to-landscape overlay — only visible on portrait phones */
.rotate-overlay {
display: none;
position: fixed; inset: 0;
z-index: 9999;
background: var(--primary);
color: var(--white);
flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 40px;
font-family: var(--sans);
}
.rotate-overlay .rotate-icon {
margin-bottom: 24px;
color: var(--accent);
animation: rotate-hint 2.4s ease-in-out infinite;
}
.rotate-overlay h2 {
font-family: var(--serif);
font-size: 30px; font-weight: 400;
margin-bottom: 12px; letter-spacing: -.01em;
}
.rotate-overlay p {
font-size: 14px; line-height: 1.6;
opacity: .72; max-width: 280px;
}
@keyframes rotate-hint {
0%, 40%, 100% { transform: rotate(0deg); }
60%, 80% { transform: rotate(90deg); }
}
@media (orientation: portrait) and (max-width: 900px) {
.rotate-overlay { display: flex; }
}
オーバーレイの HTML は、上記のシェル構造に含まれています。両方を追加すれば、スライドごとに変更を加えなくても、デッキが縦向きのスマートフォンに適切に対応します。
モバイルに完全対応しない理由:分割パネル、統計値の列、写真背景は、情報を伝えるために横方向のスペースを必要とします。モバイル向けに再配置したものは、実質的に別のデッキであり、しかも元より質が低くなります。PowerPoint や Google Slides もモバイルの縦向き表示には対応しきれていません。この形式では、すでにユーザーもその点を受け入れています。
印刷/PDF エクスポート
ユーザーが Cmd+P →「PDF として保存」を選ぶと、1 ページに 1 枚のスライドが配置された、きれいな PDF を出力できるようにします。画面上のデッキではスライドを絶対配置で重ね、非アクティブなスライドは opacity: 0 で非表示にしているため、単純に印刷するとアクティブなスライドしか出力されません。解決策は、すべてのスライドを重ねずに配置し直し、アニメーションを無効にし、ナビゲーションやオーバーレイを非表示にして、ページサイズを横向きに設定する @media print ブロックです。
<style> ブロックの末尾近くに追加します。
/* Print / PDF export — Cmd+P → Save as PDF gives one slide per page */
@media print {
@page {
size: 1600px 1000px; /* native deck aspect ratio; user can fit-to-page */
margin: 0;
}
html, body {
width: 1600px; height: auto;
overflow: visible !important;
background: #fff;
}
.deck {
width: 1600px; height: auto;
position: static !important;
}
.slide {
position: relative !important;
inset: auto !important;
width: 1600px; height: 1000px;
opacity: 1 !important;
pointer-events: auto !important;
page-break-after: always;
break-after: page;
transform: none !important;
}
/* Kill all animations and transitions so content is in final state */
*, *::before, *::after {
animation: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
}
.slide .reveal {
opacity: 1 !important;
transform: none !important;
}
/* Hide interactive chrome */
.nav, .rotate-overlay { display: none !important; }
/* Preserve backgrounds and colors in print output */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
使い方: ユーザーはブラウザーでデッキを開き、Cmd+P(または Ctrl+P)を押して、保存先に「PDF として保存」を選択します。印刷ダイアログに「背景のグラフィック」オプションがあれば、必要に応じてオンにします。ダイアログのデフォルトの用紙サイズはあまり重要ではありません。@page ルールがコンテンツの枠を設定し、ブラウザーが収まるように拡大縮小するためです。ピクセル単位で正確に出力するには、ダイアログで 1600 × 1000 に合う「カスタム」用紙サイズを選択できます。
注意点:
- グラデーションや一部の写真パネルのラスタライズは、画面上と少し異なる場合があります(Chrome と Safari では違いがあります)。印刷物でもデッキの見た目を完全に一致させる必要がある場合は、背景を単色にしてください。
- 高さが 1000px を超えるスライドは切り取られます。画面上でコンテンツをスライドの範囲からはみ出さないようにすれば、印刷時に切り取られることもありません。
- Firefox は
@page size: <pixels>を Chrome/Safari ほど確実には適用しません。Firefox を使うユーザーにとって重要な場合は、size: 10in 6.25in(比率は同じで、物理単位)に置き換えてください。
ステップ 3:レイアウトパターン
以下を構成要素として使い、スライドごとに組み合わせてください。
分割パネル(最も汎用的)
左側に暗いパネル(ブランドのプライマリカラー)、右側に明るいパネル(白またはクリーム色)を配置します。両者の境界は斜めに切るか、直線にします。
<div class="slide" id="sN" style="display:flex;">
<div class="left-panel"></div>
<div class="right-panel"></div>
</div>
.left-panel {
width: 50%; background: var(--primary);
display: flex; flex-direction: column; justify-content: center;
padding: 72px 64px; position: relative; overflow: hidden;
}
.right-panel {
flex: 1;
display: flex; flex-direction: column; justify-content: center;
padding: 72px 56px 72px 64px; background: var(--cream);
}
数値カラム(実績、指標)
右側のカラムに、大きなセリフ体の数字や単語と、小さな大文字のラベルを配置します。写真背景の上でも使えます。
<div class="stats-col">
<div class="stat">
<div class="stat-n">13+</div>
<div class="stat-l">Years at institution</div>
</div>
</div>
バリューカード(価値観、柱)
左側に色付きのアクセントバーを配置し、タイトルと説明を添えます。視覚的に区別できるよう、価値観ごとにバーの色を変えます。
<div class="value-card">
<div class="value-bar" style="background:#2e7d32"></div>
<div>
<div class="value-title">Principle Name</div>
<div class="value-text">Description of the principle.</div>
</div>
</div>
アプローチ行(プロセス、手順)
大きなゴースト数字、縦線、本文の順に配置します。3 ステップのプロセスに適しています。
<div class="approach">
<div class="approach-num">01</div>
<div class="approach-body">
<div class="approach-title">Step Title</div>
<div class="approach-text">Detail text.</div>
<div class="approach-tags"><span>Tag 1</span><span>Tag 2</span></div>
</div>
</div>
ツール/ステータスカード
名前、ステータスバッジ、説明を配置します。ステータスバッジの種類:稼働中(緑)、進行中(琥珀色)、計画中(ニュートラル)。
<div class="tool-card">
<div class="tool-body">
<div class="tool-name">Tool Name</div>
<span class="status status-live">Live</span>
<div class="tool-desc">What it does and who it's for.</div>
</div>
</div>
写真背景パネル
画像を base64 として埋め込み、グラデーションオーバーレイを適用してテキストを読みやすくします。
<div class="photo-panel" style="
position:absolute; top:0; right:0; bottom:0; width:42%;
background: url('data:image/png;base64,...') center/cover no-repeat;
z-index:1;
">
<div style="position:absolute;inset:0;
background:linear-gradient(to right, var(--primary) 0%, rgba(0,0,0,.55) 40%, rgba(0,0,0,.1) 100%);
"></div>
</div>
画像を base64 として埋め込むには:
import base64
data = 'data:image/png;base64,' + base64.b64encode(open('image.png','rb').read()).decode()
ステップ 4:コンテンツスライドの構成
組織紹介に適した、5 枚構成のデッキの例:
| スライド | 目的 | レイアウト |
|---|---|---|
| 1 | 自己紹介+エレベーターピッチ | 全面に暗い背景+写真+数値カラム |
| 2 | 価値観/原則 | 分割パネル+バリューカード |
| 3 | アプローチ/フレームワーク | 明るい背景+アプローチ行 |
| 4 | ツール/実現支援 | 暗い背景+2 カラム |
| 5 | 締めくくり+議論のきっかけ | 分割パネル+写真背景 |
自由に調整してください。重要なのは、各スライドの役割を 1 つに絞り、視覚的な主役を 1 つにすることです。
ステップ 5:タイポグラフィのルール
/* Display heading — large, light weight, letter-spaced */
.display {
font-family: var(--serif);
font-size: clamp(52px, 7vw, 88px);
font-weight: 300; line-height: .95;
letter-spacing: -.01em;
}
/* Section heading */
.heading {
font-family: var(--serif);
font-size: clamp(32px, 4vw, 50px);
font-weight: 400; line-height: 1.1;
}
/* Eyebrow label */
.label {
font-family: var(--sans);
font-size: 10px; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase;
}
/* Body */
.body {
font-family: var(--sans);
font-size: 13px; font-weight: 300; line-height: 1.65;
}
/* Use clamp() for all font sizes to scale with viewport */
重大な落とし穴
JavaScript を含むファイルに対して、文字列全体や正規表現を使った一括置換を絶対に行わないでください
算術演算子が壊れてしまいます。問題の例:
# DANGEROUS — corrupts JS
content = content.replace(' - ', ', ')
# cur - 1 becomes cur, 1 (prev button breaks)
# clientX - tx becomes clientX, tx (swipe breaks)
安全な方法: 対象が一意で具体的な文字列に対して str_replace を使います。自動置換を行った後は必ず JS のセクションを確認し、算術演算子が壊れていないことを確かめてください。
使用前に必ず const slides を宣言する
ナビゲーションブロックより前に新しい関数を挿入する場合は、const slides = document.querySelectorAll('.slide') が存在し、slides.forEach(...) より前にあることを確認してください。
str_replace には一意な文字列が必要です
str_replace を使う前に、対象の文字列を grep し、出現箇所が正確に 1 つであることを確認してください。複数箇所にある場合は、前後の行を追加して対象を一意にしてください。
Base64 画像はファイルを大きくしますが、自己完結型です
高解像度の PNG 1 枚で、ファイルサイズが 150~200KB 増えることがあります。プレゼンテーション資料であれば問題ありません。サイズ設定には cover を、配置には center center を使用してください。テキストを読みやすくするため、必ずグラデーションのオーバーレイ用 div を追加してください。
コンテンツ内の絵文字
リスト項目やカードのタイトルの先頭に絵文字を置くのは避けてください。AI が生成したように見え、すぐに古く感じられます。ユーザーから絵文字の削除を求められた場合は、sed ではなく Python の Unicode 置換を使用してください。
import re
content = re.sub(r'[\U00002600-\U0001FAFF]+', '', content)
content = content.replace('\ufe0f', '') # variation selector
反復的な編集ワークフロー
初回の作成後に変更を加える場合:
- まず該当箇所を表示する — 行範囲を指定して
viewツールを使用します - 部分的な編集には
str_replaceを使用する — 小さな変更のためにファイル全体を書き直してはいけません - 自動テキスト処理の後は JS を確認する —
cur、dx、+、-演算子を含む行を確認します - 変更後は最新のファイルを提示する — ユーザーはキャッシュされたバージョンを見ている可能性があります
ユーザーが「スライド 3:X を変更」のように指示した場合は、その変更だけを行ってください。隣接する内容まで改善しようとしないでください。
Codex / API の使用上の注意
このスキルは Claude API(Codex)での使用にも対応しています。主な考慮事項:
- ナビゲーションとフルスクリーンの完全な JS ブロックは一体として扱い、改変せずそのまま含めてください。算術処理は変更しないでください
- API 経由で生成する場合は、ファイルをディスクにストリーミングし、プレビューのためにブラウザーで開いてください
- Base64 埋め込み方式は API のコンテキストでも同じように機能します
- CSS 変数(
--primary、--accentなど)を使うとテーマをプログラムで設定できます。ブランドカラーをパラメーターとして渡し、テンプレートに代入してください
PPTX エクスポート(PowerPoint)
ユーザーが明示的に .pptx ファイルを求めた場合は、python-pptx を使用してプログラムで資料を作成する Python スクリプトを生成してください。ユーザーは次のように実行します:
uv run --with python-pptx <script>.py
システム全体へのインストールは不要です。依存関係は uv がインラインで処理します。
HTML の代わりに PPTX を使用する場合
- ユーザーが
.pptxまたは「PowerPoint」を明示的に求めている - ユーザーが Office でスライドを編集できるようにする必要がある
- ユーザーが「PowerPoint を使っている人にこれを共有したい」と言っている
仕組み
HTML と同じように、コンテンツを抽出し、ブランドのロジックを適用します。プロンプトから色、フォント、レイアウトの意図を抽出し、.html ファイルの代わりに自己完結型の .py スクリプトを出力します。
PPTX 生成パターン
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# Brand colors mapped from CSS variables
PRIMARY = RGBColor(0x1A, 0x1A, 0x2E)
ACCENT = RGBColor(0xE9, 0x45, 0x60)
prs = Presentation()
prs.slide_width = Inches(16)
prs.slide_height = Inches(10)
blank_layout = prs.slide_layouts[6] # blank — full control
def set_slide_bg(slide, color):
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def add_text_box(slide, left, top, width, height, text,
font_size=18, bold=False, italic=False,
color=RGBColor(0xFF,0xFF,0xFF), font_name="Arial"):
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.italic = italic
p.font.color.rgb = color
p.font.name = font_name
# Build slides...
slide1 = prs.slides.add_slide(blank_layout)
set_slide_bg(slide1, PRIMARY)
add_text_box(slide1, Inches(0.8), Inches(3), Inches(10), Inches(3),
"Title Here", font_size=72, font_name="Georgia")
prs.save("output.pptx")
print("Saved: output.pptx")
HTML レイアウトと PPTX の対応
| HTML パターン | PPTX での対応 |
|---|---|
| 分割パネル(左が暗色/右が明色) | 左側の背景に長方形を配置し、両側にテキストボックスを配置 |
| 統計値の列 | 大きなフォントのテキストボックスと、その下に小さなラベル用テキストボックスを配置 |
| 色付きバーのある価値カード | アクセント用の細い長方形と、その横にテキストボックスを配置 |
| アプローチの行(番号付きステップ) | 大きな番号のテキストボックスと、右にずらしたタイトル/本文のテキストボックスを配置 |
| 全面背景 | ブランドカラーを指定して set_slide_bg() を使用 |
重要な注意事項
ユーザーに警告してください: PPTX 出力は HTML 資料の近似であり、ピクセル単位で完全に再現したものではありません。python-pptx は CSS、グラデーション、clamp() によるスケーリング、Web フォントに対応していません。生成された資料には次の制限があります:
- Google Fonts の代替としてシステムフォント(Arial、Georgia)を使用します
- グラデーションを単色で近似します
- アニメーションとリビール効果は省略されます(静的なスライドになります)
- 正確な配置には手動での微調整が必要です
必ずユーザーに次のように伝えてください:"この .pptx は優れた出発点になりますが、PowerPoint で多少の調整が必要な場合があります。発表前に配置とフォントのレンダリングを確認してください。"
フォントの対応表
| HTML(Google Font) | PPTX の代替フォント |
|---|---|
| Cormorant Garamond | Georgia |
| DM Sans | Arial |
| Playfair Display | Georgia |
| Inter | Arial |
| セリフ体全般 | Georgia または Times New Roman |
| サンセリフ体全般 | Arial または Calibri |
クイックリファレンス:パレットスロット
すべての資料でこれらのスロットを使用します。ブランドガイドから値を設定した後、CSS 変数でプログラムによりテーマを適用してください。
--primary Dominant backgrounds and dark panels
--accent Rules, spine bars, CTAs, status dots, hover states
--cream Light slide backgrounds (warmer than pure white)
--stone Dividers, subtle borders (one tint above cream)
--ink Body text on light backgrounds (softer than black)
--muted Labels, metadata, secondary text
--go Positive / live / green status
--warn Review / caution / amber status
--alt Fifth accent for value/pillar differentiation
<style> の先頭に実際の 16 進カラーコードを設定し、すべての箇所で var(--name) を使って参照してください。