# Playable Ads

本项目是一个基于 Vue3 + TypeScript + Vite 的项目，核心是基于webgl的web端填色，用于创建可播放的广告。

## 背景

我们有一个填色应用app，已经上架到google play 和 app store。 但是投放的广告基本上是静态图片或视频， 没有playable的广告， 本项目立足制作playable的广告， 用于提升广告的互动性和效果。

playable广告的核心是H5页面， 本工程已经实现了基于webgl的web端填色核心玩法，现在需要进一步完善制作成可播放的广告。

## 竞品分析

以下是几家竞品的playbale 广告， 可以作为分析参考， 尤其是页面的规范方面。

### Paint by Number:

1.

```
https://creative-ag-global.umcdn.cn/html/14/df/cf/14dfcf467412065a43ec4d2f1ad63971.html
```

主要展示了一屏十几个填色作品的填列表页，所谓互动的部分其实就是可以滑动该列表页，点击某个列表页进入一个详情页，这属于简单的内容互动展示，没有涉及游戏核心玩法

2.

```
https://creative-ag-global.umcdn.cn/html/df/3a/14/df3a14870fcb67b02292a0eac34c5ab1.html
```

可以滑动选择几个有吸引力的上色作品，点击进入填色详情页， 但同样不能进行填色操作，没有涉及游戏核心玩法，本质还是简单的内容互动展示

### Happy Color

1.

```
https://creative-ag-global.umcdn.cn/html/83/d5/81/83d5810d37e36b92ee5d90517e31b0f9.html
```

页面动效较好， 但也属于内容展示，没有体现核心玩法， 能够互动的部分就是一个手指指向的 PLAY NOW 按钮，点击即进入安装弹框

2.

```
https://creative-ag-global.umcdn.cn/html/85/8a/03/858a031936edfda1009dd6acb5c3ec2f.html
```

这个有涉及一点核心玩法，有提示手型，可点击上色。

### Color asis

```
https://creative-ag-global.umcdn.cn/html/09/2d/44/092d441b80639f462fa667bb3f8964bf.html
```

这个有涉及核心玩法， 可点击上色，结束后撒花动画，展示典型内容，显示“Over 10,000+ pictures”， 可以作为重点对标研究对象

## playable广告规范

各家广告平台对于playable的广告规范如下， 需要仔细研读理解，我们最终发布的内容要符合所有平台的规范：

1. mintergal

```
https://www.playturbo.com/review/doc
```

2. applovin

```
https://p.applov.in/playablePreview?create=1
```

3. unity

```
https://docs.unity.com/zh-cn/grow/acquire/creatives/playable/specifications
```

4. google admod

```
https://support.google.com/google-ads/answer/9981650?hl=en#_HTML
```

gemini关于playable广告的讲解：

```
https://gemini.google.com/share/8340c20dd2d1
```

## 项目目标

制作符合各广告平台的playable ads， 当前可聚焦核心填色玩法的模版， 未来可以可能开发更多模版。

## 技术栈

| 层级         | 技术                                                                 |
| ------------ | -------------------------------------------------------------------- |
| 渲染引擎     | WebGL 2（自研，无外部 GL 框架）                                      |
| 构建工具     | Vite 5 + TypeScript 5                                                |
| 打包插件     | vite-plugin-singlefile（输出单文件 HTML）                            |
| 广告平台适配 | AdPlatformAdapter（Applovin / Unity / Playturbo-Mintegral / Google） |
| 单文件产物   | 默认与各平台 profile 均输出自包含 HTML                               |

## 项目结构

```
src/
  filler/         # 广告主逻辑
    index.ts      # 入口，整合加载/CTA/平台初始化
    cta.ts        # CTA 按钮点击 + 高亮动画
    ad-platform/  # 平台 Adapter；构建时通过 Vite alias 选择目标平台
    FillerScene.ts# WebGL 填色场景
    FingerHint.ts # 指引手指 DOM overlay
    Loader.ts     # 资源加载
  base/           # 自研 WebGL 2 渲染层
assets/
  res/            # 填色资源包（config.json + 图片）
  css/            # 样式
dist/
  index.html                 # 默认产物
  applovin/index.html        # Applovin 产物
  unity/index.html           # Unity 产物
  playturbo/index.html       # Playturbo 产物
  mintegral/index.html       # Mintegral 产物
  google/index.html          # Google 产物
```

## 开发调试

### 安装依赖

```bash
npm install
```

### 启动本地开发服务器

```bash
npm run dev
```

### 映射端口到云服务器

```bash
ssh -R 0.0.0.0:5173:localhost:5173 ecs
```

然后可以在浏览器打开：

```
http://42.193.231.145:5173
```

## 构建

本项目保持一套业务源码，通过 Vite `--mode` 在构建期选择平台 Adapter，并输出平台专用单文件 HTML。默认构建使用 Google-like adapter，输出 `dist/index.html`。

```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       # 依次输出以上全部产物
```

所有产物均为单文件自包含 HTML，JS/CSS/图片/字体均内联。构建后会移除 Vite 单文件产物中的 `type="module"` 和 `crossorigin` 属性，以满足 Playturbo 对本地 HTML 文件的扫描要求。

| 指标         | 数值                 |
| ------------ | -------------------- |
| 文件大小     | ~1.02 MB             |
| Gzip 大小    | ~673 KB              |
| 平台大小限制 | 5 MB（各平台均满足） |

平台选择由 `vite.config.js` 中的 `platformBuilds` 控制：`playturbo` 与 `mintegral` 都使用 Playturbo adapter，但输出到不同目录和文件名。当前不需要 `.env.applovin` / `.env.unity` 等环境文件。

> **注意**：非 Playturbo/Mintegral 平台的 Store 链接集中在 `src/filler/ad-platform/adapters/storeUrls.ts`：
>
> - **测试阶段**：使用 PBN 落地页（当前默认），避免产生无效转化
> - **正式上线**：换回自己 app 的 Store 链接（文件内有注释说明）
>
> Playturbo/Mintegral 产物不会内置 Store URL，CTA 只调用平台要求的 `window.install()`。

## 多平台测试

### 测试前准备

1. 根据目标平台执行对应构建命令，或执行 `npm run build:all` 生成全部产物
2. 检查 `src/filler/ad-platform/adapters/storeUrls.ts` 中的落地页 URL 符合当前测试目的

### Applovin

**预览工具**：https://p.applov.in/playablePreview?create=1

1. 执行 `npm run build:applovin`，打开预览工具，上传 `dist/applovin/index.html`
2. 检查项：
   - [ ] 广告正常加载，WebGL 填色可交互
   - [ ] CTA 按钮可点击（Applovin 通过 `ExitApi.exit()` 关闭广告，落地页由平台后台配置）
   - [ ] 手指引导动画正常显示
   - [ ] 完成填色后撒花 + 结算屏正常展示

### Unity Ads

**规范文档**：https://docs.unity.com/zh-cn/grow/acquire/creatives/playable/specifications

**验证方式**：Unity 没有 Web 端预览工具，需通过 **Ad Testing App**（iOS / Android，最新版 4.0.0）扫码验证。

1. 执行 `npm run build:unity`，将 `dist/unity/index.html` 托管到可访问的 HTTPS URL
2. 在 Unity Ads Dashboard 上传素材，或在 Ad Testing App 中直接扫码加载
3. 检查项：
   - [ ] `dapi.gameReady()` 被正确调用（可在控制台确认）
   - [ ] CTA 跳转正常（Unity 使用 MRAID `mraid.open(url)` 跳转）
   - [ ] 广告尺寸符合规范（单文件 HTML，无外部请求）
   - [ ] 文件大小 ≤ 5 MB

### Google AdMob / Google Ads

**规范文档**：https://support.google.com/google-ads/answer/9981650?hl=en#_HTML

**验证工具**：https://h5validator.appspot.com/adwords/asset

Google 的官方验证器要求上传 `.zip` 而非裸 HTML。需要先将产物打包为 zip，上传时勾选 **"Select for App Campaigns"**，然后点击眼睛图标预览。

1. 执行 `npm run build:google`，将 `dist/google/index.html` 打包为 `google-ad.zip`
2. 打开 h5validator，上传 zip，勾选 "Select for App Campaigns"
3. 点击眼睛图标预览，测试交互和 CTA 点击
4. 检查项：
   - [ ] 无外部网络请求（所有资源均已内联）
   - [ ] 文件大小满足限制（≤5 MB，zip 内 ≤512 个文件）
   - [ ] CTA 可正常触发跳转（Google 平台使用 `ExitApi.exit()`）
   - [ ] 不依赖 `document.write` 或被禁用 API
   - [ ] 包含方向 meta 标签（`<meta name="ad.orientation" content="portrait">`）

### Mintergal / Playturbo

**规范文档**：https://www.playturbo.com/review/doc

**预览工具**：https://www.playturbo.com/review

1. 执行 `npm run build:playturbo` 或 `npm run build:mintegral`，按平台文档要求上传对应产物
2. 检查项：
   - [ ] 资源加载完成后调用 `window.gameReady()`
   - [ ] 结束流程调用 `window.gameEnd()`
   - [ ] CTA 只调用 `window.install()`，不主动 `window.open` 或跳 Store URL
   - [ ] 产物不包含 `type="module"`、`crossorigin`、`import`、`export`
   - [ ] 暴露 `window.gameStart` / `window.gameClose`
   - [ ] 音频在用户首次交互后解锁（iOS 限制）

### 真机测试

在真实设备上直接用浏览器打开 `dist/index.html`（可通过云服务器地址访问）：

```
http://42.193.231.145:5173   # dev 模式
# 或将 dist/index.html 放到静态服务器后访问
```

| 设备    | 浏览器  | 检查项                                         |
| ------- | ------- | ---------------------------------------------- |
| iOS     | Safari  | 音频首次交互解锁、手势填色、CTA 跳转 App Store |
| Android | Chrome  | 音频、手势、CTA 跳转 Google Play               |
| Android | WebView | 广告平台内嵌 WebView 行为是否一致              |

## 常见问题

**Q: 填色区域点击没反应？**
A: 检查 WebGL context 是否初始化成功，Console 有无报错。

**Q: 音效不播放（iOS）？**
A: iOS 要求用户交互后才能播放音频，代码已有 unlock 逻辑，确认首次点击后音效正常即可。

**Q: CTA 点击没有跳转？**
A: 各平台跳转机制不同：Applovin 优先用 `ExitApi.exit()`；Unity/通用 MRAID 环境用 `mraid.open(url)`；Google-like 环境用 `window.open(url)`；Playturbo/Mintegral 只允许调用 `window.install()`，不应主动跳 Store URL。

**Q: 构建产物超过平台大小限制？**
A: 检查 `assets/res/` 下的图片资源，压缩大尺寸图片后重新构建。

# 二期规划

在当前一期 playable 广告的基础上，构建一个 **广告制作管理平台**（Playable Ads Platform），供广告运营团队（5-10人）通过浏览器完成"上传素材 → 配置参数 → 选择平台 → 一键生成广告文件"的完整流程，无需接触源代码和命令行。

> 详细技术方案见 [二期细化方案](./docs/phase2-plan.md)

## 核心概念

| 概念 | 说明 |
|------|------|
| **模板（Template）** | 可复用的广告框架。当前有 `coloring`（填色玩法），未来可扩展更多 |
| **创意（Creative）** | 一个模板的实例 = 运营选择的模板 + 上传的素材 + 主题配置 + 构建产物 |
| **素材包** | 运营上传的 `.zip` 文件，包含 `config.json`（必）、`page.png`（必）、`map.png`（必）、`special.jpeg`（可选） |
| **构建产物** | 各广告平台的单文件 HTML，以及全部产物的 ZIP 包 |

## 技术架构

```
React SPA (浏览器)
    │
    ▼
Express API (:3001)
    ├── REST API (/api/v1/*)
    ├── Build Service (调用 Vite 程序化构建)
    ├── SQLite (creatives, builds, templates)
    └── storage/creatives/<id>/  (素材 + 产物)
```

- **前端**：React + Vite + TypeScript
- **后端**：Node.js / Express + TypeScript
- **数据库**：SQLite (better-sqlite3)
- **构建**：调用现有 Vite 项目，通过 `#ad-config` alias 注入用户素材和主题
- **部署**：单体部署到 ECS，Express 同时服务 API + 静态文件

## 项目结构

```
playableads-platform/
├── templates/                  # 模板根目录（扩展点：新模板 = 新子目录）
│   └── coloring/               #   填色玩法模板（当前一期代码）
│       ├── manifest.json       #     模板元信息（驱动前后端 UI 和校验）
│       ├── src/filler/         #     业务逻辑
│       │   ├── index.ts        #       入口（从 #ad-config 导入素材+主题）
│       │   ├── ad-config.ts    #       默认配置（dev / CLI 构建）
│       │   └── _ad_config_.ts  #       gitignored，平台构建时动态生成
│       └── assets/user/        #       gitignored，平台构建时 symlink → storage
│
├── platform/                   # 二期平台
│   ├── server/                 #   Express API + Build Service
│   └── client/                 #   React SPA
│
└── storage/                    # gitignored，运行时数据
    ├── data.db                 #   SQLite
    └── creatives/<uuid>/
        ├── assets/             #   用户上传的 zip 解压产物
        └── builds/<uuid>/      #   构建输出
```

## 运营操作流程

```
新建创意 → 选择模板（填色玩法）
    │
    ▼
上传素材 zip（config.json + page.png + map.png ± special.jpeg）
    │
    ▼
配置主题（背景渐变、CTA 文案/颜色、进度条颜色…）
    │
    ▼
勾选目标平台（Google / Applovin / Unity / Playturbo / Mintegral）
    │
    ▼
点击构建 → 等待完成 → 下载单平台 HTML 或全部产物 ZIP
```

## 模板参数化

核心思路：**配置集中化 + Vite alias 切换**。

- 模板 `index.ts` 从 `#ad-config` 单入口导入所有素材和主题
- 日常 `npm run dev` → alias 指向 `ad-config.ts`（默认素材）
- 平台构建 → 环境变量 `AD_CONFIG_PATH=_ad_config_.ts`，alias 指向动态生成的配置
- 每个模板通过 `manifest.json` 声明需要哪些素材、哪些主题参数，前端据此渲染 UI，后端据此校验

## 实施阶段

| 阶段 | 内容 | 预计 |
|------|------|------|
| 1. 基础搭建 | 目录重构、Express/React 脚手架、DB 初始化 | 2-3天 |
| 2. 模板参数化 | index.ts 重构、#ad-config alias、dev/CLI 行为不变 | 1.5-2天 |
| 3. 后端 API | 素材存取、构建服务、全部 API 端点 | 2-3天 |
| 4. 前端 UI | Dashboard、CreativeDetail、构建交互 | 2-3天 |
| 5. 部署 | Docker 化、ECS 部署、端到端测试 | 1-2天 |
