【神回】Googleスライドが一瞬で完成する"奇跡"のプロンプト教えます
こんにちは、まじんです。
この記事は、私の2025年上半期の集大成だと思ってます!
ずっとスライド生成を研究してたんでね…。
有料記事にしようか本気で悩みましたが、この感動をより多くの人に届けたいと思い、無料で公開することに決めました。
まずは、このプロンプトで「何ができるのか」をサクッとお見せします。
1️⃣Googleスライド完成までの流れ
①Geminiに原稿を渡す
プロンプトをセットしたGemに、スライド化したい元ネタを渡します。
文字起こし・PDF・動画など、AIが解析できるデータなら何でもOK!
②GoogleスライドでGASを開く
コードをまるごとコピーし、空のGoogleスライドから「拡張機能」→「Apps Script」を開きます。
③コードを実行する
コピーしたコードを貼り付けて保存 → 「実行」をクリック。
④スライド完成!
Googleスライド に戻ると…この通り。
スライドショーGIF:
すべて純粋なオブジェクト(テキストや図形)なので、自由に編集可能。
もちろん、好きな画像を挿入することもできます。
なんと…「スピーカーノート」まで自動で生成してくれます。
2️⃣はじめに
きっかけは「スライド生成AI、会社で使えない問題」
最近はスライドを自動生成してくれるAIがたくさんありますよね。Gamma、イルシル、Genspark…
特に「manus」はGoogleスライド形式でエクスポートできて最高です。
しかし会社でmanusを使えるかと言うと…
「中国発のAIエージェント?そんなの使っていいわけないじゃん。」
…情シスとのこんなやり取り、身に覚えのある方は多いのではないでしょうか。
じゃあどうするか。
Google Workspaceだけでスライド生成を完結できないか?
ここから私の研究が始まりました。
そして月日は流れ…約半年。
ついに、私は「答え」にたどり着きました。
必要なのは Gemini だけ。
3️⃣プロンプトの設計思想
一昔前は「プロンプトエンジニア」という職業が生まれるほど、AIへの指示の出し方が重要でした。ですが今では、「〇〇するコードを書いて」といったざっくりした命令でも、AIが意図を汲んでくれるようになっています。
シンプルなGASなら、それで作れるかも知れません。
しかし、Googleスライドの自動生成は絶対に無理です(断言)。
シンプルプロンプトで実験
試しに以下のシンプルなプロンプトを、今をときめく「ChatGPT 5 Thinking」、「Gemini 2.5 Pro」、「Claude Opus 4.1」に送信してみました。
次の文字起こしをもとに、Google スライドを作成したいです。
Google Apps Script でスライドを自動生成するコードを書いてください。
# 文字起こし
〇〇〇〇
結果は、
ChatGPT 5 Thinking → 構文エラーで動かず
Gemini 2.5 Pro → 構文エラーで動かず
唯一Claude Opus 4.1は成功したけど、生成されたものはコレでした。
なぜこうなってしまうのか?
原因は、相手が「Googleスライド」だからです。
GASでスライドを操作するのは、構文も構造も独特で、コーディング難易度がやたら高い。
意図が少しでもズレると、AIが生成するコードはすぐ破綻します。
さらに、UI操作・レイアウト・ビジュアル設計まで含むため、自由度が高いぶん不確実性も跳ね上がります。
たとえるなら、シンプルなプロンプトはAIにこう頼んでいるようなものです。
「ゼロから自動車工場を設計・建設し、ついでに最新のコンセプトカーも一台作ってくれ」
設計図もなければ、部品の規格も曖昧。
そんな状態で、まともな車が安定して出力されるはずがありません。
4️⃣私が導き出した逆転のアプローチ
では、どうすればスライド生成を安定させられるのか?
答えはシンプルです。
「AIにコードを書かせない」。
毎回イチからコードを書かせるから破綻する。
ならば コードは人間が設計して固定し、変わるのは入力データだけ にする。
この「コード固定 × データ可変」こそが、再現性を飛躍的に高める鍵でした。
【ステップ1】完璧に動く「工場」を人間がつくる
まず、人間が 堅牢なGASテンプレート を構築します。
さまざまなスライドを自動生成できる、万能の生産ラインのようなコードです。
この段階で「どんなスライドでも安定して出力できる基盤」を完成させます。
【ステップ2】「車種カタログ」をあらかじめ用意する
次に、その工場で製造可能な スライドパターン(ブループリント) を一覧化します。
いわば「どんな車種が作れるか」を明文化したカタログです。
content:1~2カラムの箇条書き+小見出し
compare:左右比較
process:手順・工程
timeline:時系列
diagram:レーン図+カード+自動矢印(Mermaid風)
cards:カードグリッド(2~3列)
table:表(失敗時はフォールバック描画)
progress:進捗バー
どのパターンを使うかは、AIが素材を読んで自動選択します。
つまり「ストーリーに最適な見せ方」を文脈に応じて決める仕組みです。
【ステップ3】AIには「生産計画書」だけを作らせる
コードは人間が用意済み。スライドの型も決まっている。
ではAIの役割は何か?
「どの型をどんな順番で並べればベストか」
その 設計プラン(JSON形式の生産計画書) を作らせるだけです。
これならAIは苦手な「構文の整合性を保ったコード生成」をせず、
得意な「構造化」と「文脈理解」に専念できます。
その結果、驚くほど安定したスライド生成が可能になります。
5️⃣この設計思想がもたらす3つのメリット
1. 構文エラーの確率がガクッと下がる
AIが手を出すのはデータだけ。
コードには一切触れさせない。
「今日はAIの機嫌が悪いな……」みたいな不安からは解放されます。
それでもAIはランダム性があるのでたまにエラーがでますが…
2. AIを「アシスタント」から「戦略コンサル」へ
この設計では、AIにこう命じます:
「ただ情報を並べるだけじゃなく、伝わるストーリーを組み立てろ。
そして、各要素に最適な“見せ方”を選べ。」
つまり、単なる作業員ではなく 戦略設計者 として働かせるのです。
コード生成という雑務から解放されたAIは、「どう見せれば伝わるか」に全力を注げます。
3. 自己検証機能で信頼性UP
このプロンプトには、AIが自分で自分の出力をチェックする自己検証プロセスが含まれています。
仕様通りに構成されているか、典型的なミスがないかを、出力前にAI自身が確認する。
いわば 品質保証付きのAIワークフロー。
ミスは未然に防がれ、完成度は常に高水準を維持できます。
6️⃣まとめ:制約こそが、創造のエンジンになる
AIエージェントがどれだけ進化しても、
企業や実務の現場で本当に使えるツールには、まだまだ制限があります。
でも、制約があるからこそ、人間の発想力が鍛えられる。
今はまだ、プロンプトの質次第でアウトプットの精度が大きく変わる段階。
だからこそ、人間の知恵が活きる余地があります。
与えられたカードの中で勝負する——燃えますね🔥
【全文公開】Googleスライド自動生成マスタープロンプト
お待たせしました。
ここまで読んでくれた方、ありがとうございます。
プロンプトをコピーする前に注意点をお読みください!
【ご利用上の注意】
✅ 1. 生成物はあくまでドラフト
スライドもスピーカーノートも AI が生成したたたき台です。
必ずご自身で内容を確認・調整のうえご使用ください。
✅ 2. 使用する AI モデルは高性能な LLM に限定
Gemini 以外でも動作するはずですが、高精度なモデルの利用を推奨します。Gemini 2.5 Pro で動作確認済みで、成功率も高くおすすめです。
✅ 3. エラーが出たら、やり直しが早い
まれに構文エラーが発生します。細かくデバッグするより、
新規チャットでやり直すほうが早い場合があります。
✅ 4. 初期設定はカスタマイズ可能
初期状態の設定:
・プライマリカラー:ブルー(#4285F4)
・フッター:© 2025 Your Organization(ダミー)
・ロゴ:Google ロゴ
AI にプロンプトを渡して、次のように変更できます。
例:「プライマリカラーを赤(#ff0000)、フッターを“〇〇株式会社”に、ロゴをこの画像 URL に変更して」
ロゴ画像は Web 上で公開され、URL で取得できる必要があります。
ヘッダー(右上ロゴ)とクロージング(最終スライドのロゴ)は別々の画像も指定可能です。
✅ 5. ロジック改変は慎重に
コードの基幹ロジックが崩れると正常に動作しません。
基礎デザインは固定ですが、プロンプト調整によりデザインのアレンジは可能です。高度な修正には GPT-5 Pro の利用を推奨します。
それでは、こちらがプロンプトです!
## **1.0 PRIMARY\_OBJECTIVE — 最終目標**
あなたは、ユーザーから与えられた非構造テキスト情報を解析し、後述する **【GOOGLE\_TEMPLATE\_BLUEPRINT】** で定義された Google Apps Script(GAS)フレームワーク内で機能する、**slideData** という名の JavaScript オブジェクト配列を**生成**することだけに特化した、超高精度データサイエンティスト兼プレゼンテーション設計AIです。
あなたの**絶対的かつ唯一の使命**は、ユーザーの入力内容から論理的なプレゼンテーション構造を抽出し、各セクションに最適な「表現パターン(Pattern)」を選定し、さらに各スライドで話すべき発表原稿(スピーカーノート)のドラフトまで含んだ、ブループリント内の const slideData \\= \\\[...\\\] を完全に置き換えるための、完璧でエラーのない JavaScript オブジェクト配列を生成することです。
**slideData の生成以外のタスクを一切実行してはなりません。** ブループリントのロジック、デザイン設定、関数名、変数名など、1文字たりとも変更することは固く禁じられています。あなたの思考と出力のすべては、最高の slideData を生成するためだけに費やされます。
## **2.0 GENERATION\_WORKFLOW — 厳守すべき思考と生成のプロセス**
1. **【ステップ1: コンテキストの完全分解と正規化】**
* **分解**: ユーザー提供のテキスト(議事録、記事、企画書、メモ等)を読み込み、**目的・意図・聞き手**を把握。内容を「**章(Chapter)→ 節(Section)→ 要点(Point)**」の階層に内部マッピング。
* **正規化**: 入力前処理を自動実行。(タブ→スペース、連続スペース→1つ、スマートクォート→ASCIIクォート、改行コード→LF、用語統一)
2. **【ステップ2: パターン選定と論理ストーリーの再構築】**
* 章・節ごとに、後述の**サポート済み表現パターン**から最適なものを選定(例: 比較なら compare、時系列なら timeline)。
* 聞き手に最適な**説得ライン**(問題解決型、PREP法、時系列など)へ再配列。
3. **【ステップ3: スライドタイプへのマッピング】**
* ストーリー要素を **Googleパターン・スキーマ**に**最適割当**。
* 表紙 → title / 章扉 → section(※背景に**半透明の大きな章番号**を描画) / 本文 → content, compare, process, timeline, diagram, cards, table, progress / 結び → closing
4. **【ステップ4: オブジェクトの厳密な生成】**
* **3.0 スキーマ**と**4.0 ルール**に準拠し、文字列をエスケープ(' → \\', \\ → \\\\)して1件ずつ生成。
* **インライン強調記法**を使用可:
* \*\*太字\*\* → 太字
* \[\[重要語\]\] → **太字+Googleブルー**(\#4285F4)
* **画像URLの抽出**: 入力テキスト内の \!\[\](...png|.jpg|.jpeg|.gif|.webp) 形式、または裸URLで末尾が画像拡張子のものを抽出し、該当スライドの images 配列に格納(説明文がある場合は media の caption に入れる)。
* **スピーカーノート生成**: 各スライドの内容に基づき、発表者が話すべき内容の**ドラフトを生成**し、notesプロパティに格納する。
5. **【ステップ5: 自己検証と反復修正】**
* **チェックリスト**:
* 文字数・行数・要素数の上限遵守(各パターンの規定に従うこと)
* 箇条書き要素に**改行(\\n)を含めない**
* テキスト内に**禁止記号**(■ / →)を含めない(※装飾・矢印はスクリプトが描画)
* 箇条書き文末に **句点「。」を付けない**(体言止め推奨)
* notesプロパティが各スライドに適切に設定されているか確認
* title.dateはYYYY.MM.DD形式
* **アジェンダ安全装置**: 「アジェンダ/Agenda/目次/本日お伝えすること」等のタイトルで points が空の場合、**章扉(section.title)から自動生成**するため、空配列を返さず **ダミー3点**以上を必ず生成
6. **【ステップ6: 最終出力】**
* 検証済みオブジェクトを論理順に const slideData \= \[...\] に格納。**【GOOGLE\_TEMPLATE\_BLUEPRINT】全文**をそのまま出力し、**サンプルの slideData ブロックだけ**をあなたが生成した slideData で**完全置換**した **単一 .gs ファイルの中身**のみを出力すること。**解説・前置き・後書き一切禁止**。
## **3.0 slideDataスキーマ定義(GooglePatternVer.+SpeakerNotes)**
**共通プロパティ**
* **notes?: string**: すべてのスライドオブジェクトに任意で追加可能。スピーカーノートに設定する発表原稿のドラフト(プレーンテキスト)。
**スライドタイプ別定義**
* **タイトル**: { type: 'title', title: '...', date: 'YYYY.MM.DD', notes?: '...' }
* **章扉**: { type: 'section', title: '...', sectionNo?: number, notes?: '...' } ※sectionNo を指定しない場合は自動連番
* **クロージング**: { type: 'closing', notes?: '...' }
**本文パターン(必要に応じて選択)**
* **content(1カラム/2カラム+画像+小見出し)** { type: 'content', title: '...', subhead?: string, points?: string\[\], twoColumn?: boolean, columns?: \[string\[\], string\[\]\], images?: (string | { url: string, caption?: string })\[\], notes?: '...' }
* **compare(対比)** { type: 'compare', title: '...', subhead?: string, leftTitle: '...', rightTitle: '...', leftItems: string\[\], rightItems: string\[\], images?: string\[\], notes?: '...' }
* **process(手順・工程)** { type: 'process', title: '...', subhead?: string, steps: string\[\], images?: string\[\], notes?: '...' }
* **timeline(時系列)** { type: 'timeline', title: '...', subhead?: string, milestones: { label: string, date: string, state?: 'done'|'next'|'todo' }\[\], images?: string\[\], notes?: '...' }
* **diagram(レーン図)** { type: 'diagram', title: '...', subhead?: string, lanes: { title: string, items: string\[\] }\[\], images?: string\[\], notes?: '...' }
* **cards(カードグリッド)** { type: 'cards', title: '...', subhead?: string, columns?: 2|3, items: (string | { title: string, desc?: string })\[\], images?: string\[\], notes?: '...' }
* **table(表)** { type: 'table', title: '...', subhead?: string, headers: string\[\], rows: string\[\]\[\], notes?: '...' }
* **progress(進捗)** { type: 'progress', title: '...', subhead?: string, items: { label: string, percent: number }\[\], notes?: '...' }
## **4.0 COMPOSITION\_RULES(GooglePatternVer.) — 美しさと論理性を最大化する絶対規則**
* **全体構成**:
1. title(表紙)
2. content(アジェンダ、※章が2つ以上のときのみ)
3. section
4. 本文(content/compare/process/timeline/diagram/cards/table/progress から2〜5枚)
5. (3〜4を章の数だけ繰り返し)
6. closing(結び)
* **テキスト表現・字数**(最大目安):
* title.title: 全角35文字以内
* section.title: 全角30文字以内
* 各パターンの title: 全角40文字以内
* **subhead**: 全角50文字以内(フォント18)
* 箇条書き等の要素テキスト: 各90文字以内・**改行禁止**
* **notes(スピーカーノート)**: 発表内容を想定したドラフト。文字数制限は緩やかだが、要点を簡潔に。**プレーンテキスト**とし、強調記法は用いないこと。
* **禁止記号**: ■ / → を含めない(矢印や区切りはスクリプト側で描画)
* 箇条書き文末の句点「。」**禁止**(体言止め推奨)
* **インライン強調記法**: \*\*太字\*\* と \[\[重要語\]\](太字+Googleブルー)を必要箇所に使用可
## **5.0 SAFETY\_GUIDELINES — GASエラー回避とAPI負荷の配慮**
* スライド上限: **最大50枚**
* 画像制約: **50MB未満・25MP以下**の **PNG/JPEG/GIF/WebP**
* 実行時間: Apps Script 全体で約 **6分**
* テキストオーバーフロー回避: 本命令の**上限値厳守**
* フォント: Arial が無い環境では標準サンセリフに自動フォールバック
* 文字列リテラルの安全性: ' と \\ を確実にエスケープ
## **6.0 OUTPUT\_FORMAT — 最終出力形式**
* 出力は **【GOOGLE\_TEMPLATE\_BLUEPRINT】の完全な全文**であり、唯一の差分が const slideData \=...
の中身になるように生成すること。
* **コード以外のテキスト(前置き/解説/謝罪/補足)は一切含めない。**
## **7.0 GOOGLE\_TEMPLATE\_BLUEPRINT — 【Universal Google Design Ver.】完成済み設計図**
/\*\*
\* @OnlyCurrentDoc
\* このスクリプトは、Google風デザインテンプレートに基づきGoogleスライドを自動生成します。
\* Version: 12.0 (Universal Google Design \- Final)
\* Author: Googleスライド自動生成マスター
\* Description: 指定されたslideData配列を元に、Google風デザインに準拠したスライドを生成します。
\*/
// \--- 1\. 実行設定 \---
const SETTINGS \= {
SHOULD\_CLEAR\_ALL\_SLIDES: true,
TARGET\_PRESENTATION\_ID: null
};
// \--- 2\. マスターデザイン設定 (Google Design Ver.) \---
const CONFIG \= {
BASE\_PX: { W: 960, H: 540 },
// レイアウトの基準となる不変のpx値
POS\_PX: {
titleSlide: {
logo: { left: 55, top: 105, width: 135 },
title: { left: 50, top: 230, width: 800, height: 90 },
date: { left: 50, top: 340, width: 250, height: 40 },
},
```
// 共通ヘッダーを持つ各スライド
contentSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
body: { left: 25, top: 172, width: 910, height: 303 },
twoColLeft: { left: 25, top: 172, width: 440, height: 303 },
twoColRight: { left: 495, top: 172, width: 440, height: 303 }
},
compareSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
leftBox: { left: 25, top: 172, width: 430, height: 303 },
rightBox: { left: 505, top: 172, width: 430, height: 303 }
},
processSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
area: { left: 25, top: 172, width: 910, height: 303 }
},
timelineSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
area: { left: 25, top: 172, width: 910, height: 303 }
},
diagramSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
lanesArea: { left: 25, top: 172, width: 910, height: 303 }
},
cardsSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
gridArea: { left: 25, top: 172, width: 910, height: 303 }
},
tableSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
area: { left: 25, top: 172, width: 910, height: 303 }
},
progressSlide: {
headerLogo: { right: 20, top: 20, width: 75 },
title: { left: 25, top: 60, width: 830, height: 65 },
titleUnderline: { left: 25, top: 128, width: 260, height: 4 },
subhead: { left: 25, top: 140, width: 830, height: 30 },
area: { left: 25, top: 172, width: 910, height: 303 }
},
// 章扉(背景に大きなゴースト番号)
sectionSlide: {
title: { left: 55, top: 230, width: 840, height: 80 },
ghostNum: { left: 35, top: 120, width: 300, height: 200 }
},
footer: {
leftText: { left: 15, top: 505, width: 250, height: 20 },
rightPage: { right: 15, top: 505, width: 50, height: 20 }
},
bottomBar: { left: 0, top: 534, width: 960, height: 6 }
```
},
FONTS: {
family: 'Arial',
sizes: {
title: 45,
date: 16,
sectionTitle: 38,
contentTitle: 28,
subhead: 18,
body: 14,
footer: 9,
chip: 11,
laneTitle: 13,
small: 10,
processStep: 14,
axis: 12,
ghostNum: 180
}
},
COLORS: {
primary\_blue: '\#4285F4',
google\_red: '\#EA4335',
google\_yellow: '\#FBBC04',
google\_green: '\#34A853',
text\_primary: '\#333333',
background\_white: '\#FFFFFF',
background\_gray: '\#f8f9fa',
faint\_gray: '\#e8eaed',
lane\_title\_bg: '\#f5f5f3',
lane\_border: '\#dadce0',
card\_bg: '\#ffffff',
card\_border: '\#dadce0',
neutral\_gray: '\#9e9e9e',
ghost\_gray: '\#efefed'
},
DIAGRAM: {
laneGap\_px: 24, lanePad\_px: 10, laneTitle\_h\_px: 30,
cardGap\_px: 12, cardMin\_h\_px: 48, cardMax\_h\_px: 70,
arrow\_h\_px: 10, arrowGap\_px: 8
},
LOGOS: {
header: '[https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google\\\_2015\\\_logo.svg/1024px-Google\\\_2015\\\_logo.svg.png](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google\\_2015\\_logo.svg/1024px-Google\\_2015\\_logo.svg.png)',
closing: '[https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google\\\_2015\\\_logo.svg/1024px-Google\\\_2015\\\_logo.svg.png](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Google\\_2015\\_logo.svg/1024px-Google\\_2015\\_logo.svg.png)'
},
FOOTER\_TEXT: \`© ${new Date().getFullYear()} Your Organization\`
};
// \--- 3\. スライドデータ(サンプル:必ず置換してください) \---
const slideData \= \[
{ type: 'title', title: 'サンプルプレゼンテーション', date: '2025.08.12', notes: '本日はお集まりいただきありがとうございます。このプレゼンテーションは、Google風デザインテンプレートの機能と可能性についてご説明するものです。' },
{ type: 'section', title: '1. はじめに', notes: '最初のセクションでは、このテンプレートが持つ主要な表現パターンについて概観します。' },
{ type: 'cards', title: 'Google風デザインのテスト', subhead: 'モダンなデザインパターン', columns: 3, items: \[
{ title: 'パターン1', desc: '現状:\[\[重要機能\]\]実装済み\\n課題:パフォーマンス\*\*最適化\*\*が必要' },
{ title: 'パターン2', desc: '現状:デザイン更新完了\\n課題:\[\[ユーザビリティ改善\]\]を検討' },
{ title: 'パターン3', desc: '現状:テスト環境構築\\n課題:\*\*本番環境への移行準備\*\*' }
\], notes: 'こちらがカード形式のスライドです。3つの異なる項目を並べて比較検討する際に便利です。それぞれのカードにはタイトルと説明を設定できます。' },
{ type: 'closing', notes: '以上で説明を終わります。ご清聴ありがとうございました。何かご質問はありますでしょうか。' }
\];
// \--- 4\. メイン実行関数 \---
let \_\_SECTION\_COUNTER \= 0; // 章番号カウンタ(ゴースト数字用)
function generatePresentation() {
let presentation;
try {
presentation \= SETTINGS.TARGET\_PRESENTATION\_ID
? SlidesApp.openById(SETTINGS.TARGET\_PRESENTATION\_ID)
: SlidesApp.getActivePresentation();
if (\!presentation) throw new Error('対象のプレゼンテーションが見つかりません。');
```
if (SETTINGS.SHOULD\_CLEAR\_ALL\_SLIDES) {
const slides \= presentation.getSlides();
for (let i \= slides.length \- 1; i \>= 0; i--) slides\[i\].remove();
}
\_\_SECTION\_COUNTER \= 0;
const layout \= createLayoutManager(presentation.getPageWidth(), presentation.getPageHeight());
let pageCounter \= 0;
for (const data of slideData) {
const generator \= slideGenerators\[data.type\];
if (data.type \!== 'title' && data.type \!== 'closing') pageCounter++;
if (generator) {
const slide \= presentation.appendSlide(SlidesApp.PredefinedLayout.BLANK);
generator(slide, data, layout, pageCounter);
// スピーカーノートを設定する処理
if (data.notes) {
try {
const notesShape \= slide.getNotesPage().getSpeakerNotesShape();
if (notesShape) {
notesShape.getText().setText(data.notes);
}
} catch (e) {
Logger.log(\`スピーカーノートの設定に失敗しました: ${e.message}\`);
}
}
}
}
```
} catch (e) {
Logger.log(\`処理が中断されました: ${e.message}\\nStack: ${e.stack}\`);
}
}
// \--- 5\. スライド生成ディスパッチャ \---
const slideGenerators \= {
title: createTitleSlide,
section: createSectionSlide,
content: createContentSlide,
compare: createCompareSlide,
process: createProcessSlide,
timeline: createTimelineSlide,
diagram: createDiagramSlide,
cards: createCardsSlide,
table: createTableSlide,
progress: createProgressSlide,
closing: createClosingSlide
};
// \--- 6\. スライド生成関数群 \---
function createTitleSlide(slide, data, layout) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
const logoRect \= layout.getRect('titleSlide.logo');
const logo \= slide.insertImage(CONFIG.LOGOS.header);
const aspect \= logo.getHeight() / logo.getWidth();
logo.setLeft(logoRect.left).setTop(logoRect.top).setWidth(logoRect.width).setHeight(logoRect.width \* aspect);
const titleRect \= layout.getRect('titleSlide.title');
const titleShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height);
setStyledText(titleShape, data.title, { size: CONFIG.FONTS.sizes.title, bold: true });
const dateRect \= layout.getRect('titleSlide.date');
const dateShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, dateRect.left, dateRect.top, dateRect.width, dateRect.height);
dateShape.getText().setText(data.date || '');
applyTextStyle(dateShape.getText(), { size: CONFIG.FONTS.sizes.date });
drawBottomBar(slide, layout);
}
function createSectionSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_gray);
// 透かし番号:sectionNo \> タイトル先頭の数字 \> 自動連番
\_\_SECTION\_COUNTER++;
const parsedNum \= (() \=\> {
if (Number.isFinite(data.sectionNo)) return Number(data.sectionNo);
const m \= String(data.title || '').match(/^\\s\*(\\d+)\[\\.\\.\]/);
return m ? Number(m\[1\]) : \_\_SECTION\_COUNTER;
})();
const num \= String(parsedNum).padStart(2, '0');
const ghostRect \= layout.getRect('sectionSlide.ghostNum');
const ghost \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, ghostRect.left, ghostRect.top, ghostRect.width, ghostRect.height);
ghost.getText().setText(num);
applyTextStyle(ghost.getText(), { size: CONFIG.FONTS.sizes.ghostNum, color: CONFIG.COLORS.ghost\_gray, bold: true });
try { ghost.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e) {}
const titleRect \= layout.getRect('sectionSlide.title');
const titleShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height);
titleShape.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE);
setStyledText(titleShape, data.title, { size: CONFIG.FONTS.sizes.sectionTitle, bold: true, align: SlidesApp.ParagraphAlignment.CENTER });
addGoogleFooter(slide, layout, pageNum);
}
// content(1/2カラム \+ 小見出し \+ 画像)
function createContentSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'contentSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'contentSlide', data.subhead);
// アジェンダ安全装置
const isAgenda \= isAgendaTitle(data.title || '');
let points \= Array.isArray(data.points) ? data.points.slice(0) : \[\];
if (isAgenda && (\!points || points.length \=== 0)) {
points \= buildAgendaFromSlideData();
if (points.length \=== 0\) points \= \['本日の目的', '進め方', '次のアクション'\];
}
const hasImages \= Array.isArray(data.images) && data.images.length \> 0;
const isTwo \= \!\!(data.twoColumn || data.columns);
if ((isTwo && (data.columns || points)) || (\!isTwo && points && points.length \> 0)) {
if (isTwo) {
let L \= \[\], R \= \[\];
if (Array.isArray(data.columns) && data.columns.length \=== 2\) {
L \= data.columns\[0\] || \[\]; R \= data.columns\[1\] || \[\];
} else {
const mid \= Math.ceil(points.length / 2);
L \= points.slice(0, mid); R \= points.slice(mid);
}
const leftRect \= offsetRect(layout.getRect('contentSlide.twoColLeft'), 0, dy);
const rightRect \= offsetRect(layout.getRect('contentSlide.twoColRight'), 0, dy);
const leftShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, leftRect.left, leftRect.top, leftRect.width, leftRect.height);
const rightShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, rightRect.left, rightRect.top, rightRect.width, rightRect.height);
setBulletsWithInlineStyles(leftShape, L);
setBulletsWithInlineStyles(rightShape, R);
} else {
const bodyRect \= offsetRect(layout.getRect('contentSlide.body'), 0, dy);
const bodyShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, bodyRect.left, bodyRect.top, bodyRect.width, bodyRect.height);
setBulletsWithInlineStyles(bodyShape, points);
}
}
// 画像(任意)
if (hasImages) {
const area \= offsetRect(layout.getRect('contentSlide.body'), 0, dy);
renderImagesInArea(slide, layout, area, normalizeImages(data.images));
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
// compare(左右ボックス:ヘッダー青+白文字)
function createCompareSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'compareSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'compareSlide', data.subhead);
const leftBox \= offsetRect(layout.getRect('compareSlide.leftBox'), 0, dy);
const rightBox \= offsetRect(layout.getRect('compareSlide.rightBox'), 0, dy);
drawCompareBox(slide, leftBox, data.leftTitle || '選択肢A', data.leftItems || \[\]);
drawCompareBox(slide, rightBox, data.rightTitle || '選択肢B', data.rightItems || \[\]);
drawBottomBarAndFooter(slide, layout, pageNum);
}
function drawCompareBox(slide, rect, title, items) {
const box \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, rect.left, rect.top, rect.width, rect.height);
box.getFill().setSolidFill(CONFIG.COLORS.lane\_title\_bg);
box.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane\_border);
box.getBorder().setWeight(1);
const th \= 0.75 \* 40; // 約30px相当
const titleBar \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, rect.left, rect.top, rect.width, th);
titleBar.getFill().setSolidFill(CONFIG.COLORS.primary\_blue);
titleBar.getBorder().setTransparent();
setStyledText(titleBar, title, { size: CONFIG.FONTS.sizes.laneTitle, bold: true, color: CONFIG.COLORS.background\_white, align: SlidesApp.ParagraphAlignment.CENTER });
const pad \= 0.75 \* 12;
const textRect \= { left: rect.left \+ pad, top: rect.top \+ th \+ pad, width: rect.width \- pad \* 2, height: rect.height \- th \- pad \* 2 };
const body \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, textRect.left, textRect.top, textRect.width, textRect.height);
setBulletsWithInlineStyles(body, items);
}
// process(角枠1px+一桁数字)
function createProcessSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'processSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'processSlide', data.subhead);
const area \= offsetRect(layout.getRect('processSlide.area'), 0, dy);
const steps \= Array.isArray(data.steps) ? data.steps : \[\];
const n \= Math.max(1, steps.length);
const gapY \= (area.height \- layout.pxToPt(40)) / Math.max(1, n \- 1);
const cx \= area.left \+ layout.pxToPt(44);
const top0 \= area.top \+ layout.pxToPt(10);
const line \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx \- layout.pxToPt(1), top0 \+ layout.pxToPt(6), layout.pxToPt(2), gapY \* (n \- 1));
line.getFill().setSolidFill(CONFIG.COLORS.faint\_gray);
line.getBorder().setTransparent();
for (let i \= 0; i \< n; i++) {
const cy \= top0 \+ gapY \* i;
const sz \= layout.pxToPt(28);
const numBox \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, cx \- sz/2, cy \- sz/2, sz, sz);
numBox.getFill().setSolidFill(CONFIG.COLORS.background\_white);
numBox.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.primary\_blue);
numBox.getBorder().setWeight(1);
const num \= numBox.getText(); num.setText(String(i \+ 1));
applyTextStyle(num, { size: 12, bold: true, color: CONFIG.COLORS.primary\_blue, align: SlidesApp.ParagraphAlignment.CENTER });
```
const txt \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, cx \+ layout.pxToPt(28), cy \- layout.pxToPt(16), area.width \- layout.pxToPt(70), layout.pxToPt(32));
setStyledText(txt, steps\[i\] || '', { size: CONFIG.FONTS.sizes.processStep });
try { txt.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){}
```
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
// timeline(左右余白広め)
function createTimelineSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'timelineSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'timelineSlide', data.subhead);
const area \= offsetRect(layout.getRect('timelineSlide.area'), 0, dy);
const milestones \= Array.isArray(data.milestones) ? data.milestones : \[\];
if (milestones.length \=== 0\) { drawBottomBarAndFooter(slide, layout, pageNum); return; }
const inner \= layout.pxToPt(60);
const baseY \= area.top \+ area.height \* 0.55;
const leftX \= area.left \+ inner;
const rightX \= area.left \+ area.width \- inner;
const line \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, leftX, baseY \- layout.pxToPt(1), rightX \- leftX, layout.pxToPt(2));
line.getFill().setSolidFill(CONFIG.COLORS.faint\_gray);
line.getBorder().setTransparent();
const dotR \= layout.pxToPt(8);
const gap \= (rightX \- leftX) / Math.max(1, (milestones.length \- 1));
milestones.forEach((m, i) \=\> {
const x \= leftX \+ gap \* i \- dotR / 2;
const dot \= slide.insertShape(SlidesApp.ShapeType.ELLIPSE, x, baseY \- dotR / 2, dotR, dotR);
const state \= (m.state || 'todo').toLowerCase();
if (state \=== 'done') { dot.getFill().setSolidFill(CONFIG.COLORS.google\_green); dot.getBorder().setTransparent(); }
else if (state \=== 'next') { dot.getFill().setSolidFill(CONFIG.COLORS.background\_white); dot.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.google\_yellow); dot.getBorder().setWeight(2); }
else { dot.getFill().setSolidFill(CONFIG.COLORS.background\_white); dot.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.neutral\_gray); dot.getBorder().setWeight(1); }
```
const t \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, x \- layout.pxToPt(40), baseY \- layout.pxToPt(40), layout.pxToPt(90), layout.pxToPt(20));
t.getText().setText(String(m.label || ''));
applyTextStyle(t.getText(), { size: CONFIG.FONTS.sizes.small, bold: true, align: SlidesApp.ParagraphAlignment.CENTER });
const d \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, x \- layout.pxToPt(40), baseY \+ layout.pxToPt(8), layout.pxToPt(90), layout.pxToPt(18));
d.getText().setText(String(m.date || ''));
applyTextStyle(d.getText(), { size: CONFIG.FONTS.sizes.small, color: CONFIG.COLORS.neutral\_gray, align: SlidesApp.ParagraphAlignment.CENTER });
```
});
drawBottomBarAndFooter(slide, layout, pageNum);
}
// diagram(Mermaid風・レーン+カード+自動矢印)
function createDiagramSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'diagramSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'diagramSlide', data.subhead);
const lanes \= Array.isArray(data.lanes) ? data.lanes : \[\];
const area0 \= layout.getRect('diagramSlide.lanesArea');
const area \= offsetRect(area0, 0, dy);
const px \= (p)=\> layout.pxToPt(p);
const laneGap \= px(CONFIG.DIAGRAM.laneGap\_px);
const lanePad \= px(CONFIG.DIAGRAM.lanePad\_px);
const laneTitleH \= px(CONFIG.DIAGRAM.laneTitle\_h\_px);
const cardGap \= px(CONFIG.DIAGRAM.cardGap\_px);
const cardMinH \= px(CONFIG.DIAGRAM.cardMin\_h\_px);
const cardMaxH \= px(CONFIG.DIAGRAM.cardMax\_h\_px);
const arrowH \= px(CONFIG.DIAGRAM.arrow\_h\_px);
const arrowGap \= px(CONFIG.DIAGRAM.arrowGap\_px);
const n \= Math.max(1, lanes.length);
const laneW \= (area.width \- laneGap \* (n \- 1)) / n;
const cardBoxes \= \[\];
for (let j \= 0; j \< n; j++) {
const lane \= lanes\[j\] || { title: '', items: \[\] };
const left \= area.left \+ j \* (laneW \+ laneGap);
const top \= area.top;
```
const lt \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, laneW, laneTitleH);
lt.getFill().setSolidFill(CONFIG.COLORS.lane\_title\_bg);
lt.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.lane\_border);
lt.getBorder().setWeight(1);
lt.getText().setText(lane.title || '');
applyTextStyle(lt.getText(), { size: CONFIG.FONTS.sizes.laneTitle, bold: true, align: SlidesApp.ParagraphAlignment.CENTER });
const items \= Array.isArray(lane.items) ? lane.items : \[\];
const availH \= area.height \- laneTitleH \- lanePad \* 2;
const rows \= Math.max(1, items.length);
const idealH \= (availH \- cardGap \* (rows \- 1)) / rows;
const cardH \= Math.max(cardMinH, Math.min(cardMaxH, idealH));
const totalH \= cardH \* rows \+ cardGap \* (rows \- 1);
const firstTop \= top \+ laneTitleH \+ lanePad \+ Math.max(0, (availH \- totalH) / 2);
cardBoxes\[j\] \= \[\];
for (let i \= 0; i \< rows; i++) {
const cardTop \= firstTop \+ i \* (cardH \+ cardGap);
const cardLeft \= left \+ lanePad;
const cardWidth \= laneW \- lanePad \* 2;
const card \= slide.insertShape(SlidesApp.ShapeType.ROUND\_RECTANGLE, cardLeft, cardTop, cardWidth, cardH);
card.getFill().setSolidFill(CONFIG.COLORS.card\_bg);
card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card\_border);
card.getBorder().setWeight(1);
setStyledText(card, items\[i\] || '', { size: CONFIG.FONTS.sizes.body });
try { card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){}
cardBoxes\[j\]\[i\] \= { left: cardLeft, top: cardTop, width: cardWidth, height: cardH };
}
```
}
// 同行カード間を矢印で接続
const maxRows \= Math.max(...cardBoxes.map(a \=\> a.length));
for (let j \= 0; j \< n \- 1; j++) {
const L \= cardBoxes\[j\], R \= cardBoxes\[j \+ 1\];
for (let i \= 0; i \< maxRows; i++) {
const a \= L\[i\], b \= R\[i\];
if (a && b) drawArrowBetweenRects(slide, a, b, arrowH, arrowGap);
}
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
// cards(グレー枠のみ)— title/desc 両方でインライン装飾を有効化
function createCardsSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'cardsSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'cardsSlide', data.subhead);
const area \= offsetRect(layout.getRect('cardsSlide.gridArea'), 0, dy);
const items \= Array.isArray(data.items) ? data.items : \[\];
const cols \= Math.min(3, Math.max(2, Number(data.columns) || (items.length \<= 4 ? 2 : 3)));
const gap \= layout.pxToPt(16);
const rows \= Math.ceil(items.length / cols);
const cardW \= (area.width \- gap \* (cols \- 1)) / cols;
const cardH \= Math.max(layout.pxToPt(92), (area.height \- gap \* (rows \- 1)) / rows);
for (let idx \= 0; idx \< items.length; idx++) {
const r \= Math.floor(idx / cols), c \= idx % cols;
const left \= area.left \+ c \* (cardW \+ gap);
const top \= area.top \+ r \* (cardH \+ gap);
```
const card \= slide.insertShape(SlidesApp.ShapeType.ROUND\_RECTANGLE, left, top, cardW, cardH);
card.getFill().setSolidFill(CONFIG.COLORS.card\_bg);
card.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card\_border);
card.getBorder().setWeight(1);
const obj \= items\[idx\];
if (typeof obj \=== 'string') {
setStyledText(card, obj, { size: CONFIG.FONTS.sizes.body });
} else {
const title \= String(obj.title || '');
const desc \= String(obj.desc || '');
const combined \= \`${title}${desc ? '\\n' \+ desc : ''}\`;
setStyledText(card, combined, { size: CONFIG.FONTS.sizes.body });
if (title.length \> 0\) {
try { card.getText().getRange(0, title.length).getTextStyle().setBold(true); } catch(e){}
}
}
try { card.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e) {}
```
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
// table(Slidesテーブルでもインライン装飾対応。失敗時は矩形代替でも対応)
function createTableSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'tableSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'tableSlide', data.subhead);
const area \= offsetRect(layout.getRect('tableSlide.area'), 0, dy);
const headers \= Array.isArray(data.headers) ? data.headers : \[\];
const rows \= Array.isArray(data.rows) ? data.rows : \[\];
try {
if (headers.length \> 0\) {
const table \= slide.insertTable(rows.length \+ 1, headers.length);
table.setLeft(area.left).setTop(area.top).setWidth(area.width);
for (let c \= 0; c \< headers.length; c++) {
const cell \= table.getCell(0, c);
setStyledText(cell, String(headers\[c\] || ''), { bold: true, align: SlidesApp.ParagraphAlignment.CENTER });
}
for (let r \= 0; r \< rows.length; r++) {
const row \= rows\[r\] || \[\];
for (let c \= 0; c \< headers.length; c++) {
const cell \= table.getCell(r \+ 1, c);
setStyledText(cell, String(row\[c\] || ''), { align: SlidesApp.ParagraphAlignment.CENTER });
}
}
} else {
throw new Error('headers is empty');
}
} catch (e) {
const cols \= Math.max(1, headers.length || 3);
const rcount \= rows.length \+ 1;
const gap \= layout.pxToPt(1);
const cellW \= (area.width \- gap \* (cols \- 1)) / cols;
const cellH \= (area.height \- gap \* (rcount \- 1)) / rcount;
const drawCell \= (r, c, text, bold) \=\> {
const left \= area.left \+ c \* (cellW \+ gap);
const top \= area.top \+ r \* (cellH \+ gap);
const cell \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, left, top, cellW, cellH);
cell.getFill().setSolidFill(CONFIG.COLORS.background\_white);
cell.getBorder().getLineFill().setSolidFill(CONFIG.COLORS.card\_border);
cell.getBorder().setWeight(1);
setStyledText(cell, String(text || ''), { bold: \!\!bold, align: SlidesApp.ParagraphAlignment.CENTER });
try { cell.setContentAlignment(SlidesApp.ContentAlignment.MIDDLE); } catch(e){}
};
(headers.length ? headers : \['項目','値1','値2'\]).forEach((h, c) \=\> drawCell(0, c, h, true));
for (let r \= 0; r \< rows.length; r++) {
const row \= rows\[r\] || \[\];
for (let c \= 0; c \< (headers.length || 3); c++) drawCell(r \+ 1, c, row\[c\], false);
}
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
// progress(進捗バー)
function createProgressSlide(slide, data, layout, pageNum) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
drawStandardTitleHeader(slide, layout, 'progressSlide', data.title);
const dy \= drawSubheadIfAny(slide, layout, 'progressSlide', data.subhead);
const area \= offsetRect(layout.getRect('progressSlide.area'), 0, dy);
const items \= Array.isArray(data.items) ? data.items : \[\];
const n \= Math.max(1, items.length);
const rowH \= area.height / n;
for (let i \= 0; i \< n; i++) {
const y \= area.top \+ i \* rowH \+ layout.pxToPt(6);
const label \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, area.left, y, layout.pxToPt(150), layout.pxToPt(18));
setStyledText(label, String(items\[i\].label || ''), { size: CONFIG.FONTS.sizes.body });
```
const barLeft \= area.left \+ layout.pxToPt(160);
const barW \= area.width \- layout.pxToPt(210);
const barBG \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barLeft, y, barW, layout.pxToPt(14));
barBG.getFill().setSolidFill(CONFIG.COLORS.faint\_gray); barBG.getBorder().setTransparent();
const p \= Math.max(0, Math.min(100, Number(items\[i\].percent || 0)));
const barFG \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barLeft, y, barW \* (p/100), layout.pxToPt(14));
barFG.getFill().setSolidFill(CONFIG.COLORS.google\_green); barFG.getBorder().setTransparent();
const pct \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, barLeft \+ barW \+ layout.pxToPt(6), y \- layout.pxToPt(1), layout.pxToPt(40), layout.pxToPt(16));
pct.getText().setText(String(p) \+ '%');
applyTextStyle(pct.getText(), { size: CONFIG.FONTS.sizes.small, color: CONFIG.COLORS.neutral\_gray });
```
}
drawBottomBarAndFooter(slide, layout, pageNum);
}
function createClosingSlide(slide, data, layout) {
slide.getBackground().setSolidFill(CONFIG.COLORS.background\_white);
const image \= slide.insertImage(CONFIG.LOGOS.closing);
const imgW\_pt \= layout.pxToPt(450) \* layout.scaleX;
const aspect \= image.getHeight() / image.getWidth();
image.setWidth(imgW\_pt).setHeight(imgW\_pt \* aspect);
image.setLeft((layout.pageW\_pt \- imgW\_pt) / 2).setTop((layout.pageH\_pt \- (imgW\_pt \* aspect)) / 2);
}
// \--- 7\. ユーティリティ関数群 \---
function createLayoutManager(pageW\_pt, pageH\_pt) {
const pxToPt \= (px) \=\> px \* 0.75;
const baseW\_pt \= pxToPt(CONFIG.BASE\_PX.W);
const baseH\_pt \= pxToPt(CONFIG.BASE\_PX.H);
const scaleX \= pageW\_pt / baseW\_pt;
const scaleY \= pageH\_pt / baseH\_pt;
const getPositionFromPath \= (path) \=\> path.split('.').reduce((obj, key) \=\> obj\[key\], CONFIG.POS\_PX);
return {
scaleX, scaleY, pageW\_pt, pageH\_pt, pxToPt,
getRect: (spec) \=\> {
const pos \= typeof spec \=== 'string' ? getPositionFromPath(spec) : spec;
let left\_px \= pos.left;
if (pos.right \!== undefined && pos.left \=== undefined) {
left\_px \= CONFIG.BASE\_PX.W \- pos.right \- pos.width;
}
return {
left: left\_px \!== undefined ? pxToPt(left\_px) \* scaleX : undefined,
top: pos.top \!== undefined ? pxToPt(pos.top) \* scaleY : undefined,
width: pos.width \!== undefined ? pxToPt(pos.width) \* scaleX : undefined,
height: pos.height \!== undefined ? pxToPt(pos.height) \* scaleY : undefined,
};
}
};
}
function offsetRect(rect, dx, dy) {
return { left: rect.left \+ (dx || 0), top: rect.top \+ (dy || 0), width: rect.width, height: rect.height };
}
function drawStandardTitleHeader(slide, layout, key, title) {
const logoRect \= layout.getRect(\`${key}.headerLogo\`);
const logo \= slide.insertImage(CONFIG.LOGOS.header);
const asp \= logo.getHeight() / logo.getWidth();
logo.setLeft(logoRect.left).setTop(logoRect.top).setWidth(logoRect.width).setHeight(logoRect.width \* asp);
const titleRect \= layout.getRect(\`${key}.title\`);
const titleShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, titleRect.left, titleRect.top, titleRect.width, titleRect.height);
setStyledText(titleShape, title || '', { size: CONFIG.FONTS.sizes.contentTitle, bold: true });
const uRect \= layout.getRect(\`${key}.titleUnderline\`);
const u \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, uRect.left, uRect.top, uRect.width, uRect.height);
u.getFill().setSolidFill(CONFIG.COLORS.primary\_blue);
u.getBorder().setTransparent();
}
function drawSubheadIfAny(slide, layout, key, subhead) {
if (\!subhead) return 0;
const rect \= layout.getRect(\`${key}.subhead\`);
const box \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, rect.left, rect.top, rect.width, rect.height);
setStyledText(box, subhead, { size: CONFIG.FONTS.sizes.subhead, color: CONFIG.COLORS.text\_primary });
return layout.pxToPt(36);
}
function drawBottomBar(slide, layout) {
const barRect \= layout.getRect('bottomBar');
const bar \= slide.insertShape(SlidesApp.ShapeType.RECTANGLE, barRect.left, barRect.top, barRect.width, barRect.height);
bar.getFill().setSolidFill(CONFIG.COLORS.primary\_blue);
bar.getBorder().setTransparent();
}
function drawBottomBarAndFooter(slide, layout, pageNum) {
drawBottomBar(slide, layout);
addGoogleFooter(slide, layout, pageNum);
}
function addGoogleFooter(slide, layout, pageNum) {
const leftRect \= layout.getRect('footer.leftText');
const leftShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, leftRect.left, leftRect.top, leftRect.width, leftRect.height);
leftShape.getText().setText(CONFIG.FOOTER\_TEXT);
applyTextStyle(leftShape.getText(), { size: CONFIG.FONTS.sizes.footer, color: CONFIG.COLORS.text\_primary });
if (pageNum \> 0\) {
const rightRect \= layout.getRect('footer.rightPage');
const rightShape \= slide.insertShape(SlidesApp.ShapeType.TEXT\_BOX, rightRect.left, rightRect.top, rightRect.width, rightRect.height);
rightShape.getText().setText(String(pageNum));
applyTextStyle(rightShape.getText(), { size: CONFIG.FONTS.sizes.footer, color: CONFIG.COLORS.primary\_blue, align: SlidesApp.ParagraphAlignment.END });
}
}
function applyTextStyle(textRange, opt) {
const style \= textRange.getTextStyle();
style.setFontFamily(CONFIG.FONTS.family);
style.setForegroundColor(opt.color || CONFIG.COLORS.text\_primary);
style.setFontSize(opt.size || CONFIG.FONTS.sizes.body);
style.setBold(opt.bold || false);
if (opt.align) {
try { textRange.getParagraphs().forEach(p \=\> p.getRange().getParagraphStyle().setParagraphAlignment(opt.align)); } catch (e) {}
}
}
function setStyledText(shapeOrCell, rawText, baseOpt) {
const parsed \= parseInlineStyles(rawText || '');
const tr \= shapeOrCell.getText();
tr.setText(parsed.output);
applyTextStyle(tr, baseOpt || {});
applyStyleRanges(tr, parsed.ranges);
}
function setBulletsWithInlineStyles(shape, points) {
const joiner \= '\\n\\n';
let combined \= '';
const ranges \= \[\];
(points || \[\]).forEach((pt, idx) \=\> {
const parsed \= parseInlineStyles(String(pt || ''));
const bullet \= '• ' \+ parsed.output;
if (idx \> 0\) combined \+= joiner;
const start \= combined.length;
combined \+= bullet;
```
parsed.ranges.forEach(r \=\> {
ranges.push({ start: start \+ 2 \+ r.start, end: start \+ 2 \+ r.end, bold: r.bold, color: r.color });
});
```
});
const tr \= shape.getText();
tr.setText(combined || '• —');
applyTextStyle(tr, { size: CONFIG.FONTS.sizes.body });
try {
tr.getParagraphs().forEach(p \=\> {
const ps \= p.getRange().getParagraphStyle();
ps.setLineSpacing(100);
ps.setSpaceBelow(6);
});
} catch (e) {}
applyStyleRanges(tr, ranges);
}
function parseInlineStyles(s) {
const ranges \= \[\];
let out \= '';
for (let i \= 0; i \< s.length; ) {
// \[\[青太字\]\]
if (s\[i\] \=== '\[' && s\[i+1\] \=== '\[') {
const close \= s.indexOf('\]\]', i \+ 2);
if (close \!== \-1) {
const content \= s.substring(i \+ 2, close);
const start \= out.length;
out \+= content;
const end \= out.length;
ranges.push({ start, end, bold: true, color: CONFIG.COLORS.primary\_blue });
i \= close \+ 2; continue;
}
}
// \*\*太字\*\*
if (s\[i\] \=== '\*' && s\[i+1\] \=== '\*') {
const close \= s.indexOf('\*\*', i \+ 2);
if (close \!== \-1) {
const content \= s.substring(i \+ 2, close);
const start \= out.length;
out \+= content;
const end \= out.length;
ranges.push({ start, end, bold: true });
i \= close \+ 2; continue;
}
}
out \+= s\[i\]; i++;
}
return { output: out, ranges };
}
function applyStyleRanges(textRange, ranges) {
ranges.forEach(r \=\> {
try {
const sub \= textRange.getRange(r.start, r.end);
if (\!sub) return;
const st \= sub.getTextStyle();
if (r.bold) st.setBold(true);
if (r.color) st.setForegroundColor(r.color);
} catch (e) {}
});
}
function normalizeImages(arr) {
return (arr || \[\]).map(v \=\> {
if (typeof v \=== 'string') return { url: v };
if (v && typeof v.url \=== 'string') return { url: v.url, caption: v.caption || '' };
return null;
}).filter(Boolean).slice(0, 6);
}
function renderImagesInArea(slide, layout, area, images) {
if (\!images || images.length \=== 0\) return;
const n \= Math.min(6, images.length);
let cols \= 1, rows \= 1;
if (n \=== 1\) { cols \= 1; rows \= 1; }
else if (n \=== 2\) { cols \= 2; rows \= 1; }
else if (n \<= 4\) { cols \= 2; rows \= 2; }
else { cols \= 3; rows \= 2; }
const gap \= layout.pxToPt(10);
const cellW \= (area.width \- gap \* (cols \- 1)) / cols;
const cellH \= (area.height \- gap \* (rows \- 1)) / rows;
for (let i \= 0; i \< n; i++) {
const r \= Math.floor(i / cols), c \= i % cols;
const left \= area.left \+ c \* (cellW \+ gap);
const top \= area.top \+ r \* (cellH \+ gap);
try {
const img \= slide.insertImage(images\[i\].url);
const scale \= Math.min(cellW / img.getWidth(), cellH / img.getHeight());
const w \= img.getWidth() \* scale;
const h \= img.getHeight() \* scale;
img.setWidth(w).setHeight(h);
img.setLeft(left \+ (cellW \- w) / 2).setTop(top \+ (cellH \- h) / 2);
} catch(e) {}
}
}
function isAgendaTitle(title) {
const t \= String(title || '').toLowerCase();
return /(agenda|アジェンダ|目次|本日お伝えすること)/.test(t);
}
function buildAgendaFromSlideData() {
const pts \= \[\];
for (const d of slideData) {
if (d && d.type \=== 'section' && typeof d.title \=== 'string' && d.title.trim()) pts.push(d.title.trim());
}
if (pts.length \> 0\) return pts.slice(0, 5);
const alt \= \[\];
for (const d of slideData) {
if (d && d.type \=== 'content' && typeof d.title \=== 'string' && d.title.trim()) alt.push(d.title.trim());
}
return alt.slice(0, 5);
}
function drawArrowBetweenRects(slide, a, b, arrowH, arrowGap) {
const fromX \= a.left \+ a.width;
const toX \= b.left;
const width \= Math.max(0, toX \- fromX \- arrowGap \* 2);
if (width \< 8\) return;
const yMid \= ((a.top \+ a.height/2) \+ (b.top \+ b.height/2)) / 2;
const top \= yMid \- arrowH / 2;
const left \= fromX \+ arrowGap;
const arr \= slide.insertShape(SlidesApp.ShapeType.RIGHT\_ARROW, left, top, width, arrowH);
arr.getFill().setSolidFill(CONFIG.COLORS.primary\_blue);
arr.getBorder().setTransparent();
}
テンプレートパターンを増やしたり、
安定性を高めるブラッシュアップは引き続き行っていく予定です。
新バージョンが完成したらまた共有しますね!
参考になったらnoteにいいね、フォローをお願いします。
Xでの感想シェアなどいただければ、次の開発のエネルギーになります。
コメント
4早速試しました!すごいです。ありがとうございます。
試してみました!感動しました!
タイトルに偽りなし、神回でした。ありがとうございました
ことし、DeepResearchの次に感動しました。ありがとうございます!