如何将 ECharts 动画导出为视频?前端实现方案分享


在数据可视化场景中,ECharts 的动态图表(如柱状图竞赛、折线图动画)能直观展现数据变化趋势,但默认只能在网页中展示。如果想把这些动画保存为视频分享或嵌入报告,该怎么做?

今天分享一个前端实现方案:通过采集 ECharts 动画帧,再用 FFmpeg 合成视频,全程在浏览器内完成,无需后端参与。

一、总体思路

核心原理可分为两步:采集帧 → 合成视频

  1. 帧采集:利用 ECharts 提供的 getDataURL 方法,在图表动画播放过程中,按一定帧率(如 30fps)截取每一帧的图片。

    • getDataURL 能直接将当前图表状态转为 base64 图片,方便后续处理;
    • requestAnimationFrame 控制采集时机,确保帧与动画同步。
  2. 视频合成:将采集到的所有帧图片,通过 FFmpeg (WebAssembly 版本)合成为 MP4 视频。

    • 前端 FFmpeg 库(@ffmpeg/ffmpeg)可在浏览器内执行视频编码;
    • 配置合适的编码器(如 H.264)和参数,保证视频质量与体积平衡。

二、具体实现代码

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

三、注意事项

  1. FFmpeg 核心文件获取
    代码中用到的 ffmpeg-core.jsffmpeg-core.wasm 需要单独下载,可从 FFmpeg.wasm 官方仓库 或 CDN 获取,放置到项目静态资源目录(如 public/ffmpeg)。

  2. 浏览器兼容性

    • 依赖 WebAssembly 和 requestAnimationFrame,支持 Chrome、Firefox、Edge 等现代浏览器,IE 不兼容;
    • 本地开发需启动服务(如 localhost),直接打开 HTML 文件可能因跨域报错。
  3. 性能优化

    • 帧率不宜过高(建议 24-30fps),否则帧数量过多会导致内存占用激增;
    • 图表尺寸不宜过大(如 1080p 以内),避免合成视频时卡顿;
    • 可将 FFmpeg 操作放入 Web Worker,避免阻塞主线程。
  4. 视频质量调整

    • quality 参数(crf)越小,质量越好但体积越大,推荐 18-24;
    • 如需更小体积,可降低 pixelRatio(如改为 2)。

四、总结

通过 ECharts 的 getDataURL 采集帧 + FFmpeg.wasm 合成视频,我们可以在前端轻松实现动态图表到视频的导出,无需后端介入。这个方案适合数据可视化报告、动态数据分享等场景,且完全在浏览器内完成,用户体验更流畅。

如果需要更复杂的功能(如添加水印、背景音乐),可以扩展 FFmpeg 命令,例如在合成时加入音频文件或叠加图片。

试试用这个方案,让你的数据动画更易传播吧!