TkAstral3D/packages/sdk/lib/core/animation/TimelineTrack.ts
2025-10-04 23:36:07 +08:00

667 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as THREE from "three";
import {
Timeline,
TimelineRow,
TimelineModel,
TimelineOptions,
TimelineKeyframe,
TimelineInteractionMode,
TimelineKeyframeChangedEvent, TimelineClickEvent
} from "@/core/libs/astral-timeline/animation-timeline";
import {useAddSignal, useDispatchSignal} from "@/hooks";
import { getParentPath,debounce, deepAssign, getNestedProperty } from "@/utils";
import { KeyframeTrackFactory } from "@/core/animation/AnimationManager";
import App from "@/core/app/App";
export interface ITimelineKeyframe extends TimelineKeyframe {
data: number[] | boolean[]
}
export interface ITimelineRow extends TimelineRow {
id: string;
name: string;
keyframes?: ITimelineKeyframe[];
track?: THREE.KeyframeTrack;
}
export interface ITimelineModel extends TimelineModel {
rows: ITimelineRow[]
}
// 定义事件类型
type CustomEvents = {
'contextmenu': { args: TimelineClickEvent };
'mousedown': { args: TimelineClickEvent };
};
let _aniamtionMixerUpdateFn;
class TimelineTrack extends THREE.EventDispatcher<CustomEvents> {
container: HTMLDivElement;
outlineContainer: HTMLDivElement;
timeline: Timeline;
model: ITimelineModel;
options: TimelineOptions;
/**
* 动画编辑轨道当前正在处理的(绑定的)动画
*/
bindAction: THREE.AnimationAction | null = null;
private resizeObserver: ResizeObserver;
constructor(container: HTMLDivElement, outlineContainer: HTMLDivElement, _options: TimelineOptions) {
super();
this.container = container;
this.outlineContainer = outlineContainer;
this.model = {rows: []} as ITimelineModel;
this.options = {
id: container,
headerHeight: 40,
font: "0.7rem sans-serif",
leftMargin: 22,
headerFillColor: "#00000066",
fillColor: "#333333",
labelsColor: "#FFFFFFCC",
tickColor: "#FFFFFF4C",
// 选中矩形颜色
selectionColor: "blue",
zoom: 120,
zoomMin: 30,
zoomMax: 120,
// 一步的长度默认一步一个像素代表1000ms
stepVal: 1000,
rowsStyle: {
height: 40,
fillColor: "#252526",
marginBottom: 2,
// 关键帧样式
keyframesStyle: {
fillColor: "#9A9A9A"
},
// 组的样式。关键帧组也可以单独设置样式。
groupsStyle: {
text: {
label: "",
isStroke: false,
font: "1.5rem sans-serif",
textAlign: "center",
textBaseline: "top",
direction: "inherit",
fillColor: "#fff"
}
}
},
// 时间轴指示器样式(竖线)
timelineStyle: {
marginTop: 0,
fillColor: "#00ff00",
strokeColor: "#00ff00",
cursor: "e-resize",
// 顶帽样式
capStyle: {
width: 8,
height: 12,
fillColor: "#00ff00",
capType: "rect"
}
},
// 关键帧组可拖动
groupsDraggable: true,
// 关键帧可拖动
keyframesDraggable: true,
// 用于确定要呈现的仪表“漂亮”数字的分母数组。
denominators: [1, 6]
} as TimelineOptions;
deepAssign(this.options, _options);
this.timeline = this.init();
this.updateTrackLength();
this.initEvent();
this.resizeObserver = new ResizeObserver(this.resize.bind(this));
this.resizeObserver.observe(container);
}
// 当前所有关键帧中的最大值,单位为ms
get _maxDuration() {
let max = 0;
this.model.rows.forEach((row) => {
if (!row.keyframes) return;
row.keyframes.forEach((kf) => {
if (kf.val > max) {
max = kf.val;
}
});
});
return max;
}
init() {
// const dpr = window.devicePixelRatio || 1;
// this.container.style.width = this.container.width / scale + 'px';
// this.container.style.height = this.container.height / scale + 'px';
const tl = new Timeline(this.options, this.model);
// 可横向拖动
tl.setInteractionMode(TimelineInteractionMode.Pan);
//重写方法来更改显示的单位文本,显示为 00:00
tl._formatUnitsText = (val) => {
const v = Math.floor(val / 1000);
const minutes = Math.floor(v / 60);
const seconds = v - minutes * 60;
return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
};
if (window.devicePixelRatio !== 1) {
tl._pixelRatio = window.devicePixelRatio;
const scale = 1 / tl._pixelRatio;
const translate = (1 - scale) * 100 / 2 * window.devicePixelRatio;
if (tl._canvas) {
tl._canvas.style.transform = `scale(${scale}) translate(-${translate}%, -${translate}%)`;
}
}
return tl;
}
initEvent() {
this.timeline.onScroll((args) => {
//滚动同步
if (this.outlineContainer) {
this.outlineContainer.style.minHeight = args.scrollHeight + "px";
if (this.outlineContainer.parentElement) {
this.outlineContainer.parentElement.scrollTop = args.scrollTop;
}
}
});
// this.timeline.onScrollFinished((args) => {});
// 关键帧被改变时触发(防抖)
const _keyframeChanged = debounce(this.onKeyframeChanged.bind(this), 100);
this.timeline.onKeyframeChanged(_keyframeChanged);
// this.timeline.onSelected((args) => {});
this.timeline.onContextMenu(async (args) => {
// 禁用默认右键菜单
(args.args as MouseEvent).preventDefault();
if (args.elements.length === 0) return;
this.dispatchEvent({type: "contextmenu", args: args});
});
this.timeline.onMouseDown((args) => {
const e = args.args as MouseEvent;
e.stopPropagation();
if (e.button === 2) return;
this.dispatchEvent({type: "mousedown", args: args});
});
this.timeline.onTimeChanged((args) => {
useDispatchSignal("timelineTimeChanged", args);
if (!this.bindAction) return;
this.bindAction.enabled = true;
const _second = args.val / 1000;
const duration = this.bindAction.getClip().duration;
if (_second > duration) {
this.bindAction.time = duration;
} else {
this.bindAction.time = _second;
}
// 如果动作没激活过则激活一次
if (!this.bindAction.isScheduled()) {
this.bindAction.play();
this.bindAction.paused = true;
}
this.bindAction.getMixer().update(0.016);
// this.bindAction.getRoot() 获取到的对象可能是editor.locked对象需要获取正在操作的对象
if (App.selected){
useDispatchSignal("objectChanged", App.selected);
useDispatchSignal("materialChanged", App.selected.material);
}
});
// this.timeline.onDrag((args) => {});
_aniamtionMixerUpdateFn = this.handleMixerUpdate.bind(this);
useAddSignal("animationMixerUpdate", _aniamtionMixerUpdateFn)
}
/**
* 改变时间轴长度,可视区域默认一分钟
*/
updateTrackLength() {
this.options.stepVal = 60 * 1000 / (this.timeline._canvasClientWidth() - (this.options.leftMargin || 30));
this.timeline.setOptions(this.options);
}
/**
* 设置轨道行this.model.rows 永远都只通过此方法变更
*/
setRows(rows: Array<ITimelineRow>) {
const newRows: Array<ITimelineRow> = [];
rows.forEach((row) => {
newRows.push(row);
});
this.model.rows = newRows;
this.timeline.setModel(this.model);
}
/**
* 设置节点是否可见
* @param keys 节点id数组
* @param visible 是否可见
*/
setRowIsVisible(keys: string[], visible: boolean) {
this.model.rows.forEach(row => {
if (keys.includes(row.id)) {
row.hidden = !visible;
}
})
this.timeline.redraw();
}
/**
* 动画混合器更新渲染
* @param mixer d
* @param delta
*/
handleMixerUpdate(mixer: THREE.AnimationMixer, delta: number) {
if (!this.bindAction || !mixer || !delta) return;
if (!this.bindAction.isRunning()) return;
if (this.bindAction.getMixer() !== mixer) return;
const fromPx = this.timeline.scrollLeft;
const toPx = this.timeline.scrollLeft + this.timeline.getClientWidth();
const positionInPixels =
this.timeline.valToPx(this.timeline.getTime()) + this.timeline._leftMargin();
// 如果时间轴超出界限,则滚动至时间轴位置:
if (positionInPixels <= fromPx || positionInPixels >= toPx) {
this.timeline.scrollLeft = positionInPixels;
}
this.timeline.setTime(this.bindAction.time * 1000);
}
/**
* 删除轨道行
* @param row 轨道行
*/
deleteRow(row: ITimelineRow) {
const track = row.track;
if (!this.bindAction || !track) return;
const clip = this.bindAction.getClip();
clip.tracks.splice(clip.tracks.indexOf(track), 1);
// 更新剪辑时间
clip.resetDuration();
// 重新剪辑action
this.bindAction = App.animationManager.reClipAction(this.bindAction,this.timeline.getTime() / 1000) as THREE.AnimationAction;
this.model.rows.splice(this.model.rows.indexOf(row), 1);
// 刷新
this.timeline.redraw();
this.bindAction.getMixer().update(0.016);
useDispatchSignal("sceneGraphChanged");
useDispatchSignal("timelineRowChanged", row, "remove");
}
/**
* 添加关键帧
* @param attr 动画属性名 ('position' | 'rotation' | 'quaternion' |'scale')
*/
addKeyframe(attr: string) {
if (!this.bindAction || !App.selected) return;
// 当前时间轴时间(秒)
const currentTime = this.timeline.getTime() / 1000;
const currentClip = this.bindAction.getClip();
// this.bindAction.getRoot() 获取到的对象可能是editor.locked对象需要获取正在操作的对象
let val = getNestedProperty(App.selected,attr);
const insertValue = (valueTrack: number[] | boolean[], index: number, delLength: number = 0) => {
let keyData: any[];
switch (attr) {
case "position":
case "rotation":
case "scale":
keyData = [val.x, val.y, val.z];
valueTrack.splice(index, delLength, ...keyData);
break;
case "quaternion":
keyData = [val.x, val.y, val.z, val.w];
valueTrack.splice(index, delLength, ...keyData);
break;
case "visible":
case "fov":
case "near":
case "far":
case "intensity":
case "distance":
case "renderOrder":
case "material.shininess":
case "material.reflectivity":
case "material.roughness":
case "material.metalness":
case "material.clearcoat":
case "material.clearcoatRoughness":
case "material.iridescence":
case "material.iridescenceIOR":
case "material.sheen":
case "material.sheenRoughness":
case "material.transmission":
case "material.attenuationDistance":
case "material.thickness":
case "material.size":
case "material.opacity":
case "material.alphaTest":
// boolean
case "material.vertexColors":
case "material.sizeAttenuation":
case "material.flatShading":
case "material.transparent":
case "material.depthTest":
case "material.depthWrite":
case "material.wireframe":
keyData = [val];
valueTrack.splice(index, delLength, ...keyData);
break;
case "color":
case "groundcolor":
case "material.color":
case "material.specular":
case "material.emissive":
case "material.sheenColor":
case "material.attenuationColor":
if(!(val instanceof THREE.Color)){
val = new THREE.Color(val);
}
keyData = [val.r, val.g, val.b];
valueTrack.splice(index, delLength, ...keyData);
break;
default:
keyData = [val];
valueTrack.splice(index, delLength, ...keyData);
break;
}
return keyData;
}
// 获取当前添加关键帧的模型的属性轨道
let track = App.animationManager.hasExistingTrack(currentClip, attr) as THREE.KeyframeTrack;
// 如果不存在当前属性轨道,则新增轨道
if (!track) {
// 先获取锁定对象到选中对象路径
let path = App.selected?.name;
if (App.locked && App.selected && App.locked !== App.selected) {
path = getParentPath(App.locked, App.selected);
}
let _times = [currentTime], _values: any[] = [];
const keyData = insertValue(_values, 0);
const _row: ITimelineRow = {
id: `${path}.${attr}`,
name: `${path}.${attr}`,
keyframes: [
{
val: this.timeline.getTime(),
data: keyData,
selected: true
}
]
}
// 如果新建轨道默认关键帧不在0位则补0
if (currentTime !== 0) {
_times.unshift(0);
_values.unshift(...keyData);
_row.keyframes?.unshift({
val:0,
data: keyData,
selected: true
})
}
track = KeyframeTrackFactory(`${path}.${attr}`, _times, _values);
// 新增轨道
currentClip.tracks.push(track);
_row.track = track;
this.model.rows.push(_row)
useDispatchSignal("timelineRowChanged", _row, "add");
} else {
const _times: number[] = Array.from(track.times);
const _values: number[] = Array.from(track.values);
const dataLength = Math.floor(_values.length / _times.length);
const row = this.model.rows.find(row => row.track === track) as ITimelineRow;
// 判断当前时间是否已存在关键帧
let index = _times.findIndex(time => time === currentTime);
let keyData;
if (index !== -1) {
// 更新当前时间的关键帧数据
keyData = insertValue(_values, index, dataLength);
// 动画轨道UI修改关键帧值
if (row && row.keyframes) {
row.keyframes.splice(index, 1, {
val: this.timeline.getTime(),
data: keyData,
selected: true
});
}
} else {
// 获取关键帧数据插入位置
index = _times.length;
for (let i = 0; i < _times.length; i++) {
if (_times[i] > currentTime) {
index = i;
break;
}
}
// 插入关键帧时间
_times.splice(index, 0, currentTime);
// 插入关键帧数据
keyData = insertValue(_values, index * dataLength);
// 动画轨道UI添加关键帧
if (row && row.keyframes) {
row.keyframes.splice(index, 0, {
val: this.timeline.getTime(),
data: keyData,
selected: true
});
}
}
// 创建新的关键帧轨道替换
const newTrack = KeyframeTrackFactory(track.name, _times, _values, track.getInterpolation());
currentClip.tracks.splice(currentClip.tracks.indexOf(track), 1, newTrack);
row.track = newTrack;
}
// 更新剪辑时间
currentClip.resetDuration();
// 重新剪辑action
this.bindAction = App.animationManager.reClipAction(this.bindAction,currentTime) as THREE.AnimationAction;
// 刷新
this.timeline.redraw();
this.bindAction.getMixer().update(0.016);
useDispatchSignal("sceneGraphChanged");
}
/**
* 关键帧被改变时触发(关键帧被拖动)
*/
onKeyframeChanged(args:TimelineKeyframeChangedEvent) {
const row = args.target?.row as ITimelineRow;
const track = row.track;
if (!this.bindAction || !track || !row.keyframes?.length) return;
const clip = this.bindAction.getClip();
// 确保完整,直接重建轨道
const _times: number[] = [], _values: any = [];
row.keyframes.forEach((kf) => {
_times.push(kf.val / 1000);
_values.push(...kf.data);
})
// 创建新的关键帧轨道替换
const newTrack = KeyframeTrackFactory(track.name, _times, _values, track.getInterpolation());
clip.tracks.splice(clip.tracks.indexOf(track), 1, newTrack);
row.track = newTrack;
// 更新剪辑时间
clip.resetDuration();
// 重新剪辑action
this.bindAction = App.animationManager.reClipAction(this.bindAction,this.timeline.getTime() / 1000) as THREE.AnimationAction;
// 刷新
this.timeline.redraw();
this.bindAction.getMixer().update(0.016);
useDispatchSignal("sceneGraphChanged");
}
/**
* 删除选中的关键帧
*/
deleteSelectedKeyframes() {
if (!this.bindAction) return;
const selectedRows = this.model.rows.filter(row => row.keyframes?.some(kf => kf.selected));
selectedRows.forEach(row => {
if(!row.keyframes) return;
// 先删除关键帧
row.keyframes = row.keyframes.filter(kf => !kf.selected);
// 如果关键帧为空,则删除轨道
if (row.keyframes.length === 0) {
this.deleteRow(row);
return;
}
// @ts-ignore
this.onKeyframeChanged({target:{row: row}});
});
}
resize() {
if (!this.timeline) return;
this.timeline._handleWindowResizeEvent();
}
/**
* 播放action
*/
play() {
if (!this.bindAction) return;
// 不允许在播放过程中操纵时间轴(可选)。
this.timeline.setOptions({
timelineDraggable: false,
groupsDraggable: false,
keyframesDraggable: false,
zoom: this.timeline._currentZoom
});
this.bindAction.play();
this.bindAction.paused = false;
}
/**
* 暂停/继续播放action
*/
pause() {
if (!this.bindAction) return;
if (this.bindAction.paused) {
this.bindAction.paused = false;
this.timeline.setOptions({
timelineDraggable: false,
groupsDraggable: false,
keyframesDraggable: false,
zoom: this.timeline._currentZoom
});
} else {
this.bindAction.paused = true;
this.timeline.setOptions({
timelineDraggable: true,
groupsDraggable: true,
keyframesDraggable: true,
zoom: this.timeline._currentZoom
});
}
}
/**
* 停止播放action
*/
stop() {
if (!this.bindAction) return;
this.timeline.setOptions({
timelineDraggable: true,
groupsDraggable: true,
keyframesDraggable: true,
zoom: this.timeline._currentZoom
});
this.timeline.scrollLeft = 0;
this.timeline.setTime(0);
this.bindAction.stop();
}
/**
* 更新配置
*/
setOptions(_options: TimelineOptions) {
deepAssign(this.options, _options);
this.timeline.setOptions(this.options);
}
dispose() {
this.resizeObserver.disconnect();
this.timeline?.dispose();
}
}
export {TimelineTrack}