# av.video

动效工具：只补 gdip 没有的东西——时间与运动、排版到路径的文字、粒子、像素内核和帧缓冲。它不包装 gdip 绘图原语，自定义画面照写原生 gdip。`av.film` 建在它之上，它也能单独用于 plus 预览、游戏和图片处理。

- `av.video`：缓动（Penner 名称）、关键帧、弹簧（解析解）、种子随机与噪声、节拍、颜色、帧缓冲 `av.video.frame`。
- `av.video.text`：文字排版（逐字/逐词/逐行单元、`**强调**`、自动换行、中文分词）与绘制，图标名表。
- `av.video.particles`：解析式粒子（星光、彩纸）。
- `av.video.kernel`：像素内核（C 函数，预编译成 DLL 内嵌在库里、内存加载；C 源码与重建脚本在库目录的 `.res/`）：精确渐变背景、图片重采样、混合、故障效果。

设计原则：帧是时间的纯函数 `f(t)`。这里的函数都不累积状态（不用 dt 积分），随机数全部来自种子，所以任意帧可以单独渲染、乱序渲染，结果可重复。

## 时间与运动

```aardio
import av.video;
var V = av.video;

var y = V.ease("easeOutCubic",0.5);   //0.875；名字忽略大小写、可省略 ease 前缀
var f = V.ease("outBack");            //不给 t 则返回函数

//值：数值 | 关键帧 {{t,v[,ease]}}（缓动写在段末点，默认 easeInOutSine）| function(t)
var x = V.value({{0,900};{2.5,1020,"easeOutQuad"}},1.2);

var s = V.spring(0.3,2,0.4);          //弹簧 0→1：频率 2Hz、阻尼比 0.4（会过冲）
var d = V.decay(0.1,3,8);             //衰减振荡 0→摆动→0（落地挤压）
var r = V.random(42,7);               //[0,1) 种子随机，同一 seed,i 永远相同
var dx = V.wiggle(1.25,24,14,7);      //平滑抖动：每秒 24 次、幅度 14、种子 7（镜头震动）
var t4 = V.beat(120,4);               //120 BPM 第 4 拍 = 2 秒

var c = V.mixColor(0xFFFFFFFF,V.color("#3BD6FF"),0.5);
return {y;x;s;d;r;dx;t4;c};
```

颜色一律是 0xAARRGGBB（无符号）。`V.color` 解析 `"#RRGGBB"`、`"#AARRGGBB"`、`"#RGB"` 或 0xRRGGBB（视为不透明）；`V.fade(c,a)` 把 alpha 乘以 a；`V.contrast(c1,c2)` 是 WCAG 对比度。

## 帧缓冲与像素内核

`av.video.frame(w,h)` 是自有内存的 PARGB 位图：gdip 画在 `fr.graphics` 上即写进 `fr.buffer`，像素内核直接处理 `fr.buffer`，`sys.mediaFoundation.writer` 的 `writeFrame(fr.buffer)` 零拷贝写入。画板已设 8×8 抗锯齿和像素中心 +0.5（与内核一致）。**内核读写 buffer 前先调用 `fr.flush()`**。

```aardio
import av.video.kernel;
import gdip.solidBrush;
var K = av.video.kernel;
var fr = av.video.frame(1280,720);

//背景：浮点渐变 + 暗角 + 静态粗颗粒 ±3，量化一次（比 GDI+ 渐变刷色带少）
K.background(fr.buffer,fr.width,fr.height,{
	type="radial";stops={{0,0x1B2D55};{0.6,0x0B1530};{1,0x04070F}};
	cx=0.5;cy=0.36;vignette=0.25;grain=3
});

//图片：载入到预乘 BGRA 缓冲，再用浮点目标矩形重采样叠加
var img = gdip.bitmap("~\examples\Graphics\.gdip.jpg");
var src = av.video.frame(img.width,img.height);
src.graphics.compositingMode = 1;
src.graphics.drawImageRectRect(img,0,0,img.width,img.height,0,0,img.width,img.height);
src.flush();
fr.flush();
K.resample(fr.buffer,fr.width,fr.height,src.buffer,src.width,src.height,{x=100.4;y=80.25;width=500.5;height=375.4},null,{opacity=0.9});
img.dispose();

//之后继续用 gdip 在同一帧上画其他内容
fr.graphics.fillRectangle(gdip.solidBrush(0x803BD6FF),700,80,300,200);
return fr.save("/kernel-demo.png");
```

- `K.resample`：轴对齐缩放 + 平移，坐标精确到亚像素（慢推慢移逐帧误差 < 0.01 像素），缩小时按比例展宽滤波核（不闪烁），边缘按覆盖率抗锯齿。opt：`opacity`、`filter`（0 Mitchell 默认 / 1 Catmull-Rom 更锐）、`clip`、`copy`（覆盖而非叠加，用于预缩放）。GDI+ `drawImage` 即使用浮点矩形也有最大 1.5 像素的逐帧跳动，运动的图片请用它。大图先一次性预缩放到约 1.15 倍所需尺寸，逐帧更快。
- `K.background`：`type` linear（`angle` 度，0=左→右，90=上→下）或 radial（`cx`,`cy` 为宽高比例，`radius` 为半对角线比例）；`stops` = `{{位置,0xRRGGBB}}` 或颜色数组；`vignette` 0~1；`grain` 颗粒幅度（8 位色阶）；`cell` 颗粒格子（默认 3px）。颗粒是静态的（逐帧变化会让码率涨到原来的十倍）。整帧生成约 180ms，请缓存复用。
- `K.blend(dst,src,像素数,a256)` 交叉混合；`K.blit` 整数偏移复制；`K.glitch(dst,src,w,h,amount,seed[,split])` 故障：少量粗条带错位，只在错位条带里做轻微 RGB 分离（`split` 为满幅分离占宽度比例，默认 0.004，0 = 不分离）；`seed` 建议约 15Hz 换一次（每个状态停留约 2 帧，逐帧换像雪花噪声）。`K.glitchMix(dst,a,b,w,h,amount,mix,seed,layoutSeed[,split])` 撕裂转场：旧画面 a 按固定细条逐条、单调地换成新画面 b（`mix` 0~1），另加少量错位；`split` 默认 0（细彩边经 4:2:0 色度抽样会糊成色块）。

## 文字 av.video.text

文字一律用路径字形（`path.addString` + `fillPath`）：配合 8×8 抗锯齿，慢速平移、缩放、旋转时质心误差约 1/8 像素、笔画粗细不闪。`drawString` 在 96px 以下走字形缓存，缩放时墨量跳变可达 14%，不要用于动画。路径在排版时建好，逐帧只做世界变换 + fillPath。

```aardio
import av.video.text;
var T = av.video.text;
var fr = av.video.frame(1280,720);
fr.clear(0xFF101820);

var lay = T.layout("一个语言，**桌面全能**",{size=72;bold=true;split="char"});
//逐字状态：第 i 个字在 t 时刻的透明度与位移（缩放、旋转绕字的中心）
var t = 0.5;
T.draw(fr.graphics,lay,(1280 - lay.width) / 2,(720 - lay.height) / 2,{
	color = 0xFFFFFFFF;emphColor = 0xFF3BD6FF;
	state = function(i,unit){
		var p = av.video.progress(t,i * 0.04,i * 0.04 + 0.3);
		return {alpha = p;dy = (1 - av.video.ease("easeOutCubic",p)) * 30};
	}
});
return fr.save("/text-demo.png");
```

- `T.layout(str,opt)`：opt `font`（默认微软雅黑 UI）、`size`（像素）、`bold`、`lineHeight`（倍数，默认 1.25）、`maxWidth`（超宽换行，按词断行，标点不出现在行首、开括号不出现在行尾）、`align`、`split`（char/word/line）。返回 `width`、`height`、`lines`、`units`（每个单元有 `runs` 路径段、`ink` 墨迹边界、`cx,cy` 中心、`penX0/penX1` 笔位）。
- 逐字位置用「点数切分法」从整行路径里切出，含字距调整和字体回退，逐字合成与整串绘制像素级一致；行内按视觉中心（“中H”的墨迹）垂直居中。
- 分词用 `string.wordsSegmenter`（系统分词），新词会被拆开（织影 → 织|影）：`split="word"` 时在文本里写 `|` 手动分词（`"织影|用代码|编织视频"`）。
- `T.draw(g,lay,x,y,opt)`：(x,y) 是块左上角；opt `color`、`emphColor`、`alpha`、`state`、`shadow={color;dx;dy}`、`glow={color;width}`（宽半透明笔描边形成光晕）。
- `T.icon(名字)` 返回 FontAwesome 字符（名字兼容 FA6 与 FA4：check、circle-check、star、rocket、bolt、gear……见 `T.icons`），用 `T.getFamily("FontAwesome")` addString。

## 粒子 av.video.particles

```aardio
import av.video.particles;
var ps = av.video.particles.create("confetti",{seed=3}); //或 "sparkle"
var fr = av.video.frame(1280,720);
fr.clear(0xFF101820);
av.video.particles.draw(fr.graphics,ps,0.6,640,500);    //爆发后第 0.6 秒、爆发点 (640,500)
return {life = ps.life;png = fr.save("/particles-demo.png")};
```

位置是时间的解析函数（线性阻力 + 重力），任意时刻可直接求值。`create` 的 opt：`seed`、`count`、`scale`（按 1080p 设计）、`colors`、`spread`（彩纸喷射扇形角度）；星光另有 `radius`（散布半径，默认 260×scale，最快的星星约飞到 1.35 倍）、`ringRadius`（冲击环半径，默认 = radius）、`avoid`（要避开的矩形数组 `{x0;y0;x1;y1}`，相对爆发中心；飞向它们的星星在碰到之前淡出）。

## 闪烁检测 av.video.flicker

找“单帧闪现”：某一帧在局部突然比前后两帧都强（或都弱），下一帧又退回去。典型原因是入场首帧误用完全不透明（`alpha : 1` 把 0 当成未指定）、差一帧的时间计算、丢帧。这种问题只持续 33ms，联系表和单帧截图都看不出来，人眼却能察觉。`av.film` 的 `render` 已自动调用；其他帧源（自己画的帧、web.view 截图、外部视频）可以直接用：

```aardio
import av.video.flicker;
import gdip.solidBrush;
var V = av.video;
var W,H = 1280,720;
var fr = V.frame(W,H);
var det = V.flicker(W,H);
var brush = gdip.solidBrush(0xFFFFFFFF);
for(k=0;59){
	fr.clear(0xFF101820);
	if(k == 30) fr.graphics.fillRectangle(brush,203,157,301,187); //第 30 帧误画了一个白块
	fr.flush();
	det.add(fr.buffer,fr.stride); //可选第 3 个参数排除有意的急变：true = 整帧，或 {{x0;y0;x1;y1}}（像素区域）
}
brush.delete();
fr.delete();
return det.finish(); //[{frame=30;score=38.8;sign=1;x;y;width;height;window;channel}]
```

扫描已有的视频文件：`var events,det = av.video.flicker.scanFile("/demo.mp4"[,exclude])`，`exclude(frame,time)` 的返回值同 `add` 的第 3 个参数，事件多一个 `time` 字段（秒）。

判据：每帧按格子（短边 36 格）统计亮度的标准差（局部对比度，随元素不透明度线性变化，与前景、背景谁亮无关）和平均亮度（补上大块纯色：纯色块内部没有对比度）。然后对多个窗口做 5 帧判据：中间帧必须是局部极值，并且同时偏离左、右两侧的线性外推。所以硬切（台阶）、平滑加减速、白闪转场的三角形亮度曲线、打字的阶梯式出现都不算。快速运动在大窗口里会互相抵消，平均亮度只用大窗口。

`score` 是超过阈值的倍数，≥1 就是事件；`sign` +1 表示突然变强，-1 表示突然变弱。阈值按 av.film 的三条目标成片标定：用户验收的成片最高约 0.8，v0.1 里 30 处入场闪现全部 ≥1.07。转场、镜头震动、数字滚动这类有意的逐帧急变无法与闪现区分，请用 exclude 排除（av.film 会按时间线自动排除）。1080p 每帧约 2ms（每格约 15×15 个采样点），运动复核的缩略图另加约 1.5ms。

运动复核（opt `verify`，默认开）：规则纹理（城市窗格、栅栏）快速平移时，格子统计量随相位起伏，会被 5 帧判据当成闪现。超过阈值的候选再在缩略亮度图（短边 135 格）上找“前一帧→这一帧”“这一帧→后一帧”的平移，对齐后能解释掉大部分突变（残差 ≤ 35%，明显在动时 ≤ 45%），并且跟着内容看第 1→2、4→5 帧也在动（速度不低于一半）的，归入 `det.motion` 时间段 `{from;to;frames;maxScore;speed;x;y;width;height}`（speed 为像素/帧），不报闪现。单帧丢失、单帧错位也能被一对来回的平移解释，但前后帧不动，照样报。平移只比速度大小不比方向：纹理平移速度接近半个周期时方向估计会来回跳。已知误报：小物体边转边飞、每帧移动超过短边约 9%（搜索半径）时平移解释不了。事件带 `motionRatio`（平移后残差比）、`motionSpeed`，前后帧不动的另带 `motionOuter`，用来判断是哪一种。
