如何将 ECharts 动画导出为视频?前端实现方案分享
在数据可视化场景中,ECharts 的动态图表(如柱状图竞赛、折线图动画)能直观展现数据变化趋势,但默认只能在网页中展示。如果想把这些动画保存为视频分享或嵌入报告,该怎么做?
今天分享一个前端实现方案:通过采集 ECharts 动画帧,再用 FFmpeg 合成视频,全程在浏览器内完成,无需后端参与。
一、总体思路
核心原理可分为两步:采集帧 → 合成视频
-
帧采集:利用 ECharts 提供的
getDataURL方法,在图表动画播放过程中,按一定帧率(如 30fps)截取每一帧的图片。getDataURL能直接将当前图表状态转为 base64 图片,方便后续处理;- 用
requestAnimationFrame控制采集时机,确保帧与动画同步。
-
视频合成:将采集到的所有帧图片,通过 FFmpeg (WebAssembly 版本)合成为 MP4 视频。
- 前端 FFmpeg 库(
@ffmpeg/ffmpeg)可在浏览器内执行视频编码; - 配置合适的编码器(如 H.264)和参数,保证视频质量与体积平衡。
- 前端 FFmpeg 库(
二、具体实现代码
1. 依赖安装
首先需要安装必要的库:ECharts 用于绘制图表,FFmpeg 相关库用于视频合成。
# 安装 ECharts
npm install echarts
# 安装 FFmpeg 及工具库
npm install @ffmpeg/ffmpeg @ffmpeg/util
2. 核心代码:帧采集与视频合成
下面是封装好的核心函数,包含 FFmpeg 初始化、帧采集、视频合成逻辑(已补充详细注释):
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
import type { ECharts } from 'echarts';
// 视频导出配置项
interface ExportChartToVideoOptions {
duration: number; // 视频总时长(毫秒)
quality?: number; // 视频质量(0-51,越小质量越好,默认18)
}
// FFmpeg 实例缓存(避免重复初始化)
let ffmpegInstance: FFmpeg | null = null;
/**
* 获取或初始化 FFmpeg 实例
* 注意:需要提前准备 ffmpeg-core.js 和 ffmpeg-core.wasm 文件
*/
async function getFFmpeg(): Promise<FFmpeg> {
if (ffmpegInstance) {
return ffmpegInstance;
}
const ffmpeg = new FFmpeg();
// FFmpeg 核心文件路径(需自行放置到项目静态资源目录)
const baseURL = '/ffmpeg'; // 例如 public/ffmpeg 目录下
// 加载 FFmpeg 核心(WebAssembly)
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
ffmpegInstance = ffmpeg;
return ffmpeg;
}
/**
* 导出 ECharts 图表动画为视频
* @param chart ECharts 实例
* @param options 导出配置
*/
export async function exportChartToVideo(
chart: ECharts,
options: ExportChartToVideoOptions
): Promise<void> {
const {
duration,
quality = 18, // 推荐 18-24,平衡质量与体积
} = options;
const frames: Blob[] = []; // 存储采集的帧
const startTime = performance.now(); // 记录开始时间
return new Promise(async (resolve, reject) => {
try {
// 帧采集函数:用 requestAnimationFrame 同步动画帧率
const captureFrame = () => {
const elapsed = performance.now() - startTime;
// 调用 ECharts 的 getDataURL 获取当前帧图片
const dataURL = chart.getDataURL({
pixelRatio: 3, // 3倍像素比,避免视频模糊
backgroundColor: '#fff', // 背景色(与图表主题一致)
});
// 将 base64 转为 Blob 并存入帧数组
fetch(dataURL)
.then(res => res.blob())
.then(blob => {
frames.push(blob);
})
.catch(err => {
console.error('帧采集失败:', err);
});
// 未到指定时长则继续采集
if (elapsed < duration) {
requestAnimationFrame(captureFrame);
} else {
// 采集完成,开始合成视频
composeVideo();
}
};
// 开始采集(与图表动画同步启动)
requestAnimationFrame(captureFrame);
// 视频合成函数
async function composeVideo() {
try {
const ffmpeg = await getFFmpeg();
// 计算实际帧率(总帧数 / 时长(秒))
const actualFps = Math.round(frames.length / (duration / 1000));
console.log(`采集完成:共 ${frames.length} 帧,帧率 ${actualFps} fps`);
// 1. 将所有帧写入 FFmpeg 虚拟文件系统
for (let i = 0; i < frames.length; i++) {
// 文件名格式:frame00000.png、frame00001.png...(FFmpeg 要求连续编号)
const fileName = `frame${String(i).padStart(5, '0')}.png`;
const frameData = await fetchFile(frames[i]); // 转换为 FFmpeg 可识别的格式
await ffmpeg.writeFile(fileName, frameData);
}
// 2. 执行 FFmpeg 命令合成视频
// 命令说明:
// -framerate:输入帧率
// -i:输入文件格式(%05d 表示5位数字编号)
// -c:v libx264:使用 H.264 编码器
// -crf:质量控制(越小质量越好)
// -pix_fmt yuv420p:兼容所有设备的像素格式
await ffmpeg.exec([
'-framerate', String(actualFps),
'-i', 'frame%05d.png',
'-c:v', 'libx264',
'-crf', String(quality),
'-pix_fmt', 'yuv420p',
'-movflags', '+faststart', // 优化视频加载速度(网页播放友好)
'output.mp4' // 输出文件名
]);
// 3. 读取合成后的视频并下载
const videoData = await ffmpeg.readFile('output.mp4');
const videoBlob = new Blob([videoData as BlobPart], { type: 'video/mp4' });
// 创建下载链接
const url = URL.createObjectURL(videoBlob);
const a = document.createElement('a');
a.href = url;
a.download = `chart-animation-${Date.now()}.mp4`; // 文件名带时间戳
document.body.appendChild(a);
a.click(); // 触发下载
document.body.removeChild(a);
URL.revokeObjectURL(url); // 释放内存
// 4. 清理虚拟文件系统(避免占用内存)
for (let i = 0; i < frames.length; i++) {
const fileName = `frame${String(i).padStart(5, '0')}.png`;
try {
await ffmpeg.deleteFile(fileName);
} catch (e) { /* 忽略删除错误 */ }
}
try {
await ffmpeg.deleteFile('output.mp4');
} catch (e) { /* 忽略删除错误 */ }
resolve();
} catch (error) {
console.error('视频合成失败:', error);
reject(error);
}
}
} catch (error) {
console.error('帧采集过程出错:', error);
reject(error);
}
});
}
3. 使用示例:导出动态图表
下面以一个简单的动态柱状图为例,演示如何调用上述函数:
import * as echarts from 'echarts';
import { exportChartToVideo } from './exportVideo'; // 导入上面的函数
// 1. 初始化 ECharts 实例
const chartDom = document.getElementById('chart-container');
const chart = echarts.init(chartDom);
// 2. 配置动态图表(以柱状图动画为例)
const option = {
xAxis: { type: 'category', data: ['A', 'B', 'C', 'D'] },
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [10, 20, 30, 40],
// 开启动画(关键:需要让图表有动画效果)
animationDuration: 3000, // 动画持续3秒
animationEasing: 'elasticOut'
}]
};
chart.setOption(option);
// 3. 播放动画并启动导出(例如点击按钮触发)
document.getElementById('export-btn').addEventListener('click', async () => {
try {
// 开始导出:时长与图表动画一致(3000毫秒)
await exportChartToVideo(chart, { duration: 3000 });
console.log('视频导出成功!');
} catch (err) {
console.error('导出失败:', err);
}
});
三、注意事项
-
FFmpeg 核心文件获取
代码中用到的ffmpeg-core.js和ffmpeg-core.wasm需要单独下载,可从 FFmpeg.wasm 官方仓库 或 CDN 获取,放置到项目静态资源目录(如public/ffmpeg)。 -
浏览器兼容性
- 依赖 WebAssembly 和
requestAnimationFrame,支持 Chrome、Firefox、Edge 等现代浏览器,IE 不兼容; - 本地开发需启动服务(如
localhost),直接打开 HTML 文件可能因跨域报错。
- 依赖 WebAssembly 和
-
性能优化
- 帧率不宜过高(建议 24-30fps),否则帧数量过多会导致内存占用激增;
- 图表尺寸不宜过大(如 1080p 以内),避免合成视频时卡顿;
- 可将 FFmpeg 操作放入 Web Worker,避免阻塞主线程。
-
视频质量调整
quality参数(crf)越小,质量越好但体积越大,推荐 18-24;- 如需更小体积,可降低
pixelRatio(如改为 2)。
四、总结
通过 ECharts 的 getDataURL 采集帧 + FFmpeg.wasm 合成视频,我们可以在前端轻松实现动态图表到视频的导出,无需后端介入。这个方案适合数据可视化报告、动态数据分享等场景,且完全在浏览器内完成,用户体验更流畅。
如果需要更复杂的功能(如添加水印、背景音乐),可以扩展 FFmpeg 命令,例如在合成时加入音频文件或叠加图片。
试试用这个方案,让你的数据动画更易传播吧!