@onerjs/animation-editor

A Blender-like keyframe & curve animation editor for Babylon.js, built with Vue 3 + TypeScript.
2,379
Install
npm install @onerjs/animation-editor
Latest Version:0.1.33
Vue:3
License:MIT
Last Updated:Oct 20, 2026
Links: npm
Maintainer: richard_lin

@onerjs/animation-editor

一个用 Vue 3 + TypeScript 实现的、类 Blender 的 Babylon.js 关键帧 / 曲线动画编辑器组件库。

它把 @babylonjs/inspector-v2 中那套 React 版 AnimationCurveEditor 的能力,重写为 可自由定制、易用性更高的 Vue 组件,并直接针对 节点、材质、贴图、灯光、相机 的属性做 动画编辑、播放、导出与导入。

无 DOM 之外依赖,核心逻辑与 UI 解耦:既可以整体使用 <AnimationEditor>,也可以只用 useAnimationEditor() 做无界面(headless)集成。


特性

  • 曲线编辑器(Graph Editor):基于 Canvas 绘制 F-Curve,支持拖拽关键帧、拖拽贝塞尔手柄、 框选、平移缩放(滚轮 / 中键 / Alt)、适应视图(F)。支持多条曲线多选(点曲线/通道行切换选中, 点关键帧选中其所属通道),反选只影响单条曲线;通道较多时活动/悬停通道高亮加粗、其余自动变淡, 与时间轴行双向联动,支持「聚焦」只显示当前通道,并在聚焦曲线上标注通道名。

  • 时间轴 / 摄影表(Dope Sheet):逐通道列出关键帧菱形,拖拽移动、点击选中、拖动标尺逐帧预览。 标题栏带全局「眼睛」(一键显示 / 隐藏所有通道,状态含全显 / 全隐 / 部分)与「清空」。 「打关键帧」按钮在当前帧记录场景实时值:有选中通道时只作用于选中的通道(支持多选), 未选择任何通道时作用于面板内全部通道;「记录全部」则始终作用于全部通道。 插入后曲线编辑器会自动扩展值轴把新关键帧纳入视野(仅在其超出当前视图时扩展,帧轴不变)。

  • 播放控制:播放 / 暂停 / 停止 / 上一帧 / 下一帧 / 循环 / 变速(0.25x–4x),可设 FPS 与开始/结束帧(循环区间)。 播放由场景的 onBeforeRenderObservable 驱动,与 Babylon 渲染循环同步(场景停止渲染即自动暂停)。

  • 插值方式:贝塞尔(自动 / 手动切线)、线性、阶梯,可对选中关键帧批量切换。 离散属性(如 billboardMode、布尔开关、renderingGroupId)强制使用阶梯插值,工具栏中贝塞尔/线性对其禁用。

  • 对象浏览器:自动扫描场景,列出所有可动画目标及其属性,一键加入动画通道; 弹窗列出场景全部可动画对象,选中不在 objects 中的对象属性时会自动把该对象纳入编辑器。 当某节点拥有动画通道时(如导入动画后),左侧节点行会自动展开(用户手动折叠过的除外)。 每个节点行带一个「眼睛」按钮,可一键显示 / 隐藏该节点的全部动画通道(对应时间轴里的通道显隐), 眼睛状态反映整体:全部显示=正常、全部隐藏=置灰、部分隐藏=半亮;属性行左侧的彩色圆点也会随通道显隐变暗。

  • 多种动画目标(左侧属性标签均已汉化)。各目标可动画属性对照如下(path 即通道的 propertyPath):

    节点 / 网格(node / mesh):

    path 标签 说明
    position 位置 Vector3
    rotation 旋转 欧拉角,当节点无四元数时暴露
    rotationQuaternion 旋转(四元数) 当 rotationQuaternion != null 时暴露(如 glTF 骨骼);此时欧拉 rotation 无效
    scaling 缩放 Vector3
    billboardMode 广告牌模式 离散(阶梯)
    visibility 可见度 0–1
    outlineWidth 描边宽度
    outlineColor 描边颜色 Color3
    overlayColor 叠加层颜色 Color3
    overlayAlpha 叠加层透明度 0–1
    alphaIndex 透明排序 离散(阶梯)
    renderingGroupId 渲染分组 离散(阶梯)
    edgesWidth 边线宽度
    edgesColor 边线颜色 Color4
    isVisible 显示/隐藏 布尔
    isPickable 可拾取 布尔
    receiveShadows 接收阴影 布尔
    renderOverlay 渲染叠加层 布尔
    renderOutline 渲染描边 布尔

    灯光(light):

    path 标签 条件
    intensity 强度
    diffuse 漫反射颜色 Color3
    specular 高光颜色 Color3
    position 位置 有 position 时
    direction 方向 有 direction 时
    range 范围 有 range 时
    angle 角度 SpotLight
    exponent 衰减指数 SpotLight
    groundColor 地面颜色 HemisphericLight

    相机(camera):

    path 标签 条件
    position 位置
    fov 视野
    minZ 近裁剪面
    maxZ 远裁剪面
    inertia 惯性
    target 目标点 TargetCamera 且有 target
    alpha 水平角 (Alpha) ArcRotateCamera
    beta 俯仰角 (Beta) ArcRotateCamera
    radius 半径 ArcRotateCamera
    rotation 旋转 FreeCamera

    材质(material)——公共:

    path 标签
    alpha 透明度(0–1)

    StandardMaterial:

    path 标签
    diffuseColor 漫反射颜色
    specularColor 高光颜色
    emissiveColor 自发光颜色
    ambientColor 环境光颜色
    specularPower 高光锐度

    PBRMaterial:

    path 标签
    albedoColor 基础色
    emissiveColor 自发光颜色
    emissiveIntensity 自发光强度
    reflectivityColor 高光颜色
    reflectionColor 反射颜色
    metallicReflectanceColor 金属反射色
    metallicF0Factor 金属 F0 值
    ambientColor 环境光颜色
    ambientTextureStrength 环境纹理强度
    specularIntensity 高光强度
    metallic 金属度
    roughness 粗糙度
    baseDiffuseRoughness 漫反射粗糙度
    indexOfRefraction 折射率 (IOR)
    microSurface 微表面光泽度
    environmentIntensity 环境强度
    directIntensity 直接光强度

    PBRMaterial 次表面(subSurface.*):

    path 标签
    subSurface.isRefractionEnabled 次表面·启用折射
    subSurface.isTranslucencyEnabled 次表面·启用半透明
    subSurface.isDispersionEnabled 次表面·启用色散
    subSurface.isScatteringEnabled 次表面·启用散射
    subSurface.indexOfRefraction 次表面·折射率
    subSurface.refractionIntensity 次表面·折射强度
    subSurface.translucencyIntensity 次表面·半透明强度
    subSurface.minimumThickness 次表面·最小厚度
    subSurface.maximumThickness 次表面·最大厚度
    subSurface.tintColor 次表面·染色
    subSurface.tintColorAtDistance 次表面·染色距离
    subSurface.dispersion 次表面·色散
    subSurface.diffusionDistance 次表面·扩散距离
    subSurface.useAlbedoToTintRefraction 次表面·基础色染色折射
    subSurface.useAlbedoToTintTranslucency 次表面·基础色染色半透明
    subSurface.invertRefractionY 次表面·反转折射 Y
    subSurface.linkRefractionWithTransparency 次表面·折射联动透明
    subSurface.useThicknessAsDepth 次表面·厚度作深度

    贴图(texture,识别所有 BaseTexture 子类:Texture、CubeTexture、HDRCubeTexture、VideoTexture、MirrorTexture、RenderTargetTexture、ReflectionProbeTexture 等):

    path 标签 条件
    uOffset U 偏移 仅 Texture(等有该字段的)
    vOffset V 偏移 同上
    uScale U 缩放 同上
    vScale V 缩放 同上
    level 亮度 所有 BaseTexture
    rotationY 旋转 Y CubeTexture
    boundingBoxSize 包围盒尺寸 CubeTexture

    场景(scene,仅当 useSceneAnimation 为 true):

    path 标签
    clearColor 背景颜色(RGBA)
    ambientColor 环境光颜色
    environmentIntensity 环境强度
    iblIntensity IBL 强度
    fogMode 雾模式(离散)
    fogColor 雾颜色
    fogDensity 雾强度
    fogStart 雾起点
    fogEnd 雾终点
    animationTimeScale 动画时间缩放

    离散 / 布尔属性通过 PropertyDefinition.stepOnly = true 标记(如 billboardMode、各类布尔开关、renderingGroupId、alphaIndex、fogMode), 编辑器会强制其关键帧使用阶梯插值且不允许改为其它类型。布尔在底层以 FLOAT 0/1 + 阶梯表示(Babylon 无 boolean 动画类型)。

  • Babylon 原生互操作:

    • 导出为 Animation[],可直接 scene.beginDirectAnimation() 播放;
    • 关键帧为空的通道也会被导出(生成无关键帧的 Animation,便于宿主做全量覆盖/清理);只跳过**隐藏(禁用)**的通道;
    • 分量不完整时拆分导出:某多分量属性(如 position)若缺少某个分量通道(例如用户删除了 position.y),则不再拼成带 0 的残缺 Vector3,而是把保留的分量导出为组件级标量动画(position.x / position.z),缺失分量完全不参与(与编辑器预览一致);分量齐全时才合并为一条 Vector3;
    • 工具栏「保存到场景」触发 save 事件,回传按目标分组的 Babylon Animation[](AnimationSaveGroup[]),由宿主决定是否挂载/持久化;也可直接调用 store.buildAnimationGroups();
    • 工具栏「保存为动画组」触发 save-group 事件,回传一个包含全部动画(按目标绑定)的 Babylon AnimationGroup(store.buildAnimationGroup(name?, metadata?)),宿主可 scene.addAnimationGroup(group); group.start(true); 导出的组会带上编辑器当前播放上下文:group.from = startFrame、group.to = duration、group.speedRatio = speed、group.loopAnimation = loop(帧率已写入各 Animation.framePerSecond);
    • 传入 saveAnimationGroup 绑定一个动画组后,工具栏隐藏「保存为动画组」按钮,「保存」按钮改为保存为动画组(同样触发 save-group),并保留该组的 name/metadata;
    • 从场景对象上已有的 Animation 反向导入为可编辑通道;
    • 导入的 Animation 若带 metadata,在 save / save-group 导出时会保留到新的动画上(通道仍存在时);
    • 无损往返:导出的 Animation.metadata 会内嵌编辑器原始关键帧数据(保留键 EDITOR_METADATA_KEY = "bjsAnimationEditor")。 多分量贝塞尔通道为满足 Babylon 运行时而逐帧烘焙(Animation.getKeys() 会变密),但宿主只需原样持久化 metadata, 再通过 saveTargetAnimations / saveAnimationGroup 传回,编辑器即会据此还原出原始的稀疏关键帧与插值/手柄,不会出现导入导出不一致。 宿主自身的 metadata 键会与编辑器块同级并存,不会被覆盖;
    • 防脏数据:编辑器块内含 signature(导出时对烘焙关键帧的指纹)。导入时若 Animation 的关键帧被宿主修改过、 指纹不再匹配,则忽略已过期的 metadata,改为解析实际关键帧——即宿主的改动优先,不会被旧曲线覆盖;
    • 导入时若同一目标同一属性存在多条动画,会弹出对话框让用户选择保留哪一条;未选择或关闭对话框则默认保留最后一条;
    • 编辑器状态可序列化为 JSON 并重新载入。
  • 内置快捷键帮助:工具栏「快捷键」按钮弹出面板,随时查阅所有操作(见下方“交互快捷键”)。

  • 零强绑定:组件只依赖 vue 与 @onerjs/core(均为 peerDependency)。


安装

npm install @onerjs/animation-editor @onerjs/core vue

快速开始

<script setup lang="ts">
import { Engine, Scene, ArcRotateCamera, Vector3, MeshBuilder } from '@onerjs/core'
import { AnimationEditor } from '@onerjs/animation-editor'
import '@onerjs/animation-editor/style.css'
import { onMounted, shallowRef, ref } from 'vue'

const canvasRef = ref<HTMLCanvasElement | null>(null)
const scene = shallowRef<Scene | null>(null)

onMounted(() => {
  const engine = new Engine(canvasRef.value!, true)
  const s = new Scene(engine)
  const camera = new ArcRotateCamera('camera', -Math.PI / 2, Math.PI / 3, 10, Vector3.Zero(), s)
  camera.attachControl(canvasRef.value!, true)
  const box = MeshBuilder.CreateBox('box', { size: 1 }, s)
  // ... 构建你的场景 ...
  engine.runRenderLoop(() => s.render())
  scene.value = s
})
</script>

<template>
  <canvas ref="canvasRef" />
  <AnimationEditor
    v-if="scene"
    :scene="scene"
    :objects="[box, light, material]"
    :fps="30"
    :duration="120"
  />
</template>

objects 决定左侧面板暴露哪些对象(可传 Babylon 实例或对象名)。为空时左侧面板没有可编辑对象。

运行 Demo

npm install
npm run dev      # 打开 http://localhost:5173

Demo 会创建一个包含 PBR 球体、可旋转方块、动态贴图平面与点光源的场景。 点击右下角「打开动画编辑器」,用「+」添加属性动画,即可编辑曲线并播放。

骨骼动画测试:点「加载 x-bot.glb」加载 models/x-bot.glb(含 1 个骨架、65 根骨骼、5 个动画组), 再用模型下方按钮选择其中一个动画组(Climbing / fall / idle / jump / run)绑定到编辑器。 该模型的骨骼动画以 TransformNode + rotationQuaternion 表达,会被导入为可编辑通道; 在编辑器里点播放即可看到骨骼动画正常播放(此前因旋转通道匹配不上而被丢弃,看起来像“没播放”)。

Demo 左下角还提供性能压测按钮(走 saveAnimationGroup 导入路径),可一键导入大量通道 / 密集关键帧,用来验证时间轴与曲线编辑器在大数据量下的流畅度:

  • 120 通道 × 40 帧:压测「通道数多」的行虚拟化与画布渲染;
  • 1 通道 × 4000 帧:压测「关键帧密集」的曲线绘制与命中;
  • 清除:卸载压测动画组。

GLB 加载依赖 @onerjs/loaders(已在 devDependencies 中,仅 Demo 需要,运行时按需 import '@onerjs/loaders/glTF/index.js' 注册 glTF 插件)。


性能

时间轴(Dope Sheet)与曲线编辑器针对「通道多、关键帧多」做了针对性优化:

  • 时间轴行虚拟化:只渲染视口内的行(± overscan),行数再多 DOM 也保持恒定。
  • 关键帧画布化:每行的关键帧菱形画在 <canvas> 上,不再是「一键一个 DOM 节点 + 一个响应式 effect」。
  • 曲线编辑器:按 store.state.revision 浅监听失效(不再 deep 遍历整棵通道树);只绘制可见帧区间(二分定位);命中/悬停节流到 rAF 并缓存画布 rect;主题色读取一次缓存。
  • 曲线 LOD 与批量绘制:关键帧菱形按通道批量成一个路径(一次 fill + 一次 stroke),不再逐键 beginPath;当关键帧密集到亚像素(间距 < 1.5px,缩放很小时)或可见关键帧总数超过阈值时,自动切换为抽稀折线并只给「活动 / 悬停 / 选中」通道画菱形;单通道点数按画布宽度封顶。
  • 静态层缓存:网格 + 曲线 + 关键帧渲染到离屏 canvas,仅在数据 / 视口 / 选中变化时重建;播放时只 blit 静态层并画播放头,因此哪怕几百条密集通道也不会每帧重绘。
  • 四元数骨骼:applyFrame 对四元数属性用 slerp + 归一化求值(四个分量对齐时),避免逐分量线性插值导致的骨骼“跳跃/扭曲”。
  • O(1) 查找:store.selectedKeySet(选中键 Set)与内部 channelIndex(通道 id → 通道)避免线性扫描;moveKeyframes 一次分组、一次 apply。
  • store revision:任何通道 / 关键帧变更都会 state.revision++,视图据此用 O(1) 的浅监听触发重绘。

组件 API

<AnimationEditor>

Prop 类型 默认 说明
scene Scene — 必填,要编辑的 Babylon 场景
fps number 30 帧率
startFrame number 0 动画开始帧(播放循环起点、时间轴与「适应」的参考起点)
duration number 120 结束帧(时间轴长度、播放结束帧)
showIo boolean false 是否在工具栏显示导入/导出按钮(导入场景、导出/导入 JSON)
objects Array<object | string> [] 左侧对象面板默认显示的对象(Babylon 实例或对象名)。默认为空,不自动加载全部场景对象;但「添加动画属性」弹窗始终列出场景中所有可动画对象,若选中未在 objects 中的对象的属性,该对象会被自动纳入编辑器
useSceneAnimation boolean false 为 true 时把 Scene 本身也作为可动画目标暴露(背景色、雾、环境光等)
selectChannelOnClick boolean true 在曲线画布空白处点击时,按最近曲线选中对应通道(再次点击同一曲线取消选中)
groupChannels boolean false 时间轴按对象分组显示,支持折叠(折叠时显示一条合并关键帧示意)
nodeInterceptor AnimationTargetInterceptor<Node> — 「添加动画属性」弹窗中 node/mesh/camera/light 列表项的拦截器
materialInterceptor AnimationTargetInterceptor<Material> — 材质 列表项的拦截器
textureInterceptor AnimationTargetInterceptor<BaseTexture> — 贴图 列表项的拦截器
saveTargetAnimations AnimationRecord[] [] 传入后自动解析为编辑器通道,AnimationRecord = { object, animations }。动画的 targetProperty 支持属性级(如 position)与组件级(如 position.x、color.r、rotationQuaternion.w):组件级导入会记为组件级导出(导出为同名 position.x 的标量 Animation,保持原样,且同样内嵌编辑器原始曲线 metadata,可无损再导入),编辑器内新建的通道仍按属性合并为向量动画;若删除了该导入通道则回退为默认(向量)导出。再次传入会先清除上一次由该 prop 导入的通道(按来源标记),再重新解析;传空数组可清除
saveAnimationGroup AnimationGroup | null null 传入 Babylon 动画组后自动解析为编辑器通道(按目标绑定)。存在时隐藏「保存为动画组」按钮,并把「保存」按钮改为保存为动画组:点击时用当前通道重新构建 AnimationGroup 并触发 save-group,同时保留该组的 name 与 metadata(组内动画的 metadata 也会随通道往返保留)。再次传入会替换上一次由该 prop 导入的通道;传 null 可清除

AnimationTargetPickerItem<T> = { id, name, kind, kindLabel, object: T, target } AnimationTargetInterceptor<T> = (object: T, item: AnimationTargetPickerItem<T>) => AnimationTargetPickerItem<T> \| null \| undefined 第一个参数是场景中的真实 Babylon 对象(类型由泛型 T 指定),第二个是列表中要显示的数据。 返回修改后的 item 即可自定义显示,返回 null/undefined 则该项不显示。 若在拦截器中替换了 item.object,编辑器会据此重新推导 id / kind / kindLabel / target(含 target.properties)。

<AnimationEditor
  :scene="scene"
  :objects="[box]"
  :node-interceptor="(obj, item) => (obj.name.startsWith('_') ? null : { ...item, name: obj.name })"
  :material-interceptor="(obj, item) => item"
  :texture-interceptor="() => null"
/>

<!-- 绑定一个动画组:隐藏「保存为动画组」按钮,「保存」按钮改为保存为动画组 -->
<AnimationEditor :scene="scene" :objects="[box]" :save-animation-group="boundGroup" />
事件 载荷 说明
ready AnimationEditorStore 组件挂载并完成场景扫描后触发
frame-change (frame: number, time: number) 当前帧(播放头)变化,time = frame / fps
channel-change (channelId: string | null, channel: AnimationChannel | null) 当前编辑通道变化
keyframe-change (info: KeyframeChangeInfo) 通道关键帧发生变化(增 / 删 / 移动 / 改值 / 插值 / 手柄)
save (groups: AnimationSaveGroup[], info: AnimationSaveInfo) 点击「保存」时触发(未绑定 saveAnimationGroup)。按 target 分组整理好的 Babylon Animation[],由宿主决定是否挂载/持久化
save-group (group: AnimationGroup, info: AnimationSaveInfo) 点击「保存为动画组」或(绑定了 saveAnimationGroup 时)点击「保存」时触发。返回一个包含全部动画(按目标绑定)的 Babylon AnimationGroup,并保留绑定组的 name/metadata

KeyframeChangeInfo:{ action: 'add' \| 'value' \| 'remove' \| 'move' \| 'interpolation' \| 'handle' \| 'auto'; channelId: string; keyId?: string; keys: Keyframe[] }。

AnimationSaveInfo:{ startFrame: number; endFrame: number; fps: number; speed: number }(endFrame 即时间轴的 duration),随 save / save-group 一起回传当前编辑器的时间区间、帧率与播放速度。

<AnimationEditor
  :scene="scene"
  @frame-change="(f, t) => console.log('frame', f, t)"
  @channel-change="(id, ch) => console.log('channel', id, ch?.propertyLabel)"
  @keyframe-change="(info) => console.log('key', info.action, info.channelId)"
  @save="(groups, info) => {
    // groups: Array<{ targetId, targetName, targetKind, object, animations: Animation[] }>
    // info: { startFrame, endFrame, fps }
    for (const g of groups) {
      g.object.animations = (g.object.animations ?? []).filter(a => !a.name.startsWith('__editor__')).concat(
        g.animations.map(a => (a.name = '__editor__' + a.name, a))
      )
    }
  }"
  @save-group="(group, info) => {
    console.log('range', info.startFrame, info.endFrame, info.fps)
    scene.addAnimationGroup(group)
    group.start(true) // 循环播放
  }"
/>

Headless 用法下可用 useAnimationEditor({ scene, onEvent }) 接收同样的三类事件。

内部布局为:工具栏 + 对象面板 + 曲线编辑器 + 摄影表 + 属性选择弹窗。

AnimationEditor 同时把 store 与时间轴视口通过 provide 注入,子组件用 inject(ANIMATION_EDITOR_KEY) / inject(TIMELINE_VIEWPORT_KEY) 获取。若你想自定义界面, 可以直接使用这些注入键组合出你自己的布局。

AnimationEditor 也通过 defineExpose 暴露便捷方法,可用模板 ref 调用:

成员 说明
store 完整的 AnimationEditorStore(可用全部 store 方法,如 addKeyframe / buildAnimationGroup 等)
insertKeyframe(channelId, frame?) 在指定通道的 frame(默认当前帧)按该属性当前场景值插入关键帧;通道不存在时返回 false,否则返回 true
getChannels() 返回当前编辑器内的全部通道(AnimationChannel[])
getSelectedChannels() 返回当前已选中的通道(AnimationChannel[],含多选与活动通道)
getCurrentFrame() 返回当前帧(播放头位置,number)
setStartFrame(frame) 设置播放起始帧(钳制到 [0, duration])
setEndFrame(frame) 设置播放结束帧(即 duration,钳制为 >= 0 且 >= startFrame)
setFps(fps) 设置帧率(仅接受 > 0 的值)
setSpeed(speed) 设置播放速度倍率(仅接受 > 0 的值)
setLoopMode(loop) 设置是否循环播放(对应工具栏「循环」开关)
getChannelAnimateItems() 返回每个动画通道一条的 { object, propertyPath }[]:propertyPath 为组件级路径(如 position.x、rotationQuaternion.w),单分量属性用属性名(如 intensity)。结果用 computed 缓存(随 revision 失效)
<AnimationEditor ref="editorRef" :scene="scene" />
<script setup lang="ts">
import { ref } from 'vue'
const editorRef = ref()
// 选中某条通道后,在当前帧按它的实时场景值插一帧
editorRef.value?.insertKeyframe('ch_xxx')
</script>

<FloatingPanel>

一个可拖拽移动、可调整大小的浮动面板容器,常用来包裹 <AnimationEditor> 做成弹窗:

Prop 类型 默认 说明
v-model boolean true 显隐控制(false 时不渲染;未绑定时默认显示)
title string '' 标题栏文字
width / height number 980 / 660 初始尺寸
minWidth / minHeight number 420 / 300 最小尺寸
offsetX / offsetY number 0 相对居中的偏移
movable boolean true 是否允许拖拽标题栏移动
resizable boolean true 是否显示 8 向缩放手柄
storageKey string '' 设置后将位置/尺寸持久化到 localStorage,关闭重开或刷新后自动恢复
事件 说明
close 点击关闭按钮时触发(同时会把 v-model 置为 false)
插槽 说明
default 面板内容
actions 标题栏右侧额外按钮

defineExpose 暴露 center()(重新居中)与 rect(当前 {x,y,w,h})。

<!-- 显隐交给 v-model,无需额外 overlay 包裹 -->
<FloatingPanel v-model="open" title="动画编辑器">
  <template #actions>
    <button @click="panelRef?.center()">居中</button>
  </template>
  <AnimationEditor :scene="scene" />
</FloatingPanel>

面板以父元素为边界定位,父容器需为 position: relative/absolute。

useAnimationEditor(options)

Headless 用法,返回响应式 store:

import { useAnimationEditor } from '@onerjs/animation-editor'

const store = useAnimationEditor({ scene, fps: 30, duration: 120 })
store.syncScene()
// 或仅暴露指定对象:useAnimationEditor({ scene, objects: [box, light] })

// 给 box 的 position 添加动画(会为 x/y/z 各生成一条通道)
const target = store.state.targets.find((t) => t.name === 'box')!
store.addProperty(target.id, 'position')

// 打关键帧 / 播放
store.setFrame(0)
store.insertAllKeyframes()
store.setFrame(60)
store.state.channels.forEach((c) => store.addKeyframe(c.id, 60, /* value */ undefined))
store.play()

主要方法:

分类 方法
场景 syncScene()、getTarget(id)
通道 addProperty(targetId, path)、removeChannel(id)、removeTargetChannels(id)、clearAll()、setChannelEnabled、toggleChannelEnabled、setTargetChannelsEnabled(targetId, enabled)、setAllChannelsEnabled(enabled)
关键帧 addKeyframe(channelId, frame, value?)、insertKeyframeFromScene(id)、insertAllKeyframes()、insertKeyframeFromSceneOnSelected(frame?)、removeKeyframe、removeSelectedKeys、moveKeyframes(moves)、setKeyframeInterpolation、setKeyframeHandle、setHandleAuto
选择 selectKey、setSelectedKeys、clearSelection、isKeySelected
播放 apply()、setFrame(f)、stepFrame(d)、play()、pause()、stop()、togglePlay()
导入导出 exportAnimations()、buildAnimationGroups()、buildAnimationGroup(name?, metadata?)、importSceneAnimations()、importTargetAnimations(records)、importAnimationGroups(groups)、applyImportedAnimations(records, source)、exportJSON()、importJSON(json)
其他 dispose()

响应式 state:fps、duration、currentFrame、playing、loop、speed、snap、 targets、channels、selectedKeys、activeChannelId、selectedTargetId。

独立导出 Babylon 动画

import { exportBabylonAnimations, applyFrame } from '@onerjs/animation-editor'

const animations = exportBabylonAnimations(store.state.targets, store.state.channels, 30)
scene.beginDirectAnimation(box, animations, 0, 120, true, 1)

交互快捷键

操作 说明
左键点击关键帧 选中;Shift 加选
左键拖拽关键帧 移动选中关键帧(默认吸附整数帧,按 Alt 关闭)
拖拽手柄圆点 编辑贝塞尔切线(自动切线改为手动)
左键空白处拖拽 框选关键帧
中键 / 右键 / Alt+左键 平移视图
滚轮 水平缩放;Shift+滚轮垂直缩放
双击 有选中通道时在这些通道上按当前坐标插入关键帧;未选中时对全部(可见)通道按当前坐标插入
F 适应视图
Delete / Backspace 删除选中关键帧

目录结构

src/
├── index.ts                     # 库入口
├── styles/editor.css            # 主题与 CSS 变量
├── core/                        # 与框架无关的核心逻辑
│   ├── types.ts                 # 数据模型(Keyframe / Channel / Property / Target)
│   ├── curves.ts                # 贝塞尔求值、自动切线、范围计算
│   ├── scanner.ts               # 扫描场景,生成可动画属性
│   ├── apply.ts                 # 按帧把通道值写回 Babylon 对象
│   ├── picker.ts                # 添加属性弹窗的列表构建 + 拦截器
│   └── bridge.ts                # 导出/导入 Babylon Animation、JSON 序列化
├── composables/
│   ├── useAnimationEditor.ts    # 响应式 store + 播放循环
│   └── injection.ts             # provide/inject 键
└── components/
    ├── AnimationEditor.vue      # 根组件(布局 + 注入 + Babylon 预览)
    ├── EditorToolbar.vue        # 播放、帧率、插值、导入导出
    ├── TrackPanel.vue           # 场景对象 / 属性浏览器
    ├── CurveEditor.vue          # Canvas 曲线编辑器
    ├── DopeSheet.vue            # 时间轴 / 摄影表(可分组折叠)
    ├── DopeChannelRow.vue       # 时间轴单通道行(关键帧拖拽)
    ├── PropertyPicker.vue       # 添加动画属性弹窗
    ├── ConflictDialog.vue       # 导入重复通道选择弹窗
    ├── ShortcutHelp.vue         # 快捷键说明弹层
    └── FloatingPanel.vue        # 可拖拽 / 可缩放的浮动面板容器

工作原理

  1. 扫描:scanScene(scene) 遍历 meshes / transformNodes / lights / cameras / materials / textures, 为每类对象注册一组 PropertyDefinition(含 get/set、组件标签、Babylon 数据类型与 targetProperty)。
  2. 建模:每个(属性, 分量)对应一条 AnimationChannel(即一条 F-Curve),内部保存标量 Keyframe (贝塞尔手柄、插值方式、自动切线标记)。
  3. 求值:evaluateChannel 对线性 / 阶梯 / 三次贝塞尔分段求值,贝塞尔通过二分求解 x→t 再取 y。
  4. 应用:applyFrame 只覆盖「拥有通道」的分量,其余分量保持对象当前值,然后写回 Babylon 对象。
  5. 导出:exportBabylonAnimations 把同一属性的各分量按帧合并回一条 Babylon Animation; 含贝塞尔的通道按帧烘焙(Babylon 的逐关键帧三次 Hermite 需要显式切线,这里选择烘焙以保证运行时与预览一致), 线性 / 阶梯通道保留稀疏关键帧;同时把编辑器原始曲线写入 Animation.metadata[EDITOR_METADATA_KEY],供无损往返。

脚本

npm run dev        # 启动 Demo 开发服务器
npm run build      # 构建库产物(ESM + UMD + d.ts)到 dist/
npm run typecheck  # vue-tsc 类型检查
npm run preview    # 预览构建产物

License

MIT

Related Packages

@morev/vue-transitions

Shareable Vue transitions library

412,603 222
animated-number-vue

A vue component with number animation

158,718 211
vue-starport

Shared Vue component across routes with animations

57,522 1,949
vue-animated-list

A Vue.js plugin for easily animating `v-for` rendered lists.

4,659 458

Version History

Version Released License
0.1.33 MIT
0.1.32 MIT
0.1.31 MIT
0.1.30 MIT
0.1.29 MIT
Pre-releases (1)
Version Released License
0.0.0-stage