# 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// → 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/applovin.html npm run build:unity # dist/unity/unity.html npm run build:playturbo # dist/playturbo/playturbo.html npm run build:mintegral # dist/mintegral/mintegral.html npm run build:google # dist/google/google.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`