# 吉祥物 (v0.2.2)

FAB 圓形按鈕本體 = 吉祥物。透過表情 emote、跟著 state 動畫、首訪時橫向膨脹成膠囊招呼使用者。**所有素材、動畫時長、顏色、大小都用 CSS 變數開放給你調**。

## 接線

SDK `dist/duck/` 出貨 **七張預設鴨子精靈**，`tokens.css` 用 `--dddk-*-url` 綁在 `:root` 上。基本安裝就會有吉祥物、不用自己 host PNG。

七張預設：

| 變數 | 預設 | 使用位置 |
|---|---|---|
| `--dddk-avatar-url` | `./duck/neutral.png` | 字幕條頭像、Dwell chip、FAB fallback |
| `--dddk-swim-url` | `./duck/swim-side.png` | Proactive 游泳 indicator、字幕條游泳接力 |
| `--dddk-hero-url` | `./duck/hero-greet.png` | HERO 膠囊大顯身手精靈 |
| `--dddk-chill-url` | `./duck/chill-shades.png` | Dwell 框角落吉祥物（墨鏡鴨） |
| `--dddk-swim-cycle-url` | `./duck/swim-cycle.png` | Dwell 頂邊游泳 spritesheet（8 幀） |
| `--dddk-brand-mark-url` | `./duck/logo.png` | Palette「Powered by dotdotduck」品牌 mark |
| `--dddk-cursor-url` | `./duck/cursor.png` | WebAgent 合成 cursor（鴨子騎紙飛機） |

FAB 每個狀態的表情動畫要 host 自己在 `MobileTrigger` 上接 face URL——這不放 `:root` 是因為 host 每 instance 都可能不同：

```ts
new MobileTrigger({
  fab: {
    alwaysVisible: true,
    faces: {
      neutral:   '/duck/neutral.png',
      smile:     '/duck/smile.png',
      thinking:  '/duck/thinking.png',
      listening: '/duck/listening.png',
      done:      '/duck/done.png',
      error:     '/duck/error.png',
      wink:      '/duck/wink.png',
      'side-left': '/duck/swim-side.png', // HERO 膠囊用
    },
  },
}).attachTo(dddk);
```

想換自家品牌角色，`:root` 上一行覆蓋即可：

```css
:root {
  --dddk-avatar-url: url('/my-brand/mascot.png');
  --dddk-swim-url:   url('/my-brand/swim.png');
  /* 想留 palette 上的 dotdotduck 品牌歸屬，別動 --dddk-brand-mark-url。 */
}
```

**子目錄注意**——Chrome 對 CSS custom property 裡的 `url()`，如果透過 JS-injected `<style>` 套用（Dwell / palette / cursor 都是這樣載 style），會用 **文件 URL** 當基準解析、不是 CSS 檔位置。你的 app 若掛在 `/tools/dddk/` 這類子目錄，SDK 預設的 `./duck/…` 會失效。修法：`:root` 上覆蓋成 root-absolute 路徑——`--dddk-swim-cycle-url: url('/my-app/duck/swim-cycle.png');`。同樣規則套用到 `--dddk-cursor-url` 跟其他從 JS-injected CSS 讀的 sprite 變數。

## 狀態 → 表情 → 動畫

`mobile.setState(state)` 同時換臉 + 切 FAB 動畫：

| 狀態          | 表情          | 動畫                                        |
|---------------|---------------|--------------------------------------------|
| `idle`        | `neutral`     | 溫和上下浮動 + 微幅擺盪                    |
| `thinking`    | `thinking`    | 呼吸縮放 + 環繞進度弧線                    |
| `listening`   | `listening`   | 兩層**聲納波紋** + 黃色暖光暈              |
| `voice-hold`  | `listening`   | 氣球呼吸（按住時脹縮）                     |
| `done`        | `done`        | 一次性彈跳                                 |
| `error`       | `error`       | 一次性橫向抖動                             |

`mobile.setFace(slug)` 只換臉不動動畫。`mobile.celebrate()` 閃一下 `done` 1.2s。

## HERO 招呼（膠囊）

`mobile.showHeroGreeting(text, opts)` 讓 FAB 圓形**橫向膨脹成黃色膠囊**，鴨頭滑到右邊、文字填左半：

```ts
await mobile.showHeroGreeting(
  '嗨！我是 dotdotduck。',
  {
    bodyHtml: `
      <div style="font-weight:700">嗨！我是 dotdotduck</div>
      <div>點我 → 打開指令面板。按住我 → 對我說話。</div>
      <div style="opacity:.7">或按 <kbd>Ctrl</kbd>+<kbd>K</kbd> · 按住 <kbd>Space</kbd></div>
    `,
    ariaLabel: '嗨，我是 dotdotduck。點我打開指令面板，按住我對我說話。',
    autoDismissMs: 20000,
  },
);
```

行為：
- 點**鴨頭端**（膠囊右邊）→ 觸發 FAB 原本的 `onTap`（預設打開 palette）+ 收回。
- 點**文字區**→ 不觸發（讓使用者讀 / 選字不會誤關）。
- 點**頁面其他任何地方** → 收回、不打開 palette。
- 到 `autoDismissMs` 自動收回。

### 主題化 — 每一顆旋鈕

全部是 CSS 變數，寫在 `:root` 或 FAB 元素上都行。

**形狀 + 顏色**

| 變數                              | 預設                                                                                | 效果                     |
|-----------------------------------|-------------------------------------------------------------------------------------|--------------------------|
| `--dddk-fab-hero-width`           | `min(460px, calc(100vw - 40px))`                                                     | 展開寬度                 |
| `--dddk-fab-hero-height`          | `88px`                                                                               | 膠囊高度                 |
| `--dddk-fab-hero-radius`          | `44px`                                                                               | 端點圓角                 |
| `--dddk-fab-hero-bg`              | 暖黃線性漸層                                                                         | 填色                     |
| `--dddk-fab-hero-fg`              | `#1a2a4a`                                                                            | 文字色                   |
| `--dddk-fab-hero-shadow`          | `0 12px 32px rgba(255,180,40,0.35), 0 4px 12px rgba(0,0,0,0.08)`                     | 陰影                     |
| `--dddk-fab-hero-padding`         | `12px 90px 12px 22px`                                                                | 文字面板 padding         |
| `--dddk-fab-hero-face-size`       | `68px`                                                                               | 膠囊內鴨頭大小           |
| `--dddk-fab-hero-face-inset`      | `10px`                                                                               | 鴨頭離右邊距離           |
| `--dddk-fab-hero-font`            | `600 14px/1.45 var(--dddk-font, ...)`                                                | 文字 font shorthand      |
| `--dddk-fab-hero-gap`             | `4px`                                                                                | 文字行距                 |

**時間 + 動畫**

| 變數                                     | 預設                                     | 效果               |
|------------------------------------------|------------------------------------------|--------------------|
| `--dddk-fab-hero-transition-ms`          | `520ms`                                  | 變形時長           |
| `--dddk-fab-hero-easing`                 | `cubic-bezier(0.34, 1.4, 0.64, 1)`       | 變形曲線（彈跳感） |
| `--dddk-fab-hero-content-delay`          | `220ms`                                  | 文字淡入延遲       |

範例 — 用品牌顏色覆寫：

```css
:root {
  --dddk-fab-hero-bg: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
  --dddk-fab-hero-fg: #ffffff;
  --dddk-fab-hero-width: min(520px, calc(100vw - 32px));
  --dddk-fab-hero-height: 76px;
  --dddk-fab-hero-radius: 38px;
  --dddk-fab-hero-transition-ms: 380ms;
}
```

## 換掉整組 sprite

不一定要用我們家的鴨。任意的透明 PNG 集都行 — FAB 用方形臉貼、Dwell + HERO 用側面 sprite。全部設 URL 就好：

```css
:root {
  --dddk-avatar-url: url('/mascot/mine-neutral.png');
  --dddk-swim-url:   url('/mascot/mine-side.png');
  --dddk-hero-url:   url('/mascot/mine-wave.png');
  --dddk-chill-url:  url('/mascot/mine-chill.png');
}
```

```ts
new MobileTrigger({
  fab: {
    faces: {
      neutral:   '/mascot/mine-neutral.png',
      thinking:  '/mascot/mine-thinking.png',
      // ...
    },
  },
}).attachTo(dddk);
```

臉貼**必須 alpha-key**（去背透明），才能讓 FAB 圓 / 膠囊背景乾淨透出來。參考版是用 `gpt-image-2` i2i + `sharp` 後處理生的，程式碼在 `promo-cli/projects/dddk-mascot/`。

## 字幕條指示器

`dddk.subtitle.showIndicator(state)` 三種 status pip：

| 狀態          | 視覺                                                                                     |
|---------------|------------------------------------------------------------------------------------------|
| `processing`  | 1 個大 hero + 3 個小 pip 側面鴨依序彈跳（「努力游泳中」）                                 |
| `listening`   | 5 根黃色 waveform bar 律動（音波感）                                                     |
| `done`        | 靜態 ✓                                                                                    |

`--dddk-swim-url` 自動被撿去做游泳 relay。`--dddk-indicator-wave-color`（預設 `#ffd93d`）控 waveform 顏色。

## Dwell 選取框

長按 Dwell 時：**一隻鴨在框上壓著線游泳** + **右下角一隻戴墨鏡的懶鴨**蹲點。都是純 CSS + 可換 sprite。

| 變數                                       | 預設                            | 效果                                  |
|--------------------------------------------|---------------------------------|--------------------------------------|
| `--dddk-swim-url`                          | 必填                            | 沒設 → 沒游泳鴨                       |
| `--dddk-chill-url`                         | 必填                            | 沒設 → 沒墨鏡鴨                       |
| `--dddk-dwell-swim-eraser`                 | `#ffffff`                       | 「橡皮擦」條，把 outline 咬出斷點     |
| `--dddk-dwell-frame-width`                 | `3px`                           | 外框粗細                              |
| `--dddk-dwell-frame-color`                 | `#ffd93d`                       | 外框顏色                              |
| `--dddk-dwell-frame-offset`                | `7px`                           | 外框離元素距離                        |
| `--dddk-dwell-frame-radius`                | `var(--dddk-radius-sm, 6px)`    | 外框圓角                              |

游泳動畫用 **spritesheet** — 8 幀側面鴨橫排在 `swim-cycle.png` 裡。ping-pong 方向（`animation-direction: alternate`）讓兩端 frame 變成自然停頓點。速度**恆定 ~22 px/s** 不隨元素寬度變化，公式 `duration = 2W / 22`（clamp 6s..26s）。

## Proactive 送達

Proactive 觸發時，一隻側面鴨會**從 FAB 位置游到字幕條的落點**，然後字幕條才展開。讀起來是「鴨鴨把訊息送過來」不是「彈窗跳出來」。自動觸發，只要 `--dddk-swim-url`（或 fallback `--dddk-avatar-url`）有設就會跑。

尊重 `prefers-reduced-motion: reduce` — 游泳路徑跳過、字幕條直接出現。

## 合成 cursor（WebAgent）

Agent 執行 `click`、`scroll_to`、導航這類動作時，一個合成 cursor 會**滑到目標**才觸發 DOM 事件——讓使用者讀成「agent 剛剛在這裡點下去」而不是「那邊突然發生了什麼」。

Pointer mode 預設用 bitmap sprite：**一隻黃色小鴨騎在白色紙飛機上**，尖角朝左上作為點擊起點。素材 `dist/duck/cursor.png`（215×256、透明背景）。想換 sprite 一行搞定：

```css
:root {
  --dddk-cursor-url: url('/my-brand/cursor.png');
}
```

Scroll 跟 reading mode 保留 SVG，主題化仍走 `--webagent-cursor-{fill,stroke}`。

| 變數 | 預設 | 效果 |
|---|---|---|
| `--dddk-cursor-url` | `./duck/cursor.png` | Pointer mode sprite |
| `--webagent-cursor-fill` | `#111`（scroll/reading） | Scroll + reading mode 的 SVG fill |
| `--webagent-cursor-stroke` | `#fff`（scroll/reading） | Scroll + reading mode 的 SVG stroke |
| `--webagent-cursor-glide-ms` | `360ms` | 目標間滑動時間 |

## Palette 品牌 mark

Palette footer 的 **「Powered by dotdotduck」** 條右邊有一隻小鴨 icon 慢慢浮沉——預設是內建的 `dist/duck/logo.png`。Host 覆蓋 avatar 換成自家角色時，不會意外把 palette 上的品牌歸屬弄不見，因為 brand mark 讀自己的變數、只在沒設時 fallback 到 avatar。

```css
:root {
  /* 把品牌 mark 換成自家 logo——palette 歸屬條保留但不再顯示我們的鴨。 */
  --dddk-brand-mark-url: url('/my-brand/logo.png');
}
```

想完全隱藏：`[data-dddk-ui="palette-footer-brand-mark"] { display: none; }`。（「Powered by dotdotduck」文字是獨立 `<span>`；用 `[data-dddk-ui="palette-footer-brand-text"] { display: none; }` 藏掉。）

## 動畫時長 tokens

每一條吉祥物迴圈都可以用 CSS 變數調時長。想放慢配合品牌節奏、加速做玩心足的感覺、或完全關掉走自訂 reduced-motion，都改一個變數。

| 變數 | 預設 | 觸發位置 |
|---|---|---|
| `--dddk-avatar-swim-duration` | `620ms` | 字幕條頭像 swim-in 入場 |
| `--dddk-avatar-swim-easing` | `cubic-bezier(0.34, 1.4, 0.64, 1)` | 入場 easing |
| `--dddk-avatar-bob-duration` | `2.6s` | 字幕條頭像浮沉 loop |
| `--dddk-indicator-swim-duration` | `1.5s` | 思考中 indicator 主鴨游泳 |
| `--dddk-indicator-swim-pip-duration` | `1s` | 思考中 indicator 小點跳動 |
| `--dddk-indicator-wave-duration` | `0.9s` | 聽取中 waveform 波紋 |
| `--dddk-brand-mark-bob-duration` | `3.2s` | Palette footer 品牌 mark 浮沉 |
| `--dddk-dwell-avatar-bob-duration` | `2s` | Dwell 角落墨鏡鴨浮沉 |
| `--dddk-dwell-chill-breath-duration` | `3s` | Dwell 角落墨鏡鴨呼吸 |
| `--dddk-fab-hero-transition-ms` | `520ms` | HERO 膠囊變形時長 |
| `--dddk-swim-duration` | JS 計算 | Dwell 頂邊游泳（JS 依元素寬度算） |

想完全關掉某條動畫，直接對選擇器下 `animation: none`：

```css
[data-dddk-ui="palette-footer-brand-mark"] { animation: none; }
```

`prefers-reduced-motion: reduce` 環境 SDK 已經自動把每條吉祥物 loop 關掉——上面的變數是給品牌調速用，不是給無障礙。

## Reduced motion

吉祥物層每一條動畫都尊重 `prefers-reduced-motion: reduce`：

- FAB 浮動 / 聲納 / orbit / 火花 / hero 變形 → 靜止
- Dwell 游泳 / 懶鴨呼吸 / 外框漣漪 → 靜止
- Proactive 游泳路徑 → 跳過
- 字幕條內容淡入 → 跳過
- WebAgent cursor 滑動 → 跳過（仍會淡入淡出，但不會滑）
