@onerjs/animation-editor
| Install | |
|---|---|
npm install @onerjs/animation-editor |
|
| Latest Version: | 0.1.33 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 20, 2026 |
| Links: | npm |
@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 且有 targetalpha水平角 (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标签 条件 uOffsetU 偏移 仅 Texture(等有该字段的)vOffsetV 偏移 同上 uScaleU 缩放 同上 vScaleV 缩放 同上 level亮度 所有 BaseTexturerotationY旋转 Y CubeTextureboundingBoxSize包围盒尺寸 CubeTexture场景(
scene,仅当useSceneAnimation为true):path标签 clearColor背景颜色(RGBA) ambientColor环境光颜色 environmentIntensity环境强度 iblIntensityIBL 强度 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事件,回传按目标分组的 BabylonAnimation[](AnimationSaveGroup[]),由宿主决定是否挂载/持久化;也可直接调用store.buildAnimationGroups(); - 工具栏「保存为动画组」触发
save-group事件,回传一个包含全部动画(按目标绑定)的 BabylonAnimationGroup(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 # 可拖拽 / 可缩放的浮动面板容器
工作原理
- 扫描:
scanScene(scene)遍历meshes / transformNodes / lights / cameras / materials / textures, 为每类对象注册一组PropertyDefinition(含get/set、组件标签、Babylon 数据类型与targetProperty)。 - 建模:每个(属性, 分量)对应一条
AnimationChannel(即一条 F-Curve),内部保存标量Keyframe(贝塞尔手柄、插值方式、自动切线标记)。 - 求值:
evaluateChannel对线性 / 阶梯 / 三次贝塞尔分段求值,贝塞尔通过二分求解 x→t 再取 y。 - 应用:
applyFrame只覆盖「拥有通道」的分量,其余分量保持对象当前值,然后写回 Babylon 对象。 - 导出:
exportBabylonAnimations把同一属性的各分量按帧合并回一条 BabylonAnimation; 含贝塞尔的通道按帧烘焙(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
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 | |
| 0.1.28 | MIT | |
| 0.1.27 | MIT | |
| 0.1.26 | MIT | |
| 0.1.25 | MIT | |
| 0.1.24 | MIT | |
| 0.1.23 | MIT | |
| 0.1.22 | MIT | |
| 0.1.21 | MIT | |
| 0.1.20 | MIT | |
| 0.1.19 | MIT | |
| 0.1.18 | MIT | |
| 0.1.17 | MIT | |
| 0.1.16 | MIT | |
| 0.1.15 | MIT | |
| 0.1.14 | MIT | |
| 0.1.13 | MIT | |
| 0.1.12 | MIT | |
| 0.1.11 | MIT | |
| 0.1.10 | MIT | |
| 0.1.9 | MIT | |
| 0.1.8 | MIT | |
| 0.1.7 | MIT | |
| 0.1.6 | MIT | |
| 0.1.5 | MIT | |
| 0.1.4 | MIT |
Showing the latest 30 of 34. See every release on npm
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |