视频混剪-LeaferJS 贴纸系统的实现

视频混剪-LeaferJS 贴纸系统的实现

BaseCut 技术博客第六篇,也是最后一篇。这篇讲贴纸系统------怎么实现可拖拽、可缩放、可旋转的图片叠加。


需求分析

用户想在视频上叠加各种元素:

复制代码
贴纸类型:
├── 表情包、GIF
├── 图片水印
├── 品牌 Logo
└── 静态/动态贴图

交互需求:
├── 选中高亮
├── 拖拽移动
├── 八向缩放
├── 旋转
└── 控制出现/消失时间

这些需求听起来简单,但自己实现会非常复杂:

  • 鼠标点击在哪个元素上?(命中检测)
  • 怎么画选中框和控制手柄?
  • 怎么响应拖拽、缩放、旋转?

所以需要找一个 Canvas 2D 渲染库。


技术选型:为什么选 LeaferJS

备选方案对比

库 优点 缺点
原生 Canvas 轻量 要自己实现所有交互逻辑
Fabric.js 生态成熟,功能全 包体 200KB+,代码老旧
Konva.js 性能还行 API 设计一般
LeaferJS 极致性能、TS 原生、编辑器功能开箱即用 社区相对小

LeaferJS 简介

LeaferJS 是一个国产开源的 Canvas 2D 渲染引擎,有几个特点非常吸引我:

1. 性能炸裂

官方 benchmark 数据:

场景 LeaferJS Fabric.js
创建 1 万个矩形 15ms 800ms
创建 100 万个矩形 1.5s 崩溃
内存占用(100万矩形) 350MB -

性能差距是 50-100 倍。

2. 包体积极小

复制代码
LeaferJS (gzip): 42KB
Fabric.js (gzip): 200KB+

只有 Fabric.js 的 1/5。

3. TypeScript 原生

整个库用 TypeScript 编写,API 类型定义完善,开发体验很好。

4. 编辑器功能开箱即用

@leafer-in/editor 插件提供了:

  • 选中框
  • 八向缩放手柄
  • 旋转手柄
  • 多选
  • 对齐吸附

不用自己实现,直接用。


LeaferJS 基础使用

安装

bash 复制代码
npm install leafer-ui @leafer-in/editor

创建画布

typescript 复制代码
import { App, Rect, Image } from 'leafer-ui'
import '@leafer-in/editor'  // 引入编辑器插件

// 创建应用
const app = new App({
  view: 'canvas-container',  // 容器 ID
  type: 'design',            // 设计模式,支持编辑
  editor: {}                 // 启用编辑器
})

// 添加一个矩形
const rect = new Rect({
  x: 100,
  y: 100,
  width: 200,
  height: 150,
  fill: '#32cd79',
  editable: true  // 可编辑
})

app.tree.add(rect)

加上 editable: true,矩形就可以拖拽、缩放、旋转了。

添加图片

typescript 复制代码
const sticker = new Image({
  url: '/stickers/emoji.png',
  x: 200,
  y: 200,
  width: 100,
  height: 100,
  editable: true
})

app.tree.add(sticker)

监听事件

typescript 复制代码
// 拖拽结束
sticker.on('drag.end', (e) => {
  console.log('新位置:', e.target.x, e.target.y)
})

// 缩放/旋转结束
sticker.on('transform.end', (e) => {
  const { x, y, scaleX, scaleY, rotation } = e.target
  console.log('变换:', { x, y, scaleX, scaleY, rotation })
})

贴纸数据模型

类型定义

typescript 复制代码
interface StickerClip {
  id: string
  trackId: string
  resourceId: string  // 图片资源 ID
  
  // 时间范围
  startTime: number
  duration: number
  
  // 变换参数(百分比,0-1)
  transform: {
    x: number        // 0.5 表示居中
    y: number
    scaleX: number   // 1 表示原始大小
    scaleY: number
    rotation: number // 角度,0-360
    opacity: number  // 透明度,0-1
  }
}

为什么用百分比存位置?

预览窗口和导出分辨率可能不一样。用百分比可以自动适配:

typescript 复制代码
// 百分比 → 像素
function toPixel(percent: number, canvasSize: number) {
  return percent * canvasSize
}

// 像素 → 百分比
function toPercent(pixel: number, canvasSize: number) {
  return pixel / canvasSize
}

核心问题:双向数据同步

问题描述

系统里有两份"数据":

复制代码
1. Pinia Store 里的贴纸数据 → 真正的数据源
2. LeaferJS 画布上的图形对象 → 可视化表现

它们必须保持同步,但有两个方向:

Store → LeaferJS(正常渲染)

复制代码
当前时间变化时:
1. 查询 Store 里有哪些贴纸应该显示
2. 在 LeaferJS 画布上创建/更新对应图形

LeaferJS → Store(用户交互)

复制代码
用户拖拽贴纸时:
1. LeaferJS 更新图形位置
2. 把新位置写回 Store

踩的坑:交互时的冲突

一开始我没注意这个问题,结果出现了"贴纸跳来跳去"的 bug:

复制代码
用户正在拖拽贴纸(x: 100 → 101 → 102 → 103...)
    ↓
播放器在渲染每一帧
    ↓
渲染时从 Store 读取 x = 100(还没更新)
    ↓
LeaferJS 图形被强制重置回 100
    ↓
用户感觉贴纸"跳"了一下

解决方案:交互锁

typescript 复制代码
let isInteracting = false

// 开始拖拽时上锁
function onDragStart() {
  isInteracting = true
}

// 结束拖拽时解锁,并同步到 Store
function onDragEnd(e) {
  isInteracting = false
  
  const { x, y, scaleX, scaleY, rotation } = e.target
  store.updateClip(clipId, {
    transform: {
      x: toPercent(x, canvasWidth),
      y: toPercent(y, canvasHeight),
      scaleX,
      scaleY,
      rotation
    }
  })
}

// 渲染时检查锁状态
function syncFromStore() {
  if (isInteracting) return  // 有锁,跳过同步
  
  // 正常同步...
}

时间轴联动

贴纸有出场和退场时间,需要和时间轴联动。

typescript 复制代码
function onTimeUpdate(currentTime: number) {
  // 1. 获取当前时间应该显示的贴纸
  const activeClips = store.getActiveStickers(currentTime)
  
  // 2. 遍历所有贴纸
  for (const clip of activeClips) {
    let leaferObj = leaferMap.get(clip.id)
    
    // 如果不存在,创建
    if (!leaferObj) {
      leaferObj = createLeaferImage(clip)
      app.tree.add(leaferObj)
      leaferMap.set(clip.id, leaferObj)
    }
    
    // 如果没在交互,从 Store 同步位置
    if (!isInteracting) {
      leaferObj.x = toPixel(clip.transform.x, canvasWidth)
      leaferObj.y = toPixel(clip.transform.y, canvasHeight)
      leaferObj.scaleX = clip.transform.scaleX
      leaferObj.scaleY = clip.transform.scaleY
      leaferObj.rotation = clip.transform.rotation
    }
  }
  
  // 3. 移除不该显示的贴纸
  for (const [id, obj] of leaferMap) {
    if (!activeClips.find(c => c.id === id)) {
      obj.remove()
      leaferMap.delete(id)
    }
  }
}

导出时的贴纸渲染

预览时用 LeaferJS 渲染贴纸(支持交互),导出时用 WebGL 渲染贴纸(性能更好)。

为什么导出时换 WebGL?

复制代码
LeaferJS(Canvas 2D):
├── 优点:交互方便
├── 缺点:每帧都要 drawImage,叠加多个贴纸时性能下降

WebGL:
├── 优点:GPU 加速,性能稳定
├── 缺点:不支持交互(但导出时不需要交互)

导出渲染逻辑

typescript 复制代码
function renderFrame(time: number) {
  // 1. 渲染视频底图
  webgl.drawVideo(videoFrame)
  
  // 2. 获取当前时间的贴纸
  const stickers = store.getActiveStickers(time)
  
  // 3. 按层级顺序渲染
  for (const sticker of stickers) {
    // 把贴纸图片作为纹理
    const texture = getTexture(sticker.resourceId)
    
    // 应用变换矩阵
    webgl.drawTexture(texture, {
      x: sticker.transform.x * canvasWidth,
      y: sticker.transform.y * canvasHeight,
      scaleX: sticker.transform.scaleX,
      scaleY: sticker.transform.scaleY,
      rotation: sticker.transform.rotation,
      opacity: sticker.transform.opacity
    })
  }
}

系列总结

六篇写完了!回顾一下整个项目用到的核心技术:

模块 技术 为什么选它
前端框架 Vue 3 响应式系统适合复杂状态联动
类型系统 TypeScript 3 万行代码必须有类型约束
状态管理 Pinia 简洁,比 Vuex 好用
视频渲染 WebGL 60fps 必须 GPU 加速
视频编码 WebCodecs 硬件加速,比 FFmpeg.wasm 快 10 倍
MP4 封装 mp4-muxer 轻量,专注做一件事
贴纸渲染 LeaferJS 性能炸裂,编辑器开箱即用

这套方案的局限

  • Safari 不支持 WebCodecs,暂时无法导出
  • 4K 导出比较慢(WebGL 渲染是瓶颈)
  • 音频处理还比较简陋(只支持简单混音)

但作为一个 MVP,已经能满足基本需求了。

如果你也想做类似的项目,希望这个项目能帮到你。


系列目录

  1. 技术选型与项目结构
  2. 时间轴数据模型
  3. WebGL 渲染与滤镜
  4. 转场动画实现
  5. WebCodecs 视频导出
  6. LeaferJS 贴纸系统(本文)
相关推荐
lingyubb1 小时前
影视解说配音工具的技术选型与对比(行业观察)
音视频·ai配音·多语种
科技小E7 小时前
监控设备品牌杂乱无法互通?EasyGBS打通孤岛,统一汇聚视频
服务器·网络·音视频
EasyGBS12 小时前
通信基站/铁塔机房无人值守,国标GB28181公网平台EasyGBS视频能力怎么去管
深度学习·安全·音视频
鲲穹AI种草13 小时前
音频剪辑降噪格式转换,鲲穹音频处理大师实测分享
音视频·音频剪辑
江节胜-胜行全栈AI13 小时前
实现海康/宇视NVR硬盘录像机/IPC摄像机监控历史回放录像rstp流视频的倍数下载
音视频
林墨聊AIGC14 小时前
AI分镜模板:一键生成高效短剧视觉流程
人工智能·ai作画·aigc·音视频·ai写作
程序员JerrySUN14 小时前
Jetson Edge AI 实战05:Secure Boot 到底在保护什么?PKC、SBK、Fuse 与可信启动链一次讲清【视频讲解】
arm开发·人工智能·安全·计算机视觉·目标跟踪·音视频
voipmaker14 小时前
选煤厂智能化:UWB 人员定位系统 WebAPI 对接融合通信平台实战
物联网·实时互动·json·音视频·实时音视频·web·视频
木头左16 小时前
音频转文字与说话人识别部署与使用指南
音视频
DogDaoDao16 小时前
当视频理解撞上大语言模型:一篇 37 页综述的全景拆解
人工智能·语言模型·音视频·知识图谱·视频