动效工具:只补 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 积分),随机数全部来自种子,所以任意帧可以单独渲染、乱序渲染,结果可重复。
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");
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 色度抽样会糊成色块)。文字一律用路径字形(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");
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 笔位)。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。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},相对爆发中心;飞向它们的星星在碰到之前淡出)。
找“单帧闪现”:某一帧在局部突然比前后两帧都强(或都弱),下一帧又退回去。典型原因是入场首帧误用完全不透明(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,用来判断是哪一种。