lexgui 0.7.15 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/README.md +14 -5
- package/build/components/AlertDialog.d.ts +7 -0
- package/build/components/ArrayInput.d.ts +9 -0
- package/build/components/BaseComponent.d.ts +73 -0
- package/build/components/Button.d.ts +14 -0
- package/build/components/Calendar.d.ts +41 -0
- package/build/components/CalendarRange.d.ts +16 -0
- package/build/components/CanvasCurve.d.ts +10 -0
- package/build/components/CanvasDial.d.ts +11 -0
- package/build/components/CanvasMap2D.d.ts +61 -0
- package/build/components/Card.d.ts +8 -0
- package/build/components/Checkbox.d.ts +8 -0
- package/build/components/Color.d.ts +20 -0
- package/build/components/ColorInput.d.ts +13 -0
- package/build/components/ColorPicker.d.ts +29 -0
- package/build/components/ComboButtons.d.ts +8 -0
- package/build/components/ContextMenu.d.ts +16 -0
- package/build/components/Counter.d.ts +9 -0
- package/build/components/Curve.d.ts +10 -0
- package/build/components/DatePicker.d.ts +13 -0
- package/build/components/Dial.d.ts +10 -0
- package/build/components/Dialog.d.ts +20 -0
- package/build/components/DropdownMenu.d.ts +32 -0
- package/build/components/FileInput.d.ts +8 -0
- package/build/components/Footer.d.ts +14 -0
- package/build/components/Form.d.ts +8 -0
- package/build/components/Layers.d.ts +9 -0
- package/build/components/List.d.ts +9 -0
- package/build/components/Map2D.d.ts +12 -0
- package/build/components/Menubar.d.ts +59 -0
- package/build/components/NodeTree.d.ts +26 -0
- package/build/components/NumberInput.d.ts +9 -0
- package/build/components/OTPInput.d.ts +8 -0
- package/build/components/Pad.d.ts +8 -0
- package/build/components/Pagination.d.ts +26 -0
- package/build/components/PocketDialog.d.ts +11 -0
- package/build/components/Popover.d.ts +20 -0
- package/build/components/Progress.d.ts +8 -0
- package/build/components/RadioGroup.d.ts +8 -0
- package/build/components/RangeInput.d.ts +11 -0
- package/build/components/Rate.d.ts +8 -0
- package/build/components/Select.d.ts +10 -0
- package/build/components/Sheet.d.ts +10 -0
- package/build/components/Sidebar.d.ts +84 -0
- package/build/components/SizeInput.d.ts +8 -0
- package/build/components/Skeleton.d.ts +5 -0
- package/build/components/Spinner.d.ts +9 -0
- package/build/components/TabSections.d.ts +11 -0
- package/build/components/Table.d.ts +34 -0
- package/build/components/Tabs.d.ts +20 -0
- package/build/components/Tags.d.ts +9 -0
- package/build/components/TextArea.d.ts +8 -0
- package/build/components/TextInput.d.ts +11 -0
- package/build/components/Title.d.ts +8 -0
- package/build/components/Toggle.d.ts +8 -0
- package/build/components/Tour.d.ts +36 -0
- package/build/components/Vector.d.ts +9 -0
- package/build/core/Area.d.ts +143 -0
- package/build/core/Branch.d.ts +19 -0
- package/build/core/Core.d.ts +1 -0
- package/build/core/Event.d.ts +26 -0
- package/build/core/Icons.d.ts +4 -0
- package/build/core/Namespace.d.ts +2 -0
- package/build/core/Namespace.js +34 -0
- package/build/core/Namespace.js.map +1 -0
- package/build/core/Panel.d.ts +538 -0
- package/build/core/Utils.d.ts +1 -0
- package/build/core/Vec2.d.ts +21 -0
- package/build/extensions/AssetView.d.ts +136 -0
- package/build/extensions/AssetView.js +1367 -0
- package/build/extensions/AssetView.js.map +1 -0
- package/build/extensions/Audio.d.ts +9 -0
- package/build/extensions/Audio.js +163 -0
- package/build/extensions/Audio.js.map +1 -0
- package/build/extensions/CodeEditor.d.ts +350 -0
- package/build/extensions/CodeEditor.js +5022 -0
- package/build/extensions/CodeEditor.js.map +1 -0
- package/build/extensions/DocMaker.d.ts +27 -0
- package/build/extensions/DocMaker.js +327 -0
- package/build/extensions/DocMaker.js.map +1 -0
- package/build/extensions/GraphEditor.d.ts +276 -0
- package/build/extensions/GraphEditor.js +2770 -0
- package/build/extensions/GraphEditor.js.map +1 -0
- package/build/extensions/ImUi.d.ts +46 -0
- package/build/extensions/ImUi.js +227 -0
- package/build/extensions/ImUi.js.map +1 -0
- package/build/extensions/Timeline.d.ts +670 -0
- package/build/extensions/Timeline.js +3955 -0
- package/build/extensions/Timeline.js.map +1 -0
- package/build/extensions/VideoEditor.d.ts +128 -0
- package/build/extensions/VideoEditor.js +898 -0
- package/build/extensions/VideoEditor.js.map +1 -0
- package/build/extensions/index.d.ts +8 -0
- package/build/extensions/index.js +10 -0
- package/build/extensions/index.js.map +1 -0
- package/build/index.all.d.ts +2 -0
- package/build/index.css.d.ts +4 -0
- package/build/index.d.ts +56 -0
- package/build/lexgui.all.js +28498 -0
- package/build/lexgui.all.js.map +1 -0
- package/build/lexgui.all.min.js +1 -0
- package/build/lexgui.all.module.js +28422 -0
- package/build/lexgui.all.module.js.map +1 -0
- package/build/lexgui.all.module.min.js +1 -0
- package/build/lexgui.css +939 -346
- package/build/lexgui.js +13406 -17286
- package/build/lexgui.js.map +1 -0
- package/build/lexgui.min.css +3 -10
- package/build/lexgui.min.js +1 -1
- package/build/lexgui.module.js +12762 -16698
- package/build/lexgui.module.js.map +1 -0
- package/build/lexgui.module.min.js +1 -1
- package/changelog.md +170 -74
- package/demo.js +162 -48
- package/examples/all-components.html +45 -14
- package/examples/asset-view.html +110 -47
- package/examples/code-editor.html +5 -5
- package/examples/dialogs.html +3 -3
- package/examples/editor.html +27 -13
- package/examples/index.html +19 -14
- package/examples/node-graph.html +2 -2
- package/examples/previews/video-editor.png +0 -0
- package/examples/timeline.html +1 -1
- package/examples/video-editor.html +2 -2
- package/package.json +25 -9
- package/build/extensions/audio.js +0 -212
- package/build/extensions/codeeditor.js +0 -6319
- package/build/extensions/docmaker.js +0 -432
- package/build/extensions/imui.js +0 -325
- package/build/extensions/nodegraph.js +0 -3696
- package/build/extensions/timeline.js +0 -4636
- package/build/extensions/videoeditor.js +0 -953
- package/build/lexgui-docs.css +0 -352
|
@@ -1,4636 +0,0 @@
|
|
|
1
|
-
import { LX } from 'lexgui';
|
|
2
|
-
|
|
3
|
-
if(!LX) {
|
|
4
|
-
throw("lexgui.js missing!");
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
LX.extensions.push( 'Timeline' );
|
|
8
|
-
|
|
9
|
-
LX.registerIcon("TimelineLock", '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path fill="none" d="M7 11V7a4 4 0 0 1 9 0v4 M5,11h13 a2 2 0 0 1 2 2 v7 a2 2 0 0 1 -2 2 h-13 a2 2 0 0 1 -2 -2 v-7 a2 2 0 0 1 2 -2 M12 16 v2"/></svg>' );
|
|
10
|
-
LX.registerIcon("TimelineLockOpen", '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path fill="none" d="M14 11V7a4 4 0 0 1 9 0v2 M3,11h13 a2 2 0 0 1 2 2 v7 a2 2 0 0 1 -2 2 h-13 a2 2 0 0 1 -2 -2 v-7 a2 2 0 0 1 2 -2 M8 17 h3"/></svg>' );
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @class Timeline
|
|
14
|
-
* @description Agnostic timeline, do not impose any timeline content. Renders to a canvas
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
class Timeline {
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @param {String} name = string unique id
|
|
21
|
-
* @param {Object} options = {skipLock, skipVisibility}
|
|
22
|
-
*/
|
|
23
|
-
constructor( id, options = {} ) {
|
|
24
|
-
|
|
25
|
-
this.uniqueID = id ?? ('timeline' + Math.floor(Math.random()*0xffffffff));
|
|
26
|
-
this.timelineTitle = options.title ?? null;
|
|
27
|
-
this.currentTime = 0;
|
|
28
|
-
this.visualTimeRange = [0,0]; // [start time, end time] - visible range of time. 0 <= time <= duration
|
|
29
|
-
this.visualOriginTime = 0; // time visible at pixel 0. -infinity < time < infinity
|
|
30
|
-
this.topMargin = 40;
|
|
31
|
-
this.clickDiscardTimeout = 200; // ms
|
|
32
|
-
this.lastMouse = [0,0];
|
|
33
|
-
this.historyUndo = [];
|
|
34
|
-
this.historyRedo = [];
|
|
35
|
-
this.historySaveEnabler = true; // used in saveState
|
|
36
|
-
this.historyMaxSteps = 100; // used in saveState
|
|
37
|
-
this.clipboard = null;
|
|
38
|
-
this.grabbing = false;
|
|
39
|
-
this.grabTime = 0;
|
|
40
|
-
this.grabbingTimeBar = false;
|
|
41
|
-
this.grabbingScroll = false;
|
|
42
|
-
this.movingKeys = false;
|
|
43
|
-
this.timeBeforeMove = 0;
|
|
44
|
-
|
|
45
|
-
// required before updateHeader
|
|
46
|
-
this.onCreateBeforeTopBar = options.onCreateBeforeTopBar;
|
|
47
|
-
this.onCreateAfterTopBar = options.onCreateAfterTopBar;
|
|
48
|
-
this.onCreateControlsButtons = options.onCreateControlsButtons;
|
|
49
|
-
this.onCreateSettingsButtons = options.onCreateSettingsButtons;
|
|
50
|
-
this.onShowOptimizeMenu = options.onShowOptimizeMenu == undefined ? this._onShowOptimizeMenu : options.onShowOptimizeMenu; // _onShowOptimizeMenu might not exist
|
|
51
|
-
this.onShowConfiguration = options.onShowConfiguration;
|
|
52
|
-
|
|
53
|
-
this.playing = false;
|
|
54
|
-
this.loop = options.loop ?? true;
|
|
55
|
-
|
|
56
|
-
this.canvas = document.createElement('canvas');
|
|
57
|
-
this.canvas.style.width = "100%";
|
|
58
|
-
this.canvas.style.height = "100%";
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.duration = 1;
|
|
62
|
-
this.size = [0.000001, 0.000001];
|
|
63
|
-
|
|
64
|
-
this.currentScroll = 0; //in percentage
|
|
65
|
-
this.currentScrollInPixels = 0; //in pixels
|
|
66
|
-
|
|
67
|
-
this.pixelsPerSecond = 300;
|
|
68
|
-
this.secondsPerPixel = 1 / this.pixelsPerSecond;
|
|
69
|
-
this.animationClip = this.instantiateAnimationClip();
|
|
70
|
-
|
|
71
|
-
this.trackHeight = 32;
|
|
72
|
-
this.timeSeparators = [0.01, 0.1, 0.5, 1, 5];
|
|
73
|
-
|
|
74
|
-
this.boxSelection = false;
|
|
75
|
-
this.boxSelectionStart = [0,0];
|
|
76
|
-
this.boxSelectionEnd = [0,0];
|
|
77
|
-
|
|
78
|
-
this.active = true;
|
|
79
|
-
this.skipVisibility = options.skipVisibility ?? false;
|
|
80
|
-
this.skipLock = options.skipLock ?? false;
|
|
81
|
-
this.disableNewTracks = options.disableNewTracks ?? false;
|
|
82
|
-
|
|
83
|
-
this.optimizeThreshold = 0.01;
|
|
84
|
-
|
|
85
|
-
this.header_offset = 48;
|
|
86
|
-
|
|
87
|
-
// main area -- root
|
|
88
|
-
this.mainArea = new LX.Area({className : 'lextimeline'});
|
|
89
|
-
this.root = this.mainArea.root;
|
|
90
|
-
this.mainArea.split({ type: "vertical", sizes: [this.header_offset, "auto"], resize: false});
|
|
91
|
-
|
|
92
|
-
// header
|
|
93
|
-
this.header = new LX.Panel( { id: 'lextimelineheader'} );
|
|
94
|
-
this.mainArea.sections[0].attach( this.header );
|
|
95
|
-
this.updateHeader();
|
|
96
|
-
|
|
97
|
-
// content area
|
|
98
|
-
const contentArea = this.mainArea.sections[1];
|
|
99
|
-
contentArea.root.id = "bottom-timeline-area";
|
|
100
|
-
contentArea.split({ type: "horizontal", sizes: ["15%", "85%"] });
|
|
101
|
-
let [ left, right ] = contentArea.sections;
|
|
102
|
-
|
|
103
|
-
right.attach( this.canvas );
|
|
104
|
-
this.canvasArea = right;
|
|
105
|
-
this.canvasArea.root.classList.add("lextimelinearea");
|
|
106
|
-
|
|
107
|
-
this.selectedTracks = []; // [track, track] contains selected (highlighted) tracks. That is, tracks with .isSelected == true. Elements in array are not ordered. Only visible tracks should be selected
|
|
108
|
-
this.selectedItems = []; // [trackInfo, "groupId"], contains the visible items (tracks or groups) of the timeline
|
|
109
|
-
this.leftPanel = left.addPanel( { className: 'lextimelinepanel', width: "100%", height: "100%" } );
|
|
110
|
-
this.trackTreesPanel = null;
|
|
111
|
-
this.trackTreesComponent = null;
|
|
112
|
-
this.lastTrackTreesComponentOffset = 0; // this.trackTreesComponent.innerTree.domEl.offsetTop - canvas.offsetTop. Check draw()
|
|
113
|
-
this.updateLeftPanel();
|
|
114
|
-
|
|
115
|
-
if(this.uniqueID != '') {
|
|
116
|
-
this.root.id = this.uniqueID;
|
|
117
|
-
this.canvas.id = this.uniqueID + '-canvas';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Process mouse events
|
|
121
|
-
this.canvas.addEventListener("mousedown", this.processMouse.bind(this));
|
|
122
|
-
this.canvas.addEventListener("mouseup", this.processMouse.bind(this));
|
|
123
|
-
this.canvas.addEventListener("mousemove", this.processMouse.bind(this));
|
|
124
|
-
this.canvas.addEventListener("wheel", this.processMouse.bind(this));
|
|
125
|
-
this.canvas.addEventListener("dblclick", this.processMouse.bind(this));
|
|
126
|
-
this.canvas.addEventListener("contextmenu", this.processMouse.bind(this));
|
|
127
|
-
|
|
128
|
-
this.canvas.tabIndex = 1;
|
|
129
|
-
// Process keys events
|
|
130
|
-
this.canvasArea.root.addEventListener("keydown", (e) =>{ this.processKeys(e); }); // so this.processKeys can be overwritten by the user
|
|
131
|
-
|
|
132
|
-
this.canvasArea.onresize = bounding => {
|
|
133
|
-
if(!(bounding.width && bounding.height))
|
|
134
|
-
return;
|
|
135
|
-
this.resizeCanvas();
|
|
136
|
-
}
|
|
137
|
-
this.resize(this.size);
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* updates theme (light - dark) based on LX's current theme
|
|
141
|
-
*/
|
|
142
|
-
function updateTheme( ){
|
|
143
|
-
Timeline.BACKGROUND_COLOR = LX.getThemeColor("global-blur-background");
|
|
144
|
-
Timeline.TRACK_COLOR_PRIMARY = LX.getThemeColor("global-color-primary");
|
|
145
|
-
Timeline.TRACK_COLOR_SECONDARY = LX.getThemeColor("global-color-secondary");
|
|
146
|
-
Timeline.TRACK_COLOR_TERCIARY = LX.getThemeColor("global-color-terciary");
|
|
147
|
-
Timeline.TRACK_COLOR_QUATERNARY = LX.getThemeColor("global-color-quaternary");
|
|
148
|
-
Timeline.FONT = LX.getThemeColor("global-font");
|
|
149
|
-
Timeline.FONT_COLOR_PRIMARY = LX.getThemeColor("global-text-primary");
|
|
150
|
-
Timeline.FONT_COLOR_TERTIARY = LX.getThemeColor("global-text-tertiary");
|
|
151
|
-
Timeline.FONT_COLOR_QUATERNARY = LX.getThemeColor("global-text-quaternary");
|
|
152
|
-
|
|
153
|
-
Timeline.KEYFRAME_COLOR = LX.getThemeColor("lxTimeline-keyframe");
|
|
154
|
-
Timeline.KEYFRAME_COLOR_SELECTED = Timeline.KEYFRAME_COLOR_HOVERED = LX.getThemeColor("lxTimeline-keyframe-selected");
|
|
155
|
-
Timeline.KEYFRAME_COLOR_LOCK = LX.getThemeColor("lxTimeline-keyframe-locked");
|
|
156
|
-
Timeline.KEYFRAME_COLOR_EDITED = LX.getThemeColor("lxTimeline-keyframe-edited");
|
|
157
|
-
Timeline.KEYFRAME_COLOR_INACTIVE = LX.getThemeColor("lxTimeline-keyframe-inactive");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
this.updateTheme = updateTheme.bind(this);
|
|
161
|
-
LX.addSignal( "@on_new_color_scheme", this.updateTheme );
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// makes it ready to be deleted
|
|
165
|
-
clear(){
|
|
166
|
-
if( this.header ){
|
|
167
|
-
this.header.clear();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if( this.leftPanel ){
|
|
171
|
-
this.leftPanel.clear();
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if( this.updateTheme ){
|
|
175
|
-
let signals = LX.signals[ "@on_new_color_scheme" ] ?? [];
|
|
176
|
-
for( let i =0; i < signals.length; ++i ){
|
|
177
|
-
if( signals[i] == this.updateTheme ){
|
|
178
|
-
signals.splice(i, 1);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @method updateHeader
|
|
187
|
-
*/
|
|
188
|
-
|
|
189
|
-
updateHeader() {
|
|
190
|
-
this.header.clear();
|
|
191
|
-
|
|
192
|
-
const header = this.header;
|
|
193
|
-
header.sameLine();
|
|
194
|
-
|
|
195
|
-
if( this.timelineTitle )
|
|
196
|
-
{
|
|
197
|
-
header.addTitle(this.timelineTitle, { style: { background: "none", fontSize: "18px", fontStyle: "bold", alignItems: "center" } } );
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const buttonContainer = LX.makeContainer( ["auto", "100%"], "flex flex-row gap-1" );
|
|
201
|
-
|
|
202
|
-
header.queue( buttonContainer );
|
|
203
|
-
|
|
204
|
-
const playbtn = header.addButton("playBtn", '', (value, event) => {
|
|
205
|
-
this.changeState();
|
|
206
|
-
}, { buttonClass: "accept", title: "Play", hideName: true, icon: "Play@solid", swap: "Pause@solid" });
|
|
207
|
-
playbtn.setState(this.playing, true);
|
|
208
|
-
|
|
209
|
-
header.addButton("stopBtn", '', (value, event) => {
|
|
210
|
-
this.setState(false, true); // skip callback of set state
|
|
211
|
-
if ( this.onStateStop ){
|
|
212
|
-
this.onStateStop();
|
|
213
|
-
}
|
|
214
|
-
}, { buttonClass: "accept", title: "Stop", hideName: true, icon: "Stop@solid" });
|
|
215
|
-
|
|
216
|
-
header.addButton("loopBtn", '', ( value, event ) => {
|
|
217
|
-
this.setLoopMode(!this.loop);
|
|
218
|
-
}, { selectable: true, selected: this.loop, title: 'Loop', hideName: true, icon: "RefreshCw" });
|
|
219
|
-
|
|
220
|
-
if( this.onCreateControlsButtons ){
|
|
221
|
-
this.onCreateControlsButtons( header );
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
header.clearQueue( buttonContainer );
|
|
225
|
-
header.addContent( "header-buttons", buttonContainer );
|
|
226
|
-
|
|
227
|
-
// time number inputs - duration, current time, etc
|
|
228
|
-
|
|
229
|
-
if( this.onCreateBeforeTopBar )
|
|
230
|
-
{
|
|
231
|
-
this.onCreateBeforeTopBar( header );
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
header.addNumber("Current Time", this.currentTime, (value, event) => {
|
|
235
|
-
this.setTime(value);
|
|
236
|
-
}, {
|
|
237
|
-
units: "s",
|
|
238
|
-
step: 0.01, min: 0, precision: 3,
|
|
239
|
-
skipSlider: true,
|
|
240
|
-
skipReset: true,
|
|
241
|
-
nameWidth: "auto"
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
header.addNumber("Duration", + this.duration.toFixed(3), (value, event) => {
|
|
245
|
-
this.setDuration(value, false, false);
|
|
246
|
-
}, {
|
|
247
|
-
units: "s",
|
|
248
|
-
step: 0.01, min: 0,
|
|
249
|
-
skipReset: true,
|
|
250
|
-
nameWidth: "auto"
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
if( this.onCreateAfterTopBar )
|
|
254
|
-
{
|
|
255
|
-
this.onCreateAfterTopBar( header );
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// settings buttons - optimize, settings, etc
|
|
259
|
-
|
|
260
|
-
const buttonContainerEnd = LX.makeContainer( ["auto", "100%"], "flex flex-row gap-1" );
|
|
261
|
-
header.queue( buttonContainerEnd );
|
|
262
|
-
|
|
263
|
-
if( this.onCreateSettingsButtons ){
|
|
264
|
-
this.onCreateSettingsButtons( header );
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if( this.onShowOptimizeMenu )
|
|
268
|
-
{
|
|
269
|
-
header.addButton(null, "", (value, event) => {this.onShowOptimizeMenu(event)}, { tooltip: true, title: "Optimize", icon:"Filter" });
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if( this.onShowConfiguration )
|
|
273
|
-
{
|
|
274
|
-
header.addButton(null, "", (value, event) => {
|
|
275
|
-
if(this.configurationDialog){
|
|
276
|
-
this.configurationDialog.close();
|
|
277
|
-
this.configurationDialog = null;
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
this.configurationDialog = new LX.Dialog("Configuration", dialog => {
|
|
281
|
-
this.onShowConfiguration(dialog);
|
|
282
|
-
}, {
|
|
283
|
-
onclose: (root) => {
|
|
284
|
-
this.configurationDialog.panel.clear(); // clear signals
|
|
285
|
-
this.configurationDialog = null;
|
|
286
|
-
root.remove();
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
}, { title: "Settings", icon: "Settings", tooltip: true })
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
header.clearQueue( buttonContainerEnd );
|
|
293
|
-
header.addContent( "header-buttons-end", buttonContainerEnd );
|
|
294
|
-
|
|
295
|
-
header.endLine( "justify-between" );
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* @method updateLeftPanel
|
|
300
|
-
*
|
|
301
|
-
*/
|
|
302
|
-
updateLeftPanel( ) {
|
|
303
|
-
|
|
304
|
-
this.leftPanel.clear();
|
|
305
|
-
|
|
306
|
-
const panel = this.leftPanel;
|
|
307
|
-
|
|
308
|
-
panel.sameLine();
|
|
309
|
-
let titleComponent = panel.addTitle( "Tracks", { style: { background: "none"}, className: "fg-secondary text-lg px-4"} );
|
|
310
|
-
let title = titleComponent.root;
|
|
311
|
-
|
|
312
|
-
if( !this.disableNewTracks )
|
|
313
|
-
{
|
|
314
|
-
panel.addButton("addTrackBtn", '', (value, event) => {
|
|
315
|
-
if ( this.onAddNewTrackButton ){
|
|
316
|
-
this.onAddNewTrackButton();
|
|
317
|
-
}else{
|
|
318
|
-
this.addNewTrack();
|
|
319
|
-
}
|
|
320
|
-
}, { hideName: true, title: "Add Track", icon: "Plus" });
|
|
321
|
-
}
|
|
322
|
-
panel.endLine();
|
|
323
|
-
|
|
324
|
-
const styles = window.getComputedStyle( title );
|
|
325
|
-
const titleHeight = title.clientHeight + parseFloat(styles['marginTop']) + parseFloat(styles['marginBottom']);
|
|
326
|
-
|
|
327
|
-
let p = new LX.Panel({height: "calc(100% - " + titleHeight + "px)"});
|
|
328
|
-
|
|
329
|
-
let treeTracks = [];
|
|
330
|
-
if( this.animationClip && this.selectedItems.length )
|
|
331
|
-
{
|
|
332
|
-
treeTracks = this.generateSelectedItemsTreeData();
|
|
333
|
-
}
|
|
334
|
-
this.trackTreesComponent = p.addTree(null, treeTracks, {filter: false, rename: false, draggable: false, onevent: (e) => {
|
|
335
|
-
switch(e.type) {
|
|
336
|
-
case LX.TreeEvent.NODE_SELECTED:
|
|
337
|
-
if ( !e.event.shiftKey ){
|
|
338
|
-
this.deselectAllTracks( false ); // no need to update left panel
|
|
339
|
-
}
|
|
340
|
-
if (e.node.trackData){
|
|
341
|
-
const flag = e.event.shiftKey? !e.node.trackData.isSelected : true;
|
|
342
|
-
this.setTrackSelection( e.node.trackData.trackIdx, flag, false, false ); // do callback, do not update left panel
|
|
343
|
-
}
|
|
344
|
-
break;
|
|
345
|
-
case LX.TreeEvent.NODE_VISIBILITY:
|
|
346
|
-
if (e.node.trackData){
|
|
347
|
-
this.setTrackState( e.node.trackData.trackIdx, e.value, false, false ); // do not update left panel
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if ( this.onTrackTreeEvent ){
|
|
353
|
-
this.onTrackTreeEvent(e);
|
|
354
|
-
}
|
|
355
|
-
}});
|
|
356
|
-
|
|
357
|
-
const that = this;
|
|
358
|
-
this.trackTreesComponent.innerTree._refresh = this.trackTreesComponent.innerTree.refresh;
|
|
359
|
-
this.trackTreesComponent.innerTree.refresh = function( newData, selectedId ){
|
|
360
|
-
this._refresh( newData, selectedId );
|
|
361
|
-
that.setTrackHeight( that.trackHeight );
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if ( this.selectedTracks.length ){
|
|
365
|
-
this._updateTrackTreeSelection(); // select visible tracks
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// setting a name in the addTree function adds an undesired node
|
|
369
|
-
this.trackTreesComponent.name = "tracksTrees";
|
|
370
|
-
p.components[this.trackTreesComponent.name] = this.trackTreesComponent;
|
|
371
|
-
|
|
372
|
-
this.trackTreesPanel = p;
|
|
373
|
-
panel.attach( p.root );
|
|
374
|
-
p.root.addEventListener("scroll", e => {
|
|
375
|
-
if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight){
|
|
376
|
-
this.currentScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
|
|
377
|
-
this.currentScrollInPixels = e.currentTarget.scrollTop;
|
|
378
|
-
}
|
|
379
|
-
else{
|
|
380
|
-
this.currentScroll = 0;
|
|
381
|
-
this.currentScrollInPixels = 0;
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
this.trackTreesPanel.root.scrollTop = this.currentScrollInPixels;
|
|
386
|
-
this.setTrackHeight( this.trackHeight );
|
|
387
|
-
|
|
388
|
-
if( this.leftPanel.parent.root.classList.contains("hidden") || !this.root.parentElement ){
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
this.resizeCanvas();
|
|
393
|
-
|
|
394
|
-
this.setScroll( this.currentScroll ); // avoid scroll bugs
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
setTrackHeight( trackHeight ){
|
|
399
|
-
// ul list has a "gap" of 0.25rem. Compute pixel count of 0.25 rem
|
|
400
|
-
const gapSize = parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.25;
|
|
401
|
-
|
|
402
|
-
this.trackHeight = trackHeight = Math.max(gapSize, trackHeight);
|
|
403
|
-
|
|
404
|
-
if ( !this.trackTreesComponent ){
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
trackHeight -= gapSize;
|
|
409
|
-
const tracks = this.trackTreesComponent.root.querySelector("ul").children;
|
|
410
|
-
for( let i = 0; i < tracks.length; ++i ){
|
|
411
|
-
tracks[i].style.height = trackHeight + "px";
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* @param {Object} options options for the new track
|
|
417
|
-
* { id: string, active: bool, locked: bool, }
|
|
418
|
-
* @returns
|
|
419
|
-
*/
|
|
420
|
-
addNewTrack( options = {}, skipCallback = false ) {
|
|
421
|
-
|
|
422
|
-
const trackInfo = this.instantiateTrack(options);
|
|
423
|
-
trackInfo.trackIdx = this.animationClip.tracks.length;
|
|
424
|
-
this.animationClip.tracks.push( trackInfo );
|
|
425
|
-
|
|
426
|
-
if ( this.onAddNewTrack && !skipCallback ){
|
|
427
|
-
this.onAddNewTrack( trackInfo, options );
|
|
428
|
-
}
|
|
429
|
-
return trackInfo.trackIdx;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Finds tracks (wholy and partially) inside the range minY maxY.
|
|
434
|
-
* (Full) Canvas local coordinates.
|
|
435
|
-
* @param {Number} minY
|
|
436
|
-
* @param {Number} maxY
|
|
437
|
-
* @returns array of trackDatas
|
|
438
|
-
*/
|
|
439
|
-
getTracksInRange( minY, maxY ) {
|
|
440
|
-
|
|
441
|
-
let tracks = [];
|
|
442
|
-
|
|
443
|
-
// Manage negative selection
|
|
444
|
-
if( minY > maxY )
|
|
445
|
-
{
|
|
446
|
-
let aux = minY;
|
|
447
|
-
minY = maxY;
|
|
448
|
-
maxY = aux;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const elements = this.getVisibleItems();
|
|
452
|
-
if ( elements.length < 1 ){
|
|
453
|
-
return [];
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const startY = minY - this.lastTrackTreesComponentOffset + this.currentScrollInPixels;
|
|
457
|
-
const endY = maxY - this.lastTrackTreesComponentOffset + this.currentScrollInPixels;
|
|
458
|
-
|
|
459
|
-
const startIdx = Math.max( 0, Math.floor( startY / this.trackHeight ) );
|
|
460
|
-
const endIdx = Math.min( elements.length-1, Math.floor( endY / this.trackHeight ) ) + 1;
|
|
461
|
-
|
|
462
|
-
for(let i = startIdx; i < endIdx; ++i)
|
|
463
|
-
{
|
|
464
|
-
const e = elements[i];
|
|
465
|
-
if ( e.treeData && e.treeData.trackData ){
|
|
466
|
-
tracks.push(e.treeData.trackData);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
return tracks;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* @method setAnimationClip
|
|
475
|
-
* @param {*} animation
|
|
476
|
-
* @param {Boolean} needsToProcess
|
|
477
|
-
* @param {Object} processOptions
|
|
478
|
-
* [KeyFrameTimeline] - each track should contain an attribute "dim" to indicate the value dimension (e.g. vector3 -> dim=3). Otherwise dimensions will be infered from track's values and times. Default is 1
|
|
479
|
-
*/
|
|
480
|
-
setAnimationClip( animation, needsToProcess = true ) {
|
|
481
|
-
|
|
482
|
-
this.deselectAllElements();
|
|
483
|
-
this.deselectAllTracks( false ); // no need to update left panel yet
|
|
484
|
-
|
|
485
|
-
this.selectedItems = [];
|
|
486
|
-
|
|
487
|
-
this.clearState();
|
|
488
|
-
|
|
489
|
-
if( !animation || !animation.tracks || needsToProcess )
|
|
490
|
-
{
|
|
491
|
-
this.animationClip = this.instantiateAnimationClip( animation ); // generate default animationclip or process the user's one
|
|
492
|
-
}
|
|
493
|
-
else
|
|
494
|
-
{
|
|
495
|
-
this.animationClip = animation;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
this.setDuration(this.animationClip.duration, true, true);
|
|
499
|
-
|
|
500
|
-
this.updateLeftPanel();
|
|
501
|
-
|
|
502
|
-
this.resize();
|
|
503
|
-
|
|
504
|
-
return this.animationClip;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
drawTimeInfo( w, h = this.topMargin ) {
|
|
508
|
-
|
|
509
|
-
let ctx = this.canvas.getContext( "2d" );
|
|
510
|
-
ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
|
|
511
|
-
ctx.textAlign = "center";
|
|
512
|
-
|
|
513
|
-
// Draw time markers
|
|
514
|
-
ctx.save();
|
|
515
|
-
|
|
516
|
-
// background of timeinfo
|
|
517
|
-
ctx.fillStyle = Timeline.BACKGROUND_COLOR;
|
|
518
|
-
ctx.fillRect( 0, 0, this.canvas.width, h );
|
|
519
|
-
ctx.strokeStyle = Timeline.FONT_COLOR_PRIMARY;
|
|
520
|
-
|
|
521
|
-
// set tick and sub tick times
|
|
522
|
-
let tickTime = 4;
|
|
523
|
-
if ( this.pixelsPerSecond > 900 ) { tickTime = 1; }
|
|
524
|
-
else if ( this.pixelsPerSecond > 100 ) { tickTime = 2; }
|
|
525
|
-
else if ( this.pixelsPerSecond > 50 ) { tickTime = 3; }
|
|
526
|
-
|
|
527
|
-
let subtickTime = this.timeSeparators[tickTime - 1];
|
|
528
|
-
tickTime = this.timeSeparators[tickTime];
|
|
529
|
-
|
|
530
|
-
const startTime = this.visualTimeRange[0];
|
|
531
|
-
const endTime = this.visualTimeRange[1];
|
|
532
|
-
// Transform times into pixel coords
|
|
533
|
-
let tickX = this.timeToX( startTime + tickTime ) - this.timeToX( startTime );
|
|
534
|
-
let subtickX = subtickTime * tickX / tickTime;
|
|
535
|
-
|
|
536
|
-
let startx = this.timeToX( Math.floor( startTime / tickTime) * tickTime ); // floor because might need to draw previous subticks
|
|
537
|
-
startx += 0.0000001; // slight offset to avoid "-0.0"
|
|
538
|
-
let endx = this.timeToX( endTime ); // draw up to endTime
|
|
539
|
-
|
|
540
|
-
// Begin drawing
|
|
541
|
-
ctx.beginPath();
|
|
542
|
-
ctx.fillStyle = Timeline.FONT_COLOR_PRIMARY;
|
|
543
|
-
ctx.globalAlpha = 1;
|
|
544
|
-
|
|
545
|
-
for( let x = startx; x <= endx; x += tickX )
|
|
546
|
-
{
|
|
547
|
-
// Draw main line
|
|
548
|
-
ctx.moveTo( Math.round( x ) + 0.5, h * 0.4 + h * 0.3 );
|
|
549
|
-
ctx.lineTo( Math.round( x ) + 0.5, h * 0.95 );
|
|
550
|
-
|
|
551
|
-
// Draw following sub lines
|
|
552
|
-
let endsub = x + tickX - subtickX * 0.5;
|
|
553
|
-
for ( let subX = x; subX < endsub && subX < endx; subX += subtickX )
|
|
554
|
-
{
|
|
555
|
-
ctx.moveTo( Math.round( subX ) + 0.5, h * 0.4 + h * 0.45 );
|
|
556
|
-
ctx.lineTo( Math.round( subX ) + 0.5, h * 0.95 );
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Draw time number
|
|
560
|
-
let t = this.xToTime( x );
|
|
561
|
-
ctx.fillText( t.toFixed( tickTime < 1 ? 1 : 0 ), x, h * 0.6 );
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
ctx.stroke();
|
|
565
|
-
ctx.restore();
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
drawTracksBackground( w, h ) {
|
|
569
|
-
|
|
570
|
-
let canvas = this.canvas;
|
|
571
|
-
let ctx = canvas.getContext("2d");
|
|
572
|
-
let duration = this.duration;
|
|
573
|
-
ctx.globalAlpha = 1;
|
|
574
|
-
|
|
575
|
-
// Content
|
|
576
|
-
const topMargin = this.topMargin;
|
|
577
|
-
const treeOffset = this.lastTrackTreesComponentOffset;
|
|
578
|
-
const line_height = this.trackHeight;
|
|
579
|
-
|
|
580
|
-
//fill track lines
|
|
581
|
-
w = w || canvas.width;
|
|
582
|
-
let max_tracks = Math.ceil( (h - topMargin) / line_height ) + 1;
|
|
583
|
-
|
|
584
|
-
ctx.save();
|
|
585
|
-
ctx.fillStyle = Timeline.TRACK_COLOR_SECONDARY;
|
|
586
|
-
|
|
587
|
-
const rectsOffset = this.currentScrollInPixels % line_height;
|
|
588
|
-
const blackOrWhite = 1 - Math.floor(this.currentScrollInPixels / line_height ) % 2;
|
|
589
|
-
for(let i = blackOrWhite; i <= max_tracks; i+=2)
|
|
590
|
-
{
|
|
591
|
-
ctx.fillRect(0, treeOffset - rectsOffset + i * line_height, w, line_height );
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//bg lines
|
|
595
|
-
ctx.strokeStyle = Timeline.TRACK_COLOR_TERCIARY;
|
|
596
|
-
ctx.beginPath();
|
|
597
|
-
|
|
598
|
-
let pos = this.timeToX( 0 );
|
|
599
|
-
if(pos < 0)
|
|
600
|
-
pos = 0;
|
|
601
|
-
ctx.lineWidth = 1;
|
|
602
|
-
ctx.moveTo( pos + 0.5, topMargin);
|
|
603
|
-
ctx.lineTo( pos + 0.5, canvas.height);
|
|
604
|
-
ctx.moveTo( Math.round( this.timeToX( duration ) ) + 0.5, topMargin);
|
|
605
|
-
ctx.lineTo( Math.round( this.timeToX( duration ) ) + 0.5, canvas.height);
|
|
606
|
-
ctx.stroke();
|
|
607
|
-
|
|
608
|
-
ctx.restore();
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
/**
|
|
612
|
-
* @method draw
|
|
613
|
-
*/
|
|
614
|
-
|
|
615
|
-
draw( ) {
|
|
616
|
-
|
|
617
|
-
let ctx = this.canvas.getContext("2d");
|
|
618
|
-
ctx.textBaseline = "bottom";
|
|
619
|
-
ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
|
|
620
|
-
ctx.globalAlpha = 1;
|
|
621
|
-
|
|
622
|
-
const w = ctx.canvas.width;
|
|
623
|
-
const h = ctx.canvas.height;
|
|
624
|
-
|
|
625
|
-
const scrollableHeight = this.trackTreesComponent.root.scrollHeight;
|
|
626
|
-
// tree has gaps of 0.25rem (4px) inbetween entries but not in the beginning nor ending. Move half gap upwards.
|
|
627
|
-
const treeOffset = this.lastTrackTreesComponentOffset = this.trackTreesComponent.innerTree.domEl.offsetTop - this.canvas.offsetTop -2;
|
|
628
|
-
|
|
629
|
-
//zoom
|
|
630
|
-
let startTime = this.visualOriginTime; //seconds
|
|
631
|
-
startTime = Math.min( this.duration, Math.max( 0, startTime ) );
|
|
632
|
-
let endTime = this.visualOriginTime + w * this.secondsPerPixel; //seconds
|
|
633
|
-
endTime = Math.max( startTime, Math.min( this.duration, endTime ) );
|
|
634
|
-
this.visualTimeRange[0] = startTime;
|
|
635
|
-
this.visualTimeRange[1] = endTime;
|
|
636
|
-
|
|
637
|
-
// Background
|
|
638
|
-
ctx.globalAlpha = 1;
|
|
639
|
-
ctx.fillStyle = Timeline.TRACK_COLOR_SECONDARY;
|
|
640
|
-
ctx.clearRect(0,0, this.canvas.width, this.canvas.height );
|
|
641
|
-
|
|
642
|
-
this.drawTracksBackground(w, h);
|
|
643
|
-
|
|
644
|
-
if( this.onBeforeDrawContent ){
|
|
645
|
-
this.onBeforeDrawContent(ctx);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if(this.animationClip) {
|
|
649
|
-
ctx.translate( 0, treeOffset );
|
|
650
|
-
this.drawContent( ctx, this.timeStart, this.timeEnd, this );
|
|
651
|
-
ctx.translate( 0, -treeOffset );
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
//scrollbar
|
|
655
|
-
if( (h-this.topMargin) < scrollableHeight ){
|
|
656
|
-
ctx.fillStyle = "#222";
|
|
657
|
-
ctx.fillRect( w - 10, 0, 10, h );
|
|
658
|
-
|
|
659
|
-
ctx.fillStyle = this.grabbingScroll ? Timeline.FONT_COLOR_TERTIARY : Timeline.FONT_COLOR_QUATERNARY;
|
|
660
|
-
|
|
661
|
-
let scrollBarHeight = Math.max( 10, (h-this.topMargin)* (h-this.topMargin)/ this.trackTreesPanel.root.scrollHeight);
|
|
662
|
-
let scrollLoc = this.currentScroll * ( h - this.topMargin - scrollBarHeight ) + this.topMargin;
|
|
663
|
-
ctx.roundRect( w - 10, scrollLoc, 10, scrollBarHeight, 5, true );
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
this.drawTimeInfo(w);
|
|
667
|
-
|
|
668
|
-
// Current time marker vertical line
|
|
669
|
-
let posx = Math.round( this.timeToX( this.currentTime ) );
|
|
670
|
-
let posy = this.topMargin * 0.4;
|
|
671
|
-
if(posx >= 0)
|
|
672
|
-
{
|
|
673
|
-
ctx.strokeStyle = ctx.fillStyle = Timeline.TIME_MARKER_COLOR;
|
|
674
|
-
ctx.globalAlpha = 1;
|
|
675
|
-
ctx.beginPath();
|
|
676
|
-
ctx.moveTo(posx, posy * 0.6); ctx.lineTo(posx, this.canvas.height);//line
|
|
677
|
-
ctx.stroke();
|
|
678
|
-
ctx.closePath();
|
|
679
|
-
ctx.shadowBlur = 8;
|
|
680
|
-
ctx.shadowColor = Timeline.TIME_MARKER_COLOR;
|
|
681
|
-
ctx.shadowOffsetX = 1;
|
|
682
|
-
ctx.shadowOffsetY = 1;
|
|
683
|
-
|
|
684
|
-
ctx.roundRect( posx - 10, posy * 0.6, 20, posy, 5, true );
|
|
685
|
-
ctx.fill();
|
|
686
|
-
ctx.shadowBlur = 0;
|
|
687
|
-
ctx.shadowOffsetX = 0;
|
|
688
|
-
ctx.shadowOffsetY = 0;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
// Current time seconds in text
|
|
692
|
-
ctx.font = "11px " + Timeline.FONT;//"11px Calibri";
|
|
693
|
-
ctx.textAlign = "center";
|
|
694
|
-
//ctx.textBaseline = "middle";
|
|
695
|
-
ctx.fillStyle = Timeline.TIME_MARKER_COLOR_TEXT;
|
|
696
|
-
ctx.fillText( (Math.floor(this.currentTime*10)*0.1).toFixed(1), posx, this.topMargin * 0.6 );
|
|
697
|
-
|
|
698
|
-
// Selections
|
|
699
|
-
ctx.strokeStyle = ctx.fillStyle = Timeline.FONT_COLOR_PRIMARY;
|
|
700
|
-
if(this.boxSelection) {
|
|
701
|
-
ctx.globalAlpha = 0.15;
|
|
702
|
-
ctx.fillStyle = Timeline.BOX_SELECTION_COLOR;
|
|
703
|
-
ctx.strokeRect( this.boxSelectionStart[0], this.boxSelectionStart[1], this.boxSelectionEnd[0] - this.boxSelectionStart[0], this.boxSelectionEnd[1] - this.boxSelectionStart[1]);
|
|
704
|
-
ctx.fillRect( this.boxSelectionStart[0], this.boxSelectionStart[1], this.boxSelectionEnd[0] - this.boxSelectionStart[0], this.boxSelectionEnd[1] - this.boxSelectionStart[1]);
|
|
705
|
-
ctx.stroke();
|
|
706
|
-
ctx.globalAlpha = 1;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* @method drawMarkers
|
|
713
|
-
* @param {*} ctx
|
|
714
|
-
* @param {*} markers
|
|
715
|
-
*/
|
|
716
|
-
|
|
717
|
-
drawMarkers( ctx, markers ) {
|
|
718
|
-
|
|
719
|
-
//render markers
|
|
720
|
-
ctx.fillStyle = "white";
|
|
721
|
-
ctx.textAlign = "left";
|
|
722
|
-
let markersPos = [];
|
|
723
|
-
for (let i = 0; i < markers.length; ++i) {
|
|
724
|
-
let marker = markers[i];
|
|
725
|
-
if (marker.time < this.visualTimeRange[0] - this.secondsPerPixel * 100 ||
|
|
726
|
-
marker.time > this.visualTimeRange[1])
|
|
727
|
-
continue;
|
|
728
|
-
var x = this.timeToX(marker.time);
|
|
729
|
-
markersPos.push(x);
|
|
730
|
-
ctx.save();
|
|
731
|
-
ctx.translate(x, 0);
|
|
732
|
-
ctx.rotate(Math.PI * -0.25);
|
|
733
|
-
ctx.fillText(marker.title, 20, 4);
|
|
734
|
-
ctx.restore();
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (markersPos.length) {
|
|
738
|
-
ctx.beginPath();
|
|
739
|
-
for (var i = 0; i < markersPos.length; ++i) {
|
|
740
|
-
ctx.moveTo(markersPos[i] - 5, 0);
|
|
741
|
-
ctx.lineTo(markersPos[i], -5);
|
|
742
|
-
ctx.lineTo(markersPos[i] + 5, 0);
|
|
743
|
-
ctx.lineTo(markersPos[i], 5);
|
|
744
|
-
ctx.lineTo(markersPos[i] - 5, 0);
|
|
745
|
-
}
|
|
746
|
-
ctx.fill();
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/**
|
|
751
|
-
* @method clearState
|
|
752
|
-
*/
|
|
753
|
-
|
|
754
|
-
clearState() {
|
|
755
|
-
this.historyUndo = [];
|
|
756
|
-
this.historyRedo = [];
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* @method setDuration
|
|
761
|
-
* @param {Number} t
|
|
762
|
-
*/
|
|
763
|
-
|
|
764
|
-
setDuration( t, skipCallback = false, updateHeader = true ) {
|
|
765
|
-
let v = Math.max(0,t);
|
|
766
|
-
this.duration = this.animationClip.duration = v;
|
|
767
|
-
|
|
768
|
-
if(updateHeader) {
|
|
769
|
-
this.header.components["Duration"].set( +this.duration.toFixed(2), true ); // skipcallback = true
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if( this.onSetDuration && !skipCallback )
|
|
773
|
-
this.onSetDuration( this.duration );
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
setTime(time, skipCallback = false ){
|
|
777
|
-
this.currentTime = Math.max(0,Math.min(time,this.duration));
|
|
778
|
-
this.header.components["Current Time"].set( +this.currentTime.toFixed(2), true ); // skipcallback = true
|
|
779
|
-
|
|
780
|
-
if(this.onSetTime && !skipCallback)
|
|
781
|
-
this.onSetTime(this.currentTime);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Converts distance in pixels to time
|
|
785
|
-
xToTime( x ) {
|
|
786
|
-
return x * this.secondsPerPixel + this.visualOriginTime;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Converts time to disance in pixels
|
|
790
|
-
timeToX( t ) {
|
|
791
|
-
return (t - this.visualOriginTime) * this.pixelsPerSecond;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* @method setScale
|
|
796
|
-
* @param {*} pixelsPerSecond >0. totalVisiblePixels / totalVisibleSeconds.
|
|
797
|
-
*/
|
|
798
|
-
|
|
799
|
-
setScale( pixelsPerSecond ) {
|
|
800
|
-
const xCurrentTime = this.timeToX(this.currentTime);
|
|
801
|
-
this.pixelsPerSecond = pixelsPerSecond;
|
|
802
|
-
this.pixelsPerSecond = Math.max( 0.00001, this.pixelsPerSecond );
|
|
803
|
-
|
|
804
|
-
this.secondsPerPixel = 1 / this.pixelsPerSecond;
|
|
805
|
-
this.visualOriginTime += this.currentTime - this.xToTime(xCurrentTime);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* @method setScroll
|
|
810
|
-
* not delta from last state, but full scroll amount.
|
|
811
|
-
* @param {Number} scrollY either pixels or [0,1]
|
|
812
|
-
* @param {Boolean} normalized if true, scrollY is in range[0,1] being 1 fully scrolled. Otherwised scrollY represents pixels
|
|
813
|
-
* @returns
|
|
814
|
-
*/
|
|
815
|
-
|
|
816
|
-
setScroll( scrollY, normalized = true ){
|
|
817
|
-
if ( !this.trackTreesPanel ){
|
|
818
|
-
this.currentScroll = 0;
|
|
819
|
-
this.currentScrollInPixels = 0;
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const r = this.trackTreesPanel.root;
|
|
824
|
-
if (r.scrollHeight > r.clientHeight){
|
|
825
|
-
if ( normalized ){
|
|
826
|
-
this.currentScroll = scrollY;
|
|
827
|
-
this.currentScrollInPixels = scrollY * (r.scrollHeight - r.clientHeight);
|
|
828
|
-
}else{
|
|
829
|
-
this.currentScroll = scrollY / (r.scrollHeight - r.clientHeight);
|
|
830
|
-
this.currentScrollInPixels = scrollY;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
else{
|
|
834
|
-
this.currentScroll = 0;
|
|
835
|
-
this.currentScrollInPixels = 0;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// automatically calls event.
|
|
839
|
-
this.trackTreesPanel.root.scrollTop = this.currentScrollInPixels;
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* @method processMouse
|
|
845
|
-
* @param {*} e
|
|
846
|
-
*/
|
|
847
|
-
|
|
848
|
-
processMouse( e ) {
|
|
849
|
-
|
|
850
|
-
if(!this.canvas)
|
|
851
|
-
return;
|
|
852
|
-
|
|
853
|
-
let h = this.canvas.height;
|
|
854
|
-
let w = this.canvas.width;
|
|
855
|
-
|
|
856
|
-
// Process mouse
|
|
857
|
-
let x = e.offsetX;
|
|
858
|
-
let y = e.offsetY;
|
|
859
|
-
e.deltax = x - this.lastMouse[0];
|
|
860
|
-
e.deltay = y - this.lastMouse[1];
|
|
861
|
-
let localX = e.offsetX;
|
|
862
|
-
let localY = e.offsetY;
|
|
863
|
-
|
|
864
|
-
let timeX = this.timeToX( this.currentTime );
|
|
865
|
-
let isHoveringTimeBar = localY < this.topMargin &&
|
|
866
|
-
localX > (timeX - 6) && localX < (timeX + 6);
|
|
867
|
-
|
|
868
|
-
const time = this.xToTime(x);
|
|
869
|
-
|
|
870
|
-
if( isHoveringTimeBar ) {
|
|
871
|
-
this.canvas.style.cursor = "col-resize";
|
|
872
|
-
}
|
|
873
|
-
else if(this.movingKeys) {
|
|
874
|
-
this.canvas.style.cursor = "grabbing";
|
|
875
|
-
}
|
|
876
|
-
else if(e.shiftKey) {
|
|
877
|
-
this.canvas.style.cursor = "crosshair";
|
|
878
|
-
}
|
|
879
|
-
else {
|
|
880
|
-
this.canvas.style.cursor = "default";
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
if( e.type == "wheel" ) {
|
|
884
|
-
if(e.shiftKey)
|
|
885
|
-
{
|
|
886
|
-
if ( e.wheelDelta ){
|
|
887
|
-
let mouseTime = this.xToTime(localX);
|
|
888
|
-
this.setScale( this.pixelsPerSecond * (e.wheelDelta < 0 ? 0.95 : 1.05) );
|
|
889
|
-
this.visualOriginTime = mouseTime - localX * this.secondsPerPixel;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
}
|
|
893
|
-
else if( (h-this.topMargin) < this.trackTreesComponent.root.scrollHeight)
|
|
894
|
-
{
|
|
895
|
-
this.trackTreesPanel.root.scrollTop += e.deltaY; // wheel deltaY
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
if ( this.onMouse ){
|
|
899
|
-
this.onMouse(e, time);
|
|
900
|
-
}
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const is_inside = x >= 0 && x <= this.size[0] &&
|
|
905
|
-
y >= 0 && y <= this.size[1];
|
|
906
|
-
|
|
907
|
-
let track = this.getTracksInRange(localY, localY);
|
|
908
|
-
track = track.length ? track[0] : null;
|
|
909
|
-
|
|
910
|
-
e.track = track;
|
|
911
|
-
e.localX = localX;
|
|
912
|
-
e.localY = localY;
|
|
913
|
-
|
|
914
|
-
if( e.type == "mouseup" )
|
|
915
|
-
{
|
|
916
|
-
if(!this.active) {
|
|
917
|
-
this.grabbing = false;
|
|
918
|
-
this.grabbingTimeBar = false;
|
|
919
|
-
this.grabbingScroll = false;
|
|
920
|
-
this.movingKeys = false;
|
|
921
|
-
this.timeBeforeMove = null;
|
|
922
|
-
this.boxSelection = false;
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
// this.canvas.style.cursor = "default";
|
|
926
|
-
const discard = this.movingKeys || (LX.getTime() - this.clickTime) > this.clickDiscardTimeout; // ms
|
|
927
|
-
|
|
928
|
-
e.discard = discard;
|
|
929
|
-
|
|
930
|
-
if( !this.grabbingScroll && !this.grabbingTimeBar && e.button == 0 && this.onMouseUp ) {
|
|
931
|
-
this.onMouseUp(e, time);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
this.grabbing = false;
|
|
935
|
-
this.grabbingTimeBar = false;
|
|
936
|
-
this.grabbingScroll = false;
|
|
937
|
-
this.movingKeys = false;
|
|
938
|
-
this.timeBeforeMove = null;
|
|
939
|
-
this.boxSelection = false; // after mouseup
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
if( e.type == "mousedown") {
|
|
944
|
-
window.getSelection().empty(); // if canvas DOM is selected, dragging does not work properly. Deselect it
|
|
945
|
-
|
|
946
|
-
// e.preventDefault();
|
|
947
|
-
|
|
948
|
-
this.clickTime = LX.getTime();
|
|
949
|
-
|
|
950
|
-
if(e.shiftKey && this.active) {
|
|
951
|
-
this.boxSelection = true;
|
|
952
|
-
this.boxSelectionEnd[0] = this.boxSelectionStart[0] = localX;
|
|
953
|
-
this.boxSelectionEnd[1] = this.boxSelectionStart[1] = localY;
|
|
954
|
-
return; // Handled
|
|
955
|
-
}
|
|
956
|
-
else if( e.localY < this.topMargin ){
|
|
957
|
-
this.grabbing = true;
|
|
958
|
-
this.grabbingTimeBar = true;
|
|
959
|
-
this.setTime(time);
|
|
960
|
-
}
|
|
961
|
-
else if( (h-this.topMargin) < this.trackTreesComponent.root.scrollHeight && x > w - 10 ) { // grabbing scroll bar
|
|
962
|
-
this.grabbing = true;
|
|
963
|
-
this.grabbingScroll = true;
|
|
964
|
-
}
|
|
965
|
-
else { // grabbing canvas
|
|
966
|
-
|
|
967
|
-
this.grabbing = true;
|
|
968
|
-
this.grabTime = time;
|
|
969
|
-
this.grabbingTimeBar = isHoveringTimeBar;
|
|
970
|
-
if(this.onMouseDown && this.active )
|
|
971
|
-
this.onMouseDown(e, time);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
else if( e.type == "mousemove" ) {
|
|
975
|
-
|
|
976
|
-
if(e.shiftKey && this.active && this.boxSelection) {
|
|
977
|
-
this.boxSelectionEnd[0] = localX;
|
|
978
|
-
this.boxSelectionEnd[1] = localY;
|
|
979
|
-
return; // Handled
|
|
980
|
-
}
|
|
981
|
-
else if(this.grabbing && e.button !=2 && !this.movingKeys ) { // e.buttons != 2 on mousemove needs to be plural
|
|
982
|
-
this.canvas.style.cursor = "grabbing";
|
|
983
|
-
if(this.grabbingTimeBar && this.active)
|
|
984
|
-
{
|
|
985
|
-
this.setTime(time);
|
|
986
|
-
}
|
|
987
|
-
else if(this.grabbingScroll)
|
|
988
|
-
{
|
|
989
|
-
// will automatically call scroll event
|
|
990
|
-
if ( y < this.topMargin ){
|
|
991
|
-
this.trackTreesPanel.root.scrollTop = 0;
|
|
992
|
-
}
|
|
993
|
-
else{
|
|
994
|
-
this.trackTreesPanel.root.scrollTop += this.trackTreesPanel.root.scrollHeight * e.deltay / (h-this.topMargin);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
else
|
|
998
|
-
{
|
|
999
|
-
// Move timeline in X (independent of current time)
|
|
1000
|
-
var old = this.xToTime( this.lastMouse[0] );
|
|
1001
|
-
var now = this.xToTime( e.offsetX );
|
|
1002
|
-
this.visualOriginTime += (old - now);
|
|
1003
|
-
|
|
1004
|
-
this.trackTreesPanel.root.scrollTop -= e.deltay; // will automatically call scroll event
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if(this.onMouseMove) {
|
|
1010
|
-
this.onMouseMove(e, time);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
else if (e.type == "dblclick" && this.onDblClick) {
|
|
1014
|
-
this.onDblClick(e);
|
|
1015
|
-
}
|
|
1016
|
-
else if (e.type == "contextmenu" && this.showContextMenu && this.active) {
|
|
1017
|
-
this.showContextMenu(e);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
this.lastMouse[0] = x;
|
|
1021
|
-
this.lastMouse[1] = y;
|
|
1022
|
-
|
|
1023
|
-
if( !is_inside && !this.grabbing && !(e.metaKey || e.altKey ) ) {
|
|
1024
|
-
return true;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
if( this.onMouse )
|
|
1028
|
-
this.onMouse( e, time );
|
|
1029
|
-
|
|
1030
|
-
return true;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* keydown
|
|
1035
|
-
* @method processKeys
|
|
1036
|
-
* @param {*} e
|
|
1037
|
-
*/
|
|
1038
|
-
processKeys(e) {
|
|
1039
|
-
switch(e.key) {
|
|
1040
|
-
case 'Delete': case 'Backspace':
|
|
1041
|
-
this.deleteSelectedContent();
|
|
1042
|
-
break;
|
|
1043
|
-
case 'c': case 'C':
|
|
1044
|
-
if(e.ctrlKey)
|
|
1045
|
-
this.copySelectedContent();
|
|
1046
|
-
break;
|
|
1047
|
-
case 'v': case 'V':
|
|
1048
|
-
if(e.ctrlKey)
|
|
1049
|
-
this.pasteContent();
|
|
1050
|
-
break;
|
|
1051
|
-
case ' ':
|
|
1052
|
-
e.preventDefault();
|
|
1053
|
-
e.stopImmediatePropagation();
|
|
1054
|
-
this.changeState();
|
|
1055
|
-
break;
|
|
1056
|
-
|
|
1057
|
-
case "Shift":
|
|
1058
|
-
this.canvas.style.cursor = "crosshair";
|
|
1059
|
-
break;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/**
|
|
1065
|
-
* @method changeState
|
|
1066
|
-
* @param {Boolean} skipCallback defaults false
|
|
1067
|
-
* @description change play/pause state
|
|
1068
|
-
**/
|
|
1069
|
-
changeState(skipCallback = false) {
|
|
1070
|
-
this.setState(!this.playing, skipCallback);
|
|
1071
|
-
}
|
|
1072
|
-
/**
|
|
1073
|
-
* @method setState
|
|
1074
|
-
* @param {Boolean} state
|
|
1075
|
-
* @param {Boolean} skipCallback defaults false
|
|
1076
|
-
* @description change play/pause state
|
|
1077
|
-
**/
|
|
1078
|
-
setState(state, skipCallback = false) {
|
|
1079
|
-
this.playing = state;
|
|
1080
|
-
|
|
1081
|
-
this.header.components.playBtn.setState(this.playing, true);
|
|
1082
|
-
|
|
1083
|
-
if(this.onStateChange && !skipCallback) {
|
|
1084
|
-
this.onStateChange(this.playing);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* @method setLoopMode
|
|
1090
|
-
* @param {Boolean} loopState
|
|
1091
|
-
* @param {Boolean} skipCallback defaults false
|
|
1092
|
-
* @description change loop mode of the timeline
|
|
1093
|
-
*/
|
|
1094
|
-
setLoopMode(loopState, skipCallback = false){
|
|
1095
|
-
this.loop = loopState;
|
|
1096
|
-
if ( this.loop ){
|
|
1097
|
-
this.header.components.loopBtn.root.children[0].classList.add("selected");
|
|
1098
|
-
}else{
|
|
1099
|
-
this.header.components.loopBtn.root.children[0].classList.remove("selected")
|
|
1100
|
-
}
|
|
1101
|
-
if( this.onChangeLoopMode && !skipCallback ){
|
|
1102
|
-
this.onChangeLoopMode( this.loop );
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* @returns the tree elements (tracks and grouops) shown in the timeline.
|
|
1108
|
-
* Each item has { treeData: { trackData: track } }, where track is the actual track information of the animationClip.
|
|
1109
|
-
* If not a track, trackData will be undefined
|
|
1110
|
-
*/
|
|
1111
|
-
getVisibleItems(){
|
|
1112
|
-
return this.trackTreesComponent.innerTree.domEl.children[0].children; // children of 'ul'
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
/**
|
|
1116
|
-
* [ trackIdx ]
|
|
1117
|
-
* @param {Array} itemsName array of numbers identifying tracks
|
|
1118
|
-
*/
|
|
1119
|
-
setSelectedItems( items, skipCallback = false ) {
|
|
1120
|
-
this.selectedItems = [];
|
|
1121
|
-
this.changeSelectedItems( items, null, skipCallback );
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* @param {Array} itemsToAdd [ trackIdx ], array of numbers identifying tracks by their index
|
|
1126
|
-
* @param {Array} itemsToRemove [ trackIdx ], array of numbers identifying tracks by their index
|
|
1127
|
-
*/
|
|
1128
|
-
changeSelectedItems( itemsToAdd = null, itemsToRemove = null, skipCallback = false ) {
|
|
1129
|
-
|
|
1130
|
-
this.deselectAllElements();
|
|
1131
|
-
this.deselectAllTracks( false ); // no need to update left panel. It is going to be rebuilt anyways
|
|
1132
|
-
|
|
1133
|
-
const tracks = this.animationClip.tracks;
|
|
1134
|
-
|
|
1135
|
-
if ( itemsToRemove ){
|
|
1136
|
-
for( let i = 0; i < itemsToRemove.length; ++i){
|
|
1137
|
-
const compareObj = itemsToRemove[i];
|
|
1138
|
-
for( let s = 0; s < this.selectedItems.length; ++s){
|
|
1139
|
-
if (this.selectedItems[s] === compareObj){
|
|
1140
|
-
this.selectedItems.splice(s, 1);
|
|
1141
|
-
break;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if ( itemsToAdd ){
|
|
1148
|
-
for( let i = 0; i < itemsToAdd.length; ++i ){
|
|
1149
|
-
const v = itemsToAdd[i];
|
|
1150
|
-
if ( tracks[v] ) {
|
|
1151
|
-
this.selectedItems.push( tracks[v] );
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
this.updateLeftPanel();
|
|
1157
|
-
|
|
1158
|
-
if(this.onItemSelected && !skipCallback){
|
|
1159
|
-
this.onItemSelected(this.selectedItems, itemsToAdd, itemsToRemove);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* It will find the first occurrence of trackId in animationClip.tracks
|
|
1165
|
-
* @param {String} trackId
|
|
1166
|
-
* @returns
|
|
1167
|
-
*/
|
|
1168
|
-
getTrack( trackId ){
|
|
1169
|
-
const tracks = this.animationClip.tracks;
|
|
1170
|
-
for( let i = 0; i < tracks.length; ++i){
|
|
1171
|
-
if ( tracks[i].id == trackId ){
|
|
1172
|
-
return tracks[i];
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
return null;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
/**
|
|
1179
|
-
* @param {Boolean} updateTrackTree whether the track tree needs a refresh
|
|
1180
|
-
* @returns
|
|
1181
|
-
*/
|
|
1182
|
-
deselectAllTracks( updateTrackTree = true ) {
|
|
1183
|
-
|
|
1184
|
-
if( !this.animationClip ){
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const tracks = this.animationClip.tracks;
|
|
1189
|
-
for(let i = 0; i < tracks.length; i++){
|
|
1190
|
-
tracks[ i ].isSelected = false;
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
this.selectedTracks.length = 0;
|
|
1194
|
-
|
|
1195
|
-
if ( updateTrackTree ){
|
|
1196
|
-
this._updateTrackTreeSelection();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* @param {Int} trackIdx
|
|
1202
|
-
* @param {Boolean} isSelected new "selected" state of the track
|
|
1203
|
-
* @param {Boolean} skipCallback whether to call onSetTrackSelection
|
|
1204
|
-
* @param {Boolean} updateTrackTree whether track tree panel needs a refresh
|
|
1205
|
-
* @returns
|
|
1206
|
-
*/
|
|
1207
|
-
setTrackSelection( trackIdx, isSelected, skipCallback = false, updateTrackTree = true ){
|
|
1208
|
-
const track = this.animationClip.tracks[ trackIdx ];
|
|
1209
|
-
const oldValue = track.isSelected;
|
|
1210
|
-
track.isSelected = isSelected;
|
|
1211
|
-
|
|
1212
|
-
const idx = this.selectedTracks.indexOf( track );
|
|
1213
|
-
if ( ( idx == -1 && !isSelected ) || ( idx > -1 && isSelected ) ){
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
if ( idx == -1 ){
|
|
1218
|
-
this.selectedTracks.push( track );
|
|
1219
|
-
}else{
|
|
1220
|
-
this.selectedTracks.splice( idx, 1 );
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
if( this.onSetTrackSelection && !skipCallback ){
|
|
1224
|
-
this.onSetTrackSelection(track, oldValue );
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
if ( updateTrackTree ){
|
|
1228
|
-
this._updateTrackTreeSelection();
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
/**
|
|
1233
|
-
* updates trackTreesComponent's nodes, to match the selectedTracks
|
|
1234
|
-
*/
|
|
1235
|
-
_updateTrackTreeSelection(){
|
|
1236
|
-
const data = this.trackTreesComponent.innerTree.data;
|
|
1237
|
-
const selected = this.trackTreesComponent.innerTree.selected;
|
|
1238
|
-
selected.length = 0;
|
|
1239
|
-
|
|
1240
|
-
const addToSelection = (nodes) =>{
|
|
1241
|
-
for( let i = 0; i < nodes.length; ++i ){
|
|
1242
|
-
if ( nodes[i].trackData && nodes[i].trackData.isSelected ){
|
|
1243
|
-
selected.push( nodes[i] );
|
|
1244
|
-
}
|
|
1245
|
-
if ( nodes[i].children ){
|
|
1246
|
-
addToSelection( nodes[i].children );
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
// update innerTree (visible) selected nodes
|
|
1252
|
-
if ( this.selectedTracks.length ){
|
|
1253
|
-
addToSelection( data );
|
|
1254
|
-
}
|
|
1255
|
-
this.trackTreesComponent.innerTree.refresh();
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
deselectAllElements(){
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* @method setTrackState
|
|
1264
|
-
* @param {Int} trackIdx
|
|
1265
|
-
* @param {Boolean} isEnbaled
|
|
1266
|
-
* @param {Boolean} skipCallback onSetTrackState
|
|
1267
|
-
* @param {Boolean} updateTrackTree updates eye icon of the track, if it is visible in the timeline
|
|
1268
|
-
*/
|
|
1269
|
-
setTrackState(trackIdx, isEnbaled = true, skipCallback = false, updateTrackTree = true ) {
|
|
1270
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
1271
|
-
|
|
1272
|
-
const oldState = track.active;
|
|
1273
|
-
track.active = isEnbaled;
|
|
1274
|
-
|
|
1275
|
-
if ( this.onSetTrackState && !skipCallback ){
|
|
1276
|
-
this.onSetTrackState(track, oldState);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
if ( updateTrackTree && !this.skipVisibility ){
|
|
1280
|
-
// TODO: a bit of an overkill. Maybe searching the node in the tree is less expensive
|
|
1281
|
-
this.updateLeftPanel();
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/**
|
|
1286
|
-
*
|
|
1287
|
-
* @param {Int} trackIdx
|
|
1288
|
-
* @param {Boolean} isLocked
|
|
1289
|
-
* @param {Boolean} skipCallback onSetTrackLock
|
|
1290
|
-
* @param {Boolean} updateTrackTree updates lock icon of the track, if it is visible in the timeline
|
|
1291
|
-
*/
|
|
1292
|
-
setTrackLock(trackIdx, isLocked = false, skipCallback = false, updateTrackTree = true ){
|
|
1293
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
1294
|
-
|
|
1295
|
-
const oldState = track.locked;
|
|
1296
|
-
track.locked = isLocked;
|
|
1297
|
-
|
|
1298
|
-
if ( this.onSetTrackLock && !skipCallback ){
|
|
1299
|
-
this.onSetTrackLock( track, oldState );
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
if ( updateTrackTree && !this.skipLock ){
|
|
1303
|
-
// TODO: a bit of an overkill. Maybe searching the node in the tree is less expensive
|
|
1304
|
-
this.updateLeftPanel();
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
/**
|
|
1309
|
-
* @param {Int} trackIdx index of track in the animation (not local index)
|
|
1310
|
-
* @param {Boolean} combineWithPrevious whether to create a new entry or unify changes into a single undo entry
|
|
1311
|
-
*/
|
|
1312
|
-
saveState( trackIdx, combineWithPrevious = false ) {
|
|
1313
|
-
if ( !this.historySaveEnabler ){ return; }
|
|
1314
|
-
|
|
1315
|
-
const undoStep = this.historyGenerateTrackStep( trackIdx );
|
|
1316
|
-
undoStep.trackIdx = trackIdx;
|
|
1317
|
-
|
|
1318
|
-
if ( combineWithPrevious && this.historyUndo.length ){
|
|
1319
|
-
this.historyUndo[ this.historyUndo.length - 1 ].push( undoStep );
|
|
1320
|
-
}
|
|
1321
|
-
else{
|
|
1322
|
-
this.historyUndo.push( [undoStep] );
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
if ( this.historyUndo.length > this.historyMaxSteps ){ this.historyUndo.shift(); } // remove first (oldest) element
|
|
1326
|
-
this.historyRedo = [];
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
#undoRedo(isUndo = true) {
|
|
1330
|
-
|
|
1331
|
-
let toBeShown = isUndo ? this.historyUndo : this.historyRedo;
|
|
1332
|
-
let toBeStored = isUndo ? this.historyRedo : this.historyUndo;
|
|
1333
|
-
|
|
1334
|
-
if (!toBeShown.length){ return false; }
|
|
1335
|
-
|
|
1336
|
-
this.deselectAllElements();
|
|
1337
|
-
|
|
1338
|
-
const combinedState = toBeShown.pop();
|
|
1339
|
-
const combinedStateToStore = [];
|
|
1340
|
-
|
|
1341
|
-
for( let i = 0; i < combinedState.length; ++i ){
|
|
1342
|
-
const state = combinedState[i];
|
|
1343
|
-
const trackIdx = state.trackIdx;
|
|
1344
|
-
|
|
1345
|
-
const stateToStore = this.historyApplyTrackStep( state, isUndo );
|
|
1346
|
-
stateToStore.trackIdx = trackIdx;
|
|
1347
|
-
combinedStateToStore.push( stateToStore );
|
|
1348
|
-
|
|
1349
|
-
// Update animation action interpolation info
|
|
1350
|
-
if(this.onUpdateTrack)
|
|
1351
|
-
this.onUpdateTrack( [state.trackIdx] );
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
toBeStored.push(combinedStateToStore);
|
|
1355
|
-
|
|
1356
|
-
return true;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
undo() { return this.#undoRedo(true); }
|
|
1360
|
-
redo() { return this.#undoRedo(false); }
|
|
1361
|
-
// historyApplyTrackStep( state, isUndo ) MUST BE IMPLEMENTED BY CHILD CLASS
|
|
1362
|
-
// historyGenerateTrackStep( trackIdx ) MUST BE IMPLEMENTED BY CHILD CLASS
|
|
1363
|
-
|
|
1364
|
-
/**
|
|
1365
|
-
* @method resize
|
|
1366
|
-
* @param {*} size
|
|
1367
|
-
*/
|
|
1368
|
-
resize( size = [this.root.parentElement.clientWidth, this.root.parentElement.clientHeight]) {
|
|
1369
|
-
|
|
1370
|
-
this.size[0] = size[0];
|
|
1371
|
-
this.size[1] = size[1];
|
|
1372
|
-
//this.content_area.setSize([size[0], size[1] - this.header_offset]);
|
|
1373
|
-
this.mainArea.sections[1].root.style.height = "calc(100% - "+ this.header_offset + "px)";
|
|
1374
|
-
|
|
1375
|
-
let w = size[0] - this.leftPanel.root.clientWidth - 8;
|
|
1376
|
-
this.mainArea.sections[1]._update(); // update area's this.size attribute
|
|
1377
|
-
|
|
1378
|
-
this.resizeCanvas();
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
resizeCanvas( ) {
|
|
1382
|
-
this.canvas.width = this.canvasArea.root.clientWidth;
|
|
1383
|
-
this.canvas.height = this.canvasArea.root.clientHeight;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/**
|
|
1387
|
-
* @method hide
|
|
1388
|
-
* Hide timeline area
|
|
1389
|
-
*/
|
|
1390
|
-
hide() {
|
|
1391
|
-
this.mainArea.hide();
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
/**
|
|
1395
|
-
* @method show
|
|
1396
|
-
* Show timeline area if it is hidden
|
|
1397
|
-
*/
|
|
1398
|
-
show() {
|
|
1399
|
-
this.mainArea.show();
|
|
1400
|
-
this.resize();
|
|
1401
|
-
this.updateLeftPanel();
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// ----- BASE FUNCTIONS -----
|
|
1405
|
-
/**
|
|
1406
|
-
These functions might be overriden by child classes. Nonetheless, they must have the same attributes, at least.
|
|
1407
|
-
Usually call a super.whateverFunction to generate its base form, and expand it with extra attributes
|
|
1408
|
-
*/
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
/**
|
|
1412
|
-
* This functions uses the selectedItems and generates the data that will feed the LX.Tree Component.
|
|
1413
|
-
* This function is used by updateLeftPanel. Some timelines might allow grouping of tracks. Such timelines may overwrite this function
|
|
1414
|
-
* WARNING: track entries MUST have an attribute of 'trackData' with the track info
|
|
1415
|
-
* @returns lexgui tree data as expecte for the creation of a tree
|
|
1416
|
-
*/
|
|
1417
|
-
generateSelectedItemsTreeData(){
|
|
1418
|
-
const treeTracks = [];
|
|
1419
|
-
for( let i = 0; i < this.selectedItems.length; i++ ){
|
|
1420
|
-
const track = this.selectedItems[ i ];
|
|
1421
|
-
treeTracks.push({'trackData': track, 'id': track.id, 'skipVisibility': this.skipVisibility, visible: track.active, 'children':[], actions : this.skipLock ? null : [{
|
|
1422
|
-
'name':'Lock edition',
|
|
1423
|
-
'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
|
|
1424
|
-
'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
|
|
1425
|
-
'callback': (node, swapValue, event) => {
|
|
1426
|
-
this.setTrackLock( node.trackData.trackIdx, !node.trackData.locked, false, false ); // do not update left panel
|
|
1427
|
-
}
|
|
1428
|
-
}]});
|
|
1429
|
-
}
|
|
1430
|
-
return treeTracks;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
/**
|
|
1435
|
-
*
|
|
1436
|
-
* @param {Object} options set some values for the track instance (groups and trackIdx not included)
|
|
1437
|
-
* @returns
|
|
1438
|
-
*/
|
|
1439
|
-
instantiateTrack(options = {}, clone = false) {
|
|
1440
|
-
return {
|
|
1441
|
-
isTrack: true,
|
|
1442
|
-
id: options.id ?? ( Math.floor(performance.now().toString()) + "_" + Math.floor(Math.random() * 0xffff) ), //must be unique, at least inside a group
|
|
1443
|
-
active: options.active ?? true,
|
|
1444
|
-
locked: options.locked ?? false,
|
|
1445
|
-
isSelected: false, // render and lexgui tree
|
|
1446
|
-
trackIdx: -1,
|
|
1447
|
-
data: options.data ?? null // user defined data
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
/**
|
|
1452
|
-
* Generates an animationClip using either the parameters set in the animation argument or using default values
|
|
1453
|
-
* @param {Object} animation data with which to generate an animationClip
|
|
1454
|
-
* @returns
|
|
1455
|
-
*/
|
|
1456
|
-
instantiateAnimationClip(options, clone = false) {
|
|
1457
|
-
options = options ?? {};
|
|
1458
|
-
const animationClip = {
|
|
1459
|
-
id: options.id ?? (options.name ?? "animationClip"),
|
|
1460
|
-
duration: options.duration ?? 0,
|
|
1461
|
-
tracks: [],
|
|
1462
|
-
data: options.data ?? null, // user defined data
|
|
1463
|
-
};
|
|
1464
|
-
return animationClip;
|
|
1465
|
-
}
|
|
1466
|
-
// ----- END OF BASE FUNCTIONS -----
|
|
1467
|
-
};
|
|
1468
|
-
|
|
1469
|
-
Timeline.BACKGROUND_COLOR = LX.getThemeColor("global-blur-background");
|
|
1470
|
-
Timeline.TRACK_COLOR_PRIMARY = LX.getThemeColor("global-color-primary");
|
|
1471
|
-
Timeline.TRACK_COLOR_SECONDARY = LX.getThemeColor("global-color-secondary");
|
|
1472
|
-
Timeline.TRACK_COLOR_TERCIARY = LX.getThemeColor("global-color-terciary");
|
|
1473
|
-
Timeline.TRACK_COLOR_QUATERNARY = LX.getThemeColor("global-color-quaternary");
|
|
1474
|
-
Timeline.TRACK_SELECTED = LX.getThemeColor("global-color-accent");
|
|
1475
|
-
Timeline.TRACK_SELECTED_LIGHT = LX.getThemeColor("global-color-accent-light");
|
|
1476
|
-
Timeline.FONT = LX.getThemeColor("global-font");
|
|
1477
|
-
Timeline.FONT_COLOR_PRIMARY = LX.getThemeColor("global-text-primary");
|
|
1478
|
-
Timeline.FONT_COLOR_TERTIARY = LX.getThemeColor("global-text-tertiary");
|
|
1479
|
-
Timeline.FONT_COLOR_QUATERNARY = LX.getThemeColor("global-text-quaternary");
|
|
1480
|
-
Timeline.TIME_MARKER_COLOR = LX.getThemeColor("global-color-accent");
|
|
1481
|
-
Timeline.TIME_MARKER_COLOR_TEXT = "#ffffff";
|
|
1482
|
-
|
|
1483
|
-
LX.setThemeColor("lxTimeline-keyframe", "light-dark(#2d69da,#2d69da)");
|
|
1484
|
-
LX.setThemeColor("lxTimeline-keyframe-selected", "light-dark(#f5c700,#fafa14)");
|
|
1485
|
-
LX.setThemeColor("lxTimeline-keyframe-hovered", "light-dark(#f5c700,#fafa14)");
|
|
1486
|
-
LX.setThemeColor("lxTimeline-keyframe-locked", "light-dark(#c62e2e,#ff7d7d)");
|
|
1487
|
-
LX.setThemeColor("lxTimeline-keyframe-edited", "light-dark(#00d000,#00d000)");
|
|
1488
|
-
LX.setThemeColor("lxTimeline-keyframe-inactive", "light-dark(#706b6b,#706b6b)");
|
|
1489
|
-
Timeline.KEYFRAME_COLOR = LX.getThemeColor("lxTimeline-keyframe");
|
|
1490
|
-
Timeline.KEYFRAME_COLOR_SELECTED = Timeline.KEYFRAME_COLOR_HOVERED = LX.getThemeColor("lxTimeline-keyframe-selected");
|
|
1491
|
-
Timeline.KEYFRAME_COLOR_LOCK = LX.getThemeColor("lxTimeline-keyframe-locked");
|
|
1492
|
-
Timeline.KEYFRAME_COLOR_EDITED = LX.getThemeColor("lxTimeline-keyframe-edited");
|
|
1493
|
-
Timeline.KEYFRAME_COLOR_INACTIVE =LX.getThemeColor("lxTimeline-keyframe-inactive");
|
|
1494
|
-
Timeline.BOX_SELECTION_COLOR = "#AAA";
|
|
1495
|
-
LX.Timeline = Timeline;
|
|
1496
|
-
|
|
1497
|
-
/**
|
|
1498
|
-
* @class KeyFramesTimeline
|
|
1499
|
-
*/
|
|
1500
|
-
|
|
1501
|
-
class KeyFramesTimeline extends Timeline {
|
|
1502
|
-
|
|
1503
|
-
static ADDKEY_VALUESINARRAYS = 0x01; // addkeyframes as [ [k0v0, k0v1...], [k1v0, k1v1...] ] instead of [k0v0,k0v1,k1v0,k1v1]
|
|
1504
|
-
/**
|
|
1505
|
-
* @param {String} name unique string
|
|
1506
|
-
* @param {Object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
|
|
1507
|
-
*/
|
|
1508
|
-
constructor(name, options = {}) {
|
|
1509
|
-
|
|
1510
|
-
super(name, options);
|
|
1511
|
-
|
|
1512
|
-
this.lastKeyFramesSelected = [];
|
|
1513
|
-
|
|
1514
|
-
// curves --- track.dim == 1
|
|
1515
|
-
this.keyValuePerPixel = 1/this.trackHeight; // used onMouseMove, vertical move only for dim==1. Normalized value movement / pixels
|
|
1516
|
-
this.defaultCurves = true; // whn a track with dim == 1 has no curves attribute, defaultCurves will be used instead. If true, track is rendered using curves
|
|
1517
|
-
this.defaultCurvesRange = [0,1]; // whn a track with dim == 1 has no curves attribute, defaultCurves will be used instead. If true, track is rendered using curves
|
|
1518
|
-
|
|
1519
|
-
this.keyframeSize = this.trackHeight * 0.5; // height of keyframe
|
|
1520
|
-
this.keyframeSizeHovered = this.trackHeight * 0.5 + 5;
|
|
1521
|
-
|
|
1522
|
-
if(this.animationClip) {
|
|
1523
|
-
this.setAnimationClip(this.animationClip);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// OVERRIDE
|
|
1528
|
-
generateSelectedItemsTreeData(){
|
|
1529
|
-
const treeTracks = [];
|
|
1530
|
-
const tracksPerGroup = this.animationClip.tracksPerGroup;
|
|
1531
|
-
for( let i = 0; i < this.selectedItems.length; i++ ){
|
|
1532
|
-
const item = this.selectedItems[ i ];
|
|
1533
|
-
const isGroup = !item.isTrack;
|
|
1534
|
-
const itemTracks = isGroup ? tracksPerGroup[item] : [item];
|
|
1535
|
-
const nodes = [];
|
|
1536
|
-
|
|
1537
|
-
for( let j = 0; j < itemTracks.length; j++ ){
|
|
1538
|
-
const track = itemTracks[j];
|
|
1539
|
-
nodes.push({'trackData': track, 'id': track.id, 'skipVisibility': this.skipVisibility, visible: track.active, 'children':[], actions : this.skipLock ? null : [{
|
|
1540
|
-
'name':'Lock edition',
|
|
1541
|
-
'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
|
|
1542
|
-
'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
|
|
1543
|
-
'callback': (node, swapValue, event) => {
|
|
1544
|
-
this.setTrackLock( node.trackData.trackIdx, !node.trackData.locked, false, false ); // do not update left panel
|
|
1545
|
-
}
|
|
1546
|
-
}]});
|
|
1547
|
-
}
|
|
1548
|
-
if ( isGroup ){
|
|
1549
|
-
const t = {
|
|
1550
|
-
'id': item,
|
|
1551
|
-
'skipVisibility': true,
|
|
1552
|
-
'children': nodes
|
|
1553
|
-
};
|
|
1554
|
-
treeTracks.push( t );
|
|
1555
|
-
}else{
|
|
1556
|
-
treeTracks.push( nodes[0] );
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
return treeTracks;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
/**
|
|
1563
|
-
* OVERRIDE
|
|
1564
|
-
* @param {Object} options track information that wants to be set to the new track
|
|
1565
|
-
* id, dim, values, times, selected, edited, hovered
|
|
1566
|
-
* @returns
|
|
1567
|
-
*/
|
|
1568
|
-
instantiateTrack(options ={}, clone = false){
|
|
1569
|
-
const track = super.instantiateTrack(options);
|
|
1570
|
-
track.dim = Math.max(1,options.dim ?? 1); // >= 1
|
|
1571
|
-
track.groupId = null,
|
|
1572
|
-
track.groupTrackIdx = -1, // track Idx inside group only if in group
|
|
1573
|
-
|
|
1574
|
-
track.values = new Float32Array(0);
|
|
1575
|
-
track.times = new Float32Array(0);
|
|
1576
|
-
track.selected = [];
|
|
1577
|
-
track.edited = [];
|
|
1578
|
-
track.hovered = [];
|
|
1579
|
-
|
|
1580
|
-
if ( options.values && options.times ){
|
|
1581
|
-
track.values = clone ? options.values.slice() : options.values;
|
|
1582
|
-
track.times = clone ? options.times.slice() : options.times;
|
|
1583
|
-
|
|
1584
|
-
const numFrames = track.times.length;
|
|
1585
|
-
if ( options.selected && options.selected.length == numFrames ){
|
|
1586
|
-
track.selected = clone ? options.selected.slice() : options.selected;
|
|
1587
|
-
}else{
|
|
1588
|
-
track.selected = (new Array(numFrames)).fill(false);
|
|
1589
|
-
}
|
|
1590
|
-
if ( options.edited && options.edited.length == numFrames ){
|
|
1591
|
-
track.edited = clone ? options.edited.slice() : options.edited;
|
|
1592
|
-
}else{
|
|
1593
|
-
track.edited = (new Array(numFrames)).fill(false);
|
|
1594
|
-
}
|
|
1595
|
-
if ( options.hovered && options.hovered.length == numFrames ){
|
|
1596
|
-
track.hovered = clone ? options.hovered.slice() : options.hovered;
|
|
1597
|
-
}else{
|
|
1598
|
-
track.hovered = (new Array(numFrames)).fill(false);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
track.curves = options.curves ?? this.defaultCurves; // only works if dim == 1
|
|
1603
|
-
track.curvesRange = ( options.curvesRange ?? this.defaultCurvesRange ).slice();
|
|
1604
|
-
return track;
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
/**
|
|
1608
|
-
* Generates an animationClip using either the parameters set in the animation argument or using default values
|
|
1609
|
-
* @param {Object} animation data with which to generate an animationClip
|
|
1610
|
-
* @returns
|
|
1611
|
-
*/
|
|
1612
|
-
instantiateAnimationClip(animation, clone = false) {
|
|
1613
|
-
|
|
1614
|
-
const animationClip = super.instantiateAnimationClip(animation, clone);
|
|
1615
|
-
|
|
1616
|
-
animationClip.tracksPerGroup = {};
|
|
1617
|
-
|
|
1618
|
-
if (animation && animation.tracks) {
|
|
1619
|
-
const tracksPerGroup = {};
|
|
1620
|
-
let duration = 0;
|
|
1621
|
-
for( let i = 0; i < animation.tracks.length; ++i ) {
|
|
1622
|
-
|
|
1623
|
-
let track = animation.tracks[i];
|
|
1624
|
-
let times = track.times ?? [];
|
|
1625
|
-
let values = track.values ?? [];
|
|
1626
|
-
|
|
1627
|
-
let valueDim = track.dim;
|
|
1628
|
-
if ( !valueDim || valueDim < 0 ){
|
|
1629
|
-
if ( times.length && values.length ){ valueDim = Math.round(values.length/times.length); }
|
|
1630
|
-
else{ valueDim = 1; }
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
let baseName = track.id ?? track.name;
|
|
1634
|
-
const [groupId, trackId] = baseName ? this._getValidTrackName(baseName) : [null, null];
|
|
1635
|
-
|
|
1636
|
-
const toInstantiate = Object.assign({}, track);
|
|
1637
|
-
toInstantiate.id = trackId;
|
|
1638
|
-
toInstantiate.dim = valueDim;
|
|
1639
|
-
const trackInfo = this.instantiateTrack(toInstantiate, clone);
|
|
1640
|
-
|
|
1641
|
-
// manual group insertion
|
|
1642
|
-
if ( groupId ){
|
|
1643
|
-
if(!tracksPerGroup[groupId]) {
|
|
1644
|
-
tracksPerGroup[groupId] = [trackInfo];
|
|
1645
|
-
}else {
|
|
1646
|
-
tracksPerGroup[groupId].push( trackInfo );
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
trackInfo.groupId = groupId;
|
|
1650
|
-
trackInfo.groupTrackIdx = tracksPerGroup[groupId].length - 1; // index of track in group
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
trackInfo.trackIdx = i; // index of track in the entire animation
|
|
1654
|
-
|
|
1655
|
-
animationClip.tracks.push(trackInfo);
|
|
1656
|
-
|
|
1657
|
-
if ( trackInfo.times.length ){ duration = Math.max( duration, trackInfo.times[trackInfo.times.length-1]); }
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
animationClip.tracksPerGroup = tracksPerGroup;
|
|
1661
|
-
if ( !animation || !animation.duration ){
|
|
1662
|
-
animationClip.duration = duration;
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// overwrite trackspergroup
|
|
1666
|
-
if ( animation.tracksPerGroup ){
|
|
1667
|
-
|
|
1668
|
-
// ungroup all tracks (just in case)
|
|
1669
|
-
animationClip.tracks.forEach( (v,i,arr) =>{ v.groupId = null; v.groupTrackIdx = -1; } );
|
|
1670
|
-
|
|
1671
|
-
animationClip.tracksPerGroup = {};
|
|
1672
|
-
let tpg = animation.tracksPerGroup;
|
|
1673
|
-
for( let groupId in tpg ){
|
|
1674
|
-
const source = tpg[groupId];
|
|
1675
|
-
const target = [];
|
|
1676
|
-
for( let ti = 0; ti < source.length; ++ti ){
|
|
1677
|
-
const trackInfo = animationClip.tracks[ source[ti].trackIdx ]; // redo references
|
|
1678
|
-
target[ti] = trackInfo;
|
|
1679
|
-
trackInfo.groupId = groupId;
|
|
1680
|
-
trackInfo.groupTrackIdx = ti; // index of track in group
|
|
1681
|
-
}
|
|
1682
|
-
animationClip.tracksPerGroup[ groupId ] = target;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
return animationClip;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// OVERRIDE
|
|
1691
|
-
deselectAllElements(){
|
|
1692
|
-
this.deselectAllKeyFrames();
|
|
1693
|
-
this.unHoverAll();
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* OVERRIDE
|
|
1698
|
-
* @param {Array} itemsToAdd [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
|
|
1699
|
-
* @param {Array} itemsToRemove [ trackIdx, "groupId" ], array of strings and/or number identifying groups and/or tracks
|
|
1700
|
-
*/
|
|
1701
|
-
changeSelectedItems( itemsToAdd = null, itemsToRemove = null, skipCallback = false ) {
|
|
1702
|
-
|
|
1703
|
-
this.deselectAllElements();
|
|
1704
|
-
this.deselectAllTracks( false ); // no need to update left panel. It is going to be rebuilt anyways
|
|
1705
|
-
|
|
1706
|
-
const tracks = this.animationClip.tracks;
|
|
1707
|
-
const tracksPerGroup = this.animationClip.tracksPerGroup;
|
|
1708
|
-
|
|
1709
|
-
if ( itemsToRemove ){
|
|
1710
|
-
for( let i = 0; i < itemsToRemove.length; ++i){
|
|
1711
|
-
const isGroup = isNaN(itemsToRemove[i]);
|
|
1712
|
-
let compareObj = isGroup ? itemsToRemove[i] : tracks[itemsToRemove[i]]; // trackData or groupId
|
|
1713
|
-
for( let s = 0; s < this.selectedItems.length; ++s){
|
|
1714
|
-
if (this.selectedItems[s] === compareObj){
|
|
1715
|
-
const size = isGroup ? tracksPerGroup[ compareObj ].length : 1;
|
|
1716
|
-
this.selectedItems.splice(s, size);
|
|
1717
|
-
break;
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if ( itemsToAdd ){
|
|
1724
|
-
for( let i = 0; i < itemsToAdd.length; ++i ){
|
|
1725
|
-
const v = itemsToAdd[i];
|
|
1726
|
-
if ( isNaN(v) ){ // assuming it is a string
|
|
1727
|
-
if ( tracksPerGroup[ v ] ){
|
|
1728
|
-
this.selectedItems.push( v );
|
|
1729
|
-
}
|
|
1730
|
-
}else if ( tracks[v] ) {
|
|
1731
|
-
this.selectedItems.push( tracks[v] );
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
this.updateLeftPanel();
|
|
1737
|
-
|
|
1738
|
-
if(this.onItemSelected && !skipCallback){
|
|
1739
|
-
this.onItemSelected(this.selectedItems, itemsToAdd, itemsToRemove);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
/**
|
|
1744
|
-
* @param {String} groupId unique identifier
|
|
1745
|
-
* @param {Array} groupTracks [ "trackID", trackIdx ] array of strings and/or numbers of the existing tracks to include in this group. A track can only be part of 1 group
|
|
1746
|
-
* if groupTracks == null, groupId is removed from the list
|
|
1747
|
-
*/
|
|
1748
|
-
setTracksGroup( groupId, groupTracks = null ){
|
|
1749
|
-
const tracks = this.animationClip.tracks;
|
|
1750
|
-
const tracksPerGroup = this.animationClip.tracksPerGroup;
|
|
1751
|
-
const result = [];
|
|
1752
|
-
|
|
1753
|
-
let selectedItemsCounter = -1;
|
|
1754
|
-
|
|
1755
|
-
if ( tracksPerGroup[groupId] ){
|
|
1756
|
-
// if group exists, ungroup tracks.
|
|
1757
|
-
tracksPerGroup[groupId].forEach(t => {
|
|
1758
|
-
t.groupId = null;
|
|
1759
|
-
t.groupTrackIdx = -1;
|
|
1760
|
-
});
|
|
1761
|
-
|
|
1762
|
-
// modify groups cannot appear more than once
|
|
1763
|
-
for( let i = 0; i < this.selectedItems.length; ++i ){
|
|
1764
|
-
if (this.selectedItems[i] === groupId){
|
|
1765
|
-
selectedItemsCounter = i;
|
|
1766
|
-
break;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
if ( !groupTracks ){
|
|
1772
|
-
delete tracksPerGroup.groupId;
|
|
1773
|
-
// remove entry from selectedItems
|
|
1774
|
-
if( selectedItemsCounter > -1 ){
|
|
1775
|
-
this.selectedItems.splice(selectedItemsCounter, 1);
|
|
1776
|
-
}
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// find tracks and group them
|
|
1781
|
-
for (let i = 0; i < groupTracks.length; ++i){
|
|
1782
|
-
const v = groupTracks[i];
|
|
1783
|
-
let track = null;
|
|
1784
|
-
if ( isNaN(v) ){
|
|
1785
|
-
// v is an id (string)
|
|
1786
|
-
for( let t = 0; t < tracks.length; ++t ){
|
|
1787
|
-
if (tracks[t].id == v){
|
|
1788
|
-
track = tracks[t];
|
|
1789
|
-
break;
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
else if( tracks[v] ) {
|
|
1794
|
-
track = tracks[v];
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
if ( track ){
|
|
1798
|
-
track.groupId = groupId;
|
|
1799
|
-
track.groupTrackIdx = result.length;
|
|
1800
|
-
result.push( track );
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
tracksPerGroup[ groupId ] = result;
|
|
1805
|
-
|
|
1806
|
-
// if group is currently visible
|
|
1807
|
-
if ( selectedItemsCounter > -1){
|
|
1808
|
-
this.updateLeftPanel();
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
/**
|
|
1813
|
-
* @param {String} groupId
|
|
1814
|
-
* @returns array of tracks or null
|
|
1815
|
-
*/
|
|
1816
|
-
getTracksGroup( groupId ){
|
|
1817
|
-
return this.animationClip.tracksPerGroup[ groupId ] ?? null;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
/**
|
|
1821
|
-
* OVERRIDE
|
|
1822
|
-
* @param {String} trackId
|
|
1823
|
-
* @param {String} groupId optionl. If not set, it will find the first occurrence of trackId in animationClip.tracks
|
|
1824
|
-
* @returns
|
|
1825
|
-
*/
|
|
1826
|
-
getTrack( trackId, groupId = null ){
|
|
1827
|
-
let tracks = this.animationClip.tracks;
|
|
1828
|
-
if (groupId){
|
|
1829
|
-
tracks = this.animationClip.tracksPerGroup[ groupId ] ?? [];
|
|
1830
|
-
}
|
|
1831
|
-
for( let i = 0; i < tracks.length; ++i){
|
|
1832
|
-
if ( tracks[i].id == trackId ){
|
|
1833
|
-
return tracks[i];
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
return null;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
/**
|
|
1840
|
-
*
|
|
1841
|
-
* @param {Number} size pixels, height of keyframe
|
|
1842
|
-
* @param {Number} sizeHovered optional, size in pixels when hovered
|
|
1843
|
-
*/
|
|
1844
|
-
setKeyframeSize( size, sizeHovered = null ){
|
|
1845
|
-
this.keyframeSizeHovered = sizeHovered ?? size;
|
|
1846
|
-
this.keyframeSize = size;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
onMouseUp( e, time ) {
|
|
1850
|
-
|
|
1851
|
-
let track = e.track;
|
|
1852
|
-
let localX = e.localX;
|
|
1853
|
-
let discard = e.discard; // true when too much time has passed between Down and Up
|
|
1854
|
-
|
|
1855
|
-
if(e.shiftKey) {
|
|
1856
|
-
// Manual multiple selection
|
|
1857
|
-
if(!discard && track) {
|
|
1858
|
-
const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
|
|
1859
|
-
const keyFrameIdx = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
|
|
1860
|
-
if ( keyFrameIdx > -1 ){
|
|
1861
|
-
track.selected[keyFrameIdx] ?
|
|
1862
|
-
this.deselectKeyFrame(track.trackIdx, keyFrameIdx) :
|
|
1863
|
-
this.processSelectionKeyFrame( track.trackIdx, keyFrameIdx, true );
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
// Box selection
|
|
1867
|
-
else if(this.boxSelection) {
|
|
1868
|
-
let tracks = this.getTracksInRange(this.boxSelectionStart[1], this.boxSelectionEnd[1]);
|
|
1869
|
-
|
|
1870
|
-
for(let t of tracks) {
|
|
1871
|
-
let keyFrameIndices = this.getKeyFramesInRange(t,
|
|
1872
|
-
this.xToTime( this.boxSelectionStart[0] ),
|
|
1873
|
-
this.xToTime( this.boxSelectionEnd[0] ),
|
|
1874
|
-
this.secondsPerPixel * 5);
|
|
1875
|
-
|
|
1876
|
-
if(keyFrameIndices) {
|
|
1877
|
-
for(let index = keyFrameIndices[0]; index <= keyFrameIndices[1]; ++index){
|
|
1878
|
-
this.processSelectionKeyFrame( t.trackIdx, index, true );
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
else if( !this.movingKeys && !discard ){ // if not moving timeline and not adding keyframes through e.shiftkey (just a click)
|
|
1885
|
-
|
|
1886
|
-
if ( this.lastKeyFramesSelected.length ){
|
|
1887
|
-
if (this.onDeselectKeyFrames){
|
|
1888
|
-
this.onDeselectKeyFrames( this.lastKeyFramesSelected );
|
|
1889
|
-
}
|
|
1890
|
-
this.deselectAllKeyFrames();
|
|
1891
|
-
}
|
|
1892
|
-
if (track){
|
|
1893
|
-
const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
|
|
1894
|
-
const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
|
|
1895
|
-
if( keyFrameIndex > -1 ) {
|
|
1896
|
-
this.processSelectionKeyFrame( track.trackIdx, keyFrameIndex, false ); // Settings this as multiple so time is not being set
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
this.canvas.classList.remove('grabbing');
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
onMouseDown( e, time ) {
|
|
1905
|
-
// function not called if shift is pressed (boxselection)
|
|
1906
|
-
|
|
1907
|
-
let localX = e.localX;
|
|
1908
|
-
let localY = e.localY;
|
|
1909
|
-
let track = e.track;
|
|
1910
|
-
|
|
1911
|
-
if( (e.ctrlKey || e.altKey) && this.lastKeyFramesSelected.length) { // move keyframes
|
|
1912
|
-
this.movingKeys = true;
|
|
1913
|
-
this.canvas.style.cursor = "grab";
|
|
1914
|
-
this.canvas.classList.add('grabbing');
|
|
1915
|
-
|
|
1916
|
-
// Set pre-move state
|
|
1917
|
-
this.moveKeyMinTime = Infinity;
|
|
1918
|
-
const tracks = this.animationClip.tracks;
|
|
1919
|
-
let lastTrackIdx = -1;
|
|
1920
|
-
for(let selectedKey of this.lastKeyFramesSelected) { // WARNING assumes lasKeyFramesSelected is sorted, so all keyframes of the same track are grouped
|
|
1921
|
-
let [trackIdx, keyIndex, keyTime] = selectedKey;
|
|
1922
|
-
const track = tracks[trackIdx];
|
|
1923
|
-
|
|
1924
|
-
selectedKey[2] = track.times[keyIndex]; // update original time just in case
|
|
1925
|
-
|
|
1926
|
-
if ( lastTrackIdx != trackIdx ){
|
|
1927
|
-
// save track states only once
|
|
1928
|
-
if (this.moveKeyMinTime < Infinity){
|
|
1929
|
-
this.saveState(track.trackIdx, true);
|
|
1930
|
-
}else{
|
|
1931
|
-
this.saveState(track.trackIdx, false);
|
|
1932
|
-
}
|
|
1933
|
-
this.moveKeyMinTime = Math.min( this.moveKeyMinTime, selectedKey[2] );
|
|
1934
|
-
lastTrackIdx = trackIdx;
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
this.timeBeforeMove = this.xToTime( localX );
|
|
1940
|
-
|
|
1941
|
-
this.grabbing = false;
|
|
1942
|
-
this.grabbingTimeBar = false;
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
onMouseMove( e, time ) {
|
|
1947
|
-
// function not called if shift is pressed (boxselection)
|
|
1948
|
-
|
|
1949
|
-
let localX = e.localX;
|
|
1950
|
-
let localY = e.localY;
|
|
1951
|
-
let track = e.track;
|
|
1952
|
-
|
|
1953
|
-
if(this.movingKeys) { // move keyframes
|
|
1954
|
-
|
|
1955
|
-
let newTime = this.xToTime( localX );
|
|
1956
|
-
let deltaTime = newTime - this.timeBeforeMove;
|
|
1957
|
-
if ( deltaTime + this.moveKeyMinTime < 0 ){
|
|
1958
|
-
deltaTime = -this.moveKeyMinTime;
|
|
1959
|
-
}
|
|
1960
|
-
this.timeBeforeMove = this.timeBeforeMove + deltaTime;
|
|
1961
|
-
|
|
1962
|
-
if ( e.ctrlKey ){
|
|
1963
|
-
this.moveKeyMinTime += deltaTime;
|
|
1964
|
-
const tracks = this.animationClip.tracks;
|
|
1965
|
-
for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
|
|
1966
|
-
let idx = i;
|
|
1967
|
-
if ( deltaTime > 0 ){
|
|
1968
|
-
idx = this.lastKeyFramesSelected.length - 1 - i;
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
const [trackIdx, keyIndex, originalKeyTime] = this.lastKeyFramesSelected[idx];
|
|
1972
|
-
track = tracks[trackIdx];
|
|
1973
|
-
if(track && track.locked)
|
|
1974
|
-
continue;
|
|
1975
|
-
|
|
1976
|
-
this.canvas.style.cursor = "grabbing";
|
|
1977
|
-
|
|
1978
|
-
const times = this.animationClip.tracks[ track.trackIdx ].times;
|
|
1979
|
-
times[ keyIndex ] = Math.max(0,times[keyIndex] + deltaTime);
|
|
1980
|
-
if (times[ keyIndex ] > this.duration){
|
|
1981
|
-
this.setDuration(times[ keyIndex ]);
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
// sort keyframe
|
|
1985
|
-
let k = keyIndex;
|
|
1986
|
-
if ( deltaTime > 0 ){
|
|
1987
|
-
for( ; k < times.length-1; ++k ){
|
|
1988
|
-
if ( times[k] < times[k+1] ){
|
|
1989
|
-
break;
|
|
1990
|
-
}
|
|
1991
|
-
this.swapKeyFrames(track, k+1, k);
|
|
1992
|
-
}
|
|
1993
|
-
}else{
|
|
1994
|
-
for( ; k > 0; --k ){
|
|
1995
|
-
if ( times[k-1] < times[k] ){
|
|
1996
|
-
break;
|
|
1997
|
-
}
|
|
1998
|
-
this.swapKeyFrames(track, k-1, k);
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
this.lastKeyFramesSelected[idx][1] = k; // update keyframe index
|
|
2002
|
-
this.lastKeyFramesSelected[idx][2] = times[k]; // update keyframe time
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
if ( this.onContentMoved ){
|
|
2006
|
-
for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
|
|
2007
|
-
const [trackIdx, keyIndex, originalKeyTime] = this.lastKeyFramesSelected[i];
|
|
2008
|
-
track = this.animationClip.tracks[trackIdx];
|
|
2009
|
-
if(track && track.locked)
|
|
2010
|
-
continue;
|
|
2011
|
-
this.onContentMoved(trackIdx, keyIndex);
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// Track.dim == 1: move keyframes vertically (change values instead of time)
|
|
2017
|
-
// RELIES ON SORTED ARRAY OF lastKeyFramesSelected
|
|
2018
|
-
if ( e.altKey && e.buttons & 0x01 ){
|
|
2019
|
-
const tracks = this.animationClip.tracks;
|
|
2020
|
-
let lastTrackChanged = -1;
|
|
2021
|
-
for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
|
|
2022
|
-
const [trackIdx, keyIndex, originalKeyTime] = this.lastKeyFramesSelected[i];
|
|
2023
|
-
track = tracks[trackIdx];
|
|
2024
|
-
if(track.locked || track.dim != 1 || !track.curves){
|
|
2025
|
-
continue;
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
let value = track.values[keyIndex];
|
|
2029
|
-
let delta = e.deltay * this.keyValuePerPixel * (track.curvesRange[1]-track.curvesRange[0]);
|
|
2030
|
-
track.values[keyIndex] = Math.max(track.curvesRange[0], Math.min(track.curvesRange[1], value - delta)); // invert delta because of screen y
|
|
2031
|
-
track.edited[keyIndex] = true;
|
|
2032
|
-
|
|
2033
|
-
if ( this.onUpdateTrack && track.trackIdx != lastTrackChanged && lastTrackChanged > -1){ // do it only once all keyframes of the same track have been modified
|
|
2034
|
-
this.onUpdateTrack( [track.trackIdx] );
|
|
2035
|
-
}
|
|
2036
|
-
lastTrackChanged = track.trackIdx;
|
|
2037
|
-
}
|
|
2038
|
-
if( this.onUpdateTrack && lastTrackChanged > -1 ){ // do the last update, once the last track has been processed
|
|
2039
|
-
this.onUpdateTrack( [track.trackIdx] );
|
|
2040
|
-
}
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
if( this.grabbing && e.button != 2) {
|
|
2047
|
-
|
|
2048
|
-
}
|
|
2049
|
-
else if(track) {
|
|
2050
|
-
|
|
2051
|
-
this.unHoverAll();
|
|
2052
|
-
const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
|
|
2053
|
-
let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
|
|
2054
|
-
if(keyFrameIndex > -1 ) {
|
|
2055
|
-
if(track && track.locked)
|
|
2056
|
-
return;
|
|
2057
|
-
|
|
2058
|
-
this.lastHovered = [track.trackIdx, keyFrameIndex];
|
|
2059
|
-
track.hovered[keyFrameIndex] = true;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
else {
|
|
2063
|
-
this.unHoverAll();
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
showContextMenu( e ) {
|
|
2068
|
-
|
|
2069
|
-
e.preventDefault();
|
|
2070
|
-
e.stopPropagation();
|
|
2071
|
-
|
|
2072
|
-
let actions = [];
|
|
2073
|
-
if(this.lastKeyFramesSelected && this.lastKeyFramesSelected.length) {
|
|
2074
|
-
actions.push(
|
|
2075
|
-
{
|
|
2076
|
-
title: "Copy",
|
|
2077
|
-
callback: () => {
|
|
2078
|
-
this.copySelectedContent();
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
);
|
|
2082
|
-
actions.push(
|
|
2083
|
-
{
|
|
2084
|
-
title: "Delete",
|
|
2085
|
-
callback: () => {
|
|
2086
|
-
this.deleteSelectedContent({});
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
);
|
|
2090
|
-
if(this.lastKeyFramesSelected.length == 1 && this.clipboard && this.clipboard.value)
|
|
2091
|
-
{
|
|
2092
|
-
actions.push(
|
|
2093
|
-
{
|
|
2094
|
-
title: "Paste Value",
|
|
2095
|
-
callback: () => {
|
|
2096
|
-
this.pasteContentValue();
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
else{
|
|
2103
|
-
|
|
2104
|
-
actions.push(
|
|
2105
|
-
{
|
|
2106
|
-
title: "Add Here",
|
|
2107
|
-
callback: () => {
|
|
2108
|
-
if ( !e.track ){ return; }
|
|
2109
|
-
const values = new Float32Array( e.track.dim );
|
|
2110
|
-
values.fill(0);
|
|
2111
|
-
this.addKeyFrames( e.track.trackIdx, values, [this.xToTime(e.localX)] );
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
);
|
|
2115
|
-
actions.push(
|
|
2116
|
-
{
|
|
2117
|
-
title: "Add",
|
|
2118
|
-
callback: () => {
|
|
2119
|
-
if ( !e.track ){ return; }
|
|
2120
|
-
const values = new Float32Array( e.track.dim );
|
|
2121
|
-
values.fill(0);
|
|
2122
|
-
this.addKeyFrames( e.track.trackIdx, values, [this.currentTime] );
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
);
|
|
2126
|
-
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
if(this.clipboard && this.clipboard.keyframes)
|
|
2130
|
-
{
|
|
2131
|
-
actions.push(
|
|
2132
|
-
{
|
|
2133
|
-
title: "Paste Here",
|
|
2134
|
-
callback: () => {
|
|
2135
|
-
this.pasteContent( this.xToTime(e.localX) );
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
);
|
|
2139
|
-
actions.push(
|
|
2140
|
-
{
|
|
2141
|
-
title: "Paste",
|
|
2142
|
-
callback: () => {
|
|
2143
|
-
this.pasteContent( this.currentTime );
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
);
|
|
2147
|
-
}
|
|
2148
|
-
|
|
2149
|
-
LX.addContextMenu("Options", e, (m) => {
|
|
2150
|
-
for(let i = 0; i < actions.length; i++) {
|
|
2151
|
-
m.add(actions[i].title, actions[i].callback )
|
|
2152
|
-
}
|
|
2153
|
-
});
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
drawContent( ctx ) {
|
|
2158
|
-
|
|
2159
|
-
if(!this.animationClip)
|
|
2160
|
-
return;
|
|
2161
|
-
|
|
2162
|
-
ctx.save();
|
|
2163
|
-
|
|
2164
|
-
const trackHeight = this.trackHeight;
|
|
2165
|
-
const scrollY = - this.currentScrollInPixels;
|
|
2166
|
-
|
|
2167
|
-
// elements from "ul" should match the visible tracks (and groups) as if this.selectedItems was flattened
|
|
2168
|
-
const visibleElements = this.getVisibleItems();
|
|
2169
|
-
|
|
2170
|
-
let offset = scrollY;
|
|
2171
|
-
|
|
2172
|
-
// compute track from which to start rendering (avoid rendering unseen tracks)
|
|
2173
|
-
let startElIdx = 0;
|
|
2174
|
-
if ( offset < -this.lastTrackTreesComponentOffset ){ // offset 0 = (0 of canvas) + track-Tree-Offset. This renders tracks under the time zone
|
|
2175
|
-
startElIdx = Math.floor( -(offset + this.lastTrackTreesComponentOffset) / this.trackHeight ); // how many tracks to skip
|
|
2176
|
-
offset += startElIdx * this.trackHeight;
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
ctx.translate(0, offset);
|
|
2180
|
-
|
|
2181
|
-
// compute track to end rendering (avoid rendering unseen tracks)
|
|
2182
|
-
let endElIdx = startElIdx + Math.ceil( ( ctx.canvas.height - this.lastTrackTreesComponentOffset - offset ) / this.trackHeight );
|
|
2183
|
-
endElIdx = endElIdx > visibleElements.length ? visibleElements.length : endElIdx;
|
|
2184
|
-
|
|
2185
|
-
for(let t = startElIdx; t < endElIdx; t++) {
|
|
2186
|
-
const track = visibleElements[t].treeData.trackData;
|
|
2187
|
-
|
|
2188
|
-
if (track){
|
|
2189
|
-
if (track.dim == 1 && track.curves){
|
|
2190
|
-
this.drawTrackWithCurves(ctx, trackHeight, track);
|
|
2191
|
-
}else{
|
|
2192
|
-
this.drawTrackWithKeyframes(ctx, trackHeight, track);
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
ctx.translate(0, trackHeight);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
ctx.restore();
|
|
2200
|
-
};
|
|
2201
|
-
|
|
2202
|
-
/**
|
|
2203
|
-
* @method drawTrackWithKeyframes
|
|
2204
|
-
* @param {*} ctx
|
|
2205
|
-
* ...
|
|
2206
|
-
* @description helper function, you can call it from drawContent to render all the keyframes
|
|
2207
|
-
*/
|
|
2208
|
-
drawTrackWithKeyframes( ctx, trackHeight, track ) {
|
|
2209
|
-
|
|
2210
|
-
if(track.isSelected) {
|
|
2211
|
-
ctx.globalAlpha = 0.2;
|
|
2212
|
-
ctx.fillStyle = Timeline.TRACK_SELECTED;
|
|
2213
|
-
ctx.fillRect(0, 0, ctx.canvas.width, trackHeight );
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR;
|
|
2217
|
-
ctx.globalAlpha = 1;
|
|
2218
|
-
|
|
2219
|
-
const keyframes = track.times;
|
|
2220
|
-
const startTime = this.visualTimeRange[0];
|
|
2221
|
-
const endTime = this.visualTimeRange[1] + 0.0000001;
|
|
2222
|
-
const defaultPointSize = this.keyframeSize / Math.SQRT2; // pythagoras with equal sides h2 = c2 + c2 = 2 * c2
|
|
2223
|
-
const hoverPointSize = this.keyframeSizeHovered / Math.SQRT2;
|
|
2224
|
-
|
|
2225
|
-
for(let j = 0; j < keyframes.length; ++j)
|
|
2226
|
-
{
|
|
2227
|
-
let time = keyframes[j];
|
|
2228
|
-
if( time < startTime || time > endTime ) {
|
|
2229
|
-
continue;
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
let keyframePosX = this.timeToX( time );
|
|
2233
|
-
let size = defaultPointSize;
|
|
2234
|
-
|
|
2235
|
-
if(!this.active || track.active == false) {
|
|
2236
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_INACTIVE;
|
|
2237
|
-
}
|
|
2238
|
-
else if(track.locked) {
|
|
2239
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_LOCK;
|
|
2240
|
-
}
|
|
2241
|
-
else if(track.hovered[j]) {
|
|
2242
|
-
size = hoverPointSize;
|
|
2243
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_HOVERED;
|
|
2244
|
-
}
|
|
2245
|
-
else if(track.selected[j]) {
|
|
2246
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_SELECTED;
|
|
2247
|
-
}
|
|
2248
|
-
else if(track.edited[j]) {
|
|
2249
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_EDITED;
|
|
2250
|
-
}
|
|
2251
|
-
else {
|
|
2252
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
ctx.save();
|
|
2256
|
-
ctx.translate(keyframePosX, trackHeight * 0.5);
|
|
2257
|
-
ctx.rotate(45 * Math.PI / 180);
|
|
2258
|
-
ctx.fillRect( -size*0.5, -size*0.5, size, size);
|
|
2259
|
-
ctx.restore();
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
ctx.globalAlpha = 1;
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
drawTrackWithCurves (ctx, trackHeight, track) {
|
|
2266
|
-
if(track.isSelected){
|
|
2267
|
-
ctx.globalAlpha = 0.2;
|
|
2268
|
-
ctx.fillStyle = Timeline.TRACK_SELECTED_LIGHT;
|
|
2269
|
-
ctx.fillRect(0, 0, ctx.canvas.width, trackHeight );
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
ctx.globalAlpha = 1;
|
|
2273
|
-
const keyframes = track.times;
|
|
2274
|
-
const values = track.values;
|
|
2275
|
-
const defaultPointSize = this.keyframeSize * 0.5; // radius
|
|
2276
|
-
const hoverPointSize = this.keyframeSizeHovered * 0.5; // radius
|
|
2277
|
-
const valueRange = track.curvesRange; //[min, max]
|
|
2278
|
-
const displayRange = trackHeight - defaultPointSize * 2;
|
|
2279
|
-
const startTime = this.visualTimeRange[0];
|
|
2280
|
-
const endTime = this.visualTimeRange[1] + 0.0000001;
|
|
2281
|
-
//draw lines
|
|
2282
|
-
ctx.strokeStyle = "white";
|
|
2283
|
-
ctx.beginPath();
|
|
2284
|
-
|
|
2285
|
-
if ( keyframes.length > 1){
|
|
2286
|
-
let startPosX = this.timeToX( keyframes[0] );
|
|
2287
|
-
let startValue = values[0];
|
|
2288
|
-
startValue = LX.clamp((startValue - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
|
|
2289
|
-
ctx.moveTo( startPosX, startValue );
|
|
2290
|
-
|
|
2291
|
-
for(let j = 1; j < keyframes.length; ++j){
|
|
2292
|
-
|
|
2293
|
-
let time = keyframes[j];
|
|
2294
|
-
let keyframePosX = this.timeToX( time );
|
|
2295
|
-
let value = values[j];
|
|
2296
|
-
value = LX.clamp((value - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
|
|
2297
|
-
|
|
2298
|
-
if( time < startTime ){
|
|
2299
|
-
ctx.moveTo( keyframePosX, value );
|
|
2300
|
-
continue;
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
if ( time > endTime ){
|
|
2304
|
-
let lastKeyframePosX = this.timeToX( keyframes[j-1] );
|
|
2305
|
-
let dt = keyframePosX - lastKeyframePosX;
|
|
2306
|
-
if ( dt > 0 ){
|
|
2307
|
-
let lastValue = values[j-1];
|
|
2308
|
-
lastValue = LX.clamp((lastValue - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
|
|
2309
|
-
let f = (this.timeToX( endTime ) - lastKeyframePosX) / dt;
|
|
2310
|
-
ctx.lineTo( lastKeyframePosX + dt * f, lastValue * (1-f) + value * f );
|
|
2311
|
-
}
|
|
2312
|
-
break; //end loop, but print line
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
//convert to timeline track range
|
|
2316
|
-
ctx.lineTo( keyframePosX, value );
|
|
2317
|
-
}
|
|
2318
|
-
ctx.stroke();
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
//draw points
|
|
2322
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR;
|
|
2323
|
-
for(let j = 0; j < keyframes.length; ++j)
|
|
2324
|
-
{
|
|
2325
|
-
let time = keyframes[j];
|
|
2326
|
-
if( time < startTime || time > endTime )
|
|
2327
|
-
continue;
|
|
2328
|
-
|
|
2329
|
-
let size = defaultPointSize;
|
|
2330
|
-
let keyframePosX = this.timeToX( time );
|
|
2331
|
-
|
|
2332
|
-
if(!this.active || !track.active)
|
|
2333
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_INACTIVE;
|
|
2334
|
-
else if(track.locked)
|
|
2335
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_LOCK;
|
|
2336
|
-
else if(track.hovered[j]) {
|
|
2337
|
-
size = hoverPointSize;
|
|
2338
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_HOVERED;
|
|
2339
|
-
}
|
|
2340
|
-
else if(track.selected[j])
|
|
2341
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_SELECTED;
|
|
2342
|
-
else if(track.edited[j])
|
|
2343
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_EDITED;
|
|
2344
|
-
else
|
|
2345
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR
|
|
2346
|
-
|
|
2347
|
-
let value = values[j];
|
|
2348
|
-
value = LX.clamp((value - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) *(-displayRange) + (trackHeight - defaultPointSize); // normalize, clamp and offset
|
|
2349
|
-
|
|
2350
|
-
ctx.beginPath();
|
|
2351
|
-
ctx.arc( keyframePosX, value, size, 0, Math.PI * 2);
|
|
2352
|
-
ctx.fill();
|
|
2353
|
-
ctx.closePath();
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
_getValidTrackName( uglyName ) {
|
|
2358
|
-
|
|
2359
|
-
let groupId = null;
|
|
2360
|
-
let trackId = null;
|
|
2361
|
-
let trackNameInfo;
|
|
2362
|
-
// Support other versions
|
|
2363
|
-
if(uglyName.includes("[")) {
|
|
2364
|
-
const nameIndex = uglyName.indexOf('[');
|
|
2365
|
-
trackNameInfo = uglyName.substr(nameIndex+1).split("].");
|
|
2366
|
-
}else {
|
|
2367
|
-
trackNameInfo = uglyName.split(".");
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
if ( trackNameInfo.length > 1 ){
|
|
2371
|
-
groupId = trackNameInfo[0];
|
|
2372
|
-
trackId = trackNameInfo[1];
|
|
2373
|
-
}else{
|
|
2374
|
-
trackId = trackNameInfo[0];
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
return [groupId, trackId];
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
/**
|
|
2381
|
-
* updates an existing track with new values and times.
|
|
2382
|
-
* @param {Int} trackIdx index of track in the animationClip
|
|
2383
|
-
* @param {*} newTrack object with two arrays: values and times. These will be set to the selected track
|
|
2384
|
-
* @returns
|
|
2385
|
-
*/
|
|
2386
|
-
updateTrack(trackIdx, newTrack) {
|
|
2387
|
-
if(!this.animationClip)
|
|
2388
|
-
return false;
|
|
2389
|
-
|
|
2390
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
2391
|
-
track.values = newTrack.values;
|
|
2392
|
-
track.times = newTrack.times;
|
|
2393
|
-
|
|
2394
|
-
track.selected = newTrack.selected ?? (new Array(track.times.length)).fill(false);
|
|
2395
|
-
track.hovered = newTrack.hovered ?? (new Array(track.times.length)).fill(false);
|
|
2396
|
-
track.edited = newTrack.edited ?? (new Array(track.times.length)).fill(false);
|
|
2397
|
-
return true;
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
/**
|
|
2401
|
-
* removes equivalent sequential keys either because of equal times or values
|
|
2402
|
-
* (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
|
|
2403
|
-
* @param {Int} trackIdx index of track in the animation
|
|
2404
|
-
* @param {Boolean} onlyEqualTime if true, removes only keyframes with equal times. Otherwise, values are ALSO compared through the class threshold
|
|
2405
|
-
* @param {Boolean} skipCallback if false, triggers "onOptimizeTracks" after optimizing
|
|
2406
|
-
*/
|
|
2407
|
-
optimizeTrack(trackIdx, onlyEqualTime = false, skipCallback = false ) {
|
|
2408
|
-
if ( !this.animationClip ){ return; }
|
|
2409
|
-
|
|
2410
|
-
const track = this.animationClip.tracks[trackIdx],
|
|
2411
|
-
times = track.times,
|
|
2412
|
-
values = track.values,
|
|
2413
|
-
stride = track.dim,
|
|
2414
|
-
threshold = this.optimizeThreshold;
|
|
2415
|
-
|
|
2416
|
-
if ( track.locked ){
|
|
2417
|
-
return;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
let cmpFunction = (v, p, n, t) => { return Math.abs(v - p) >= t || Math.abs(v - n) >= t };
|
|
2421
|
-
let lastSavedIndex = 0;
|
|
2422
|
-
const lastIndex = times.length-1;
|
|
2423
|
-
|
|
2424
|
-
this.saveState(track.trackIdx);
|
|
2425
|
-
|
|
2426
|
-
for ( let i = 1; i < lastIndex; ++ i ) {
|
|
2427
|
-
|
|
2428
|
-
let keep = false;
|
|
2429
|
-
const time = times[ i ];
|
|
2430
|
-
const timePrev = times[ lastSavedIndex ];
|
|
2431
|
-
|
|
2432
|
-
// remove adjacent keyframes scheduled at the same time
|
|
2433
|
-
if ( time !== timePrev ) {
|
|
2434
|
-
if ( ! onlyEqualTime ) {
|
|
2435
|
-
// remove unnecessary keyframes same as their neighbors
|
|
2436
|
-
const offset = i * stride,
|
|
2437
|
-
offsetP = lastSavedIndex * stride,
|
|
2438
|
-
offsetN = offset + stride;
|
|
2439
|
-
|
|
2440
|
-
for ( let j = 0; j !== stride; ++ j ) {
|
|
2441
|
-
if( cmpFunction(
|
|
2442
|
-
values[ offset + j ],
|
|
2443
|
-
values[ offsetP + j ],
|
|
2444
|
-
values[ offsetN + j ],
|
|
2445
|
-
threshold))
|
|
2446
|
-
{
|
|
2447
|
-
keep = true;
|
|
2448
|
-
break;
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
} else {
|
|
2452
|
-
keep = true;
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
// in-place compaction
|
|
2457
|
-
if ( keep ) {
|
|
2458
|
-
++lastSavedIndex;
|
|
2459
|
-
if ( i !== lastSavedIndex ) {
|
|
2460
|
-
times[ lastSavedIndex ] = times[ i ];
|
|
2461
|
-
const readOffset = i * stride,
|
|
2462
|
-
writeOffset = lastSavedIndex * stride;
|
|
2463
|
-
for ( let j = 0; j !== stride; ++ j ) {
|
|
2464
|
-
values[ writeOffset + j ] = values[ readOffset + j ];
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
// add last frame. first and last keyframes should be always kept
|
|
2471
|
-
if ( times.length > 1 ) {
|
|
2472
|
-
++lastSavedIndex;
|
|
2473
|
-
times[ lastSavedIndex ] = times[ times.length - 1 ];
|
|
2474
|
-
const readOffset = values.length - stride,
|
|
2475
|
-
writeOffset = lastSavedIndex * stride;
|
|
2476
|
-
for ( let j = 0; j !== stride; ++j ) {
|
|
2477
|
-
values[ writeOffset + j ] = values[ readOffset + j ];
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
// commit changes
|
|
2482
|
-
if ( lastSavedIndex < times.length-1 ) {
|
|
2483
|
-
track.times = times.slice( 0, lastSavedIndex + 1 );
|
|
2484
|
-
track.values = values.slice( 0, (lastSavedIndex + 1) * stride );
|
|
2485
|
-
this.updateTrack( track.trackIdx, track ); // update control variables (hover, edited, selected)
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
if(this.onOptimizeTracks && !skipCallback )
|
|
2489
|
-
this.onOptimizeTracks(trackIdx);
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
optimizeTracks(onlyEqualTime = false) {
|
|
2493
|
-
|
|
2494
|
-
if(!this.animationClip)
|
|
2495
|
-
return;
|
|
2496
|
-
|
|
2497
|
-
// save all states into a single entry
|
|
2498
|
-
if ( this.historySaveEnabler ){
|
|
2499
|
-
for( let i = 0; i < this.animationClip.tracks.length; ++i ) {
|
|
2500
|
-
this.saveState(i, i!=0);
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
|
|
2504
|
-
// disable state saving
|
|
2505
|
-
const oldStateEnabler = this.historySaveEnabler;
|
|
2506
|
-
this.historySaveEnabler = false;
|
|
2507
|
-
|
|
2508
|
-
// optimize
|
|
2509
|
-
for( let i = 0; i < this.animationClip.tracks.length; ++i ) {
|
|
2510
|
-
const track = this.animationClip.tracks[i];
|
|
2511
|
-
this.optimizeTrack( track.trackIdx, onlyEqualTime, true );
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
// restore old enabler status
|
|
2515
|
-
this.historySaveEnabler = oldStateEnabler;
|
|
2516
|
-
|
|
2517
|
-
// callback
|
|
2518
|
-
if(this.onOptimizeTracks )
|
|
2519
|
-
this.onOptimizeTracks(-1); // signal as "all tracks"
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
_onShowOptimizeMenu( e ) {
|
|
2523
|
-
|
|
2524
|
-
if(this.selectedItems.length == 0)
|
|
2525
|
-
return;
|
|
2526
|
-
|
|
2527
|
-
LX.addContextMenu("Optimize", e, m => {
|
|
2528
|
-
this.selectedItems.forEach( item => {
|
|
2529
|
-
if (item.isTrack){
|
|
2530
|
-
m.add( (item.groupId ? item.groupId : "" ) + "@" + item.id, () => {
|
|
2531
|
-
this.optimizeTrack( item.trackIdx, false);
|
|
2532
|
-
});
|
|
2533
|
-
}else{
|
|
2534
|
-
const tracks = this.animationClip.tracksPerGroup[ item ];
|
|
2535
|
-
for( let i = 0; i < tracks.length; ++i ){
|
|
2536
|
-
const t = tracks[i];
|
|
2537
|
-
m.add( (t.groupId ? t.groupId : "" ) + "@" + t.id, () => {
|
|
2538
|
-
this.optimizeTrack( t.trackIdx, false);
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
})
|
|
2543
|
-
});
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
/**
|
|
2547
|
-
* saveState function uses this to generate a "copy" of the track.
|
|
2548
|
-
* @param {Number} trackIdx
|
|
2549
|
-
* @returns All necessary information to reconstruct the track state
|
|
2550
|
-
*/
|
|
2551
|
-
historyGenerateTrackStep( trackIdx ){
|
|
2552
|
-
const trackInfo = this.animationClip.tracks[trackIdx];
|
|
2553
|
-
|
|
2554
|
-
const undoStep = {
|
|
2555
|
-
trackIdx: trackIdx, // already done by saveState
|
|
2556
|
-
t: trackInfo.times.slice(),
|
|
2557
|
-
v: trackInfo.values.slice(),
|
|
2558
|
-
edited: trackInfo.edited.slice(0, trackInfo.times.length)
|
|
2559
|
-
};
|
|
2560
|
-
|
|
2561
|
-
return undoStep;
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
/**
|
|
2565
|
-
* It should swap the previous state with the incoming state of the track. It must return the previous state.
|
|
2566
|
-
* historyGenerateTrackStep could be used to copy the previous state. However, as it is a swap, it suffices to just copy the references.
|
|
2567
|
-
* @param {Object} state object with a trackIdx:Number and whatever information was saved in historyGenerateTrackStep
|
|
2568
|
-
* @param {Boolean} isUndo
|
|
2569
|
-
* @returns previous state object
|
|
2570
|
-
*/
|
|
2571
|
-
historyApplyTrackStep( state, isUndo ){
|
|
2572
|
-
const track = this.animationClip.tracks[state.trackIdx];
|
|
2573
|
-
|
|
2574
|
-
const stateToReturn = {
|
|
2575
|
-
trackIdx: state.trackIdx,
|
|
2576
|
-
t: track.times,
|
|
2577
|
-
v: track.values,
|
|
2578
|
-
edited: track.edited
|
|
2579
|
-
};
|
|
2580
|
-
|
|
2581
|
-
track.times = state.t;
|
|
2582
|
-
track.values = state.v;
|
|
2583
|
-
track.edited = state.edited;
|
|
2584
|
-
if ( track.selected.length != track.times.length ){ track.selected.length = track.times.length; }
|
|
2585
|
-
if ( track.hovered.length != track.times.length ){ track.hovered.length = track.times.length; }
|
|
2586
|
-
track.selected.fill(false);
|
|
2587
|
-
track.hovered.fill(false);
|
|
2588
|
-
|
|
2589
|
-
return stateToReturn;
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
/**
|
|
2593
|
-
*
|
|
2594
|
-
* @param {*} track
|
|
2595
|
-
* @param {Number} srcIdx keyFrame index
|
|
2596
|
-
* @param {Number} trgIdx keyFrame index
|
|
2597
|
-
*/
|
|
2598
|
-
swapKeyFrames(track, srcIdx, trgIdx){
|
|
2599
|
-
const times = track.times;
|
|
2600
|
-
const values = track.values;
|
|
2601
|
-
|
|
2602
|
-
let tmp = times[srcIdx];
|
|
2603
|
-
times[srcIdx] = times[trgIdx];
|
|
2604
|
-
times[trgIdx] = tmp;
|
|
2605
|
-
|
|
2606
|
-
tmp = track.hovered[srcIdx];
|
|
2607
|
-
track.hovered[srcIdx] = track.hovered[trgIdx];
|
|
2608
|
-
track.hovered[trgIdx] = tmp;
|
|
2609
|
-
|
|
2610
|
-
tmp = track.edited[srcIdx];
|
|
2611
|
-
track.edited[srcIdx] = track.edited[trgIdx];
|
|
2612
|
-
track.edited[trgIdx] = tmp;
|
|
2613
|
-
|
|
2614
|
-
tmp = track.selected[srcIdx];
|
|
2615
|
-
track.selected[srcIdx] = track.selected[trgIdx];
|
|
2616
|
-
track.selected[trgIdx] = tmp;
|
|
2617
|
-
|
|
2618
|
-
let src = srcIdx * track.dim;
|
|
2619
|
-
let end = src + track.dim;
|
|
2620
|
-
let trg = trgIdx * track.dim;
|
|
2621
|
-
for( ; src < end; ++src ){
|
|
2622
|
-
tmp = values[ src ];
|
|
2623
|
-
values[ src ] = values[ trg ];
|
|
2624
|
-
values[ trg ] = tmp;
|
|
2625
|
-
++trg;
|
|
2626
|
-
}
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
copySelectedContent() {
|
|
2630
|
-
if (!this.lastKeyFramesSelected.length){
|
|
2631
|
-
return;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
if(!this.clipboard)
|
|
2635
|
-
this.clipboard = {};
|
|
2636
|
-
|
|
2637
|
-
this.clipboard.keyframes = {}; // reset clipboard
|
|
2638
|
-
|
|
2639
|
-
// sort keyframes selected by track
|
|
2640
|
-
let toCopy = {};
|
|
2641
|
-
const tracks = this.animationClip.tracks;
|
|
2642
|
-
for(let i = 0; i < this.lastKeyFramesSelected.length; i++){
|
|
2643
|
-
let [trackIdx, keyIdx] = this.lastKeyFramesSelected[i];
|
|
2644
|
-
const track = tracks[trackIdx];
|
|
2645
|
-
|
|
2646
|
-
if(toCopy[trackIdx]) {
|
|
2647
|
-
toCopy[trackIdx].idxs.push(keyIdx);
|
|
2648
|
-
} else {
|
|
2649
|
-
toCopy[trackIdx] = {track: track, idxs : [keyIdx]};
|
|
2650
|
-
}
|
|
2651
|
-
if(i == 0) {
|
|
2652
|
-
this.copyKeyFrameValue(track, keyIdx);
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
// for each track selected, copy its values
|
|
2657
|
-
for(let trackIdx in toCopy) {
|
|
2658
|
-
this.copyKeyFrames(toCopy[trackIdx].track, toCopy[trackIdx].idxs);
|
|
2659
|
-
}
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
// copies the current value of the keyframe. This value can be pasted across any track (as long as they are of the same type)
|
|
2663
|
-
copyKeyFrameValue( track, index ) {
|
|
2664
|
-
|
|
2665
|
-
// 1 element clipboard by now
|
|
2666
|
-
const start = index * track.dim;
|
|
2667
|
-
const values = this.animationClip.tracks[ track.trackIdx ].values.slice(start, start + track.dim);
|
|
2668
|
-
|
|
2669
|
-
if(!this.clipboard)
|
|
2670
|
-
this.clipboard = {};
|
|
2671
|
-
|
|
2672
|
-
this.clipboard.value = {
|
|
2673
|
-
type: track.type,
|
|
2674
|
-
values: values
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
// each track will have its own entry of copied keyframes. When pasting, only the apropiate track's keyframes are pasted
|
|
2679
|
-
copyKeyFrames( track, indices ) {
|
|
2680
|
-
|
|
2681
|
-
let trackIdx = track.trackIdx;
|
|
2682
|
-
if(!this.clipboard)
|
|
2683
|
-
this.clipboard = {};
|
|
2684
|
-
|
|
2685
|
-
indices.sort( (a,b) => a < b ? -1 : 1 ); // just in case
|
|
2686
|
-
|
|
2687
|
-
let obj = { track: track, values:[], times:[] };
|
|
2688
|
-
|
|
2689
|
-
for(let i = 0; i < indices.length; i++ ){
|
|
2690
|
-
let keyIdx = indices[i];
|
|
2691
|
-
let start = keyIdx * track.dim;
|
|
2692
|
-
let keyValues = track.values.slice(start, start + track.dim); // copy values into a new array
|
|
2693
|
-
obj.values.push(keyValues); // save to clipboard
|
|
2694
|
-
obj.times.push(track.times[keyIdx]); // save to clipboard
|
|
2695
|
-
};
|
|
2696
|
-
|
|
2697
|
-
this.clipboard.keyframes[trackIdx] = obj;
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
|
-
canPasteKeyFrame () {
|
|
2701
|
-
return this.clipboard != null;
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
// raw paste of values
|
|
2705
|
-
#paste( track, index, values ) {
|
|
2706
|
-
const start = index * track.dim;
|
|
2707
|
-
let j = 0;
|
|
2708
|
-
for(let i = start; i < start + track.dim; ++i) {
|
|
2709
|
-
track.values[i] = values[j];
|
|
2710
|
-
++j;
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
track.edited[ index ] = true;
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
// paste value on selected content (only one keyframe can be selected)
|
|
2717
|
-
pasteContentValue(){
|
|
2718
|
-
if(!this.clipboard)
|
|
2719
|
-
return false;
|
|
2720
|
-
|
|
2721
|
-
// copy the value into the only selected keyframe
|
|
2722
|
-
if(this.clipboard.value && this.lastKeyFramesSelected.length == 1) {
|
|
2723
|
-
|
|
2724
|
-
let [trackIdx, keyIdx] = this.lastKeyFramesSelected[0];
|
|
2725
|
-
this.pasteKeyFrameValue(this.animationClip.tracks[trackIdx], keyIdx);
|
|
2726
|
-
return true;
|
|
2727
|
-
}
|
|
2728
|
-
return false;
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
// paste copied keyframes. New keyframes are created and overlapping ones are overwritten
|
|
2732
|
-
pasteContent( time = this.currentTime ) {
|
|
2733
|
-
if(!this.clipboard)
|
|
2734
|
-
return false;
|
|
2735
|
-
|
|
2736
|
-
// create new keyframes from the ones copied
|
|
2737
|
-
if(this.clipboard.keyframes) {
|
|
2738
|
-
|
|
2739
|
-
for( let trackIdx in this.clipboard.keyframes ){
|
|
2740
|
-
const clipboardItem = this.animationClip.tracks[trackIdx];
|
|
2741
|
-
|
|
2742
|
-
// ensure all tracks are visible
|
|
2743
|
-
const idx = this.selectedItems.findIndex( (item) =>
|
|
2744
|
-
{
|
|
2745
|
-
if ( item.isTrack ){ return ( item === clipboardItem ) }
|
|
2746
|
-
return item === clipboardItem.groupId;
|
|
2747
|
-
} );
|
|
2748
|
-
|
|
2749
|
-
if ( idx == -1 ){
|
|
2750
|
-
return false;
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
this.pasteKeyFrames( time );
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
return true;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
pasteKeyFrameValue( track, index ) {
|
|
2761
|
-
|
|
2762
|
-
if(track.locked || this.clipboard.value.type != track.type){
|
|
2763
|
-
return;
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
this.saveState(track.trackIdx);
|
|
2767
|
-
|
|
2768
|
-
// Copy to current key
|
|
2769
|
-
this.#paste( track, index, this.clipboard.value.values );
|
|
2770
|
-
|
|
2771
|
-
if(this.onUpdateTrack){
|
|
2772
|
-
this.onUpdateTrack( [track.trackIdx] );
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
pasteKeyFrames( pasteTime = this.currentTime ){
|
|
2777
|
-
if ( !this.clipboard.keyframes ){ return false; }
|
|
2778
|
-
|
|
2779
|
-
this.unHoverAll();
|
|
2780
|
-
this.deselectAllKeyFrames();
|
|
2781
|
-
|
|
2782
|
-
let clipboardTracks = this.clipboard.keyframes;
|
|
2783
|
-
let globalStart = Infinity;
|
|
2784
|
-
for( let trackIdx in clipboardTracks ){
|
|
2785
|
-
if ( globalStart > clipboardTracks[trackIdx].times[0] ){
|
|
2786
|
-
globalStart = clipboardTracks[trackIdx].times[0];
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
if ( globalStart == Infinity ){ return false; }
|
|
2791
|
-
|
|
2792
|
-
// disable callback. It will be done once at the end
|
|
2793
|
-
const onUpdateTrack = this.onUpdateTrack;
|
|
2794
|
-
this.onUpdateTrack = null;
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
// disable history. It will be done with all changes combined into a single entry
|
|
2798
|
-
const oldSaveEnabler = this.historySaveEnabler;
|
|
2799
|
-
let trackCount = 0; // to detect when to create an entry or
|
|
2800
|
-
for( let trackIdx in clipboardTracks ){
|
|
2801
|
-
|
|
2802
|
-
const clipboardInfo = this.clipboard.keyframes[trackIdx];
|
|
2803
|
-
const times = clipboardInfo.times;
|
|
2804
|
-
const values = clipboardInfo.values;
|
|
2805
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
2806
|
-
|
|
2807
|
-
if( track.locked ){
|
|
2808
|
-
continue;
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
this.saveState(track.trackIdx, trackCount++);
|
|
2812
|
-
this.historySaveEnabler = false;
|
|
2813
|
-
this.addKeyFrames( track.trackIdx, values, times, -globalStart + pasteTime, KeyFramesTimeline.ADDKEY_VALUESINARRAYS );
|
|
2814
|
-
this.historySaveEnabler = oldSaveEnabler;
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
// do only one update
|
|
2818
|
-
if(onUpdateTrack){
|
|
2819
|
-
this.onUpdateTrack = onUpdateTrack;
|
|
2820
|
-
this.onUpdateTrack( Object.keys( clipboardTracks ) );
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
return true;
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
/**
|
|
2827
|
-
*
|
|
2828
|
-
* @param {Int} trackIdx
|
|
2829
|
-
* @param {Array} newValues array of values for each keyframe. It should be a flat array of size track.dim*numKeyframes. Check ADDKEY_VALUESINARRAYS flag
|
|
2830
|
-
* @param {Array of numbers} newTimes must be ordered ascendently
|
|
2831
|
-
* @param {Number} timeOffset
|
|
2832
|
-
* @param {Int} flags
|
|
2833
|
-
* KeyFramesTimeline.ADDKEY_VALUESINARRAYS: if set, newValues is an array of arrays, one for each entry [ [1,2,3], [5,6,7] ]. Times is still a flat array of values [ 0, 0.2 ]
|
|
2834
|
-
|
|
2835
|
-
* @returns
|
|
2836
|
-
*/
|
|
2837
|
-
addKeyFrames( trackIdx, newValues, newTimes, timeOffset = 0, flags = 0x00 ){
|
|
2838
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
2839
|
-
|
|
2840
|
-
if ( !newTimes.length || track.locked ){ return null; }
|
|
2841
|
-
|
|
2842
|
-
const valueDim = track.dim;
|
|
2843
|
-
const trackTimes = track.times;
|
|
2844
|
-
const trackValues = track.values;
|
|
2845
|
-
const times = new Float32Array( trackTimes.length + newTimes.length );
|
|
2846
|
-
const values = new Float32Array( trackValues.length + newTimes.length * valueDim );
|
|
2847
|
-
|
|
2848
|
-
// let newIdx = this.getNearestKeyFrame( track, newTimes[newTimes.length-1], -1 );
|
|
2849
|
-
this.saveState(trackIdx);
|
|
2850
|
-
|
|
2851
|
-
let newIdx = newTimes.length-1;
|
|
2852
|
-
let oldIdx = trackTimes.length-1;
|
|
2853
|
-
let resultIndices = [];
|
|
2854
|
-
if ( KeyFramesTimeline.ADDKEY_VALUESINARRAYS & flags ){
|
|
2855
|
-
|
|
2856
|
-
for( let i = times.length-1; i > -1; --i ){
|
|
2857
|
-
// copy new value in this place if needed
|
|
2858
|
-
if ( oldIdx<0 || (newIdx>-1 && trackTimes[oldIdx] < (newTimes[newIdx]+timeOffset)) ){
|
|
2859
|
-
const vals = newValues[newIdx];
|
|
2860
|
-
for( let v = 0; v < valueDim; ++v ){
|
|
2861
|
-
values[i * valueDim + v] = vals[v];
|
|
2862
|
-
}
|
|
2863
|
-
times[i] = newTimes[newIdx--]+timeOffset;
|
|
2864
|
-
// Add new entry into each control array
|
|
2865
|
-
track.hovered.splice(oldIdx+1, 0, false);
|
|
2866
|
-
track.selected.splice(oldIdx+1, 0, false);
|
|
2867
|
-
track.edited.splice(oldIdx+1, 0, true);
|
|
2868
|
-
|
|
2869
|
-
resultIndices.push(i);
|
|
2870
|
-
continue;
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
// copy old values instead
|
|
2874
|
-
for( let v = 0; v < valueDim; ++v ){
|
|
2875
|
-
values[i * valueDim + v] = trackValues[oldIdx * valueDim + v];
|
|
2876
|
-
}
|
|
2877
|
-
times[i] = trackTimes[oldIdx--];
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
else{
|
|
2881
|
-
for( let i = times.length-1; i > -1; --i ){
|
|
2882
|
-
// copy new value in this place if needed
|
|
2883
|
-
if ( oldIdx<0 || (newIdx>-1 && trackTimes[oldIdx] < (newTimes[newIdx]+timeOffset)) ){
|
|
2884
|
-
// ----------- this is different from the 'if' -----------
|
|
2885
|
-
for( let v = 0; v < valueDim; ++v ){
|
|
2886
|
-
values[i * valueDim + v] = newValues[newIdx * valueDim + v];
|
|
2887
|
-
}
|
|
2888
|
-
times[i] = newTimes[newIdx--] + timeOffset;
|
|
2889
|
-
// Add new entry into each control array
|
|
2890
|
-
track.hovered.splice(oldIdx+1, 0, false);
|
|
2891
|
-
track.selected.splice(oldIdx+1, 0, false);
|
|
2892
|
-
track.edited.splice(oldIdx+1, 0, true);
|
|
2893
|
-
|
|
2894
|
-
resultIndices.push(i);
|
|
2895
|
-
continue;
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
// copy old values instead
|
|
2899
|
-
for( let v = 0; v < valueDim; ++v ){
|
|
2900
|
-
values[i * valueDim + v] = trackValues[oldIdx * valueDim + v];
|
|
2901
|
-
}
|
|
2902
|
-
times[i] = trackTimes[oldIdx--];
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
// update track pointers
|
|
2908
|
-
track.times = times;
|
|
2909
|
-
track.values = values;
|
|
2910
|
-
|
|
2911
|
-
if ( (newTimes[newTimes.length - 1] + timeOffset) > this.duration ){
|
|
2912
|
-
this.setDuration(newTimes[newTimes.length - 1] + timeOffset);
|
|
2913
|
-
}
|
|
2914
|
-
|
|
2915
|
-
if(this.onUpdateTrack){
|
|
2916
|
-
this.onUpdateTrack( [trackIdx] );
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
return resultIndices;
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
deleteSelectedContent(skipCallback = false) {
|
|
2923
|
-
|
|
2924
|
-
//*********** WARNING: RELIES ON SORTED lastKeyFramesSelected ***********
|
|
2925
|
-
|
|
2926
|
-
if (!this.lastKeyFramesSelected.length){
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
const tracks = this.animationClip.tracks;
|
|
2931
|
-
const firstTrack = this.lastKeyFramesSelected[0][0];
|
|
2932
|
-
let trackToRemove = firstTrack;
|
|
2933
|
-
let toDelete = []; // indices to delete of the same track
|
|
2934
|
-
|
|
2935
|
-
const oldSaveEnabler = this.historySaveEnabler;
|
|
2936
|
-
|
|
2937
|
-
const numSelected = this.lastKeyFramesSelected.length;
|
|
2938
|
-
for( let i = 0; i < numSelected; ++i ){
|
|
2939
|
-
const [trackIdx, frameIdx] = this.lastKeyFramesSelected[i];
|
|
2940
|
-
|
|
2941
|
-
if ( tracks[trackIdx].locked ){
|
|
2942
|
-
tracks[trackIdx].selected[frameIdx] = false; // unselect
|
|
2943
|
-
continue;
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
|
-
if ( trackToRemove != trackIdx ){
|
|
2947
|
-
this.saveState(trackToRemove, trackToRemove != firstTrack);
|
|
2948
|
-
|
|
2949
|
-
this.historySaveEnabler = false;
|
|
2950
|
-
this.deleteKeyFrames( trackToRemove, toDelete, skipCallback );
|
|
2951
|
-
this.historySaveEnabler = oldSaveEnabler;
|
|
2952
|
-
|
|
2953
|
-
trackToRemove = trackIdx;
|
|
2954
|
-
toDelete.length = 0;
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
toDelete.push( frameIdx );
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
this.saveState(trackToRemove, trackToRemove != firstTrack);
|
|
2961
|
-
this.historySaveEnabler = false;
|
|
2962
|
-
this.deleteKeyFrames( trackToRemove, toDelete, skipCallback );
|
|
2963
|
-
this.historySaveEnabler = oldSaveEnabler;
|
|
2964
|
-
|
|
2965
|
-
this.lastKeyFramesSelected = [];
|
|
2966
|
-
}
|
|
2967
|
-
|
|
2968
|
-
// for typed arrays. Does not update lastSelectedKeyframes
|
|
2969
|
-
deleteKeyFrames( trackIdx, indices, skipCallback = false ){
|
|
2970
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
2971
|
-
|
|
2972
|
-
if ( !indices.length || track.locked ){
|
|
2973
|
-
return false;
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
|
-
this.saveState( trackIdx );
|
|
2977
|
-
|
|
2978
|
-
const oldNumFrames = track.times.length;
|
|
2979
|
-
const newNumFrames = track.times.length - indices.length;
|
|
2980
|
-
const newTimes = track.times.slice(0, newNumFrames);
|
|
2981
|
-
const newValues = track.values.slice(0, newNumFrames * track.dim);
|
|
2982
|
-
|
|
2983
|
-
let resultIdx = indices[0];
|
|
2984
|
-
let resultValIdx = indices[0] * track.dim;
|
|
2985
|
-
for(let i = 0; i < indices.length; ++i){
|
|
2986
|
-
track.edited.splice(resultIdx, 1);
|
|
2987
|
-
track.selected.splice(resultIdx, 1);
|
|
2988
|
-
track.hovered.splice(resultIdx, 1);
|
|
2989
|
-
|
|
2990
|
-
const idx = indices[i];
|
|
2991
|
-
const endIdx = (i < (indices.length-1)) ? indices[i+1] : oldNumFrames;
|
|
2992
|
-
const endValIdx = endIdx * track.dim;
|
|
2993
|
-
for(let v = (idx+1)*track.dim; v < endValIdx; ++v ){
|
|
2994
|
-
newValues[resultValIdx++] = track.values[v];
|
|
2995
|
-
}
|
|
2996
|
-
for( let f = idx+1; f < endIdx; ++f){
|
|
2997
|
-
newTimes[resultIdx++] = track.times[f];
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
|
-
track.times = newTimes;
|
|
3002
|
-
track.values = newValues;
|
|
3003
|
-
|
|
3004
|
-
// Update animation action interpolation info
|
|
3005
|
-
if(this.onDeleteKeyFrames && !skipCallback)
|
|
3006
|
-
this.onDeleteKeyFrames( trackIdx, indices );
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
if ( (newTimes[newTimes.length - 1]) > this.duration ){
|
|
3010
|
-
this.setDuration(newTimes[newTimes.length - 1]);
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
// if(this.onUpdateTrack)
|
|
3014
|
-
// this.onUpdateTrack( [trackIdx] );
|
|
3015
|
-
|
|
3016
|
-
return true;
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
/**
|
|
3020
|
-
* Binary search. Relies on track.times being a sorted array
|
|
3021
|
-
* @param {Object} track
|
|
3022
|
-
* @param {Number} time
|
|
3023
|
-
* @param {Number} mode on of the possible values
|
|
3024
|
-
* - -1 = nearest frame with t[f] <= time
|
|
3025
|
-
* - 0 = nearest frame
|
|
3026
|
-
* - 1 = nearest frame with t[f] >= time
|
|
3027
|
-
* @returns a zero/positive value if successful. On failure returnes -1 meaning either there are no frames (0), no frame-time is lower (-1) or no frame-time is higher (1)
|
|
3028
|
-
*/
|
|
3029
|
-
getNearestKeyFrame( track, time, mode = 0 ) {
|
|
3030
|
-
|
|
3031
|
-
if(!track || !track.times || !track.times.length)
|
|
3032
|
-
return -1;
|
|
3033
|
-
|
|
3034
|
-
//binary search
|
|
3035
|
-
const times = track.times;
|
|
3036
|
-
let min = 0, max = times.length - 1;
|
|
3037
|
-
|
|
3038
|
-
// edge cases
|
|
3039
|
-
if ( times[min] > time ){
|
|
3040
|
-
return mode == -1 ? -1 : 0;
|
|
3041
|
-
}
|
|
3042
|
-
if ( times[max] < time ){
|
|
3043
|
-
return mode == 1 ? -1 : max;
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
// time is between first and last frame
|
|
3047
|
-
let half = Math.floor( ( min + max ) / 2 );
|
|
3048
|
-
while ( min < half && half < max ){
|
|
3049
|
-
if ( time < times[half] ){ max = half; }
|
|
3050
|
-
else{ min = half; }
|
|
3051
|
-
half = Math.floor( ( min + max ) / 2 );
|
|
3052
|
-
}
|
|
3053
|
-
|
|
3054
|
-
if (mode == 0 ){
|
|
3055
|
-
return Math.abs( time - times[min] ) < Math.abs( time - times[max] ) ? min : max;
|
|
3056
|
-
}
|
|
3057
|
-
else if ( mode == -1 ){
|
|
3058
|
-
return times[max] == time ? max : min;
|
|
3059
|
-
}
|
|
3060
|
-
return times[min] == time ? min : max;
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
/**
|
|
3064
|
-
* get the nearest keyframe to "time" given a maximum threshold.
|
|
3065
|
-
* @param {Object} track
|
|
3066
|
-
* @param {Number} time
|
|
3067
|
-
* @param {Number} threshold must be positive value
|
|
3068
|
-
* @returns returns a postive/zero value if there is a frame inside the threshold range. Otherwise, -1
|
|
3069
|
-
*/
|
|
3070
|
-
getCurrentKeyFrame( track, time, threshold = 0.0 ) {
|
|
3071
|
-
|
|
3072
|
-
if(!track || !track.times.length)
|
|
3073
|
-
return -1;
|
|
3074
|
-
|
|
3075
|
-
let frame = this.getNearestKeyFrame( track, time );
|
|
3076
|
-
if ( frame > -1 ){
|
|
3077
|
-
frame = Math.abs(track.times[frame] - time) > threshold ? -1 : frame;
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
return frame;
|
|
3081
|
-
}
|
|
3082
|
-
|
|
3083
|
-
/**
|
|
3084
|
-
* Returns the interval of frames between minTime and maxTime (both included)
|
|
3085
|
-
* @param {Object} track
|
|
3086
|
-
* @param {Number} minTime
|
|
3087
|
-
* @param {Number} maxTime
|
|
3088
|
-
* @param {Number} threshold must be positive value
|
|
3089
|
-
* @returns an array with two values [ minFrame, maxFrame ]. Otherwise null
|
|
3090
|
-
*/
|
|
3091
|
-
getKeyFramesInRange( track, minTime, maxTime, threshold = 0.0 ) {
|
|
3092
|
-
|
|
3093
|
-
if(!track || !track.times.length)
|
|
3094
|
-
return null;
|
|
3095
|
-
|
|
3096
|
-
// Manage negative selection
|
|
3097
|
-
if(minTime > maxTime) {
|
|
3098
|
-
let aux = minTime;
|
|
3099
|
-
minTime = maxTime;
|
|
3100
|
-
maxTime = aux;
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
const minFrame = this.getNearestKeyFrame( track, minTime - threshold, 1 );
|
|
3104
|
-
const maxFrame = this.getNearestKeyFrame( track, maxTime + threshold, -1 );
|
|
3105
|
-
|
|
3106
|
-
if ( maxFrame == -1 || minFrame == -1 ){ return null; }
|
|
3107
|
-
|
|
3108
|
-
return [minFrame, maxFrame];
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
unHoverAll(){
|
|
3112
|
-
if(this.lastHovered) {
|
|
3113
|
-
this.animationClip.tracks[ this.lastHovered[0] ].hovered[ this.lastHovered[1] ] = false;
|
|
3114
|
-
}
|
|
3115
|
-
let h = this.lastHovered;
|
|
3116
|
-
this.lastHovered = null;
|
|
3117
|
-
return h;
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
deselectAllKeyFrames() {
|
|
3121
|
-
|
|
3122
|
-
for(let [trackIdx, keyIndex] of this.lastKeyFramesSelected) {
|
|
3123
|
-
this.animationClip.tracks[trackIdx].selected[keyIndex] = false;
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// Something has been deselected
|
|
3127
|
-
const deselected = this.lastKeyFramesSelected.length > 0;
|
|
3128
|
-
this.lastKeyFramesSelected.length = 0;
|
|
3129
|
-
return deselected;
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
|
-
isKeyFrameSelected( track, index ) {
|
|
3133
|
-
return track.selected[ index ];
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
/**
|
|
3137
|
-
* @param {Int} trackIdx track index of animation clip
|
|
3138
|
-
* @param {Int} frameIdx frame (index) to select inside the track
|
|
3139
|
-
* @param {Boolean} skipCallback
|
|
3140
|
-
* @returns
|
|
3141
|
-
*/
|
|
3142
|
-
selectKeyFrame( trackIdx, frameIdx, skipCallback = false ) {
|
|
3143
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
3144
|
-
if( track.locked || !track.active || track.selected[frameIdx] )
|
|
3145
|
-
return null;
|
|
3146
|
-
|
|
3147
|
-
// [track idx, keyframe, keyframe time]
|
|
3148
|
-
const selection = [track.trackIdx, frameIdx, track.times[frameIdx]];
|
|
3149
|
-
|
|
3150
|
-
// sort lastkeyframeselected ascending order (track and frame)
|
|
3151
|
-
let i = 0;
|
|
3152
|
-
for( ; i < this.lastKeyFramesSelected.length; ++i){
|
|
3153
|
-
let s = this.lastKeyFramesSelected[i];
|
|
3154
|
-
if(s[0] > trackIdx || (s[0] == trackIdx && s[1] > frameIdx)){
|
|
3155
|
-
break;
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
this.lastKeyFramesSelected.splice( i, 0, selection );
|
|
3159
|
-
track.selected[frameIdx] = true;
|
|
3160
|
-
|
|
3161
|
-
if( this.onSelectKeyFrame && !skipCallback){
|
|
3162
|
-
this.onSelectKeyFrame(selection);
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
return selection;
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
deselectKeyFrame( trackIdx, frameIdx ){
|
|
3169
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
3170
|
-
if( track.locked || !track.active || !track.selected[frameIdx] )
|
|
3171
|
-
return false;
|
|
3172
|
-
|
|
3173
|
-
track.selected[frameIdx] = false;
|
|
3174
|
-
|
|
3175
|
-
for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
|
|
3176
|
-
const sk = this.lastKeyFramesSelected[i];
|
|
3177
|
-
if ( sk[0] === trackIdx && sk[1] === frameIdx ){
|
|
3178
|
-
this.lastKeyFramesSelected.splice(i, 1);
|
|
3179
|
-
break;
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
3182
|
-
|
|
3183
|
-
return true;
|
|
3184
|
-
}
|
|
3185
|
-
|
|
3186
|
-
getNumKeyFramesSelected() {
|
|
3187
|
-
return this.lastKeyFramesSelected.length;
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
/**
|
|
3191
|
-
* helper function to process a selection with multiple keyframes. Sets the time of the timeline to the first selected keyframe
|
|
3192
|
-
* @param {Number} trackIdx
|
|
3193
|
-
* @param {Number} keyFrameIndex
|
|
3194
|
-
* @param {Boolean} multipleSelection whether to append to selection or reset it and make this keyframe the only current selection
|
|
3195
|
-
* @returns
|
|
3196
|
-
*/
|
|
3197
|
-
processSelectionKeyFrame( trackIdx, keyFrameIndex, multipleSelection = false ) {
|
|
3198
|
-
|
|
3199
|
-
const track = this.animationClip.tracks[ trackIdx ];
|
|
3200
|
-
if(track.locked)
|
|
3201
|
-
return;
|
|
3202
|
-
|
|
3203
|
-
if(!multipleSelection) {
|
|
3204
|
-
this.deselectAllKeyFrames();
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
this.selectKeyFrame(trackIdx, keyFrameIndex);
|
|
3208
|
-
|
|
3209
|
-
if( !multipleSelection ) {
|
|
3210
|
-
this.setTime(track.times[ keyFrameIndex ]);
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
/**
|
|
3215
|
-
* @method clearTrack
|
|
3216
|
-
*/
|
|
3217
|
-
clearTrack(trackIdx) {
|
|
3218
|
-
|
|
3219
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
3220
|
-
|
|
3221
|
-
this.unHoverAll();
|
|
3222
|
-
this.deselectAllKeyFrames();
|
|
3223
|
-
|
|
3224
|
-
if( track.locked ){
|
|
3225
|
-
return;
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
|
-
this.saveState(track.trackIdx);
|
|
3229
|
-
|
|
3230
|
-
track.times = track.times.slice(0,0);
|
|
3231
|
-
track.values = track.values.slice(0,0);
|
|
3232
|
-
track.edited.length = 0;
|
|
3233
|
-
track.hovered.length = 0;
|
|
3234
|
-
track.selected.length = 0;
|
|
3235
|
-
|
|
3236
|
-
return trackIdx;
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
LX.KeyFramesTimeline = KeyFramesTimeline;
|
|
3241
|
-
/**
|
|
3242
|
-
* @class ClipsTimeline
|
|
3243
|
-
*/
|
|
3244
|
-
|
|
3245
|
-
class ClipsTimeline extends Timeline {
|
|
3246
|
-
static CLONEREASON_COPY = 1;
|
|
3247
|
-
static CLONEREASON_PASTE = 2;
|
|
3248
|
-
static CLONEREASON_HISTORY = 3;
|
|
3249
|
-
static CLONEREASON_TRACKCLONE = 4;
|
|
3250
|
-
|
|
3251
|
-
/**
|
|
3252
|
-
* @param {String} name
|
|
3253
|
-
* @param {Object} options = {animationClip, selectedItems, x, y, width, height, canvas, trackHeight}
|
|
3254
|
-
*/
|
|
3255
|
-
constructor(name, options = {}) {
|
|
3256
|
-
|
|
3257
|
-
super(name, options);
|
|
3258
|
-
|
|
3259
|
-
this.lastClipsSelected = [];
|
|
3260
|
-
this.lastTrackClipsMove = 0; // vertical movement of clips, onMouseMove onMousedown
|
|
3261
|
-
this.dragClipMode = "";
|
|
3262
|
-
|
|
3263
|
-
this.setAnimationClip(this.animationClip);
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
/**
|
|
3267
|
-
* Generates an animationClip using either the parameters set in the animation argument or using default values
|
|
3268
|
-
* @param {Object} animation data with which to generate an animationClip
|
|
3269
|
-
* @returns
|
|
3270
|
-
*/
|
|
3271
|
-
instantiateAnimationClip(animation, clone = false) {
|
|
3272
|
-
|
|
3273
|
-
const animationClip = super.instantiateAnimationClip(animation);
|
|
3274
|
-
|
|
3275
|
-
if (animation && animation.tracks){
|
|
3276
|
-
for( let i = 0; i < animation.tracks.length; ++i ) {
|
|
3277
|
-
|
|
3278
|
-
const trackInfo = this.instantiateTrack( animation.tracks[i], clone );
|
|
3279
|
-
trackInfo.trackIdx = animationClip.tracks.length;
|
|
3280
|
-
|
|
3281
|
-
animationClip.tracks.push(trackInfo);
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
|
|
3285
|
-
return animationClip;
|
|
3286
|
-
}
|
|
3287
|
-
|
|
3288
|
-
/**
|
|
3289
|
-
*
|
|
3290
|
-
* @param {Object} options set some values for the track instance (groups and trackIdx not included)
|
|
3291
|
-
* @returns
|
|
3292
|
-
*/
|
|
3293
|
-
instantiateTrack(options = {}, clone = false) {
|
|
3294
|
-
const track = super.instantiateTrack(options);
|
|
3295
|
-
|
|
3296
|
-
track.trackIdx = this.animationClip.tracks.length;
|
|
3297
|
-
|
|
3298
|
-
track.selected = [];
|
|
3299
|
-
track.edited = [];
|
|
3300
|
-
track.hovered = [];
|
|
3301
|
-
|
|
3302
|
-
if( options.clips ){
|
|
3303
|
-
track.clips = clone ? this.cloneClips(options.clips, 0, ClipsTimeline.CLONEREASON_TRACKCLONE) : options.clips;
|
|
3304
|
-
}else{
|
|
3305
|
-
track.clips = [];
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
const numClips = track.clips.length;
|
|
3309
|
-
|
|
3310
|
-
if ( options.selected && options.selected.length == numClips ){
|
|
3311
|
-
track.selected = clone ? options.selected.slice() : options.selected;
|
|
3312
|
-
}else{
|
|
3313
|
-
track.selected = (new Array(numClips)).fill(false);
|
|
3314
|
-
}
|
|
3315
|
-
if ( options.edited && options.edited.length == numClips ){
|
|
3316
|
-
track.edited = clone ? options.edited.slice() : options.edited;
|
|
3317
|
-
}else{
|
|
3318
|
-
track.edited = (new Array(numClips)).fill(false);
|
|
3319
|
-
}
|
|
3320
|
-
if ( options.hovered && options.hovered.length == numClips ){
|
|
3321
|
-
track.hovered = clone ? options.hovered.slice() : options.hovered;
|
|
3322
|
-
}else{
|
|
3323
|
-
track.hovered = (new Array(numClips)).fill(false);
|
|
3324
|
-
}
|
|
3325
|
-
|
|
3326
|
-
// sanity check. Also done in addClip
|
|
3327
|
-
for( let i = 0; i < track.clips.length; ++i ){
|
|
3328
|
-
track.clips[i].active = track.clips[i].active ?? true;
|
|
3329
|
-
}
|
|
3330
|
-
return track;
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
// provides an base example of a proper clip
|
|
3334
|
-
instantiateClip(options = {}){
|
|
3335
|
-
return {
|
|
3336
|
-
id: options.id ?? (options.name ?? "clip"),
|
|
3337
|
-
|
|
3338
|
-
start: options.start ?? 0,
|
|
3339
|
-
duration: options.duration ?? 1,
|
|
3340
|
-
fadein: options.fadein ?? undefined,
|
|
3341
|
-
fadeout: options.fadeout ?? undefined,
|
|
3342
|
-
|
|
3343
|
-
clipColor: options.clipColor ?? LX.getThemeColor("global-color-accent"),
|
|
3344
|
-
fadeColor: options.fadeColor ?? null,
|
|
3345
|
-
active: options.active ?? true,
|
|
3346
|
-
trackIdx: -1, // filled by addClip
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
}
|
|
3350
|
-
// use default updateleftpanel
|
|
3351
|
-
// generateSelectedItemsTreeData(){}
|
|
3352
|
-
|
|
3353
|
-
addNewTrack( options = {}, updateLeftPanel = true, skipCallback = false ) {
|
|
3354
|
-
|
|
3355
|
-
const trackInfo = this.instantiateTrack(options ?? {});
|
|
3356
|
-
trackInfo.trackIdx = this.animationClip.tracks.length;
|
|
3357
|
-
this.animationClip.tracks.push( trackInfo );
|
|
3358
|
-
|
|
3359
|
-
if ( this.onAddNewTrack && !skipCallback ){
|
|
3360
|
-
this.onAddNewTrack( trackInfo, options );
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
|
-
this.selectedItems.push(trackInfo);
|
|
3364
|
-
if( updateLeftPanel ){
|
|
3365
|
-
this.updateLeftPanel();
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
return trackInfo.trackIdx;
|
|
3369
|
-
}
|
|
3370
|
-
|
|
3371
|
-
// OVERRIDE ITEM SELECTION - ClipsTimeline will not offer any selection. Alltracks are visible
|
|
3372
|
-
setAnimationClip( animation, needsToProcess ){
|
|
3373
|
-
super.setAnimationClip(animation, needsToProcess);
|
|
3374
|
-
this.changeSelectedItems();
|
|
3375
|
-
return this.animationClip;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// OVERRIDE
|
|
3379
|
-
deselectAllElements(){
|
|
3380
|
-
this.deselectAllClips();
|
|
3381
|
-
this.unHoverAll();
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
/**
|
|
3385
|
-
* OVERRIDE ITEM SELECTION.
|
|
3386
|
-
* CLIPS WILL OFFER NO SELECTION. All tracks are visible
|
|
3387
|
-
*/
|
|
3388
|
-
changeSelectedItems( ) {
|
|
3389
|
-
|
|
3390
|
-
this.deselectAllElements();
|
|
3391
|
-
this.deselectAllTracks( false ); // no need to update left
|
|
3392
|
-
|
|
3393
|
-
this.selectedItems = this.animationClip.tracks.slice();
|
|
3394
|
-
|
|
3395
|
-
this.updateLeftPanel();
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
unHoverAll(){
|
|
3399
|
-
if(this.lastHovered){
|
|
3400
|
-
this.animationClip.tracks[ this.lastHovered[0] ].hovered[ this.lastHovered[1] ] = false;
|
|
3401
|
-
}
|
|
3402
|
-
let h = this.lastHovered;
|
|
3403
|
-
this.lastHovered = null;
|
|
3404
|
-
return h;
|
|
3405
|
-
}
|
|
3406
|
-
|
|
3407
|
-
onMouseUp( e ) {
|
|
3408
|
-
|
|
3409
|
-
let track = e.track;
|
|
3410
|
-
let localX = e.localX;
|
|
3411
|
-
let discard = e.discard; // true when too much time has passed between Down and Up
|
|
3412
|
-
|
|
3413
|
-
if(e.shiftKey) {
|
|
3414
|
-
|
|
3415
|
-
// Manual Multiple selection
|
|
3416
|
-
if(!discard) {
|
|
3417
|
-
if ( track ){
|
|
3418
|
-
let clipIndex = this.getClipOnTime( track, this.xToTime( localX ), this.secondsPerPixel * 5 );
|
|
3419
|
-
if ( clipIndex > -1 ){
|
|
3420
|
-
track.selected[clipIndex] ?
|
|
3421
|
-
this.deselectClip( track.trackIdx, clipIndex ) :
|
|
3422
|
-
this.selectClip( track.trackIdx, clipIndex, false );
|
|
3423
|
-
}
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
// Box selection
|
|
3427
|
-
else if (this.boxSelection){
|
|
3428
|
-
|
|
3429
|
-
let tracks = this.getTracksInRange(this.boxSelectionStart[1], this.boxSelectionEnd[1]);
|
|
3430
|
-
|
|
3431
|
-
for(let t of tracks) {
|
|
3432
|
-
let clipsIndices = this.getClipsInRange(t,
|
|
3433
|
-
this.xToTime( this.boxSelectionStart[0] ),
|
|
3434
|
-
this.xToTime( this.boxSelectionEnd[0] ),
|
|
3435
|
-
0.000001);
|
|
3436
|
-
|
|
3437
|
-
if(clipsIndices) {
|
|
3438
|
-
for(let index of clipsIndices)
|
|
3439
|
-
this.selectClip( t.trackIdx, index, false );
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
}
|
|
3445
|
-
else {
|
|
3446
|
-
|
|
3447
|
-
let boundingBox = this.canvas.getBoundingClientRect()
|
|
3448
|
-
if(e.y < boundingBox.top || e.y > boundingBox.bottom)
|
|
3449
|
-
return;
|
|
3450
|
-
|
|
3451
|
-
// Check exact track clip
|
|
3452
|
-
if(!discard && track) {
|
|
3453
|
-
if(e.button!=2){
|
|
3454
|
-
const clipIdx = this.getClipOnTime(track, this.xToTime(localX), 0.001);
|
|
3455
|
-
this.selectClip( track.trackIdx, clipIdx );
|
|
3456
|
-
}
|
|
3457
|
-
}
|
|
3458
|
-
|
|
3459
|
-
}
|
|
3460
|
-
this.movingKeys = false;
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
onMouseDown( e, time ) {
|
|
3464
|
-
// function not called if shift is pressed (boxselection)
|
|
3465
|
-
let localX = e.localX;
|
|
3466
|
-
let localY = e.localY;
|
|
3467
|
-
let track = e.track;
|
|
3468
|
-
|
|
3469
|
-
if ( e.button > 0 ){
|
|
3470
|
-
return;
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
if(e.ctrlKey && track) { // move clips
|
|
3474
|
-
|
|
3475
|
-
let x = e.offsetX;
|
|
3476
|
-
// clip selection is done on MouseUP
|
|
3477
|
-
const selectedClips = this.lastClipsSelected;
|
|
3478
|
-
|
|
3479
|
-
this.canvas.style.cursor = "grab";
|
|
3480
|
-
let curTrackIdx = -1;
|
|
3481
|
-
|
|
3482
|
-
this.lastTrackClipsMove = Math.floor( (e.localY - this.topMargin + this.trackTreesPanel.root.scrollTop) / this.trackHeight );
|
|
3483
|
-
|
|
3484
|
-
for(let i = 0; i < selectedClips.length; i++)
|
|
3485
|
-
{
|
|
3486
|
-
let [trackIndex, clipIndex] = selectedClips[i];
|
|
3487
|
-
const clip = this.animationClip.tracks[trackIndex].clips[clipIndex];
|
|
3488
|
-
|
|
3489
|
-
let endingX = this.timeToX( clip.start + clip.duration );
|
|
3490
|
-
|
|
3491
|
-
if(Math.abs( endingX - x ) < 5 ) {
|
|
3492
|
-
this.dragClipMode = "duration";
|
|
3493
|
-
this.canvas.style.cursor = "column-resize";
|
|
3494
|
-
}
|
|
3495
|
-
else {
|
|
3496
|
-
this.dragClipMode = "move";
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
//*********** WARNING: RELIES ON SORTED lastClipsSelected ***********
|
|
3500
|
-
if(curTrackIdx != trackIndex){
|
|
3501
|
-
this.saveState(trackIndex, curTrackIdx != -1 );
|
|
3502
|
-
curTrackIdx = trackIndex;
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
this.movingKeys = true;
|
|
3507
|
-
}
|
|
3508
|
-
else if( !track || track && this.getClipOnTime(track, time, 0.001) == -1) { // clicked on empty space
|
|
3509
|
-
if ( this.lastClipsSelected.length ){
|
|
3510
|
-
this.deselectAllClips();
|
|
3511
|
-
if(this.onSelectClip){
|
|
3512
|
-
this.onSelectClip(null);
|
|
3513
|
-
}
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
else if (track && (this.dragClipMode == "duration" || this.dragClipMode == "fadein" || this.dragClipMode == "fadeout" )) { // clicked while mouse was over fadeIn, fadeOut, duration
|
|
3517
|
-
const clipIdx = this.getClipOnTime(track, this.xToTime(localX), 0.001);
|
|
3518
|
-
this.selectClip( track.trackIdx, clipIdx ); // select current clip if any (deselect others)
|
|
3519
|
-
if ( this.lastClipsSelected.length ){
|
|
3520
|
-
this.saveState(track.trackIdx);
|
|
3521
|
-
}
|
|
3522
|
-
this.movingKeys = true;
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
|
|
3526
|
-
onMouseMove( e, time ) {
|
|
3527
|
-
// function not called if shift is pressed (boxselection)
|
|
3528
|
-
|
|
3529
|
-
if ( this.grabbingTimeBar || this.grabbingScroll ){
|
|
3530
|
-
return;
|
|
3531
|
-
}
|
|
3532
|
-
else if(this.grabbing && e.buttons != 2) {
|
|
3533
|
-
this.unHoverAll();
|
|
3534
|
-
|
|
3535
|
-
let delta = time - this.grabTime;
|
|
3536
|
-
this.grabTime = time;
|
|
3537
|
-
if ( time < 0 && delta > 0 ){ delta = 0; }
|
|
3538
|
-
|
|
3539
|
-
if ( this.dragClipMode != "move" && this.lastClipsSelected.length == 1 ){ // change fade and duration of clips
|
|
3540
|
-
|
|
3541
|
-
const track = this.animationClip.tracks[this.lastClipsSelected[0][0]];
|
|
3542
|
-
let clip = track.clips[this.lastClipsSelected[0][1]];
|
|
3543
|
-
if( this.dragClipMode == "fadein" ) {
|
|
3544
|
-
clip.fadein = Math.min(Math.max(clip.fadein + delta, clip.start), clip.fadeout ?? (clip.start+clip.duration) );
|
|
3545
|
-
}
|
|
3546
|
-
else if( this.dragClipMode == "fadeout" ) {
|
|
3547
|
-
clip.fadeout = Math.max(Math.min(clip.fadeout + delta, clip.start+clip.duration), clip.fadein ?? clip.start );
|
|
3548
|
-
}
|
|
3549
|
-
else if( this.dragClipMode == "duration" ) {
|
|
3550
|
-
let duration = Math.max(0, clip.duration + delta);
|
|
3551
|
-
if ( this.lastClipsSelected[0][1] < track.clips.length-1 ){ // max next clip's start
|
|
3552
|
-
duration = Math.min( track.clips[this.lastClipsSelected[0][1] + 1].start - clip.start - 0.0001, duration );
|
|
3553
|
-
}
|
|
3554
|
-
clip.duration = duration;
|
|
3555
|
-
if ( clip.fadeout != undefined ){
|
|
3556
|
-
clip.fadeout = Math.max(Math.min((clip.fadeout ?? (clip.start+clip.duration)) + delta, clip.start+clip.duration), clip.start);
|
|
3557
|
-
}
|
|
3558
|
-
if ( clip.fadein != undefined ){
|
|
3559
|
-
clip.fadein = Math.max(Math.min((clip.fadein ?? (clip.start+clip.duration)), (clip.fadeout ?? (clip.start+clip.duration))), clip.start);
|
|
3560
|
-
}
|
|
3561
|
-
if(this.duration < clip.start + clip.duration){
|
|
3562
|
-
this.setDuration(clip.start + clip.duration);
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
if(this.onContentMoved) { // content changed
|
|
3566
|
-
this.onContentMoved(clip, 0);
|
|
3567
|
-
}
|
|
3568
|
-
}
|
|
3569
|
-
else if ( this.dragClipMode == "move" && this.lastClipsSelected.length ) { // move clips
|
|
3570
|
-
//*********** WARNING: RELIES ON SORTED lastClipsSelected ***********
|
|
3571
|
-
|
|
3572
|
-
const treeOffset = this.lastTrackTreesComponentOffset;
|
|
3573
|
-
let newTrackClipsMove = Math.floor( (e.localY - treeOffset) / this.trackHeight );
|
|
3574
|
-
|
|
3575
|
-
// move clips vertically
|
|
3576
|
-
if ( e.altKey ){
|
|
3577
|
-
let deltaTracks = newTrackClipsMove - this.lastTrackClipsMove;
|
|
3578
|
-
|
|
3579
|
-
if ( this.lastClipsSelected[0][0] + deltaTracks < 0 ){
|
|
3580
|
-
deltaTracks = -this.lastClipsSelected[0][0];
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
// if no movement of tracks, do not check
|
|
3584
|
-
if ( deltaTracks != 0 ){
|
|
3585
|
-
|
|
3586
|
-
// check if ALL selected clips can move track
|
|
3587
|
-
for( let i = 0; i < this.lastClipsSelected.length; ++i ){
|
|
3588
|
-
const track = this.animationClip.tracks[ this.lastClipsSelected[i][0] ];
|
|
3589
|
-
const newTrack = this.animationClip.tracks[ this.lastClipsSelected[i][0] + deltaTracks ];
|
|
3590
|
-
const clip = track.clips[ this.lastClipsSelected[i][1] ];
|
|
3591
|
-
|
|
3592
|
-
const clipsInRange = this.getClipsInRange(newTrack, clip.start, clip.start+clip.duration, 0.0001)
|
|
3593
|
-
if ( clipsInRange ){
|
|
3594
|
-
for( let c = 0; c < clipsInRange.length; ++c ){
|
|
3595
|
-
if ( !newTrack.selected[clipsInRange[c]] ){
|
|
3596
|
-
// at least one clip cannot move, abort
|
|
3597
|
-
c = clipsInRange.length;
|
|
3598
|
-
i = this.lastClipsSelected.length;
|
|
3599
|
-
deltaTracks = 0;
|
|
3600
|
-
newTrackClipsMove = this.lastTrackClipsMove;
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
}
|
|
3604
|
-
}
|
|
3605
|
-
|
|
3606
|
-
// if movement was not canceled
|
|
3607
|
-
if ( deltaTracks != 0 ){
|
|
3608
|
-
let oldStateEnabler = this.historySaveEnabler;
|
|
3609
|
-
this.historySaveEnabler = false;
|
|
3610
|
-
|
|
3611
|
-
const selectedClips = this.lastClipsSelected;
|
|
3612
|
-
this.lastClipsSelected = []; // avoid delete and addclips index reassignment loop (not necessary because of order of operations in for)
|
|
3613
|
-
|
|
3614
|
-
for( let i = selectedClips[selectedClips.length-1][0] + deltaTracks - this.animationClip.tracks.length + 1; i > 0; --i ){
|
|
3615
|
-
this.addNewTrack(null, i == 1);
|
|
3616
|
-
if ( i == 1 ){
|
|
3617
|
-
this.updateLeftPanel();
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
|
|
3621
|
-
// selected clips MUST be ordered (ascendently)
|
|
3622
|
-
let startSel = deltaTracks > 0 ? selectedClips.length - 1 : 0;
|
|
3623
|
-
let endSel = startSel;
|
|
3624
|
-
let currTrack = selectedClips[startSel][0];
|
|
3625
|
-
|
|
3626
|
-
// i <= length; to update last track. Otherwise a check outside of for would be needed
|
|
3627
|
-
for( let i = 1; i <= selectedClips.length; ++i ){
|
|
3628
|
-
|
|
3629
|
-
let idx = deltaTracks > 0 ? (selectedClips.length -1 - i) : i;
|
|
3630
|
-
if( i == selectedClips.length || selectedClips[idx][0] != currTrack ){
|
|
3631
|
-
|
|
3632
|
-
const newTrackIdx = currTrack + deltaTracks;
|
|
3633
|
-
const newTrack = this.animationClip.tracks[ newTrackIdx ];
|
|
3634
|
-
const track = this.animationClip.tracks[currTrack ];
|
|
3635
|
-
|
|
3636
|
-
// save track state if necessary
|
|
3637
|
-
const undoState = this.historyUndo[this.historyUndo.length-1];
|
|
3638
|
-
let state = 0;
|
|
3639
|
-
for( ; state < undoState.length; ++state ){
|
|
3640
|
-
if ( newTrackIdx == undoState[state].trackIdx ){ break; }
|
|
3641
|
-
}
|
|
3642
|
-
if ( state == undoState.length ){
|
|
3643
|
-
this.historySaveEnabler = true;
|
|
3644
|
-
this.saveState(newTrackIdx, true);
|
|
3645
|
-
this.historySaveEnabler = false;
|
|
3646
|
-
}
|
|
3647
|
-
|
|
3648
|
-
// add clips of a track, from first to last
|
|
3649
|
-
for( let c = startSel; c <= endSel; ++c ){
|
|
3650
|
-
let newClipIdx = this.addClip(track.clips[ selectedClips[c][1] ], newTrackIdx, 0);
|
|
3651
|
-
selectedClips[c][0] = newClipIdx; // temporarily store new clip index in trackIndex (HACK START)
|
|
3652
|
-
newTrack.selected[newClipIdx] = true;
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
// delete clips of a track, from last to first
|
|
3656
|
-
for( let c = endSel; c >=startSel ; --c ){
|
|
3657
|
-
this.#delete(currTrack, selectedClips[c][1]);
|
|
3658
|
-
selectedClips[c][1] = selectedClips[c][0]; // put new clip index (HACK)
|
|
3659
|
-
selectedClips[c][0] = newTrackIdx; // put new track index (HACK FIX)
|
|
3660
|
-
}
|
|
3661
|
-
|
|
3662
|
-
currTrack = i < selectedClips.length ? selectedClips[idx][0] : -1;
|
|
3663
|
-
startSel = idx;
|
|
3664
|
-
endSel = idx;
|
|
3665
|
-
continue;
|
|
3666
|
-
}
|
|
3667
|
-
|
|
3668
|
-
deltaTracks > 0 ? startSel = idx : endSel = idx;
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
this.lastClipsSelected = selectedClips;
|
|
3672
|
-
this.historySaveEnabler = oldStateEnabler;
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
this.lastTrackClipsMove = newTrackClipsMove;
|
|
3677
|
-
|
|
3678
|
-
// move clips horizontally
|
|
3679
|
-
|
|
3680
|
-
let leastDelta = delta;
|
|
3681
|
-
let moveAccepted = true;
|
|
3682
|
-
|
|
3683
|
-
// find if all clips can move and/or how much they can move
|
|
3684
|
-
for( let i = 0; i < this.lastClipsSelected.length; ++i ){
|
|
3685
|
-
let trackIdx = this.lastClipsSelected[i][0];
|
|
3686
|
-
let clipIdx = this.lastClipsSelected[i][1];
|
|
3687
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
3688
|
-
const trackClips = track.clips;
|
|
3689
|
-
const clip = track.clips[clipIdx];
|
|
3690
|
-
|
|
3691
|
-
if ( delta >= 0 ){
|
|
3692
|
-
if ( trackClips.length-1 == clipIdx ){ continue; } // all alowed
|
|
3693
|
-
if ( !track.selected[clipIdx+1] ){ // if next is selected, force AllOrNothing and let next clip manage the leastDelta
|
|
3694
|
-
if( trackClips[clipIdx + 1].start >= (clip.start+clip.duration+delta) ){ continue; } //has not reached next clip. Enough space. All allowed
|
|
3695
|
-
const nextClip = trackClips[clipIdx + 1];
|
|
3696
|
-
leastDelta = Math.max( 0, Math.min( leastDelta, nextClip.start - clip.start - clip.duration ) );
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
else if ( delta < 0 ){
|
|
3700
|
-
if ( clipIdx > 0 && (trackClips[clipIdx - 1].start + trackClips[clipIdx - 1].duration) <= (clip.start+delta) ){ continue; } // has not reached previous clip. Enough space
|
|
3701
|
-
if( clipIdx > 0 ){
|
|
3702
|
-
const prevClip = trackClips[clipIdx - 1];
|
|
3703
|
-
leastDelta = Math.min( 0, Math.max( leastDelta, prevClip.start + prevClip.duration - clip.start ) ); // delta is a negative value, that is why the leastDelta is the max
|
|
3704
|
-
}
|
|
3705
|
-
if ( clip.start + delta < 0 ){
|
|
3706
|
-
leastDelta = Math.max(leastDelta, -clip.start);
|
|
3707
|
-
moveAccepted = false; // force it to be a leastDelta move only. No jumps
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
if( !moveAccepted ){ continue; }
|
|
3712
|
-
let clipsInRange = this.getClipsInRange(track, clip.start + delta, clip.start + clip.duration + delta, 0.01);
|
|
3713
|
-
if ( clipsInRange && (clipsInRange[0] != clipIdx || clipsInRange[clipsInRange.length-1] != clipIdx)){
|
|
3714
|
-
for( let c = 0; c < clipsInRange.length; ++c ){
|
|
3715
|
-
if ( !track.selected[clipsInRange[c]] ){ moveAccepted = false; break; }
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
}
|
|
3719
|
-
|
|
3720
|
-
// if moveAccepted -> use full delta
|
|
3721
|
-
// if !moveAccepted -> use leastDelta
|
|
3722
|
-
if ( moveAccepted ){ leastDelta = delta; }
|
|
3723
|
-
this.grabTime = time - delta + leastDelta;
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
//*********** WARNING: RELIES ON SORTED lastClipsSelected ***********
|
|
3727
|
-
// move all selected clips using the computed delta.
|
|
3728
|
-
for( let i = 0; i < this.lastClipsSelected.length; ++i ){
|
|
3729
|
-
const lcs = this.lastClipsSelected[ delta > 0 ? (this.lastClipsSelected.length - 1 - i) : i]; //delta > 0, move last-to-first; delta < 0, move first-to-last
|
|
3730
|
-
const track = this.animationClip.tracks[lcs[0]];
|
|
3731
|
-
const trackClips = track.clips;
|
|
3732
|
-
let clipIdx = lcs[1];
|
|
3733
|
-
const clip = track.clips[clipIdx];
|
|
3734
|
-
clip.start += leastDelta;
|
|
3735
|
-
if (clip.fadein != undefined ){ clip.fadein += leastDelta; }
|
|
3736
|
-
if (clip.fadeout != undefined ){ clip.fadeout += leastDelta; }
|
|
3737
|
-
|
|
3738
|
-
// prepare swap
|
|
3739
|
-
const editedFlag = track.edited[clipIdx];
|
|
3740
|
-
const selectedFlag = track.selected[clipIdx];
|
|
3741
|
-
const hoveredFlag = track.hovered[clipIdx];
|
|
3742
|
-
|
|
3743
|
-
// move other clips
|
|
3744
|
-
if ( delta > 0 ){
|
|
3745
|
-
while( clipIdx < trackClips.length-1 ){
|
|
3746
|
-
if ( trackClips[clipIdx+1].start >= clip.start ){
|
|
3747
|
-
break;
|
|
3748
|
-
}
|
|
3749
|
-
trackClips[clipIdx] = trackClips[clipIdx+1];
|
|
3750
|
-
track.selected[clipIdx] = track.selected[clipIdx+1];
|
|
3751
|
-
track.edited[clipIdx] = track.edited[clipIdx+1];
|
|
3752
|
-
track.hovered[clipIdx] = track.hovered[clipIdx+1];
|
|
3753
|
-
clipIdx++;
|
|
3754
|
-
}
|
|
3755
|
-
}else{
|
|
3756
|
-
while( clipIdx > 0 ){
|
|
3757
|
-
if ( trackClips[clipIdx-1].start <= clip.start ){
|
|
3758
|
-
break;
|
|
3759
|
-
}
|
|
3760
|
-
trackClips[clipIdx] = trackClips[clipIdx-1];
|
|
3761
|
-
track.selected[clipIdx] = track.selected[clipIdx-1];
|
|
3762
|
-
track.edited[clipIdx] = track.edited[clipIdx-1];
|
|
3763
|
-
track.hovered[clipIdx] = track.hovered[clipIdx-1];
|
|
3764
|
-
clipIdx--;
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
// commit swap
|
|
3768
|
-
trackClips[clipIdx] = clip;
|
|
3769
|
-
track.edited[clipIdx] = editedFlag;
|
|
3770
|
-
track.selected[clipIdx] = selectedFlag;
|
|
3771
|
-
track.hovered[clipIdx] = hoveredFlag;
|
|
3772
|
-
|
|
3773
|
-
// update selected clip index
|
|
3774
|
-
lcs[1] = clipIdx;
|
|
3775
|
-
|
|
3776
|
-
if ( clip.start + clip.duration > this.duration ){
|
|
3777
|
-
this.setDuration( clip.start + clip.duration );
|
|
3778
|
-
}
|
|
3779
|
-
if(this.onContentMoved) {
|
|
3780
|
-
this.onContentMoved(clip, leastDelta);
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
}
|
|
3784
|
-
|
|
3785
|
-
return true;
|
|
3786
|
-
}
|
|
3787
|
-
else if(e.track && e.buttons == 0) { // mouse not dragging, just hovering
|
|
3788
|
-
|
|
3789
|
-
this.unHoverAll();
|
|
3790
|
-
let clips = this.getClipsInRange(e.track, time, time, 0.00001);
|
|
3791
|
-
if(!e.track.locked && clips) {
|
|
3792
|
-
|
|
3793
|
-
this.lastHovered = [e.track.trackIdx, clips[0]];
|
|
3794
|
-
e.track.hovered[clips[0]] = true;
|
|
3795
|
-
|
|
3796
|
-
let clip = e.track.clips[clips[0]];
|
|
3797
|
-
if(!clip) {
|
|
3798
|
-
return;
|
|
3799
|
-
}
|
|
3800
|
-
|
|
3801
|
-
if(Math.abs(e.localX - this.timeToX(clip.start + clip.duration)) < 8) { // duration
|
|
3802
|
-
this.canvas.style.cursor = "col-resize";
|
|
3803
|
-
this.dragClipMode = "duration";
|
|
3804
|
-
}
|
|
3805
|
-
else if(clip.fadein != undefined && Math.abs(e.localX - this.timeToX(clip.fadein)) < 8) { // fadein
|
|
3806
|
-
this.canvas.style.cursor = "e-resize";
|
|
3807
|
-
this.dragClipMode = "fadein";
|
|
3808
|
-
}
|
|
3809
|
-
else if(clip.fadeout != undefined && Math.abs(e.localX - this.timeToX(clip.fadeout)) < 8) { // fadeout
|
|
3810
|
-
this.canvas.style.cursor = "e-resize";
|
|
3811
|
-
this.dragClipMode = "fadeout";
|
|
3812
|
-
}
|
|
3813
|
-
else {
|
|
3814
|
-
this.dragClipMode = "";
|
|
3815
|
-
}
|
|
3816
|
-
}
|
|
3817
|
-
}
|
|
3818
|
-
else {
|
|
3819
|
-
this.unHoverAll();
|
|
3820
|
-
}
|
|
3821
|
-
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
onDblClick( e ) {
|
|
3825
|
-
|
|
3826
|
-
const track = e.track;
|
|
3827
|
-
const localX = e.localX;
|
|
3828
|
-
|
|
3829
|
-
if ( track ){
|
|
3830
|
-
const clipIdx = this.getClipOnTime(track, this.xToTime(localX), 0.001);
|
|
3831
|
-
this.selectClip(track.trackIdx, clipIdx); // deselect and try to select clip in localX, if any
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
|
|
3835
|
-
showContextMenu( e ) {
|
|
3836
|
-
|
|
3837
|
-
e.preventDefault();
|
|
3838
|
-
e.stopPropagation();
|
|
3839
|
-
|
|
3840
|
-
let actions = [];
|
|
3841
|
-
if(this.lastClipsSelected.length) {
|
|
3842
|
-
actions.push(
|
|
3843
|
-
{
|
|
3844
|
-
title: "Copy",
|
|
3845
|
-
callback: () => { this.copySelectedContent();}
|
|
3846
|
-
}
|
|
3847
|
-
)
|
|
3848
|
-
actions.push(
|
|
3849
|
-
{
|
|
3850
|
-
title: "Delete",
|
|
3851
|
-
callback: () => {
|
|
3852
|
-
this.deleteSelectedContent({});
|
|
3853
|
-
}
|
|
3854
|
-
}
|
|
3855
|
-
)
|
|
3856
|
-
}
|
|
3857
|
-
else{
|
|
3858
|
-
|
|
3859
|
-
if(this.clipboard)
|
|
3860
|
-
{
|
|
3861
|
-
actions.push(
|
|
3862
|
-
{
|
|
3863
|
-
title: "Paste",
|
|
3864
|
-
callback: () => {
|
|
3865
|
-
this.pasteContent();
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
);
|
|
3869
|
-
actions.push(
|
|
3870
|
-
{
|
|
3871
|
-
title: "Paste Here",
|
|
3872
|
-
callback: () => {
|
|
3873
|
-
this.pasteContent( this.xToTime(e.localX) );
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
)
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
|
-
LX.addContextMenu("Options", e, (m) => {
|
|
3881
|
-
for(let i = 0; i < actions.length; i++) {
|
|
3882
|
-
m.add(actions[i].title, actions[i].callback )
|
|
3883
|
-
}
|
|
3884
|
-
});
|
|
3885
|
-
|
|
3886
|
-
}
|
|
3887
|
-
|
|
3888
|
-
drawContent( ctx ) {
|
|
3889
|
-
|
|
3890
|
-
if(!this.animationClip)
|
|
3891
|
-
return;
|
|
3892
|
-
|
|
3893
|
-
const tracks = this.animationClip.tracks;
|
|
3894
|
-
const trackHeight = this.trackHeight;
|
|
3895
|
-
const scrollY = - this.currentScrollInPixels;
|
|
3896
|
-
|
|
3897
|
-
ctx.save();
|
|
3898
|
-
for(let i = 0; i < tracks.length; i++) {
|
|
3899
|
-
let track = tracks[i];
|
|
3900
|
-
this.drawTrackWithBoxes(ctx, i * trackHeight + scrollY, trackHeight, track.id, track);
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
ctx.restore();
|
|
3904
|
-
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
/**
|
|
3908
|
-
* @method drawTrackWithBoxes
|
|
3909
|
-
* @param {*} ctx
|
|
3910
|
-
*/
|
|
3911
|
-
drawTrackWithBoxes( ctx, y, trackHeight, title, track ) {
|
|
3912
|
-
|
|
3913
|
-
// Fill track background if it's selected
|
|
3914
|
-
ctx.globalAlpha = 0.2;
|
|
3915
|
-
ctx.fillStyle = Timeline.TRACK_SELECTED_LIGHT;
|
|
3916
|
-
if(track.isSelected) {
|
|
3917
|
-
ctx.fillRect(0, y, ctx.canvas.width, trackHeight );
|
|
3918
|
-
}
|
|
3919
|
-
|
|
3920
|
-
const clips = track.clips;
|
|
3921
|
-
|
|
3922
|
-
// set clip box size
|
|
3923
|
-
const offset = (trackHeight * 0.4) * 0.5;
|
|
3924
|
-
trackHeight *= 0.6;
|
|
3925
|
-
|
|
3926
|
-
let selectedClipArea = null;
|
|
3927
|
-
|
|
3928
|
-
ctx.font = Math.floor( trackHeight * 0.8) + "px" + Timeline.FONT;
|
|
3929
|
-
ctx.textAlign = "left";
|
|
3930
|
-
ctx.textBaseline = "middle";
|
|
3931
|
-
|
|
3932
|
-
for(var j = 0; j < clips.length; ++j)
|
|
3933
|
-
{
|
|
3934
|
-
selectedClipArea = null;
|
|
3935
|
-
const clip = clips[j];
|
|
3936
|
-
//let selected = track.selected[j];
|
|
3937
|
-
var x = Math.floor( this.timeToX(clip.start) ) + 0.5;
|
|
3938
|
-
var x2 = Math.floor( this.timeToX( clip.start + clip.duration ) ) + 0.5;
|
|
3939
|
-
var w = x2-x;
|
|
3940
|
-
|
|
3941
|
-
if( x2 < 0 || x > this.canvas.width )
|
|
3942
|
-
continue;
|
|
3943
|
-
|
|
3944
|
-
// Overwrite clip color state depending on its state
|
|
3945
|
-
ctx.globalAlpha = 1;
|
|
3946
|
-
ctx.fillStyle = clip.clipColor || (track.hovered[j] ? Timeline.KEYFRAME_COLOR_HOVERED : (track.selected[j] ? Timeline.TRACK_SELECTED : Timeline.KEYFRAME_COLOR));
|
|
3947
|
-
if(!this.active || !track.active || !clip.active) {
|
|
3948
|
-
ctx.fillStyle = Timeline.KEYFRAME_COLOR_INACTIVE;
|
|
3949
|
-
}
|
|
3950
|
-
|
|
3951
|
-
// Draw clip background
|
|
3952
|
-
ctx.roundRect( x, y + offset, w, trackHeight , 5, true);
|
|
3953
|
-
|
|
3954
|
-
if(this.active && track.active && clip.active) {
|
|
3955
|
-
|
|
3956
|
-
ctx.fillStyle = clip.fadeColor ?? "#0004";
|
|
3957
|
-
|
|
3958
|
-
if ( clip.fadein != undefined ){
|
|
3959
|
-
const fadeinX = this.pixelsPerSecond * (clip.fadein - clip.start);
|
|
3960
|
-
ctx.roundRect(x, y + offset, fadeinX, trackHeight, {tl: 5, bl: 5, tr:0, br:0}, true);
|
|
3961
|
-
}
|
|
3962
|
-
if ( clip.fadeout != undefined ){
|
|
3963
|
-
const fadeoutX = this.pixelsPerSecond * (clip.start + clip.duration - (clip.fadeout));
|
|
3964
|
-
ctx.roundRect( x + w - fadeoutX, y + offset, fadeoutX, trackHeight, {tl: 0, bl: 0, tr:5, br:5}, true);
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
|
|
3968
|
-
ctx.fillStyle = Timeline.TRACK_COLOR_PRIMARY;
|
|
3969
|
-
|
|
3970
|
-
if(track.selected[j] || track.hovered[j]) {
|
|
3971
|
-
ctx.strokeStyle = ctx.shadowColor = clip.clipColor || Timeline.TRACK_SELECTED;
|
|
3972
|
-
ctx.shadowBlur = 10;
|
|
3973
|
-
ctx.shadowOffsetX = 1.5;
|
|
3974
|
-
ctx.shadowOffsetY = 1.5;
|
|
3975
|
-
|
|
3976
|
-
selectedClipArea = [x - 1, y + offset -1, x2 - x + 2, trackHeight + 2];
|
|
3977
|
-
ctx.roundRect(selectedClipArea[0], selectedClipArea[1], selectedClipArea[2], selectedClipArea[3], 5, false, true);
|
|
3978
|
-
|
|
3979
|
-
ctx.shadowBlur = 0;
|
|
3980
|
-
ctx.shadowOffsetX = 0;
|
|
3981
|
-
ctx.shadowOffsetY = 0;
|
|
3982
|
-
|
|
3983
|
-
ctx.font = "bold" + Math.floor( trackHeight) + "px " + Timeline.FONT;
|
|
3984
|
-
ctx.fillStyle = Timeline.FONT_COLOR_PRIMARY;
|
|
3985
|
-
}
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
let text = clip.id ?? ""; //clip.id.replaceAll("_", " ").replaceAll("-", " ");
|
|
3989
|
-
const textInfo = ctx.measureText( text );
|
|
3990
|
-
|
|
3991
|
-
let textWidth = textInfo.width;
|
|
3992
|
-
if ( textWidth > w && textWidth > 0){
|
|
3993
|
-
let amount = Math.floor( w * text.length / textWidth );
|
|
3994
|
-
text = text.substr( 0, amount );
|
|
3995
|
-
textWidth = w;
|
|
3996
|
-
}
|
|
3997
|
-
ctx.fillText( text, x + (w - textWidth)*0.5, y + offset + trackHeight * 0.5);
|
|
3998
|
-
|
|
3999
|
-
ctx.fillStyle = track.hovered[j] ? "white" : "#f5f5f5"//track.hovered[j] ? "white" : Timeline.FONT_COLOR_QUATERNARY;
|
|
4000
|
-
ctx.strokeStyle = "rgba(125,125,125,0.4)";
|
|
4001
|
-
|
|
4002
|
-
// Draw resize bounding
|
|
4003
|
-
ctx.roundRect(x + w - 8 , y + offset , 8, trackHeight, {tl: 4, bl: 4, tr:4, br:4}, true, true);
|
|
4004
|
-
}
|
|
4005
|
-
|
|
4006
|
-
ctx.font = "12px" + Timeline.FONT;
|
|
4007
|
-
}
|
|
4008
|
-
|
|
4009
|
-
/**
|
|
4010
|
-
* @method optimizeTrack
|
|
4011
|
-
*/
|
|
4012
|
-
optimizeTrack(trackIdx) {
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
/**
|
|
4016
|
-
* @method optimizeTracks
|
|
4017
|
-
*/
|
|
4018
|
-
optimizeTracks() {
|
|
4019
|
-
}
|
|
4020
|
-
|
|
4021
|
-
/**
|
|
4022
|
-
*
|
|
4023
|
-
* @param {Object} clip clip to be added
|
|
4024
|
-
* @param {Int} trackIdx (optional) track where to put the clip. -1 will find the first free slot. ***WARNING*** Must call getClipsInRange, before calling this function with a valid trackdIdx
|
|
4025
|
-
* @param {Number} offsetTime (optional) offset time of current time
|
|
4026
|
-
* @param {Number} searchStartTrackIdx (optional) if trackIdx is set to -1, this idx will be used as the starting point to find a valid track
|
|
4027
|
-
* @returns a zero/positive value if successful. Otherwise, -1
|
|
4028
|
-
*/
|
|
4029
|
-
addClip( clip, trackIdx = -1, offsetTime = 0, searchStartTrackIdx = 0 ) {
|
|
4030
|
-
if ( !this.animationClip ){ return -1; }
|
|
4031
|
-
|
|
4032
|
-
this.deselectAllElements(); // TODO: consider adjusting values of hovered and selected instead of deselecting everything
|
|
4033
|
-
|
|
4034
|
-
// Update clip information
|
|
4035
|
-
let newStart = clip.start + offsetTime;
|
|
4036
|
-
if(clip.fadein != undefined)
|
|
4037
|
-
clip.fadein += (newStart - clip.start);
|
|
4038
|
-
if(clip.fadeout != undefined)
|
|
4039
|
-
clip.fadeout += (newStart - clip.start);
|
|
4040
|
-
clip.start = newStart;
|
|
4041
|
-
|
|
4042
|
-
// sanity check
|
|
4043
|
-
clip.active = clip.active ?? true;
|
|
4044
|
-
|
|
4045
|
-
// find appropriate track
|
|
4046
|
-
if ( trackIdx >= this.animationClip.tracks.length ){ // new track ad the end
|
|
4047
|
-
trackIdx = this.addNewTrack();
|
|
4048
|
-
}
|
|
4049
|
-
else if ( trackIdx < 0 ){ // find first free track slot
|
|
4050
|
-
for(let i = searchStartTrackIdx; i < this.animationClip.tracks.length; i++) {
|
|
4051
|
-
let clipInCurrentSlot = this.animationClip.tracks[i].clips.find( t => {
|
|
4052
|
-
return LX.compareThresholdRange(newStart, clip.start + clip.duration, t.start, t.start+t.duration);
|
|
4053
|
-
});
|
|
4054
|
-
|
|
4055
|
-
if(!clipInCurrentSlot){
|
|
4056
|
-
trackIdx = i;
|
|
4057
|
-
break;
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
if(trackIdx < 0){
|
|
4061
|
-
trackIdx = this.addNewTrack();
|
|
4062
|
-
}
|
|
4063
|
-
}else{ // check specific track slot
|
|
4064
|
-
// commented to avoid double checks with "addclips" fn
|
|
4065
|
-
// let clipsInRange = this.getClipsInRange(this.animationClip.tracks[trackIdx], clip.start, clip.start+clip.duration, 0.0001);
|
|
4066
|
-
// if ( clipsInRange ){
|
|
4067
|
-
// return -1;
|
|
4068
|
-
// }
|
|
4069
|
-
}
|
|
4070
|
-
|
|
4071
|
-
clip.trackIdx = trackIdx;
|
|
4072
|
-
|
|
4073
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4074
|
-
|
|
4075
|
-
// Find new index
|
|
4076
|
-
let newIdx = track.clips.findIndex( t => t.start > newStart );
|
|
4077
|
-
|
|
4078
|
-
// Add as last index
|
|
4079
|
-
if(newIdx < 0) {
|
|
4080
|
-
newIdx = track.clips.length;
|
|
4081
|
-
}
|
|
4082
|
-
|
|
4083
|
-
//Save track state before add the new clip
|
|
4084
|
-
this.saveState(trackIdx);
|
|
4085
|
-
|
|
4086
|
-
// Add clip
|
|
4087
|
-
track.clips.splice(newIdx, 0, clip); //insert clip into newIdx (or push at the end)
|
|
4088
|
-
|
|
4089
|
-
// Reset this clip's properties
|
|
4090
|
-
track.hovered.splice(newIdx, 0, false);
|
|
4091
|
-
track.selected.splice(newIdx, 0, false);
|
|
4092
|
-
track.edited.splice(newIdx, 0, false);
|
|
4093
|
-
|
|
4094
|
-
if( !this.animationClip || (clip.start + clip.duration) > this.duration ){
|
|
4095
|
-
this.setDuration(clip.start + clip.duration);
|
|
4096
|
-
}
|
|
4097
|
-
|
|
4098
|
-
// Update animation action interpolation info
|
|
4099
|
-
if(this.onUpdateTrack){
|
|
4100
|
-
this.onUpdateTrack( [trackIdx] );
|
|
4101
|
-
}
|
|
4102
|
-
|
|
4103
|
-
return newIdx;
|
|
4104
|
-
}
|
|
4105
|
-
|
|
4106
|
-
/**
|
|
4107
|
-
* Add an array of clips to the timeline in the first suitable tracks. It tries to put clips in the same track if possible. All clips will be in adjacent tracks to each other
|
|
4108
|
-
* @param {Array of objects} clips
|
|
4109
|
-
* @param {Number} offsetTime
|
|
4110
|
-
* @param {Int} searchStartTrackIdx
|
|
4111
|
-
* @returns
|
|
4112
|
-
*/
|
|
4113
|
-
addClips( clips, offsetTime = 0, searchStartTrackIdx = 0 ){
|
|
4114
|
-
if( !this.animationClip || !clips.length ){ return false; }
|
|
4115
|
-
|
|
4116
|
-
let clipTrackIdxs = new Int16Array( clips.length );
|
|
4117
|
-
let baseTrackIdx = searchStartTrackIdx -1; // every time the algorithm fails, it increments the starting track Idx
|
|
4118
|
-
let currTrackIdx = -1;
|
|
4119
|
-
const tracks = this.animationClip.tracks;
|
|
4120
|
-
const lastTrackLength = tracks.length;
|
|
4121
|
-
let c = 0;
|
|
4122
|
-
for( ; c < clips.length; ++c ){
|
|
4123
|
-
const clip = clips[c];
|
|
4124
|
-
const clipStart = clip.start + offsetTime;
|
|
4125
|
-
const clipEnd = clipStart + clip.duration;
|
|
4126
|
-
if ( c == 0 ){ // last search failed, move one track down and check again
|
|
4127
|
-
++baseTrackIdx;
|
|
4128
|
-
currTrackIdx = baseTrackIdx;
|
|
4129
|
-
|
|
4130
|
-
while ( currTrackIdx >= tracks.length ){ this.addNewTrack(null, false); }
|
|
4131
|
-
let clipsInCurrentSlot = tracks[baseTrackIdx].clips.find( t => { return LX.compareThresholdRange(clipStart, clipEnd, t.start, t.start+t.duration); });
|
|
4132
|
-
|
|
4133
|
-
// reset search
|
|
4134
|
-
if (clipsInCurrentSlot){
|
|
4135
|
-
c = -1;
|
|
4136
|
-
continue;
|
|
4137
|
-
}
|
|
4138
|
-
|
|
4139
|
-
// success
|
|
4140
|
-
clipTrackIdxs[c] = baseTrackIdx;
|
|
4141
|
-
}else{
|
|
4142
|
-
|
|
4143
|
-
// check if it fits in current track
|
|
4144
|
-
let clipsInCurrentSlot = tracks[currTrackIdx].clips.find( t => { return LX.compareThresholdRange(clipStart, clipEnd, t.start, t.start+t.duration); });
|
|
4145
|
-
|
|
4146
|
-
// check no previous added clips are in the way
|
|
4147
|
-
for( let i = c-1; i > -1; --i ){
|
|
4148
|
-
if ( clipTrackIdxs[i] != currTrackIdx || clipsInCurrentSlot ){ break; }
|
|
4149
|
-
clipsInCurrentSlot = LX.compareThresholdRange(clipStart, clipEnd, clips[i].start + offsetTime, clips[i].start + offsetTime + clips[i].duration);
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
// check if it fits in the next track
|
|
4153
|
-
if ( clipsInCurrentSlot ){
|
|
4154
|
-
++currTrackIdx;
|
|
4155
|
-
if ( currTrackIdx >= tracks.length ){ this.addNewTrack(null, false); }
|
|
4156
|
-
clipsInCurrentSlot = tracks[currTrackIdx].clips.find( t => { return LX.compareThresholdRange(clipStart, clipEnd, t.start, t.start+t.duration); });
|
|
4157
|
-
}
|
|
4158
|
-
|
|
4159
|
-
// reset search
|
|
4160
|
-
if ( clipsInCurrentSlot ){
|
|
4161
|
-
c = -1;
|
|
4162
|
-
continue;
|
|
4163
|
-
}
|
|
4164
|
-
|
|
4165
|
-
// success
|
|
4166
|
-
clipTrackIdxs[c] = currTrackIdx;
|
|
4167
|
-
}
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
|
-
// avoid updating panel on each new track. Instead just once at the end
|
|
4171
|
-
if ( lastTrackLength != tracks.length ){
|
|
4172
|
-
this.updateLeftPanel();
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
// save state for all to-be-modified tracks. Do it once for all tracks
|
|
4176
|
-
for( let i = baseTrackIdx; i <= currTrackIdx; ++i ){
|
|
4177
|
-
this.saveState( i, i != baseTrackIdx );
|
|
4178
|
-
}
|
|
4179
|
-
|
|
4180
|
-
// disable history saving
|
|
4181
|
-
let oldStateEnabler = this.historySaveEnabler;
|
|
4182
|
-
this.historySaveEnabler = false;
|
|
4183
|
-
|
|
4184
|
-
for( c = 0; c < clips.length; ++c ){
|
|
4185
|
-
this.addClip(clips[c], clipTrackIdxs[c], offsetTime);
|
|
4186
|
-
}
|
|
4187
|
-
|
|
4188
|
-
// recover old state of enabler
|
|
4189
|
-
this.historySaveEnabler = oldStateEnabler;
|
|
4190
|
-
|
|
4191
|
-
return true;
|
|
4192
|
-
}
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
deleteSelectedContent(skipCallback = false) {
|
|
4196
|
-
//*********** WARNING: RELIES ON SORTED lastClipsSelected ***********
|
|
4197
|
-
if ( !this.lastClipsSelected.length ){
|
|
4198
|
-
return;
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
// delete selected clips from last to first. lastClipsSelected is sorted
|
|
4202
|
-
const selected = this.lastClipsSelected;
|
|
4203
|
-
this.lastClipsSelected = []; // so this.#delete does not check clipsselected on each loop (all will be destroyed)
|
|
4204
|
-
let prevTrack = -1;
|
|
4205
|
-
for( let i = selected.length-1; i > -1; --i ){
|
|
4206
|
-
let s = selected[i];
|
|
4207
|
-
if ( s[0] != prevTrack){
|
|
4208
|
-
this.saveState(s[0], prevTrack != -1 );
|
|
4209
|
-
prevTrack = s[0];
|
|
4210
|
-
}
|
|
4211
|
-
|
|
4212
|
-
this.#delete(s[0], s[1]);
|
|
4213
|
-
}
|
|
4214
|
-
|
|
4215
|
-
if (this.onDeleteSelectedClips && !skipCallback){
|
|
4216
|
-
this.onDeleteSelectedClips(selected);
|
|
4217
|
-
}
|
|
4218
|
-
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
/** Delete clip from the timeline
|
|
4222
|
-
* @param {Number} trackIdx
|
|
4223
|
-
* @param {Number} clipIdx clip to be deleted
|
|
4224
|
-
*/
|
|
4225
|
-
deleteClip( trackIdx, clipIdx, skipCallback = false ) {
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
this.saveState(trackIdx);
|
|
4229
|
-
const clip = this.#delete(trackIdx, clipIdx);
|
|
4230
|
-
|
|
4231
|
-
if ( this.onDeleteClip && !skipCallback ){
|
|
4232
|
-
this.onDeleteClip( trackIdx, clipIdx, clip );
|
|
4233
|
-
}
|
|
4234
|
-
}
|
|
4235
|
-
|
|
4236
|
-
#delete(trackIdx, clipIdx) {
|
|
4237
|
-
|
|
4238
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4239
|
-
|
|
4240
|
-
// remove from selected clips
|
|
4241
|
-
for(let i = 0; i < this.lastClipsSelected.length; i++) {
|
|
4242
|
-
const [selectedTrackIdx, selectedClipIdx] = this.lastClipsSelected[i];
|
|
4243
|
-
if(selectedTrackIdx == trackIdx){
|
|
4244
|
-
if (selectedClipIdx == clipIdx){ // remove self
|
|
4245
|
-
this.lastClipsSelected.splice(i--,1);
|
|
4246
|
-
}else if (selectedClipIdx > clipIdx){ // move upper clips to the left
|
|
4247
|
-
this.lastClipsSelected[i][1]--;
|
|
4248
|
-
}
|
|
4249
|
-
}
|
|
4250
|
-
else if( trackIdx < selectedTrackIdx ){
|
|
4251
|
-
break;
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
if ( this.hovered && this.hovered[0] == trackIdx ){
|
|
4256
|
-
if ( this.hovered[1] == clipIdx ){ this.unHoverAll(); }
|
|
4257
|
-
else if( this.hovered[1] > clipIdx ){ this.hovered[1]--; }
|
|
4258
|
-
}
|
|
4259
|
-
|
|
4260
|
-
const clip = track[clipIdx];
|
|
4261
|
-
track.hovered.splice(clipIdx,1);
|
|
4262
|
-
track.selected.splice(clipIdx,1);
|
|
4263
|
-
track.edited.splice(clipIdx,1);
|
|
4264
|
-
track.clips.splice(clipIdx, 1);
|
|
4265
|
-
return clip;
|
|
4266
|
-
}
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
/**
|
|
4271
|
-
* User defined. Used when copying and pasting
|
|
4272
|
-
* @param {Array of clips} clipsToClone array of original clips. Do not modify clips in this array
|
|
4273
|
-
* @param {Number} timeOffset Value of time that should be added (or subtracted) from the timing attributes
|
|
4274
|
-
* @param {Int} reason Flag to signal the reason of the clone
|
|
4275
|
-
* @returns {Array of clips}
|
|
4276
|
-
*/
|
|
4277
|
-
cloneClips( clipsToClone, timeOffset, reason = 0 ){
|
|
4278
|
-
let clipsToReturn = JSON.parse(JSON.stringify(clipsToClone))
|
|
4279
|
-
for(let i = 0; i < clipsToReturn.length; ++i){
|
|
4280
|
-
let clip = clipsToReturn[i];
|
|
4281
|
-
clip.start += timeOffset;
|
|
4282
|
-
if (clip.fadein == null || clip.fadein == undefined ){ clip.fadein = undefined; }
|
|
4283
|
-
else{ clip.fadein += timeOffset; }
|
|
4284
|
-
if (clip.fadeout == null || clip.fadeout == undefined ){ clip.fadeout = undefined; }
|
|
4285
|
-
else{ clip.fadeout += timeOffset; }
|
|
4286
|
-
}
|
|
4287
|
-
return clipsToReturn;
|
|
4288
|
-
}
|
|
4289
|
-
|
|
4290
|
-
/**
|
|
4291
|
-
* Overwrite the "cloneClips" function to provide a custom cloning of clips. Otherwise, JSON serialization is used
|
|
4292
|
-
*/
|
|
4293
|
-
copySelectedContent() {
|
|
4294
|
-
|
|
4295
|
-
if ( this.lastClipsSelected.length == 0 ){
|
|
4296
|
-
return;
|
|
4297
|
-
}
|
|
4298
|
-
|
|
4299
|
-
let clipsToCopy = [];
|
|
4300
|
-
const lastClipsSelected = this.lastClipsSelected;
|
|
4301
|
-
const tracks = this.animationClip.tracks;
|
|
4302
|
-
let globalStart = Infinity;
|
|
4303
|
-
for(let i = 0; i < lastClipsSelected.length; ++i){
|
|
4304
|
-
let clip = tracks[ lastClipsSelected[i][0] ].clips[ lastClipsSelected[i][1] ];
|
|
4305
|
-
clipsToCopy.push( clip );
|
|
4306
|
-
if ( globalStart > clip.start ){ globalStart = clip.start; }
|
|
4307
|
-
}
|
|
4308
|
-
|
|
4309
|
-
globalStart = Math.max(0, globalStart);
|
|
4310
|
-
this.clipboard = this.cloneClips( clipsToCopy, -globalStart, ClipsTimeline.CLONEREASON_COPY );
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
pasteContent( time = this.currentTime ) {
|
|
4314
|
-
this.deselectAllClips();
|
|
4315
|
-
|
|
4316
|
-
if(!this.clipboard)
|
|
4317
|
-
return;
|
|
4318
|
-
|
|
4319
|
-
time = Math.max(0, time);
|
|
4320
|
-
|
|
4321
|
-
let clipsToAdd = this.cloneClips( this.clipboard, time, ClipsTimeline.CLONEREASON_PASTE );
|
|
4322
|
-
this.addClips(clipsToAdd, 0);
|
|
4323
|
-
}
|
|
4324
|
-
|
|
4325
|
-
/**
|
|
4326
|
-
* @method clearTrack
|
|
4327
|
-
*/
|
|
4328
|
-
|
|
4329
|
-
clearTrack(trackIdx) {
|
|
4330
|
-
|
|
4331
|
-
if (!this.animationClip) {
|
|
4332
|
-
this.animationClip = {tracks:[]};
|
|
4333
|
-
return;
|
|
4334
|
-
}
|
|
4335
|
-
this.saveState(trackIdx);
|
|
4336
|
-
|
|
4337
|
-
if (this.animationClip.tracks[trackIdx].locked ) {
|
|
4338
|
-
return;
|
|
4339
|
-
}
|
|
4340
|
-
|
|
4341
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4342
|
-
track.selected = [];
|
|
4343
|
-
track.edited = [];
|
|
4344
|
-
track.hovered = [];
|
|
4345
|
-
track.clips = [];
|
|
4346
|
-
|
|
4347
|
-
// remove from selected clips
|
|
4348
|
-
for(let i = 0; i < this.lastClipsSelected.length; i++) {
|
|
4349
|
-
const [selectedTrackIdx, selectedClipIdx] = this.lastClipsSelected[i];
|
|
4350
|
-
if(selectedTrackIdx == trackIdx){
|
|
4351
|
-
this.lastClipsSelected.splice(i--,1);
|
|
4352
|
-
}
|
|
4353
|
-
else if( trackIdx < selectedTrackIdx ){
|
|
4354
|
-
break;
|
|
4355
|
-
}
|
|
4356
|
-
}
|
|
4357
|
-
|
|
4358
|
-
if ( this.hovered && this.hovered[0] == trackIdx ){ this.unHoverAll(); }
|
|
4359
|
-
|
|
4360
|
-
return;
|
|
4361
|
-
}
|
|
4362
|
-
|
|
4363
|
-
/**
|
|
4364
|
-
* saveState function uses this to generate a "copy" of the track.
|
|
4365
|
-
* @param {Number} trackIdx
|
|
4366
|
-
* @returns All necessary information to reconstruct the track state
|
|
4367
|
-
*/
|
|
4368
|
-
historyGenerateTrackStep( trackIdx ){
|
|
4369
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4370
|
-
const clips = this.cloneClips(track.clips, 0, ClipsTimeline.CLONEREASON_HISTORY);
|
|
4371
|
-
|
|
4372
|
-
// sanity check in case cloneClips misses this
|
|
4373
|
-
for( let i = 0; i < clips.length; ++i ){
|
|
4374
|
-
clips[i].trackIdx = trackIdx;
|
|
4375
|
-
}
|
|
4376
|
-
|
|
4377
|
-
const undoStep = {
|
|
4378
|
-
trackIdx: trackIdx, // already done by saveState
|
|
4379
|
-
clips: clips,
|
|
4380
|
-
edited: track.edited.slice(0,track.clips.length)
|
|
4381
|
-
};
|
|
4382
|
-
|
|
4383
|
-
return undoStep;
|
|
4384
|
-
}
|
|
4385
|
-
|
|
4386
|
-
/**
|
|
4387
|
-
* It should swap the previous state with the incoming state of the track. It must return the previous state.
|
|
4388
|
-
* historyGenerateTrackStep could be used to copy the previous state. However, as it is a swap, it suffices to just copy the references.
|
|
4389
|
-
* @param {Object} state object with a trackIdx:Number and whatever information was saved in historyGenerateTrackStep
|
|
4390
|
-
* @param {Boolean} isUndo
|
|
4391
|
-
* @returns previous state object
|
|
4392
|
-
*/
|
|
4393
|
-
historyApplyTrackStep( state, isUndo ){
|
|
4394
|
-
const track = this.animationClip.tracks[state.trackIdx];
|
|
4395
|
-
|
|
4396
|
-
const stateToReturn = {
|
|
4397
|
-
trackIdx: state.trackIdx, // already done by saveState
|
|
4398
|
-
clips: track.clips,
|
|
4399
|
-
edited: track.edited
|
|
4400
|
-
};
|
|
4401
|
-
|
|
4402
|
-
track.clips = state.clips;
|
|
4403
|
-
track.edited = state.edited;
|
|
4404
|
-
if ( track.selected.length < track.clips.length ){ track.selected.length = track.clips.length; }
|
|
4405
|
-
if ( track.hovered.length < track.clips.length ){ track.hovered.length = track.clips.length; }
|
|
4406
|
-
track.selected.fill(false);
|
|
4407
|
-
track.hovered.fill(false);
|
|
4408
|
-
|
|
4409
|
-
// sanity check. Also done in addClip
|
|
4410
|
-
for( let i = 0; i < track.clips.length; ++i ){
|
|
4411
|
-
track.clips[i].active = track.clips[i].active ?? true;
|
|
4412
|
-
}
|
|
4413
|
-
|
|
4414
|
-
return stateToReturn;
|
|
4415
|
-
}
|
|
4416
|
-
|
|
4417
|
-
getClipOnTime( track, time, threshold ) {
|
|
4418
|
-
|
|
4419
|
-
if(!track || !track.clips.length){
|
|
4420
|
-
return -1;
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
// Avoid iterating through all timestamps
|
|
4424
|
-
if((time + threshold) < track.clips[0].start){
|
|
4425
|
-
return -1;
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
for(let i = 0; i < track.clips.length; ++i) {
|
|
4429
|
-
let t = track.clips[i];
|
|
4430
|
-
if(t.start + t.duration >= (time - threshold) &&
|
|
4431
|
-
t.start <= (time + threshold)) {
|
|
4432
|
-
return i;
|
|
4433
|
-
}
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
return -1;
|
|
4437
|
-
};
|
|
4438
|
-
|
|
4439
|
-
deselectAllClips() {
|
|
4440
|
-
|
|
4441
|
-
for(let [trackIdx, clipIdx] of this.lastClipsSelected) {
|
|
4442
|
-
this.animationClip.tracks[trackIdx].selected[clipIdx]= false;
|
|
4443
|
-
}
|
|
4444
|
-
// Something has been deselected
|
|
4445
|
-
const deselected = this.lastClipsSelected.length > 0;
|
|
4446
|
-
this.lastClipsSelected.length = 0;
|
|
4447
|
-
return deselected;
|
|
4448
|
-
}
|
|
4449
|
-
|
|
4450
|
-
selectAll( skipCallback = false) {
|
|
4451
|
-
|
|
4452
|
-
this.deselectAllClips();
|
|
4453
|
-
for(let trackIdx = 0; trackIdx < this.animationClip.tracks.length; trackIdx++) {
|
|
4454
|
-
for(let clipIdx = 0; clipIdx < this.animationClip.tracks[trackIdx].clips.length; clipIdx++) {
|
|
4455
|
-
this.animationClip.tracks[trackIdx].selected[clipIdx] = true;
|
|
4456
|
-
this.lastClipsSelected.push( [trackIdx, clipIdx] ); // already sorted
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
if(this.onSelectClip && !skipCallback)
|
|
4460
|
-
this.onSelectClip(null);
|
|
4461
|
-
}
|
|
4462
|
-
|
|
4463
|
-
selectClip( trackIdx, clipIndex, deselect = true, skipCallback = false ) {
|
|
4464
|
-
|
|
4465
|
-
if(deselect){
|
|
4466
|
-
this.deselectAllClips();
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
if(clipIndex < 0){
|
|
4470
|
-
return -1;
|
|
4471
|
-
}
|
|
4472
|
-
|
|
4473
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4474
|
-
if(track.selected[clipIndex]){
|
|
4475
|
-
return clipIndex;
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
// Select if not handled
|
|
4479
|
-
|
|
4480
|
-
// push selection sorted by track index and clip index
|
|
4481
|
-
let i = 0;
|
|
4482
|
-
for( ; i < this.lastClipsSelected.length; ++i){
|
|
4483
|
-
let t = this.lastClipsSelected[i];
|
|
4484
|
-
if ( t[0] < track.trackIdx ){ continue; }
|
|
4485
|
-
if ( t[0] > track.trackIdx || t[1] > clipIndex ){ break;}
|
|
4486
|
-
}
|
|
4487
|
-
this.lastClipsSelected.splice(i,0, [track.trackIdx, clipIndex, track.clips[clipIndex] ] ); //
|
|
4488
|
-
track.selected[clipIndex] = true;
|
|
4489
|
-
|
|
4490
|
-
if( !skipCallback && this.onSelectClip ){
|
|
4491
|
-
this.onSelectClip(track.clips[ clipIndex ]);
|
|
4492
|
-
// Event handled
|
|
4493
|
-
}
|
|
4494
|
-
return clipIndex;
|
|
4495
|
-
}
|
|
4496
|
-
|
|
4497
|
-
deselectClip( trackIdx, clipIndex ){
|
|
4498
|
-
|
|
4499
|
-
if(clipIndex == -1){
|
|
4500
|
-
return -1;
|
|
4501
|
-
}
|
|
4502
|
-
|
|
4503
|
-
const track = this.animationClip.tracks[trackIdx];
|
|
4504
|
-
if(!track.selected[clipIndex]){
|
|
4505
|
-
return -1;
|
|
4506
|
-
}
|
|
4507
|
-
|
|
4508
|
-
track.selected[clipIndex] = false;
|
|
4509
|
-
|
|
4510
|
-
// deselect
|
|
4511
|
-
for( let i = 0; i < this.lastClipsSelected.length; ++i){
|
|
4512
|
-
let t = this.lastClipsSelected[i];
|
|
4513
|
-
if ( t[0] == trackIdx && t[1] == clipIndex ){
|
|
4514
|
-
this.lastClipsSelected.splice(i,1);
|
|
4515
|
-
break;
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
return clipIndex;
|
|
4520
|
-
}
|
|
4521
|
-
|
|
4522
|
-
getClipsInRange( track, minTime, maxTime, threshold = 0 ) {
|
|
4523
|
-
|
|
4524
|
-
if(!track || !track.clips.length)
|
|
4525
|
-
return null;
|
|
4526
|
-
|
|
4527
|
-
// Manage negative selection
|
|
4528
|
-
if(minTime > maxTime) {
|
|
4529
|
-
let aux = minTime;
|
|
4530
|
-
minTime = maxTime;
|
|
4531
|
-
maxTime = aux;
|
|
4532
|
-
}
|
|
4533
|
-
|
|
4534
|
-
minTime -= threshold;
|
|
4535
|
-
maxTime += threshold;
|
|
4536
|
-
|
|
4537
|
-
// Avoid iterating through all timestamps
|
|
4538
|
-
minTime -= threshold;
|
|
4539
|
-
maxTime += threshold;
|
|
4540
|
-
|
|
4541
|
-
const clips = track.clips;
|
|
4542
|
-
if(maxTime < clips[0].start || minTime > (clips[clips.length-1].start + clips[clips.length-1].duration) )
|
|
4543
|
-
return null;
|
|
4544
|
-
|
|
4545
|
-
let indices = [];
|
|
4546
|
-
|
|
4547
|
-
for(let i = 0; i < clips.length; ++i) {
|
|
4548
|
-
const c = clips[i];
|
|
4549
|
-
if ( c.start+c.duration < minTime ){ continue; }
|
|
4550
|
-
if ( c.start > maxTime ){ break; }
|
|
4551
|
-
indices.push(i);
|
|
4552
|
-
}
|
|
4553
|
-
return indices.length ? indices : null;
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
validateDuration(t) {
|
|
4557
|
-
for(let i = 0; i < this.animationClip.tracks.length; i++) {
|
|
4558
|
-
const track = this.animationClip.tracks[i];
|
|
4559
|
-
if ( track.clips.length ){
|
|
4560
|
-
const clip = track.clips[track.clips.length-1]; // assuming they are ordered ascendently
|
|
4561
|
-
t = Math.max(t, clip.start + clip.duration);
|
|
4562
|
-
}
|
|
4563
|
-
}
|
|
4564
|
-
return t;
|
|
4565
|
-
}
|
|
4566
|
-
|
|
4567
|
-
setDuration( t, skipCallback = false, updateHeader = true ){
|
|
4568
|
-
const oldT = t;
|
|
4569
|
-
const newT = this.validateDuration(t);
|
|
4570
|
-
super.setDuration( newT, skipCallback, oldT != newT || updateHeader );
|
|
4571
|
-
}
|
|
4572
|
-
}
|
|
4573
|
-
|
|
4574
|
-
LX.ClipsTimeline = ClipsTimeline;
|
|
4575
|
-
|
|
4576
|
-
/**
|
|
4577
|
-
* Draws a rounded rectangle using the current state of the canvas.
|
|
4578
|
-
* If you omit the last three params, it will draw a rectangle
|
|
4579
|
-
* outline with a 5 pixel border radius
|
|
4580
|
-
* @param {Number} x The top left x coordinate
|
|
4581
|
-
* @param {Number} y The top left y coordinate
|
|
4582
|
-
* @param {Number} width The width of the rectangle
|
|
4583
|
-
* @param {Number} height The height of the rectangle
|
|
4584
|
-
* @param {Number} [radius = 5] The corner radius; It can also be an object
|
|
4585
|
-
* to specify different radii for corners
|
|
4586
|
-
* @param {Number} [radius.tl = 0] Top left
|
|
4587
|
-
* @param {Number} [radius.tr = 0] Top right
|
|
4588
|
-
* @param {Number} [radius.br = 0] Bottom right
|
|
4589
|
-
* @param {Number} [radius.bl = 0] Bottom left
|
|
4590
|
-
* @param {Boolean} [fill = false] Whether to fill the rectangle.
|
|
4591
|
-
* @param {Boolean} [stroke = true] Whether to stroke the rectangle.
|
|
4592
|
-
*/
|
|
4593
|
-
|
|
4594
|
-
CanvasRenderingContext2D.prototype.roundRect = function(x, y, width, height, radius = 5, fill = false, stroke = false) {
|
|
4595
|
-
|
|
4596
|
-
if (typeof radius === 'number') {
|
|
4597
|
-
radius = {tl: radius, tr: radius, br: radius, bl: radius};
|
|
4598
|
-
} else {
|
|
4599
|
-
var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
|
|
4600
|
-
for (var side in defaultRadius) {
|
|
4601
|
-
radius[side] = radius[side] || defaultRadius[side];
|
|
4602
|
-
}
|
|
4603
|
-
}
|
|
4604
|
-
|
|
4605
|
-
this.beginPath();
|
|
4606
|
-
this.moveTo(x + radius.tl, y);
|
|
4607
|
-
this.lineTo(x + width - radius.tr, y);
|
|
4608
|
-
this.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
|
|
4609
|
-
this.lineTo(x + width, y + height - radius.br);
|
|
4610
|
-
this.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
|
|
4611
|
-
this.lineTo(x + radius.bl, y + height);
|
|
4612
|
-
this.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
|
|
4613
|
-
this.lineTo(x, y + radius.tl);
|
|
4614
|
-
this.quadraticCurveTo(x, y, x + radius.tl, y);
|
|
4615
|
-
this.closePath();
|
|
4616
|
-
|
|
4617
|
-
if (fill) {
|
|
4618
|
-
this.fill();
|
|
4619
|
-
}
|
|
4620
|
-
if (stroke) {
|
|
4621
|
-
this.stroke();
|
|
4622
|
-
}
|
|
4623
|
-
}
|
|
4624
|
-
|
|
4625
|
-
LX.concatTypedArray = (arrays, ArrayType) => {
|
|
4626
|
-
let size = arrays.reduce((acc,arr) => acc + arr.length, 0);
|
|
4627
|
-
let result = new ArrayType( size ); // generate just one array
|
|
4628
|
-
let offset = 0;
|
|
4629
|
-
for( let i = 0; i < arrays.length; ++i ){
|
|
4630
|
-
result.set(arrays[i], offset ); // copy values
|
|
4631
|
-
offset += arrays[i].length;
|
|
4632
|
-
}
|
|
4633
|
-
return result;
|
|
4634
|
-
}
|
|
4635
|
-
|
|
4636
|
-
export { Timeline, KeyFramesTimeline, ClipsTimeline };
|