# 品牌短影音系統｜Agent 參考文件 v2（Jerry_AI 版型）

給 Claude 讀的規格。這份文件把 Jerry_AI 品牌系統的 Reels 版型整理成一套可重複使用的模板：
**所有版面、字型、動態細節都照 Jerry_AI，使用者只換「配色」和「主題內容」**。
每個人都讀同一份文件，做出來的片風格就會一致。

---

## 0. 你（Agent）要做的事

1. 讀完整份文件，包含最後的「附錄：模板」。
2. 從使用者訊息取得 CONFIG 的值（第 3 節）。缺的值依主題補上合理內容，並在回覆開頭列出你補了哪些。
3. 先輸出「我的品牌規格卡」（10 行以內：品牌名、主色、標語、帳號、這支片的主題）。
4. 把附錄模板**原封不動**做成一個 HTML Artifact，只替換 `CONFIG` 物件裡的值。
   不改 CSS、不改版面、不改動畫時間，不增減元素。
5. 照第 6 節檢查清單逐條回報 ✅／⚠️。
6. 只用你自己產出：不接任何外部工具、MCP 或 API。

## 1. 版型來源（Jerry_AI 品牌系統，只換色與內容）

- 底色夜色 `#0B1220`，1px 格線（7% 透明、間距 60px）。
- 字標：`>` ＋品牌名＋句點大小的方塊游標 `■`（0.22em，坐在基線上）。
- 字型：英文與數字 Inter Black／ExtraBold；中文 Noto Sans TC；指令、帳號、路徑 IBM Plex Mono。
- 圖形元素：琥珀色標籤（只放時間或好處）、巨字放在人像後面、終端機卡片（深色面板＋路徑標頭）、字幕條、方塊游標。
- 版面硬規則：全部直角、不加陰影、不加漸層；同一支片只有一個主色。

## 2. 影片結構（15 秒，9:16，1080×1920）

| 段 | 時間 | 畫面 |
|---|---|---|
| 開場 | 0–3.4 秒 | 字標打字：`>` 0.1 秒滑入 → 游標 0.3 秒出現、閃一次 → 1.3 秒開始每鍵 0.1 秒 → 打完按 Enter，標語淡入上移 10px |
| 主畫面 | 3.4–12.6 秒 | 上方帳號列 → 巨字升起 → 人像升起 → 琥珀標籤 → 標題兩行（間隔 0.2 秒）→ 卡片升起、三行逐字輸出 → 字幕條 → 游標閃 |
| 片尾 | 12.6–15 秒 | 主色滿版由右滑入，深色字標＋標語＋「追蹤 @帳號 →」按鈕，游標持續閃 |

動態一律 ease-out、不回彈、不模糊；游標 0.5 秒亮、0.5 秒暗。

## 3. CONFIG：使用者可以換的只有這些

| 欄位 | 內容 | 長度上限 |
|---|---|---|
| `brand` | 品牌名（字標） | 英文 12 字元／中文 6 字 |
| `tagline` | 標語 | 16 字 |
| `handle` | 帳號（不含 @） | 20 字元 |
| `accent` | 主色（四選一或色碼） | — |
| `topTag` | 右上「分類 · 時間」，例：`咖啡 · 1 MIN` | 12 字元 |
| `payoff` | 琥珀標籤：時間或好處，例：`1 分鐘學會` | 8 字 |
| `title1` | 標題第 1 行（大字） | 英文 13 字元／中文 8 字 |
| `title2` | 標題第 2 行 | 11 字 |
| `giant` | 巨字：數字或英文，例：`3`、`20+`、`1:15` | 4 字元 |
| `path` | 卡片標頭（像資料夾路徑），例：`~/coffee/notes` | 22 字元 |
| `rows` | 卡片 3 行：`[關鍵字, 說明]` | 每行合計 14 字 |
| `more` | 卡片最後一行（主色，可留空 `""`） | 10 字 |
| `caption` | 字幕條一句話 | 13 字 |

主色選項：終端綠 `#2FE0A3`（Jerry_AI 原色）、天藍 `#5CC8FF`、珊瑚 `#FF8A65`、紫 `#B79CFF`。
琥珀 `#FFC24B` 固定用在標籤，不能當主色。超過長度上限時，請改寫成更短的句子，不要縮小字或換行。

## 4. 頭貼

- 頁面上的「選擇頭貼」讓使用者自己上傳。
- **去背 PNG（最像 Jerry_AI）**：模板會自動裁掉透明邊、放大成人像，巨字在後面。
  去背方法：iPhone 長按照片主體 →「拷貝」或「分享」存成圖片；Mac（較新版本）用「預覽程式」的「移除背景」。
- 一般照片：自動放進直角方框，後面墊一塊主色。
- 沒有頭貼：只有巨字，版面一樣完整。

## 5. 改片（用對話剪輯）

使用者說「換成珊瑚色」「標題改成…」「卡片第 2 行改成…」時：
- 只改 `CONFIG` 對應的值；要調整時間時只改 `SCENES` 的數字。
- 其他程式碼一律不動，不要整份重寫。
- 改完列出改了哪些值，重跑受影響的檢查項目。

## 6. 檢查清單（做完逐條回報 ✅／⚠️）

1. CSS、版面、動畫和附錄模板一致，只改了 `CONFIG`。
2. 主色只有一個；琥珀只用在標籤。
3. 每個欄位都在第 3 節的長度上限內。
4. `title1`、`title2`、`caption` 都是一行，沒有換行。
5. `giant` 是數字或英文，4 個字元以內。
6. `rows` 剛好 3 行，每行都有關鍵字和說明。
7. 開場字標是 `>` ＋品牌名＋方塊游標。
8. 片尾按鈕是「追蹤 @帳號 →」。

---

## 附錄：模板（照抄，只改 CONFIG）

```html
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>品牌短影音</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500;600&family=Inter:wght@800;900&family=Noto+Sans+TC:wght@500;700;900&display=swap" rel="stylesheet">
<style>
  :root { --night:#0B1220; --paper:#F2F5F3; --panel:#18233A; --amber:#FFC24B; --mist:#C9D2DE; --mute:#8592A6; --line:#26324A; }
  html, body { margin:0; height:100%; background:#000; overflow:hidden; }
  body.playing, body.playing * { cursor:none !important; }
  #stage { position:absolute; left:0; top:0; width:1080px; height:1920px; transform-origin:0 0; overflow:hidden;
           background-color:var(--night); font-family:'Noto Sans TC', sans-serif; }
  .grid { position:absolute; inset:0;
          background-image:linear-gradient(rgba(201,210,222,.07) 1px, transparent 1px), linear-gradient(90deg, rgba(201,210,222,.07) 1px, transparent 1px);
          background-size:60px 60px; }
  .scene { position:absolute; inset:0; }
  .abs { position:absolute; }
  .mono { font-family:'IBM Plex Mono', 'Noto Sans TC', monospace; }
  .inter { font-family:'Inter', 'Noto Sans TC', sans-serif; }
  .nowrap { white-space:nowrap; }
  /* 字標：> 品牌名 ■（游標是句點大小的方塊，坐在基線上） */
  .wm { position:absolute; left:0; right:0; text-align:center; white-space:nowrap; line-height:1; font-weight:800; letter-spacing:-0.02em; }
  .wm .p { display:inline-block; color:var(--accent); margin-right:.06em; }
  .wm .cur { display:inline-block; width:.22em; height:.22em; background:var(--accent); margin-left:.06em; }
  #wm1 { color:var(--paper); }
  #tagline1 { left:0; right:0; top:1000px; text-align:center; font-size:40px; font-weight:700; color:var(--mist); }
  #handle { left:64px; top:140px; font-size:28px; font-weight:500; color:var(--mute); }
  #toptag { right:64px; top:140px; font-size:28px; font-weight:500; color:var(--mute); }
  #payoff { left:64px; top:250px; padding:12px 18px; background:var(--amber); color:var(--night); font-size:34px; font-weight:700; line-height:1; }
  #title1 { left:64px; top:330px; font-size:116px; font-weight:900; letter-spacing:-0.03em; color:var(--paper); line-height:1.1; }
  #title2 { left:64px; top:480px; font-size:80px; font-weight:700; color:var(--paper); line-height:1.15; }
  #giant { left:36px; top:650px; font-size:560px; font-weight:900; letter-spacing:-0.06em; color:var(--accent); line-height:1; }
  #portrait { left:0; top:0; }
  #portraitBack { background:var(--accent); }
  #card { left:64px; top:1180px; min-width:420px; max-width:560px; background:var(--panel); border:1px solid var(--line); padding:26px 28px 30px; box-sizing:border-box; }
  #card .path { font-size:24px; font-weight:500; color:var(--mute); line-height:1; margin-bottom:26px; }
  #card .row { font-size:28px; font-weight:500; color:var(--paper); line-height:1; height:28px; margin-top:30px; white-space:nowrap; }
  #card .row:first-of-type { margin-top:0; }
  #card .row .k { margin-right:.8em; }
  #card .more { font-size:26px; font-weight:600; color:var(--accent); line-height:1; margin-top:30px; height:26px; }
  #band { left:0; top:1530px; width:1080px; height:110px; background:rgba(11,18,32,.88); }
  #caption { left:96px; top:1560px; font-size:48px; font-weight:700; color:var(--paper); line-height:1; }
  #caption .cur { display:inline-block; width:.22em; height:.22em; background:var(--accent); margin-left:.18em; }
  #end { background:var(--accent); }
  #end .wm { color:var(--night); }
  #end .wm .p, #end .wm .cur { color:var(--night); background:none; }
  #end .wm .cur { background:var(--night); }
  #tagline2 { left:0; right:0; top:1000px; text-align:center; font-size:40px; font-weight:700; color:var(--night); }
  #cta { left:50%; top:1120px; transform:translateX(-50%); background:var(--night); color:var(--accent); font-size:34px; font-weight:600; padding:22px 34px; line-height:1; white-space:nowrap; }
  #count { position:absolute; inset:0; display:none; align-items:center; justify-content:center; font:900 360px/1 'Inter'; color:var(--accent); z-index:9; }
  #ui { position:fixed; left:50%; bottom:24px; transform:translateX(-50%); display:flex; gap:10px; z-index:10; font:700 15px/1 'Noto Sans TC', sans-serif; }
  #ui button, #ui label { background:#F2F5F3; color:#0B1220; border:0; padding:12px 18px; cursor:pointer; }
  #ui input { display:none; }
  body.playing #ui, body.still #ui { display:none; }
</style>
</head>
<body>
<div id="stage">
  <div class="grid" id="grid"></div>
  <!-- 1 開場：logo 打字 -->
  <div class="scene" id="s1">
    <div class="wm inter" id="wm1"></div>
    <div class="abs" id="tagline1"></div>
  </div>
  <!-- 2 主畫面：照 Jerry_AI Reels 版面 -->
  <div class="scene" id="s2">
    <div class="abs mono nowrap" id="handle"></div>
    <div class="abs mono nowrap" id="toptag"></div>
    <div class="abs inter nowrap" id="giant"></div>
    <div class="abs" id="portraitBack"></div>
    <img class="abs" id="portrait" alt="">
    <div class="abs nowrap" id="payoff"></div>
    <div class="abs inter nowrap" id="title1"></div>
    <div class="abs nowrap" id="title2"></div>
    <div class="abs mono" id="card"><div class="path"></div><div class="rows"></div></div>
    <div class="abs" id="band"></div>
    <div class="abs nowrap" id="caption"></div>
  </div>
  <!-- 3 片尾：主色滿版 -->
  <div class="scene" id="end">
    <div class="wm inter" id="wm2"></div>
    <div class="abs" id="tagline2"></div>
    <div class="abs mono" id="cta"></div>
  </div>
  <div id="count"></div>
</div>
<div id="ui">
  <button id="play">▶ 播放／重播</button>
  <label>選擇頭貼<input type="file" id="file" accept="image/*"></label>
  <button id="fs">全螢幕</button>
</div>
<script>
// ===================== CONFIG：只改這裡 =====================
const CONFIG = {
  brand:   "Jerry_AI",                       // 品牌名（字標）
  tagline: "從會用 AI 工具，到交出工作成果",    // 標語，一行 ≤ 16 字
  handle:  "jerry_ai",                       // 帳號（不含 @）
  accent:  "#2FE0A3",                        // 主色：終端綠 #2FE0A3／天藍 #5CC8FF／珊瑚 #FF8A65／紫 #B79CFF
  topTag:  "AI 新知 · 5 MIN",                // 右上分類 · 時間
  payoff:  "5 分鐘看完",                      // 琥珀標籤：時間或好處，≤ 8 字
  title1:  "OpenAI DevDay",                  // 標題第 1 行（英文或短中文），≤ 13 個英文字母／8 個中文字
  title2:  "一口氣 20 多項發表",               // 標題第 2 行，≤ 11 字
  giant:   "20+",                            // 巨字：數字或英文，≤ 4 字元
  path:    "~/devday/highlights",            // 卡片標頭（像資料夾路徑）
  rows: [                                    // 卡片 3 行：[關鍵字, 說明]，每行合計 ≤ 14 字
    ["Dots", "全天候 AI 代理人"],
    ["GPT-6 Sol", "複雜寫碼"],
    ["Luna 6", "高量日常任務"]
  ],
  more:    "…還有 17 項",                    // 卡片最後一行（主色），可留空 ""
  caption: "官方說：史上規模最大的一次",        // 字幕條，≤ 13 字
  portrait: null                             // 由「選擇頭貼」帶入
};
// ===================== 時間軸（秒） =====================
const SCENES = [
  { name: "開場 logo 打字", start: 0,    end: 3.4 },
  { name: "主畫面",         start: 3.4,  end: 12.6 },
  { name: "片尾",           start: 12.6, end: 15 }
];
const DURATION = 15;
// ===========================================================

const $ = id => document.getElementById(id);
const clamp = x => Math.min(Math.max(x, 0), 1);
const ease = x => 1 - Math.pow(1 - clamp(x), 3);            // ease-out，不回彈
const blink = (t, t0) => Math.floor((t - t0) / 0.5) % 2 === 0; // 0.5 秒亮、0.5 秒暗
const esc = s => String(s).replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
let P = { mode: "none", w: 0, h: 0 };   // 頭貼狀態

function rise(el, t, t0, dy, dur = 0.5) {   // 由下往上淡入
  const k = ease((t - t0) / dur);
  el.style.opacity = t < t0 ? 0 : k;
  el.style.transform = `translateY(${(1 - k) * dy}px)`;
}
function fit(el, maxW, size) {               // 字太長就縮小，不換行
  el.style.fontSize = size + "px";
  if (el.scrollWidth > maxW) el.style.fontSize = Math.floor(size * maxW / el.scrollWidth) + "px";
}
function wordmark(el, typed, cursorOn, pX = 0, pA = 1) {
  const name = [...CONFIG.brand].slice(0, typed).join("");
  el.innerHTML = `<span class="p" style="opacity:${pA};transform:translateX(${pX}px)">&gt;</span>${esc(name)}`
    + `<span class="cur" style="visibility:${cursorOn ? "visible" : "hidden"}"></span>`;
}

function applyConfig() {
  document.documentElement.style.setProperty("--accent", CONFIG.accent);
  $("tagline1").textContent = $("tagline2").textContent = CONFIG.tagline;
  $("handle").textContent = "@" + CONFIG.handle;
  $("toptag").textContent = CONFIG.topTag;
  $("payoff").textContent = CONFIG.payoff;
  $("title1").textContent = CONFIG.title1; fit($("title1"), 952, 116);
  $("title2").textContent = CONFIG.title2; fit($("title2"), 952, 80);
  $("giant").textContent = CONFIG.giant; fit($("giant"), 1040, 560);
  $("cta").textContent = `追蹤 @${CONFIG.handle} →`;
  $("card").querySelector(".path").textContent = CONFIG.path;
  // 字標字級：寬度不超過 900px
  for (const id of ["wm1", "wm2"]) { const el = $(id); wordmark(el, 99, true); fit(el, 900, 128); el.style.top = (860 - parseFloat(el.style.fontSize) / 2) + "px"; }
  layoutPortrait();
}

function layoutPortrait() {
  const img = $("portrait"), back = $("portraitBack");
  img.style.display = back.style.display = "none";
  if (P.mode === "cutout") {          // 去背 PNG：大張、置中偏右、往下出血，巨字在後面
    const h = 1200, w = h * P.w / P.h;
    Object.assign(img.style, { display: "block", height: h + "px", width: w + "px", left: (580 - w / 2) + "px", top: "740px" });
  } else if (P.mode === "photo") {    // 一般照片：直角方框，後面墊主色
    Object.assign(img.style, { display: "block", left: "420px", top: "740px", width: "596px", height: "760px", objectFit: "cover" });
    Object.assign(back.style, { display: "block", left: "444px", top: "764px", width: "596px", height: "760px" });
  }
}

function draw(t) {
  $("s1").style.display = t < SCENES[0].end ? "block" : "none";
  $("s2").style.display = t >= SCENES[1].start && t < SCENES[1].end + 0.45 ? "block" : "none";
  $("end").style.display = t >= SCENES[2].start ? "block" : "none";
  $("grid").style.opacity = 1;

  // 1 開場：> 0.1s 滑入；游標 0.3s 出現閃一次；1.3s 起每鍵 0.1s；打完按 Enter，標語淡入上移 10px
  const n = [...CONFIG.brand].length, ts = 1.3, te = ts + n * 0.1, enter = te + 0.3;
  {
    const k = ease((t - 0.1) / 0.3);
    const typed = t < ts ? 0 : Math.min(n, Math.floor((t - ts) / 0.1) + 1);
    let cur = false;
    if (t >= 0.3 && t < ts) cur = (t - 0.3) < 0.3 || (t - 0.3) >= 0.6;
    else if (t >= ts && t < enter) cur = true;
    else if (t >= enter) cur = blink(t, enter);
    wordmark($("wm1"), typed, cur, (1 - k) * -30, t < 0.1 ? 0 : k);
    rise($("tagline1"), t, enter, 10, 0.4);
  }
  // 2 主畫面：上方列 → 巨字升起 → 人像升起 → 標籤 → 標題兩行（間隔 0.2s）→ 卡片升起、逐行打字 → 字幕條 → 游標閃
  {
    const s = SCENES[1].start;
    rise($("handle"), t, s, 0, 0.4); rise($("toptag"), t, s, 0, 0.4);
    rise($("giant"), t, s + 0.2, 80, 0.6);
    rise($("portrait"), t, s + 0.5, 120, 0.7); rise($("portraitBack"), t, s + 0.5, 120, 0.7);
    rise($("payoff"), t, s + 1.0, 30);
    rise($("title1"), t, s + 1.2, 40); rise($("title2"), t, s + 1.4, 40);
    rise($("card"), t, s + 2.0, 60);
    const rowsEl = $("card").querySelector(".rows");
    let html = "";
    CONFIG.rows.forEach(([k, v], i) => {
      const t0 = s + 2.4 + i * 0.8, full = [...`${k}`].length + [...`${v}`].length;
      const typed = t < t0 ? 0 : Math.min(full, Math.floor((t - t0) / 0.04) + 1);
      const kk = [...`${k}`].slice(0, typed).join(""), vv = [...`${v}`].slice(0, Math.max(0, typed - [...`${k}`].length)).join("");
      html += `<div class="row" style="visibility:${t < t0 ? "hidden" : "visible"}"><span>&gt;</span> <span class="k">${esc(kk)}</span>${esc(vv)}</div>`;
    });
    if (CONFIG.more) {
      const t0 = s + 2.4 + CONFIG.rows.length * 0.8;
      html += `<div class="more" style="opacity:${ease((t - t0) / 0.3)}">${esc(CONFIG.more)}</div>`;
    }
    rowsEl.innerHTML = html;
    const tb = s + 2.4 + CONFIG.rows.length * 0.8 + 0.6;
    rise($("band"), t, tb, 0, 0.3);
    rise($("caption"), t, tb + 0.1, 20, 0.4);
    $("caption").innerHTML = esc(CONFIG.caption) + `<span class="cur" style="visibility:${t >= tb + 0.5 && blink(t, tb + 0.5) ? "visible" : "hidden"}"></span>`;
  }
  // 3 片尾：主色滿版由右滑入；字標＋標語＋追蹤鈕；游標持續閃
  {
    const s = SCENES[2].start, k = ease((t - s) / 0.45);
    $("end").style.transform = `translateX(${(1 - k) * 1080}px)`;
    wordmark($("wm2"), 99, blink(t, s + 0.45));
    rise($("tagline2"), t, s + 0.5, 10, 0.4);
    const c = $("cta"), kc = ease((t - s - 0.8) / 0.5);
    c.style.opacity = t < s + 0.8 ? 0 : kc;
    c.style.transform = `translate(-50%, ${(1 - kc) * 40}px)`;
  }
}

// ===================== 頭貼：判斷是不是去背 PNG =====================
function loadPortrait(url) {
  const im = new Image();
  im.onload = () => {
    const cv = document.createElement("canvas"); cv.width = im.width; cv.height = im.height;
    const cx = cv.getContext("2d"); cx.drawImage(im, 0, 0);
    const d = cx.getImageData(0, 0, im.width, im.height).data;
    let x0 = im.width, y0 = im.height, x1 = -1, y1 = -1, clear = 0;
    for (let y = 0; y < im.height; y += 2) for (let x = 0; x < im.width; x += 2) {
      const a = d[(y * im.width + x) * 4 + 3];
      if (a < 20) clear++; else { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; }
    }
    if (clear > (im.width * im.height / 4) * 0.05) {     // 有透明區 → 去背，裁到人物外框
      const w = x1 - x0 + 2, h = y1 - y0 + 2, c2 = document.createElement("canvas");
      c2.width = w; c2.height = h; c2.getContext("2d").drawImage(im, x0, y0, w, h, 0, 0, w, h);
      $("portrait").src = c2.toDataURL("image/png"); P = { mode: "cutout", w, h };
    } else { $("portrait").src = url; P = { mode: "photo", w: im.width, h: im.height }; }
    layoutPortrait(); draw(currentT);
  };
  im.src = url;
}

// ===================== 播放 =====================
let playing = false, currentT = 9.5;
function fitStage() {
  const k = Math.min(innerWidth / 1080, innerHeight / 1920);
  $("stage").style.transform = `translate(${(innerWidth - 1080 * k) / 2}px, ${(innerHeight - 1920 * k) / 2}px) scale(${k})`;
}
function play() {
  if (playing) return;
  playing = true; document.body.classList.add("playing");
  const c = $("count"), t0 = performance.now();
  draw(0);
  (function countdown() {
    const e = (performance.now() - t0) / 1000;
    if (e < 3) { c.style.display = "flex"; c.textContent = 3 - Math.floor(e); requestAnimationFrame(countdown); return; }
    c.style.display = "none";
    const start = performance.now();
    (function tick() {
      currentT = Math.min((performance.now() - start) / 1000, DURATION);
      draw(currentT);
      if (currentT < DURATION) requestAnimationFrame(tick);
      else setTimeout(() => { playing = false; document.body.classList.remove("playing"); }, 600);
    })();
  })();
}
$("play").onclick = play;
$("fs").onclick = () => document.documentElement.requestFullscreen && document.documentElement.requestFullscreen();
$("file").onchange = e => { const f = e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => loadPortrait(r.result); r.readAsDataURL(f); };
window.render = t => { currentT = t; draw(t); };
window.setPortrait = loadPortrait;
addEventListener("resize", fitStage);
function boot() {
  applyConfig(); fitStage();
  const q = new URLSearchParams(location.search).get("t");
  if (q !== null) { document.body.classList.add("still"); currentT = parseFloat(q); }
  draw(currentT);
}
boot();
document.fonts.ready.then(boot);
</script>
</body>
</html>

```
