# av.film

用代码写视频脚本：声明场景和元素（文字、清单、数字、图片、代码、对话、外部画面、粒子、自绘）和声音（配音、旁白、录音），`render` 一次生成带音效和配乐的 MP4，并在旁边输出联系表。零依赖：gdip 画帧、`av.audio` 合成声音、系统自带 Media Foundation 编码。

- 全局时钟：脚本里的数字就是成片秒数（报告、联系表、节拍都用同一个数）。
- `at` 是**命中时刻**：slam 是落地那一帧（提前 0.3 秒起跳），其余入场是开始出现。音效和节拍都对齐它；入场/退场进度按帧的中点取样，所以命中帧就能看到入场的开头（与音效同帧）。
- 内置动效是时间的纯函数：任意帧可单独渲染，同样的脚本、素材与种子得到相同画面。自绘/参数回调也应只依赖 t 与固定种子，不依赖累计状态、当前时间或无种子的随机数。
- 结构是数据，所以能自动配音效、自动布局、出报告和 lint；特殊画面用 `draw(function(g,t,e){})` 写原生 gdip（见「自绘」），淡入淡出和入场动作仍由引擎负责。
- 分工：每支片都会出现的内容（文字、清单、截图卡片、代码、对话、外部画面）用声明元素，一行一个；这支片独有的画面（招牌镜头）用自绘。两者可以叠加：自绘里用 `e.box(元素)` 拿到声明元素的位置。

生成视频是耗时任务，请放在后台线程运行。工具层见 [av.video](../video/index.html)，声音见 [av.audio](../audio/index.html)。

## 快速开始

```aardio
import av.film;
var film = av.film(1920,1080,{duration=8;theme="midnight";bpm=120;music="tech"});

//① 品牌名在 1 秒砸下（自动 impact + 镜头震动），副标题逐字浮现，两行自动垂直居中
var s1 = film.scene(0,3);
var brand = s1.text("aardio",{at=1;style="hero";enter="slam"});
film.sfx(brand.at,"riser",{dur=1}); //蓄力音效的结束处对齐落地
s1.text("小巧、快速、原生",{at=1.5;style="subtitle";split="char"});

//② 标题在上，清单逐条滑入；**强调** 用强调色
var s2 = film.scene(3,6,{transition="wipe"});
s2.text("一个语言，**桌面全能**",{at=3.3;style="title";pos="top"});
s2.list({"窗口界面";"网页界面";"自动化"},{at=3.8;stagger=0.5;icon="check"});

//③ 数字滚动（tick… + ding），结尾粒子
var s3 = film.scene(6,8,{transition="flash"});
var n = s3.counter(0,6.5,{at=6.2;dur=1;format="%.1f MB"});
s3.burst("sparkle",{on=n});

var report = film.render("/film-demo.mp4");
return report; //报告：路径、联系表、码率、响度、耗时、lint
```

渲染结束后 `film-demo.sheet.png` 是 16 格联系表（带时间标签）。迭代时先看 `film.report()` 和 `film.lint()`，再用 `film.frame(t,路径)` 看关键帧，最后才整片渲染。

## 场景与转场

`film.scene(start,end,{transition;bg;transitionSfx})` 返回场景对象，元素加在场景上。转场以场景起点为中心；转场期间上一场景的元素保持最后姿态，随转场离开。`transitionSfx=false` 只关闭这一场景的转场音效，不影响元素音效。

| transition | 时长 | 效果 | 自动音效 |
|---|---|---|---|
| cut（默认） | 0 | 硬切 | 无 |
| fade | 0.4 | 交叉淡化 | 无 |
| flash | 0.3 | 白闪，峰值处切换 | impact |
| wipe | 0.45 | 从左到右擦除，边缘带强调色光条 | whoosh |
| push | 0.45 | 新场景把旧场景推出 | whoosh |
| zoom | 0.5 | 旧场景放大淡出、新场景缩放淡入 | whoosh |
| glitch | 0.3 | 撕裂：旧画面按细条逐条换成新画面，少量条带错位（约 15Hz 换一次状态），不做色散 | glitch |

`bg`：`"dark"`（默认）、`"accent"`（强调色背景，文字自动用反色）、颜色（`0xRRGGBB` 或 `"#RRGGBB"`）、渐变表（字段同 `av.video.kernel.background`）、图片路径（cover 铺满）或 `{image=路径;grain=3}`。主题的渐变背景默认加静态粗颗粒 ±3（消除 8 位视频的圈状色带），`av.film(w,h,{grain=false})` 可关闭。图片背景默认不加颗粒（真实照片自带噪声）；**AI 生成的深色、平滑背景图要写 `grain=3`**，否则编码后容易出现色带。

**全片层**：直接加在 `film` 上的元素跨场景、不随转场移动（进度条、水印），颜色跟随当前场景的背景（强调色背景上自动变深）。

## 声音策略

`av.film(width,height,{audio={...}})` 把“连续配乐、分段配乐、只用音效、少量手动音效”写成显式策略，不要修改 `av.film.transitions` 共享表来绕过声音。常用字段：

- `musicMode="sectioned"`（默认）或 `"continuous"`；连续模式传给 `av.audio.music` 的 `mode="continuous"`，短页面蒙太奇不按每个场景替换 groove。
- `sfxMode="auto"`（默认）、`"manual"`（只保留 `film.sfx` 与元素显式 `sfx`）或 `"none"`。
- `autoAccents=false` 关闭配乐按段落自动加重音；`musicEnergy` 固定连续床层级；`musicGain`、`musicFadeOut` 控制配乐总线。
- `duck=false` 关闭音乐对音效/配音的自动闪避；也可传 `{amount=...;voiceAmount=...}`。`beatOffset` 同时用于音乐拍点、film lint 与报告。
- `scene(...,{transitionSfx=false})` 只关单个转场音效。

渲染报告的 `audio.filmEvents` 是影片层音效事件，`audio.premixEvents` 是加入配乐/配音后的预混事件；`audio.eventsBySource`、`audio.buses` 与 `audio.bars` 用于声音预检。响度和峰值仍是编码前 PCM 指标。

## 元素

| 方法 | 说明 |
|---|---|
| `text(内容,opt)` | 内容可为字符串或 `function(t)`（计时器等）；`**强调**` → 强调色；`'\n'` 分行（双引号里字面的 `\n` 也按换行处理，lint 记 info）；`split="char"\|"word"\|"line"` + `stagger` 逐字/逐词/逐行入场（word 按系统分词，标点并入前一个词；文本里的 `\|` 是手动分词点）。超宽：hero/title 自动缩小，其余自动换行 |
| `list(条目,opt)` | `icon`（图标名，如 check、circle-check、star、rocket，见 `av.video.text.icons`）、`stagger`（默认 0.3；给数组 = 每项自己的偏移秒）；块内左对齐、整块居中。`row=true` 横排；`step=true` 各项同时出现（暗色），按 stagger 依次点亮（强调色 + 轻弹 + blip），项间画箭头；`border`/`bg`（true = 主题色，或给颜色）让每项成为一颗圆角小卡（标签、步骤），`radius` 改圆角 |
| `counter(from,to,opt)` | `dur`（滚动秒数，默认 1）、`format`（printf，含前后缀）；数字向左长、后缀不动。默认 hero 样式（粗体、光晕、微呼吸）；界面里的小号计数写 `style="body"`，配 `x` + `align="right"` 右对齐 |
| `image(路径,opt)` | `width`/`height`（只给一个则等比）；`radius` 圆角（投影随之变圆）；占画面 40% 以上且没有运动属性时默认 Ken Burns 慢推；非全屏图片带柔和投影（`shadow=false` 关闭；源图四角透明时默认不投影）；版面放不下时自动等比缩小。写了卡片词（`title`/`caption`/`bg`/`border`/`zoom`）就是卡片，见「卡片」 |
| `code(源码,opt)` | 代码面板（卡片）：自动语法着色、逐字打字、`focus` 高亮行、`edits` 局部替换、行号；返回表的 `.done` 是打完的成片秒。见「代码、对话、外部画面」 |
| `chat(内容,opt)` | 字符串 = 提示词框（打字、光标、发送键）；数组 `{{说话人;消息},...}` = 对话卡（逐字，行间分隔线）；返回 `.done` |
| `clip(来源,opt)` | 外部画面：PNG 序列 `"/res/seq/frame-*.png"` 或视频文件；`from`（素材起点秒）、`speed`（0 = 定格取帧）。只接入、不采集，静音 |
| `burst("sparkle"\|"confetti",opt)` | `on=元素`：取目标的排版位置，在其视觉峰值爆发；也可给 `at`、静态 `x`、`y`。星光默认画在同层目标后面，`behind=false` 保留前景效果。大小跟随目标，冲击环与星星避让邻近静态元素；动态/自绘元素不保证避让。`radius=像素` 自定大小且不避让。彩纸不缩放、不改层序 |
| `draw(function(g,t,e){},opt)` | 原生 gdip 自绘，按不透明画：淡入淡出、入场/退场与 `opacity`/`scale`/`rotate` 由引擎作用在整个元素上。给 `width`/`height` 是方框元素（`g` 原点在框左上角，参与自动排版，默认 fade），不给是整屏画布。`e` 提供画刷、文字、图片等工具，见「自绘」 |

通用选项：

- `at` 命中时刻（默认场景起点）；`out` 完全消失的时刻（默认场景结束，随转场离开；早于场景结束则播退场，退场结束于 out）。
- `style`：hero / title / subtitle / body / caption（字号按 1080p 设计，按短边自动缩放）。`enter` 见下表，默认由样式决定。内联 `size`/`bold`/`font` 可直接写在 text、list、counter、code、chat 上，盖过样式（`size` 和 `x`/`y`/`width` 一样是画面像素，不再缩放）；不写 style 的文字用 body（主前景色）。
- 位置：`pos` = top / bottom / center / left / right（可组合如 "top-left"）；`x`,`y` 是锚点（中心，`align="left"` 时 x 是左边）。自动排版分三个区域：`pos` 含 top 的贴安全区顶、含 bottom 的贴安全区底，其余没给 `y` 的在中间（在同时出现的上下沿元素之间居中）。**同一区域里时间重叠且水平相交的元素按声明顺序从上到下堆叠**，不会叠在一起；时间不重叠的各自占同一位置（依次替换的文字）。全片层的上下沿元素（水印、进度条）会让场景内容避开它们。给了 `y` 的元素不参与排版。
- 动画值可为数值、关键帧 `{{t,v[,ease]}}`（元素本地秒，缓动写在段末点）或 `function(t)`。`opacity` 用于所有类型；`x/y` 动画用于 text/list/counter/image/方框 draw；`scale` 用于 text/counter/image/draw；`rotate`（度）用于 text/image/draw。burst 是定点爆发；不要给不支持的类型套用通用变换参数。
- `sfx`：false 关闭自动音效；音效名或 `{名字;参数}` 覆盖；列表 `{{秒,名字,参数...},...}` 按相对 `at` 的秒数放多个音效（写了列表就只有列表里的，不再加自动音效），例如 `sfx={{0.4,"pop"},{1.2,"success",gain=-8}}`。`color` 覆盖文字颜色。
- 元素返回普通表，可读 `.at`（code/chat 还有 `.done`）；共享时刻用 aardio 变量（`var drop = 14`）或元素引用（`brand.at`、`code.done`、`on=logo`）。直接修改元素/场景字段后调用 `film.invalidate()` 使准备缓存失效；追加元素、切换草稿会自动重建。通常修改脚本后重新运行即可。
- 每种元素有一份选项白名单：写错的选项（`colour=`、`duration=`、给 image 写 `size=`）lint 报 warn 并给出近似名，不再静默忽略。

## 入场预设

| enter | 时长 | 命中时刻 at | 动作 | 自动音效 |
|---|---|---|---|---|
| fade | 0.5 | 开始出现 | 淡入 | 无 |
| rise | 0.6 | 开始出现 | 淡入 + 上浮 + 轻微放大 | swish（轻） |
| slide | 0.5 | 开始出现 | 从左滑入（清单默认） | swish（轻） |
| pop | 0.5 | 开始出现 | 弹出并过冲（hero 默认） | pop |
| punch | 0.35 | 完全可见的那一帧 | 重音在开头：放大的字迅速收回（快剪用） | impact（轻） |
| slam | 0.3 起跳 + 0.3 收尾 | 落地 | 从大加速砸下 + 落地挤压 + 镜头震动 | impact |
| type | 每字 0.045 | 第一个字 | 打字 + 光标 | typing |
| none | 0 | 出现 | 无动画 | 无 |

手艺默认值已写进预设：缓动永不线性；入场同时动 2~3 个属性；退场比入场快；逐字时单字入场缩短到 0.3 秒、间隔 0.04 秒；hero 入场后微呼吸；快剪场景用 punch（slam 前面不够 0.3 秒起跳时自动改为 punch，lint 记 info）。同名音效 60ms 内去重，次要样式（subtitle/body/caption）的音效降 6dB。

## 卡片（image / clip / 方框 draw / code / chat）

```aardio
import av.film;
var film = av.film(1920,1080,{duration=6});
var s = film.scene(0,6);
//真实截图套上窗口外框（标题栏）和说明文字；卡片不动，卡内画面在 0.8~2.6 秒推近到底部
s.image("/res/aa-main.png",{at=0.3;width=1180;title="AA";caption="真实界面";
	zoom={{0.8;1;0.5;0.5};{2.6;1.8;0.55;0.9;"easeInOutCubic"}}});
return film.lint();
```

- `title="…"`：卡片**内部**顶端的窗口标题栏（三个小圆点 + 居中标题 + 细分隔线），画面放在标题栏下面；有 title 没写 bg 时自动 `bg=true`。
- `caption="…"`：卡片**外面**下方左对齐的小字（caption 样式、场景调色板），参与自动排版的占位。
- `bg`：`true` = 主题卡面色（`colors.panel`），或给颜色；`border`：`true` = 主题细线色（`colors.line`），或给颜色。`radius` 圆角（画框、裁剪与投影同步，默认 18）；`shadow` 柔和投影（只画在卡外，半透明卡面下不透出）。code/chat 默认有卡面、边框和投影；方框 draw 只在有 bg/title 时投影。
- `width`/`height` 是**整张卡**（含标题栏）；只给一个时内容区跟着画面比例，两个都给时画面等比放进（contain），空出来的地方露出 bg。显式 `y` 是**卡片中心**（说明文字在卡片下面另占位置）。
- `zoom`：卡内画面推近，卡片不动。数值 = 静态倍数；关键帧 `{{本地秒;倍数;中心x;中心y[;缓动]},...}`，中心是画面上的 0~1 坐标。写了 zoom 就不再做默认的 Ken Burns。整张卡变大用 `scale`。
- 卡内用主题原色（卡面是深色面板），说明文字跟随场景调色板。主题颜色 `panel`、`line`、`bar`（标题栏底）、`shadow`、`code={keyword;string;comment;number}` 可在 `theme.colors` 覆盖（值可为主题色名）。

## 代码、对话、外部画面

```aardio
import av.film;
var film = av.film(1920,1080,{duration=10});
var s = film.scene(0,10);
//提示词框：卡片出现后自动开始打字，打完发送键由暗转亮（typing + click 音效自动）
var ask = s.chat("把这周的销售表整理成图表",{at=0.3;width=1100;y=200});
//对话卡：说话人一列，消息逐字；上一句发完下一句才开始
var talk = s.chat({{"你";"按地区汇总"};{"AA";"好。读表、汇总、**从高到低**排。"}},{at=ask.done;width=1300;speed=16;y=470});
//代码面板：打完高亮第 2 行，再在本地 3.5 秒把升序改成降序（选框 → 旧片段上浮、新片段回弹，click 音效）
var code = s.code('var sheet = excel.Open("/周报.xlsx").Sheets(1);\nsheet.Range("A2:C13").Sort(sheet.Range("C2"), 1/*升序*/);',
	{at=talk.done;title="AA 正在写代码";speed=60;focus=2;edits={{3.5;"1/*升序*/";"2/*降序*/"}};y=800});
film.sfx(code.done,"success"); //.done 是成片秒，声明时就能算
return film.lint();
```

**code**
- 源码：多行字符串或字符串数组（一行一项）。自动着色：关键字（aardio 关键字表）、字符串、注释、数字；别的语言照样得到字符串/注释/数字着色。字体默认 Consolas，字号默认 34。
- `speed` 字符/秒（默认 50），`0` = 直接出现。行间自动停 0.1 秒，缩进不计打字时间。打字起点自动：卡片出现后稍停（无入场 0.2 秒，有入场约为入场时长的 0.6）。
- `focus=行号或数组`：打完后高亮这些行、其余行变暗（speed=0 时开场就高亮）。
- `lineNumbers`：默认不显示；`true` = 1..n；数组 = 每行显示的号码（只摘几行关键行时写原文件的行号，如 `{1;3;17;25}`，也可以是字符串）。
- `edits={{本地秒;"旧片段";"新片段"},...}`：片段必须在当前版本的源码里**唯一**，否则 lint 报 error（不猜）。多个 edits 按时间顺序作用在改过的源码上。
- 放不下：行数多于面板时跟着打字行往上滚（不缩字）；太宽的行右端淡出（lint warn：要完整显示就加宽卡片或减字号）。
- 自动音效：一条 typing 覆盖打字窗口，每个 edit 一声 click。

**chat**
- `speed` 字/秒（默认 12，中文节奏；英文、代码写大一些）；对话卡行与行之间停 0.25 秒。消息支持 `**强调**`；说话人第 1 位暗色、其余强调色（`color` 覆盖其余说话人的颜色）。
- 内容超高时裁剪并 lint 提示，不自动缩字。自动音效：每行一条 typing，每打完一行一声 click（发送/回车）。

**clip**
- 来源：PNG 序列（`前缀-*.png`，按文件名里的数字排序，按影片帧率播放）或视频文件（`sys.mediaFoundation.reader`，承诺 MP4/H.264；外来录屏先单独测，读不出 lint 报 error）。
- 素材时间 = `from + (t - at) × speed`：`speed` 默认 1，`0` = 定格取 from 那一帧（缩略图）；`from` 为负 = 先定在第一帧；播完定格在最后一帧（lint info）。同一文件的多个 clip 共用一个读取器；顺序播放不 seek（约 3ms/帧），往回或大跳才 seek（约 0.1 秒）。
- 外观与排版同 image（width/height/卡片词/zoom/pos/x/y/enter/out/opacity/scale）。原声不接（静音）。闪烁检测跳过该区域（素材自己的闪动不算）。

## 声音（配音、旁白、录音）

`s.audio(来源,opt)` 放一段外部声音。来源可以是音频文件路径（MP3、WAV、M4A 等，任意采样率）、`av.audio.clip`，或带 `path` 字段的表（可带 `text`：这段声音说的话）。av.film 只认音频，不管它是怎么来的：TTS 生成的、真人录的、别人给的 mp3 都一样。织影技能包的配音助手 `autos.skills.codingFilm.tts` 返回的就是这种表。

```aardio
import av.film;
var film = av.film(1920,1080,{duration=10;music="tech"});
var s = film.scene(0,10);
var q = s.audio({path="/res/voice/q.mp3";text="只用一句话，能做视频吗？"},{at=0.8});
s.chat(q.text,{at=q.at - 0.3;width=1000;speed=0});          //台词进对话卡
var a = s.audio("/res/voice/a.mp3",{at=q.done + 0.4;text="能。一句代码，一支短片。"});
s.text(a.text,{at=a.at;style="title"});
s.text("织影",{at=a.done + 0.6;style="hero";enter="slam"});  //说完再砸品牌名
```

- 声明时就解码。返回表里 `.at` 是开口时刻，`.done` 是说完时刻（都是成片秒，首尾静音已去掉），`.duration` 是说话时长，`.text` 是台词。下一句写 `at=v.done+0.3`；画面文字写 `s.text(v.text,{at=v.at})`，或者用 `on=v` 跟着声音出现。台词应当是画面的一部分（标题、对话卡、要点），不另做字幕条。
- 响度自动对齐：每段归一化到 -16 LUFS（不同音色、不同来源的原始响度能差 5dB 以上），`gain` 再微调（dB）。
- 配音期间配乐自动压低约 8dB：开口前 0.15 秒压下去，说完保持 0.25 秒，再用 0.6 秒回来；两段间隔不到 1 秒时中间不回升。音效不受影响。
- 对准某个词：`v.word("代码"[,n])` 是说到这个词（第 n 次出现）时的成片秒，直接写进 `at`：`s.code(code,{at=a.word("代码")})`。来源表或选项带 `words`（`{{text;at;dur},...}`，at 是相对开口的秒数；织影的配音助手自动带上）时精确到词；没有词时间时按声音里的停顿与字数估算（实测与真实词时间相差 0.06 秒以内），lint 记 info。台词里找不到的词直接报错。`v.words` 是来源给的词时间。
- 选项：`at`（默认场景起点）、`on`、`gain`、`text`、`words`、`name`。`film.audio` 放在全片层（用法相同，比如一整条旁白）。
- lint：文件找不到、读不出或整段静音报 error（拒绝渲染）；两段重叠、超出影片结尾报 warn；越过所在场景的结尾报 info（旁白跨镜头是正常用法）。时间线报告里每段声音列出开口→说完和台词，渲染报告的 `audio.voices` 是段数。
- 不做：口型、逐词出字的现成元素（需要时在自绘里拿 `e.time` 对照 `v.word(...)`）、视频原声（clip 静音）。

## 自绘（draw）

内置元素画不了的画面（示意图、游戏画面、特殊动效）写成 `draw(function(g,t,e){},opt)`：`g` 是原生 gdip 画板，坐标是设计坐标（`av.film(宽,高)`，草稿半分辨率也不用改）。

- **按不透明画**：元素的淡入淡出、入场/退场动作和 `opacity`、`scale`、`rotate` 由引擎作用在整个元素上；透明度小于 1 时先画到透明图层再整体合成，重叠部分不会互相透出。局部的渐显用 `e.progress` 自己控制。
- **两种画布**：给了 `width`、`height` 是方框元素——`g` 的原点在框左上角，和图片一样参与自动排版（`pos`，或用 `x`/`y` 指定中心），默认 fade 入场，`on=` 粒子也能跟随它；不给是整屏画布（原点在画面左上角，默认不做入场，适合整场的画面）。
- **方框放进卡片**：方框 draw 也能写卡片词（`title`/`caption`/`bg`/`border`/`radius`/`shadow`）。有 `title` 时 `g` 的原点和 `e.width`/`e.height` 是标题栏下面的内容区，画面按卡片圆角裁剪；卡面、标题栏、投影不用自己画。同一个画面放进两种尺寸的卡片，用两个小包装函数各自 translate/scale 取景即可。
- `t` 是元素本地秒（`at` 时为 0）。帧是时间的纯函数：只依赖 `t` 和固定种子（`e.seed`、`av.video.random`），不要累积状态。
- 画面里的事件（撞击、点击、打字）用 `sfx` 列表配音：把时刻写成一张表（如 `beats = {hit = 1.2}`），画面和音效共用这一份。
- **对准声明元素**：`e.box(元素)` 返回该元素在当前自绘坐标系里的矩形 `{x;y;width;height}`；`e.box(code元素,"片段")` 返回片段字形的矩形（先找当前版本，再找原文）。取排版位置与脚本里的 x/y 动画，不含该元素自己的入场动作（要贴着它一起出现，就让它用 fade 入场）；调用者自己的入场/缩放会被换算掉。
- **裁剪**：`g.setClipRect(x,y,宽,高[,模式])`、`g.setClipPath(路径[,模式])`，模式 0 替换（默认）、1 交集、2 并集、3 异或、4 排除（只画在路径外，例如投影只画在卡片外）、5 补集。先 `var s = g.save()`，画完 `g.restore(s)`。`e.image` 在轴对齐时走像素内核，只按裁剪区的外接矩形裁；要裁成圆形等路径时用 `g.drawImage`。
- 引擎的卡片、代码面板、对话、步骤清单也是用同样的 gdip 与 `av.video` 工具画的：`~/lib/av/film/_.aardio` 里的 `paintCard`、`paintCode`、`paintChat`、`drawList` 等函数可以当自绘范例读。

`e` 的成员（颜色都可写主题色名 fg/soft/dim/accent/emph、0xAARRGGBB 数值或 "#RRGGBB"）：

| 成员 | 说明 |
|---|---|
| `brush(颜色[,不透明度])`、`pen(颜色[,宽度[,不透明度]])` | 画刷、画笔，回调结束自动释放（自己创建的其他 gdip 对象仍要 delete） |
| `text(文字,x,y[,样式名或选项])` | 主题字体与字号的路径文字（运动不抖，排版自动缓存）。x 是锚点（默认居中，`align="left"` 时是左边），y 是垂直中心。选项：`style`、`size`、`color`、`emph`（强调色）、`font`、`bold`、`align`、`alpha`、`maxWidth`、`lineHeight`、`reveal`（0~1 逐字显示）、`cursor`（光标）。返回排版（`width`、`height`） |
| `image(路径,x,y[,宽[,高[,选项]]])` | x,y 是左上角；只给宽或高时按比例；选项 `opacity`、`radius`（圆角）。缓存；轴对齐时用像素内核重采样（运动不抖），遵守 `setClipRect` 矩形裁剪 |
| `glow(x,y,半径[,颜色[,不透明度]])` | 柔和光晕（halo） |
| `progress(a,b[,缓动])` | 本地时间在 [a,b] 内的进度 0~1，可选缓动名（如 `"easeOutCubic"`） |
| `color(颜色[,不透明度])` | 转成 ARGB（给渐变画刷等用） |
| `box(元素[,片段])` | 声明元素（code 可给片段）在当前坐标系里的矩形 `{x;y;width;height}` |
| `t`、`time`、`width`、`height`、`theme`、`seed` | 本地秒、全局秒、框（或内容区、画面）大小、当前背景的调色板、固定种子 |

```aardio
import av.film;
var film = av.film(1920,1080,{duration=5});
var s = film.scene(0,5);
//声明元素负责内容：代码卡片（fade 入场，自绘才能贴着它），本地 2.2 秒把 12 改成 24（click 音效自动）
var beats = {edit = 2.2}; //一份时刻：代码、自绘、音效共用
var code = s.code('var radius = 12;\ng.fillCircle(brush, x, y, radius);',
	{at=0.3;width=1100;size=48;speed=0;title="局部修改";enter="fade";edits={{beats.edit;"12";"24"}}});

//自绘负责这支片独有的画面：一道光环在命中前收拢到被改的数字上（辉光笔：宽的半透明笔垫在细线下面）
s.draw(function(g,t,e){
	var b = e.box(code,"12"); //片段在当前坐标系里的矩形；改完后按原文位置找
	var p = e.progress(beats.edit - 0.7,beats.edit,"easeInCubic");
	if(p <= 0 || p >= 1) return;
	var r = 230 - (230 - b.width) * p;
	var cx,cy = b.x + b.width / 2,b.y + b.height / 2;
	g.drawEllipse(e.pen("accent",14,0.16),cx - r,cy - r,r * 2,r * 2);
	g.drawEllipse(e.pen("accent",2.5),cx - r,cy - r,r * 2,r * 2);
	e.glow(cx,cy,r * 0.8,"accent",0.2 * p);
},{at=0.3;sfx={{beats.edit - 0.7,"swish",dur=0.7}}});

film.frame(2.2,"/check/draw.png");
return film.lint();
```

## 主题

`theme`：`"midnight"`（深蓝 + 青，默认）、`"punch"`（黑 + 黄，快剪）、`"light"`（浅色 + 蓝），或在内置主题上覆盖：

```aardio
import av.film;
var film = av.film(1920,1080,{duration=6;theme={
	base = "midnight";
	colors = {accent = 0xFF64F4DC}; //按键覆盖（颜色为 0xAARRGGBB）
	styles = {hero = {size = 126;enter = "punch"};side = {size = 108;bold = true;enter = "rise"}} //逐个样式合并字段；可新增样式
}});
return film.theme.styles.hero.bold; //未覆盖的字段沿用 midnight
```

覆盖作用在副本上，不会改动 `av.film.themes`。`bg` 也按键覆盖（`dark`、`accent` 是背景规格表）。不写 `base` 时是完整主题表（结构同 `av.film.themes.midnight`：colors、bg、font、styles）。字体默认微软雅黑 UI。

配乐 `music`：风格名（`"tech"`、`"upbeat"`、`"ambient"`、`"lofi"`，由 [av.audio.music](../audio/index.html) 现场合成）、`av.audio.clip` 或音频文件路径（MP3、M4A、WAV 等，任意采样率，自动重采样；只取用得到的长度，响度自动对齐内置配乐，比影片短时 lint 提示）。音乐总线在音效处自动闪避，整体响度归一到 -16 LUFS。

用风格名时，配乐按时间线编排：
- 段落强度：快剪（≤1 秒的场景）、数字滚动、粒子为 3；只有图片的场景为 1（让画面喘口气）；其余为 2。
- 片头：有 slam/punch 命中时，命中前是前奏；没有命中也不是快剪起手时，先铺一小节前奏。
- 重音：slam 落地、punch 命中和白闪转场处加镲；强度升两级的段落前，鼓和贝斯停一拍。
- 结尾：最后一个场景的命中处（或起点）落主和弦，一直响到片尾。
- 速度用影片的 `bpm`，没给就用风格默认值。

实际的段落强度和终止和弦时刻会写进报告 lint 的 info 行。

## 检查与验收

```aardio
import av.film;
var film = av.film(1280,720,{duration=4});
var s = film.scene(0,4);
s.text("标题",{at=0.5;style="title"});
s.text("这是一行说明文字",{at=0.8;style="body"});

//渲染任意单帧（给路径则保存 PNG，否则返回 gdip.bitmap）
film.frame(1.5,"/check/f1.5.png");

//联系表：默认均匀 16 格，也可指定时刻
film.sheet("/check/sheet.png",{times={0.5;0.7;1.0;2.0}});

return {
	timeline = film.report(); //场景、元素（at→out、入场、样式）、音效列表
	lint = film.lint(); //{level;t;msg} 数组
}
```

lint 检查：场景重叠、转场不在拍上（指定 bpm 时）、at 不在场景内、零时长元素、文字超宽缩小、图片放不下缩小、找不到图片或背景图、配乐来源（找不到或读不出配乐文件、风格名写错，error 级；配乐文件比影片短，warn）、阅读时间不足（汉字 1、拉丁词 2 个单位，>8/秒）、音效过密（0.5 秒内 >6 个，数字滚动的 tick 不计）、整屏自绘给了 x/y、未知选项（warn，附近似名）、code 的行放不下（warn，右端淡出）、提示词/对话放不下、edits 片段不唯一或找不到（error）、找不到序列帧或视频（error）；自动处理的也记一笔 info：slam 改为 punch、字面 `\n` 按换行、clip 播完定格。

`film.render(路径,opt)` 返回报告：`path`、`sheet`、`frames`、`duration`（按帧数量化后的秒数）、`fileSize`、`mbps`、`seconds`（总耗时）、`timings`（prepare/audio/draw/write/flicker 毫秒）、`audio`（lufs、truePeak、events）、`flicker`（闪烁检测，见下）、`lint`。opt：`draft=true` 半分辨率草稿；`quality`（同 `sys.mediaFoundation.writer`）；`sheet=false` 不出联系表；`flicker=false` 不做闪烁检测。

有 error 级素材/配乐问题时停止渲染。视频帧、音频与 MP4 收尾错误会向上传播；先写同目录的 `.rendering.mp4`，全部编码成功后才替换目标文件，失败保留旧 MP4 与诊断文件。

**报告边界**：audio 测量编码前 PCM（有声混音目标 -16 LUFS、真峰值上限 -1.5 dBTP；静音标记 -200）；AAC 编码后峰值可能升高。flicker 检查源帧，不是最终 MP4 的全面回读验收；不自动评判文案、审美或所有重叠。联系表、连续帧与用户看片反馈仍各有作用。

视觉模型看联系表只作补充：颜色、角度、对齐这类问题先用 `film.frame(t)` 返回的位图 `getPixel` 确定性地检查。

### 单帧闪烁检测

`render` 逐帧检查“单帧闪现”：某一帧在局部突然比前后两帧都强（或都弱），下一帧又退回去，例如入场首帧误用了完全不透明。这种问题只持续 33ms，联系表和单帧截图都看不出来，人眼却能察觉“闪了一下”或“原地抖一下”。

检测结果以 warn 级别写进 lint，写明帧号、方向和可疑元素（哪个元素的入场首帧或退场末帧），例如：

```
[warn] 7.50s 疑似单帧闪烁（第 225 帧突然变强，强度 1.9）：list（第 2 条，slide 入场首帧）；逐帧条带见 promo.flicker.png
```

同时在 MP4 旁输出 `*.flicker.png` 逐帧条带：每个事件一行，显示闪现帧及其前后各 2 帧，闪现帧描红边。报告的 `flicker` 字段为 `{events;maxScore;strip}`：`maxScore` ≥1 就会报告，用户验收过的成片最高约 0.8。强度是局部对比度变化超过阈值的倍数，不代表观感轻重：小字的闪现强度低，但同样要修。

有意的逐帧急变不检查：
- 整帧跳过：转场窗口、slam 落地后的镜头震动。
- 按元素区域跳过：数字滚动、`function(t)` 动态文字、预设声明的 `fast` 窗口（punch 命中后急缩、slam 起跳急缩）。

快速运动不报：规则纹理（城市窗格、栅栏）快速平移、物体高速划过时，格子统计量会随相位起伏，检测器用平移复核把它们归为运动（报告不计）。单帧丢失、单帧错位（原地抖一下）前后帧不动，照样报。小物体边转边飞、每帧移动超过画面短边约 9% 时平移解释不了，可能误报，看逐帧条带确认即可。

自定义预设如果故意做单帧急变，给预设加 `fast = {起,止,区域倍数}`：起止是相对命中时刻的秒数，区域倍数乘在元素框上。检测每帧约 2~4ms。判据的原理和独立用法见 [av.video](../video/index.html) 的“闪烁检测”一节。

## 性能（本机实测）

- 16 秒 720p 文字演示：总耗时约 9 秒（画帧约 5ms/帧）。
- 20 秒 1080p 宣传片（含 2.5 秒截图平移）：总耗时约 20 秒；其中音频 4 秒、画帧 11 秒、编码 2 秒、闪烁检测 2 秒。
- 15 秒 1080×1920 竖屏快剪：约 11 秒。
- 20 秒 1080p 通用软件片（提示词、对话、代码、截图卡片推近、步骤）：约 24 秒。
- 33 秒 1080p 正式宣传片（另有三维 PNG 序列、从 MP4 取帧、整场招牌自绘）：约 72 秒，画帧占八成。
- 最贵的是图片逐帧重采样（1080p 全屏约 150ms/帧）：为了慢推慢移不抖（GDI+ drawImage 有最大 1.5 像素的逐帧跳动），这是刻意的取舍。
