# Playable Ads — Agent 工作文档

> 面向 AI Agent 辅助开发的上下文与行动计划。人工协作时同样可用于 Sprint 规划。

---

## 1. 项目现状快照（2026-05）

### 1.1 技术栈

| 层面        | 技术                                             |
| ----------- | ------------------------------------------------ |
| 构建        | Vite 5 + TypeScript 5                            |
| 渲染        | WebGL 2（无外部 GL 框架）                        |
| 资源加载    | 本地 `assets/res/` 静态文件（fetch JSON/PNG）    |
| 动效/完成态 | js-confetti、Canvas 粒子爆破（自研 `Explosion`） |
| 音效        | Web Audio API via HTMLAudioElement               |
| 外部依赖    | js-confetti（jszip 已移除，playable 不需要）     |
| 平台适配    | AdPlatformAdapter（Applovin / Unity / Playturbo-Mintegral / Google） |

### 1.2 文件树职责

```
index.html          # playable 广告主入口（填色玩法页）
share.html          # 分享落地页（回放 + Download CTA）
src/
  base/             # 渲染引擎层
    Scene.ts        # 场景根节点：图层管理、变换矩阵、手势、动画帧
    Gesture.ts      # 捏合/拖拽/点击手势抽象
    Animator.ts     # 值驱动动画（Interpolator 可替换）
    m4.ts           # 4×4 矩阵运算（纯 TS 实现）
    BgLayer / BoxLayer / BorderLayer / FrameLayer / ImageLayer / TextureLayer
    ImageShaders.ts # GLSL shader 常量池
    glsl/           # 独立 GLSL 文件（Gaussian / Bicubic 等）
  filler/           # 填色玩法层
    index.ts        # 广告主入口脚本（init、UI 事件、CSS 回调）
    play.ts         # 分享页脚本（回放逻辑）
    FillerScene.ts  # 继承 Scene，增加 focusToArea / hint
    FillerData.ts   # 状态核心（coloredPercent、当前 group、getArea 命中）
    WorkLayer.ts    # 互动层：tap 填色、progress=1 动画完成后切换 group
    AnimatableMask.ts# 掩码 FBO：每个 area 以扩散圆动画绘制到纹理
    LineArtLayer.ts # 线稿渲染
    NumberLayer.ts  # 数字标注（atlas sprite）
    HintLayer.ts    # 提示高亮层
    Loader.ts       # 原 zip 解压模块（playable 中不使用，线上网站保留）
    FillerData.ts   # 数据模型：Area、AreaGroup、Data、FillerResource
    LoadingController.ts # Loading overlay 显示/隐藏
    Audio.ts        # 音效播放器
    explosion.ts    # 粒子爆破动画
    ad-platform/    # 广告平台适配层
      types.ts      # Adapter 接口、平台类型、window SDK 类型声明
      current.ts    # 默认 Google adapter；构建时由 Vite alias 替换为目标平台 adapter
      adapters/     # applovin / unity / playturbo / google 实现
  utils/
    random.ts
```

### 1.3 运行时数据流

```
assets/res/<id>/ → fetch config.json + loadImage(page.png / map.png)
  ↓  loadResource()
  FillerResource { config(JSON), page(png), map(png), numberImage, bg, special }
  ↓
FillerData (状态机，管理 area.colored / currentGroup / taskList)
  ↓
FillerScene.addLayer(...)   // BgLayer → BoxLayer → HintLayer → NumberLayer
                            // → WorkLayer → LineArtLayer → BorderLayer
  ↓  requestAnimationFrame
AnimatableMask.flush()      // 将动画进度写入 FBO
  ↓
WorkLayer.draw()            // 读取 FBO texture + colored texture → 渲染
  ↓
CSS 回调 (cssAdjustProgress / cssColorDone / cssOnFinish)
  ↓
完成：confetti + resetToResult + replay()
```

### 1.4 已完成能力

- [x] 核心填色玩法：点击区域 → 识别 area id → 扩散动画上色 → group 完成爆破
- [x] 进度条与颜色按钮 UI（含环形进度）
- [x] 填色完成后撒花 + 回放动画
- [x] 音效（colorDone / allDone / hint）
- [x] 震动反馈
- [x] 加载动画（spinner + 最小显示时长）
- [x] 手势：双指缩放、拖拽、点击
- [x] 缩放到最适合 / focus 到下一个 area
- [x] 分享落地页（图片预览 + 播放填色回放）
- [x] 本地资源加载（assets/res/ 目录，fetch JSON + loadImage PNG）
- [x] 资源内嵌为 Base64（Vite `?url`/`?raw` + vite-plugin-singlefile，构建输出单文件 HTML）
- [x] 宣传界面（promo-screen：coloring-page + slogan 入场动画）
- [x] 手指引导提示（FingerHint DOM overlay，闲置 2s 后自动指向待填区域）

### 1.5 当前主要缺口（需要本项目完成的工作）

| 缺口                     | 优先级 | 说明                                                                        |
| ------------------------ | ------ | --------------------------------------------------------------------------- |
| **广告平台规范适配**     | P0     | 单文件内联 HTML、资源 < 5 MB、无外部依赖（applovin/google/unity/mintergal） |
| **CTA 按钮与跳转**       | P0     | 所有平台都要求显眼 CTA，含平台 API 跳转 store                               |
| **自动演示（引导手势）** | P1     | 游戏前 3-5 秒内有动效/提示引导用户点击上色                                  |
| **体积优化**             | P1     | 当前依赖 js-confetti（jszip 已移除），需图片压缩 + 音效压缩                 |
| ~~**多图片数据内嵌**~~   | ~~P1~~ | ✅ 已完成：Vite `?url` 静态导入 + vite-plugin-singlefile 全量内联           |
| **广告生命周期 API**     | P1     | 各平台 SDK 回调（mraid / gameReady 等）                                     |
| **横竖屏适配**           | P2     | 目前 padding 硬编码，需响应式布局                                           |
| **埋点/事件上报**        | P2     | click / cta_click / complete 事件                                           |
| **多题材模板**           | P3     | 当前单张图，需参数化支持不同图包                                            |

---

## 2. 广告平台规范摘要（重点约束）

参考 README 中列出的竞品与规范链接，综合核心约束：

| 规范         | 要求                                                       |
| ------------ | ---------------------------------------------------------- |
| 文件格式     | 单个自包含 HTML（所有 JS/CSS/图片 Base64 内联）            |
| 文件大小     | ≤ 5 MB（Google）/ ≤ 5 MB（Unity）/ ≤ 3 MB（Applovin 建议） |
| 外部请求     | **禁止**（Google/Applovin：所有资源必须内联）              |
| HTTPS 重定向 | 通用平台 CTA 点击跳转 store；Playturbo/Mintegral 只能调用 `window.install()` |
| MRAID        | Applovin/Unity 需支持 MRAID 2.0（mraid.js 注入）           |
| 平台生命周期 | Unity 调 `dapi.gameReady()`；Playturbo 调 `gameReady/gameEnd` 并暴露 `gameStart/gameClose` |
| 帧率         | 建议 60fps，不得阻塞主线程                                 |
| 可玩时长     | 通常 15-60 秒内有 CTA                                      |
| 广告标识     | 需显示"Ad"或"Sponsored"字样（部分平台）                    |

---

## 3. 实现路径与里程碑

### 阶段 0：分析与准备 ✅ 已完成

- [x] 通读代码，建立项目认知
- [x] 研读各平台规范原文（参见 README 链接）
- [x] 对标竞品 Color asis 广告（优先级最高的对标对象）

---

### 阶段 1：打包为单文件 HTML ✅ 已完成（2026-05）

**目标**：`vite build` 输出一个完全自包含的单文件 `dist/ad.html`，无外部依赖。

**任务拆解**：

| #   | 任务                                                               | 文件                  | 状态   |
| --- | ------------------------------------------------------------------ | --------------------- | ------ |
| 1.1 | 简化入口：移除 URL id 解析和 zipUrl 逻辑，始终调 `loadResource()`  | `src/filler/index.ts` | ✅     |
| 1.2 | 图片资源以 Vite `?url` 静态导入（构建时自动内联）                  | `src/filler/index.ts` | ✅     |
| 1.3 | 音效 mp3 以 Vite `?url` 静态导入                                   | `src/filler/Audio.ts` | ✅     |
| 1.4 | 配置 Vite 输出单文件（vite-plugin-singlefile + assetsInlineLimit） | `vite.config.js`      | ✅     |
| 1.5 | 清理 `@types/jszip` devDependency                                  | `package.json`        | ✅     |
| 1.6 | 验证输出 HTML 可独立在浏览器打开无网络错误                         | —                     | 待验收 |

**验收标准**：

- 单文件体积 ≤ 5 MB（超出需压缩图片）
- `file://` 打开可完整运行，无 CORS 错误，无外部请求

---

### 阶段 2：CTA 与广告生命周期 ✅ 已完成（2026-06 更新为 AdPlatformAdapter）

**目标**：通过统一 AdPlatformAdapter 接入各平台 CTA 与生命周期 API，按构建 profile 输出平台专用 HTML。

**任务拆解**：

| #   | 任务                                      | 文件                                  | 状态 |
| --- | ----------------------------------------- | ------------------------------------- | ---- |
| 2.1 | 新增 CTA 按钮 UI（常驻 + 完成后放大高亮） | `index.html` / `assets/css/tools.css` | ✅   |
| 2.2 | CTA 点击跳转逻辑                          | `src/filler/cta.ts`                   | ✅   |
| 2.3 | 平台 Adapter 层                           | `src/filler/ad-platform/*`            | ✅   |
| 2.4 | gameReady / gameEnd / 平台初始化事件      | `src/filler/index.ts`                 | ✅   |
| 2.5 | 完成流程强化 CTA（宣传界面出现后高亮）    | `src/filler/index.ts`                 | ✅   |
| 2.6 | "Ad"标签（右上角半透明徽章）              | `index.html` / `assets/css/tools.css` | ✅   |

**验收标准**：

- CTA 按钮在 5 秒内对用户可见
- 点击 CTA 可正确跳转（本地可通过 console.log 验证跳转 URL）
- 完成填色后必定触发 CTA 强化展示

---

### 阶段 3：引导手势动画 ✅ 已完成（简化方案）

**实际方案**：以 DOM overlay 实现 `FingerHint` 类代替原计划的 WebGL GuidanceLayer，效果等同且更轻量。

| #   | 任务                     | 文件                       | 状态 |
| --- | ------------------------ | -------------------------- | ---- |
| 3.1 | 引导手型图片             | `assets/img/finger.png`    | ✅   |
| 3.2 | DOM 悬浮手指层           | `src/filler/FingerHint.ts` | ✅   |
| 3.3 | 800ms 后自动指向待填区域 | `src/filler/index.ts`      | ✅   |
| 3.4 | 用户交互后重置/停止      | `src/filler/FingerHint.ts` | ✅   |

---

### 阶段 4：横竖屏与响应式布局（P2）

**目标**：广告在主流设备（iPhone/Android 各尺寸）横竖屏均有良好视觉。

**任务拆解**：

| #   | 任务                           | 文件                   | 要点                                                         |
| --- | ------------------------------ | ---------------------- | ------------------------------------------------------------ |
| 4.1 | 底部 toolbar padding 自适应    | `src/filler/index.ts`  | `Padding` 参数根据 `window.innerWidth/Height` 动态计算       |
| 4.2 | 颜色按钮横屏时改为侧边栏       | `assets/css/tools.css` | `@media (orientation: landscape)` 布局调整                   |
| 4.3 | Canvas 尺寸 resize 处理        | `src/filler/index.ts`  | `window.addEventListener('resize')` 已有，需同步 canvas size |
| 4.4 | 安全区（iPhone 刘海/Home Bar） | `index.html`           | `env(safe-area-inset-*)` CSS 变量                            |

**验收标准**：

- Chrome DevTools 主流设备模拟器下无元素溢出或被遮挡
- 旋转屏幕后 canvas 正常重绘

---

### 阶段 5：体积与性能优化 ✅ 已完成（2026-05）

**目标**：最终产出 HTML 体积 ≤ 5 MB，60fps 无卡顿。

**任务拆解**：

| #   | 任务                  | 状态 | 结果                                                 |
| --- | --------------------- | ---- | ---------------------------------------------------- |
| 5.1 | 图片压缩              | ✅   | 当前最大图 302 KB，总资源 660 KB，无需额外压缩       |
| 5.2 | 音效压缩              | ✅   | 3 个音效合计 51 KB，已达标                           |
| 5.3 | 评估/替换 js-confetti | ✅   | 保留，js-confetti 正常 tree-shaking 引入，体积可接受 |
| 5.4 | Tree-shaking 验证     | ✅   | JS 81.7 KB，jszip 已排除，test() 已 tree-shaken      |
| 5.5 | GPU 内存估算          | ✅   | page + map 两张纹理约 4 MB GPU 显存，可接受          |

**验收结果**：

- 输出 HTML **985 KB**（gzip **641 KB**），远低于 5 MB 限制 ✅
- 无多余依赖，bundle 构成干净 ✅

---

### 阶段 6：多平台测试与提交准备（P1）← **当前阶段**

**目标**：在各平台预览工具中验证通过。

**任务拆解**：

| #   | 任务                     | 平台                        | 工具/入口                                                                   |
| --- | ------------------------ | --------------------------- | --------------------------------------------------------------------------- |
| 6.1 | Applovin 预览            | Applovin MAX                | https://p.applov.in/playablePreview?create=1                                |
| 6.2 | Unity Ads 规范自检       | Unity                       | https://docs.unity.com/zh-cn/grow/acquire/creatives/playable/specifications |
| 6.3 | Google AdMob HTML5 校验  | Google                      | https://support.google.com/google-ads/answer/9981650                        |
| 6.4 | Mintergal/Playturbo 预览 | Mintergal                   | https://www.playturbo.com/review/doc                                        |
| 6.5 | 真机测试                 | iOS Safari / Android Chrome | 重点验证音效、手势、CTA 跳转                                                |

**验收标准**：

- 各平台预览工具无报错
- CTA 点击在真机上可正常跳转 App Store

---

## 4. 关键设计决策记录

### D1：内嵌资源方案

**选择**：Vite build 时通过 `vite-plugin-singlefile` 将所有 JS/CSS inline，图片/音效在 TS 模块中以 Base64 字符串形式维护（构建前由脚本生成 `src/filler/assetData.ts`）。

**理由**：各平台明确要求单文件自包含，Vite 原生 asset inline 在 HTML 中仍产生多个文件，plugin-singlefile 是最少侵入的方案。Playturbo 会扫描本地 HTML 并拒绝 `type="module"` / `crossorigin` / ESM import-export，因此 `vite.config.js` 的 finalize plugin 会在构建后把内联 script/style 改成普通标签。

### D2：不引入 Vue/React

**选择**：UI 控制继续沿用直接 DOM 操作。

**理由**：playable 广告极度追求体积最小化，框架运行时会带来 ≥ 30 KB overhead，且当前 DOM 量极小无必要。

### D3：MRAID 适配策略

**选择**：MRAID 仅作为平台 Adapter 的底层能力，不再由业务代码直接调用。

**理由**：不同平台对 CTA 和生命周期 API 的要求不同，单纯运行时 feature detect 容易与平台审核规则冲突。

### D4：AdPlatformAdapter 平台适配架构

**选择**：保持一套填色玩法业务源码，平台差异集中在 `src/filler/ad-platform/`，通过 `vite.config.js` 根据 `--mode` 配置 Vite alias，将 `./ad-platform/current` 替换为 Applovin / Unity / Playturbo-Mintegral / Google adapter。

**理由**：Playturbo/Mintegral 要求 `window.install/gameReady/gameEnd` 且禁止自跳转，与通用 MRAID / `window.open` 策略冲突；分平台产物比单一万能 HTML 更稳。

**平台行为矩阵**：

| 平台 | Ready | End | CTA | Loading |
| --- | --- | --- | --- | --- |
| Applovin | MRAID custom close setup | no-op | `ExitApi.exit()` → `mraid.open(url)` → `window.open(url)` | 保留自定义 loading |
| Unity | `dapi.gameReady()` | no-op | `mraid.open(url)` → `window.open(url)` | 保留自定义 loading |
| Playturbo/Mintegral | `window.gameReady()` | `window.gameEnd()` | 只调用 `window.install()` | 不主动显示自定义 loading |
| Google | no-op | no-op | `window.open(url, "_blank")` | 保留自定义 loading |

**构建命令**：

```bash
npm run build           # dist/index.html
npm run build:applovin  # dist/applovin/index.html
npm run build:unity     # dist/unity/index.html
npm run build:playturbo # dist/playturbo/index.html
npm run build:mintegral # dist/mintegral/index.html
npm run build:google    # dist/google/index.html
npm run build:all       # 依次输出默认与各平台产物
```

**约束**：业务代码不得直接调用平台全局 API，只能通过 Adapter 的 `init/onResourcesLoaded/onGameStart/onGameEnd/openStore`。

---

## 5. 文件变更地图（按里程碑）

```
阶段 1（单文件打包）
  新增: src/filler/assetData.ts      ← Base64 内嵌图片/音效常量（由构建脚本生成）
  新增: scripts/embed-assets.mjs    ← 构建前预处理脚本（图片/音效→base64 TS常量）
  修改: src/filler/index.ts          ← 移除 URL 解析/zipUrl 逻辑，始终调用 loadResource()
  修改: src/filler/Audio.ts          ← 音效改用 base64 data URL
  修改: vite.config.js               ← 引入 vite-plugin-singlefile
  修改: package.json                 ← 移除 @types/jszip devDependency

阶段 2（CTA）
  新增: src/filler/cta.ts            ← CTA 跳转逻辑
  新增: src/filler/mraid.ts          ← MRAID 2.0 适配层
  修改: index.html                   ← 新增 CTA 按钮 DOM
  修改: assets/css/tools.css         ← CTA 按钮样式
  修改: src/filler/index.ts          ← cssOnFinish 后接入 CTA 展示

阶段 3（引导动画）
  新增: src/filler/GuidanceLayer.ts  ← 手势引导 Layer
  新增: assets/img/hand_hint.svg     ← 手型图标
  修改: src/filler/WorkLayer.ts      ← 首次 tap 事件通知
  修改: src/filler/index.ts          ← 启动引导动画时序

阶段 4（响应式）
  修改: assets/css/tools.css         ← 横屏 media query
  修改: src/filler/index.ts          ← 动态 Padding 计算

阶段 5（体积优化）
  修改: src/filler/explosion.ts      ← 移除 js-confetti 依赖，自研或保留
  修改: vite.config.js               ← 确认 tree-shaking
  资源: assets/res/*/page.png 压缩
  资源: assets/sound/*.mp3 压缩
```

---

## 6. 风险清单

| 风险                                            | 可能性         | 影响 | 缓解措施                                           |
| ----------------------------------------------- | -------------- | ---- | -------------------------------------------------- |
| 图片+音效内嵌后体积超 5 MB                      | 高             | P0   | 提前压缩图片至 WebP，评估 map 缩小至 512px         |
| WebGL 2 在低端 Android WebView 不支持           | 中             | P1   | 检测 `getContext('webgl2')` 失败时展示静态降级画面 |
| MRAID 版本差异导致 CTA 无效                     | 中             | P1   | 实现多版本 detect + fallback                       |
| 音效在 iOS Safari 需用户手势触发                | 高（已知问题） | P2   | 首次点击上色时解锁 AudioContext                    |
| 各平台预览工具对 inline base64 大文件的处理差异 | 低             | P1   | 提交前各平台逐一测试                               |
| Playturbo 拒绝 `type="module"` / `crossorigin` | 高（已命中）   | P1   | 构建后 finalize HTML，移除 script/style 上的 module/crossorigin 属性 |
| Playturbo 拒绝自定义 loading DOM                | 中             | P1   | Playturbo profile 不主动显示 loading；如 QA 仍拒绝，再做 HTML transform 移除 DOM |
| 平台 SDK 注入 API 差异导致分支误判              | 中             | P1   | 使用构建 profile 固定平台策略，避免依赖运行时猜测   |
| 平台生命周期重复触发                            | 中             | P2   | Adapter 内部对 ready/start/end 做幂等保护           |

---

## 7. 参考资源

- 对标竞品（最高优先）: `https://creative-ag-global.umcdn.cn/html/09/2d/44/092d441b80639f462fa667bb3f8964bf.html`（Color asis）
- Applovin 预览工具: `https://p.applov.in/playablePreview?create=1`
- Google AdMob 规范: `https://support.google.com/google-ads/answer/9981650?hl=en#_HTML`
- Unity Ads 规范: `https://docs.unity.com/zh-cn/grow/acquire/creatives/playable/specifications`
- Mintergal/Playturbo: `https://www.playturbo.com/review/doc`
- Gemini 参考分析: `https://gemini.google.com/share/8340c20dd2d1`
