'use strict'; /** * 动态缩略图的档位配置。 * * 每项是 [细节分辨率, 帧率, 编码质量],作为搜索路径上的锚点: * - 细节分辨率决定画面保留多少细节(输出画布始终 400×400,小分辨率会被放大填满); * - 帧率决定每秒采样多少帧,只改采样密度不改时间轴,每档的帧延迟会自动重算; * - 编码质量是 0–1 的 WebP 质量,APNG 是无损编码会忽略这一项。 * * 数组顺序是从最高画质到最低画质的搜索方向。运行时会在相邻锚点之间插值成一条更密的 * 路径,再按用户填写的目标体积在路径上二分定位,取命中位置附近若干档完整生成, * 所以锚点的疏密决定可搜索的粒度,锚点数量决定画廊里的候选数量。 */ const ANIMATED_PRIORITIES = { // 分辨率优先:先把帧率压到底,再降分辨率。 resolution: [ [400, 18, 1.0], [400, 15, 0.8], [400, 12, 0.6], [400, 9, 0.4], [400, 6, 0.2], [200, 18, 1.0], [200, 15, 0.8], [200, 12, 0.6], [200, 9, 0.4], [200, 6, 0.2], ], // 帧率优先:先把分辨率压到底,再降帧率。 framerate: [ [400, 18, 1.0], [400, 18, 0.5], [350, 15, 1.0], [350, 15, 0.5], [300, 9, 1.0], [300, 9, 0.5], [250, 6, 1.0], [250, 6, 0.5], [200, 9, 1.0], [200, 6, 0.5], ], // 交替进行:分辨率和帧率交替下降。 balanced: [ [400, 15, 0.8], [400, 12, 0.6], [400, 9, 0.4], [400, 6, 0.2], [200, 15, 0.8], [200, 12, 0.6], [200, 9, 0.4], [200, 6, 0.2], [100, 3, 0.5], [100, 3, 0.1], ], }; const DEFAULT_PRIORITY = 'resolution'; /** * 可选的输出画布边长(正方形),界面上的下拉就是这两项。 * * Eagle 缩略图网格里每格实际只显示 100–240px 左右,200 与 400 在这个尺寸下几乎看不出差别, * 但像素数只有四分之一,体积能降约 4 倍。配置里写的细节分辨率超过所选边长时会被压下来, * 所以同一份档位表在两种尺寸下都能用。 */ const ANIMATED_OUTPUT_SIZES = [200, 400]; const DEFAULT_OUTPUT_SIZE = 200; /** * 「缩略图大小」输入框的默认建议值:每秒动画配 25 KB,夹在 25–100 KB 之间。 * 只是打开面板时的初始值,用户随时可以手动改成 10–1024 之间的任意数。 */ const SIZE_PER_SECOND = 25; const SIZE_SUGGESTION_RANGE = [25, 100]; // 相邻锚点之间插多少个采样点。数值越大,越容易压在目标体积附近,但粗估阶段耗时略增。 const PATH_STEPS_PER_SEGMENT = 6; function suggestSize(seconds) { const [min, max] = SIZE_SUGGESTION_RANGE; return Math.max(min, Math.min(max, Math.round((Number(seconds) || 0) * SIZE_PER_SECOND))); } function animatedProfiles(priority, outputSize) { const limit = outputSize || DEFAULT_OUTPUT_SIZE; const plan = ANIMATED_PRIORITIES[priority] || ANIMATED_PRIORITIES[DEFAULT_PRIORITY]; return plan.map(([size, fps, quality]) => ({ size: Math.min(size, limit), fps, quality, })); } /** * 把锚点路径插值成更密的采样路径,供按目标体积二分定位使用。 * 分辨率取整、帧率至少 1、质量保留两位小数;首尾锚点原样保留。 */ function expandAnimatedPath(priority, outputSize) { const limit = outputSize || DEFAULT_OUTPUT_SIZE; const anchors = ANIMATED_PRIORITIES[priority] || ANIMATED_PRIORITIES[DEFAULT_PRIORITY]; const lerp = (from, to, t) => from + (to - from) * t; const path = []; for (let index = 0; index < anchors.length - 1; index++) { const [fromSize, fromFps, fromQuality] = anchors[index]; const [toSize, toFps, toQuality] = anchors[index + 1]; for (let step = 0; step < PATH_STEPS_PER_SEGMENT; step++) { const t = step / PATH_STEPS_PER_SEGMENT; path.push({ size: Math.min(limit, Math.round(lerp(fromSize, toSize, t))), fps: Math.max(1, Math.round(lerp(fromFps, toFps, t))), quality: Math.round(lerp(fromQuality, toQuality, t) * 100) / 100, }); } } const [size, fps, quality] = anchors[anchors.length - 1]; path.push({ size: Math.min(size, limit), fps, quality }); return path; } module.exports = { ANIMATED_PRIORITIES, DEFAULT_PRIORITY, ANIMATED_OUTPUT_SIZES, DEFAULT_OUTPUT_SIZE, SIZE_PER_SECOND, SIZE_SUGGESTION_RANGE, PATH_STEPS_PER_SEGMENT, animatedProfiles, expandAnimatedPath, suggestSize, };