CardScroller
滚动视频制作工具
请在右侧控制面板上传图片开始制作。
支持格式
JPEG、PNG、WebP、GIF、BMP
图片尺寸
代码逻辑无上限,实际受浏览器限制ⓘ和设备可用内存限制ⓘ。
温馨提示
图片像素尺寸越大,占用内存越多,性能开销越高。超大图片建议切片后分别上传录制,再用视频编辑软件拼接。
使用方式
上传图片调整参数(建议F11全屏)后录屏即可。
视频教程
请在右侧控制面板上传图片开始制作。
JPEG、PNG、WebP、GIF、BMP
代码逻辑无上限,实际受浏览器限制ⓘ和设备可用内存限制ⓘ。
图片像素尺寸越大,占用内存越多,性能开销越高。超大图片建议切片后分别上传录制,再用视频编辑软件拼接。
上传图片调整参数(建议F11全屏)后录屏即可。
视频教程
💡 请确保边界线完美对齐卡片边缘,否则入场动画结束时可能出现轻微跳变。
💡 第一次使用时可在右侧“卡片位置信息”下方复制边界线数组并保存,下次直接粘贴恢复,避免手动拖线标记。
卡片入场动画功能性能开销非常大,代码层面已经全面优化过了,具体优化项:
如果您的设备在使用过程中仍然出现卡顿、掉帧、不流畅、延迟等问题(根本原因是Canvas物理像素过多),建议在PPT中制作入场动画,然后配合该工具的滚动动画进行视频剪辑。
💡 提示:左键点击选项应用到当前卡片,右键点击选项应用到所有卡片。
💡 大部分动画效果为线性(10种),少数动画(5种)使用预设缓动效果ⓘ,动画缓动参数无法配置。
⚠️ 背景色说明:如果您的图片中卡片之间有背景色(如黑色),请在主界面控制面板的"页面背景色"设置为相同颜色,否则入场动画时卡片间隙会露出白色背景导致穿帮。仅支持纯色,不支持渐变色/图案。请确保原图也是纯色背景。
💡 格式:卡片n:[左边界, 右边界]。
📏 边界值为原图坐标,即从原图左边缘到边界线的距离(单位:px)。
🔄 可通过左侧"快速恢复边界线"输入框粘贴下方数组快速恢复。
💾 边界线数组会自动保存到导出的配置文件中(entryAnimationCardBoundaries字段),丢失后可从中找回。
请先在上方编辑器中标记卡片边界线。
标记完成后点击下方按钮查看卡片位置信息。
⚠️ 开启性能监控后,建议完整播放动画,中途暂停会影响数据准确性。下一次播放动画会覆盖上一次的报告。性能监控与循环播放不可同时启用。
💡 如何查看显示器刷新率:
Windows: 设置 → 系统 → 显示 → 高级显示设置。
或在PowerShell中运行:
$info = Get-CimInstance -ClassName Win32_VideoController | Select-Object Name, CurrentRefreshRate Write-Host "当前显示器刷新率:" $info.CurrentRefreshRate "Hz"想了解命令意思?
或在命令提示符(CMD)中运行:
wmic path Win32_VideoController get CurrentRefreshRate
想了解命令意思?
macOS: 系统偏好设置 → 显示器 → 刷新率。
或在终端运行:
system_profiler SPDisplaysDataType | grep "Resolution"想了解命令意思?
Linux: 终端运行:
xrandr | grep "*"想了解命令意思?
(注:在当前上下文中,FPS并不是指第一人称射击游戏,而是Frames Per Second的缩写)
FPS(帧率):动画每秒渲染多少帧画面,由硬件性能决定。
Hz(刷新率):显示器每秒能刷新多少次,由显示器硬件决定。
FPS ≤ 刷新率(如 60 FPS + 60Hz 显示器):每一帧都能显示,体验流畅。
FPS > 刷新率(如 200 FPS + 60Hz 显示器):超出的帧数会被丢弃,实际只能看到 60 帧。
FPS < 刷新率(如 30 FPS + 60Hz 显示器):屏幕会重复显示同一帧,看起来明显卡顿。
因此性能评级是相对百分比(60Hz 显示器 57FPS=95%=优秀,144Hz 显示器 137FPS=95%=优秀)。
不是 CSS Animation(@keyframes)
不是 Canvas 绘制
不是 JavaScript requestAnimationFrame
不是 图片动画(GIF/APNG)
不是 ::after / ::before 伪元素
不是 四个独立的 <div> 容器
不是 第三方动画库(如 GSAP、Anime.js)
而是:
<path> 元素绘制矩形边框。stroke-dasharray="10 90" 创建“10单位实线 + 90单位空白”的虚线模式。pathLength="100" 将路径长度标准化。<animate> 元素驱动动画(不依赖CSS或JavaScript)。stroke-dashoffset 动画:0 → ±100(控制跑马灯移动)。stroke 颜色动画:6色循环渐变。flood-color 颜色动画:发光色同步渐变。使用滤镜链实现霓虹发光:
pointer-events: none 不阻塞用户交互。viewBox + preserveAspectRatio 自适应容器尺寸,无需JS计算。
在宽高比不是1:1的容器中(如宽800px × 高200px),水平方向的线条会比垂直方向的线条更粗。这是因为使用了 preserveAspectRatio="none",导致水平和垂直方向的缩放比例不同。
为什么不修复?
SVG在非正方形容器中存在一个"不可能三角",只能选择其中两个:
最终选择:接受线条粗细不均匀,因为这是视觉影响最小的选择。在大多数容器中(宽高比接近),这个差异并不明显。
// 方法1:初始化所有section的边框跑马灯动画
_initBorderMarqueeAnimations(container) {
// 6种颜色序列(每个容器从不同颜色开始,让动画错开)
const colorSequences = [
['#3b82f6', '#10b981', '#a855f7', '#f97316', '#ef4444', '#06b6d4'], // 从蓝色开始
['#10b981', '#a855f7', '#f97316', '#ef4444', '#06b6d4', '#3b82f6'], // 从绿色开始
['#a855f7', '#f97316', '#ef4444', '#06b6d4', '#3b82f6', '#10b981'], // 从紫色开始
['#f97316', '#ef4444', '#06b6d4', '#3b82f6', '#10b981', '#a855f7'], // 从橙色开始
['#ef4444', '#06b6d4', '#3b82f6', '#10b981', '#a855f7', '#f97316'], // 从红色开始
['#06b6d4', '#3b82f6', '#10b981', '#a855f7', '#f97316', '#ef4444'] // 从青色开始
];
// 查找所有.performance-section容器
const sections = container.querySelectorAll('.performance-section');
sections.forEach((section, index) => {
// 为每个容器创建SVG边框跑马灯(使用对应的颜色序列)
const colors = colorSequences[index % colorSequences.length];
// 容器1,3,5顺时针,容器2,4,6逆时针
const isClockwise = index % 2 === 0;
const svg = this._createBorderMarqueeSVG(colors, index, isClockwise);
// 将SVG插入到容器的第一个位置
section.insertBefore(svg, section.firstChild);
});
}
// 方法2:创建单个边框跑马灯SVG元素
_createBorderMarqueeSVG(colors, index, isClockwise) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add('performance-section-border-marquee');
svg.setAttribute('viewBox', '0 0 100 100');
svg.setAttribute('preserveAspectRatio', 'none');
// 创建<defs>定义
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
// 创建发光滤镜(动态颜色)
const filterId = `border-marquee-glow-${index}`;
const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
filter.setAttribute('id', filterId);
const blur = document.createElementNS('http://www.w3.org/2000/svg', 'feGaussianBlur');
blur.setAttribute('stdDeviation', '0.8');
blur.setAttribute('result', 'blur');
const flood = document.createElementNS('http://www.w3.org/2000/svg', 'feFlood');
flood.setAttribute('flood-color', colors[0]);
flood.setAttribute('flood-opacity', '0.6');
const composite = document.createElementNS('http://www.w3.org/2000/svg', 'feComposite');
composite.setAttribute('in2', 'blur');
composite.setAttribute('operator', 'in');
const merge = document.createElementNS('http://www.w3.org/2000/svg', 'feMerge');
const mergeNode1 = document.createElementNS('http://www.w3.org/2000/svg', 'feMergeNode');
const mergeNode2 = document.createElementNS('http://www.w3.org/2000/svg', 'feMergeNode');
mergeNode2.setAttribute('in', 'SourceGraphic');
merge.appendChild(mergeNode1);
merge.appendChild(mergeNode2);
filter.appendChild(blur);
filter.appendChild(flood);
filter.appendChild(composite);
filter.appendChild(merge);
// 发光颜色动画
const floodColorAnimate = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
floodColorAnimate.setAttribute('attributeName', 'flood-color');
floodColorAnimate.setAttribute('values', colors.join(';') + ';' + colors[0]);
floodColorAnimate.setAttribute('dur', '10s');
floodColorAnimate.setAttribute('repeatCount', 'indefinite');
flood.appendChild(floodColorAnimate);
defs.appendChild(filter);
svg.appendChild(defs);
// 创建路径元素
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M 1,1 L 99,1 L 99,99 L 1,99 Z');
path.setAttribute('stroke', colors[0]);
path.setAttribute('stroke-width', '0.6');
path.setAttribute('fill', 'none');
path.setAttribute('filter', `url(#${filterId})`);
path.setAttribute('pathLength', '100');
path.setAttribute('stroke-dasharray', '10 90');
path.setAttribute('stroke-dashoffset', '0');
path.setAttribute('stroke-linecap', 'round');
// 位置动画(负数=顺时针,正数=逆时针)
const offsetAnimate = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
offsetAnimate.setAttribute('attributeName', 'stroke-dashoffset');
offsetAnimate.setAttribute('from', '0');
offsetAnimate.setAttribute('to', isClockwise ? '-100' : '100');
offsetAnimate.setAttribute('dur', '10s');
offsetAnimate.setAttribute('repeatCount', 'indefinite');
// 颜色动画
const colorAnimate = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
colorAnimate.setAttribute('attributeName', 'stroke');
colorAnimate.setAttribute('values', colors.join(';') + ';' + colors[0]);
colorAnimate.setAttribute('dur', '10s');
colorAnimate.setAttribute('repeatCount', 'indefinite');
path.appendChild(offsetAnimate);
path.appendChild(colorAnimate);
svg.appendChild(path);
return svg;
}
/* SVG 边框跑马灯动画 - 自适应容器尺寸 */
.performance-section-border-marquee {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 不阻塞用户交互 */
z-index: 1; /* 显示在内容上方 */
}
📁 实现位置:js/services/ui/PerformanceReportPage.js 的 _initBorderMarqueeAnimations() 和 _createBorderMarqueeSVG() 方法