aardio 文档

av.video

动效工具:只补 gdip 没有的东西——时间与运动、排版到路径的文字、粒子、像素内核和帧缓冲。它不包装 gdip 绘图原语,自定义画面照写原生 gdip。av.film 建在它之上,它也能单独用于 plus 预览、游戏和图片处理。

设计原则:帧是时间的纯函数 f(t)。这里的函数都不累积状态(不用 dt 积分),随机数全部来自种子,所以任意帧可以单独渲染、乱序渲染,结果可重复。

时间与运动

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()。

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");

文字 av.video.text

文字一律用路径字形(path.addString + fillPath):配合 8×8 抗锯齿,慢速平移、缩放、旋转时质心误差约 1/8 像素、笔画粗细不闪。drawString 在 96px 以下走字形缓存,缩放时墨量跳变可达 14%,不要用于动画。路径在排版时建好,逐帧只做世界变换 + fillPath。

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");

粒子 av.video.particles

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 截图、外部视频)可以直接用:

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,用来判断是哪一种。

Markdown 格式