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 { 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) { const newRows: Array = []; 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}