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.
Files changed (134) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +14 -5
  3. package/build/components/AlertDialog.d.ts +7 -0
  4. package/build/components/ArrayInput.d.ts +9 -0
  5. package/build/components/BaseComponent.d.ts +73 -0
  6. package/build/components/Button.d.ts +14 -0
  7. package/build/components/Calendar.d.ts +41 -0
  8. package/build/components/CalendarRange.d.ts +16 -0
  9. package/build/components/CanvasCurve.d.ts +10 -0
  10. package/build/components/CanvasDial.d.ts +11 -0
  11. package/build/components/CanvasMap2D.d.ts +61 -0
  12. package/build/components/Card.d.ts +8 -0
  13. package/build/components/Checkbox.d.ts +8 -0
  14. package/build/components/Color.d.ts +20 -0
  15. package/build/components/ColorInput.d.ts +13 -0
  16. package/build/components/ColorPicker.d.ts +29 -0
  17. package/build/components/ComboButtons.d.ts +8 -0
  18. package/build/components/ContextMenu.d.ts +16 -0
  19. package/build/components/Counter.d.ts +9 -0
  20. package/build/components/Curve.d.ts +10 -0
  21. package/build/components/DatePicker.d.ts +13 -0
  22. package/build/components/Dial.d.ts +10 -0
  23. package/build/components/Dialog.d.ts +20 -0
  24. package/build/components/DropdownMenu.d.ts +32 -0
  25. package/build/components/FileInput.d.ts +8 -0
  26. package/build/components/Footer.d.ts +14 -0
  27. package/build/components/Form.d.ts +8 -0
  28. package/build/components/Layers.d.ts +9 -0
  29. package/build/components/List.d.ts +9 -0
  30. package/build/components/Map2D.d.ts +12 -0
  31. package/build/components/Menubar.d.ts +59 -0
  32. package/build/components/NodeTree.d.ts +26 -0
  33. package/build/components/NumberInput.d.ts +9 -0
  34. package/build/components/OTPInput.d.ts +8 -0
  35. package/build/components/Pad.d.ts +8 -0
  36. package/build/components/Pagination.d.ts +26 -0
  37. package/build/components/PocketDialog.d.ts +11 -0
  38. package/build/components/Popover.d.ts +20 -0
  39. package/build/components/Progress.d.ts +8 -0
  40. package/build/components/RadioGroup.d.ts +8 -0
  41. package/build/components/RangeInput.d.ts +11 -0
  42. package/build/components/Rate.d.ts +8 -0
  43. package/build/components/Select.d.ts +10 -0
  44. package/build/components/Sheet.d.ts +10 -0
  45. package/build/components/Sidebar.d.ts +84 -0
  46. package/build/components/SizeInput.d.ts +8 -0
  47. package/build/components/Skeleton.d.ts +5 -0
  48. package/build/components/Spinner.d.ts +9 -0
  49. package/build/components/TabSections.d.ts +11 -0
  50. package/build/components/Table.d.ts +34 -0
  51. package/build/components/Tabs.d.ts +20 -0
  52. package/build/components/Tags.d.ts +9 -0
  53. package/build/components/TextArea.d.ts +8 -0
  54. package/build/components/TextInput.d.ts +11 -0
  55. package/build/components/Title.d.ts +8 -0
  56. package/build/components/Toggle.d.ts +8 -0
  57. package/build/components/Tour.d.ts +36 -0
  58. package/build/components/Vector.d.ts +9 -0
  59. package/build/core/Area.d.ts +143 -0
  60. package/build/core/Branch.d.ts +19 -0
  61. package/build/core/Core.d.ts +1 -0
  62. package/build/core/Event.d.ts +26 -0
  63. package/build/core/Icons.d.ts +4 -0
  64. package/build/core/Namespace.d.ts +2 -0
  65. package/build/core/Namespace.js +34 -0
  66. package/build/core/Namespace.js.map +1 -0
  67. package/build/core/Panel.d.ts +538 -0
  68. package/build/core/Utils.d.ts +1 -0
  69. package/build/core/Vec2.d.ts +21 -0
  70. package/build/extensions/AssetView.d.ts +136 -0
  71. package/build/extensions/AssetView.js +1367 -0
  72. package/build/extensions/AssetView.js.map +1 -0
  73. package/build/extensions/Audio.d.ts +9 -0
  74. package/build/extensions/Audio.js +163 -0
  75. package/build/extensions/Audio.js.map +1 -0
  76. package/build/extensions/CodeEditor.d.ts +350 -0
  77. package/build/extensions/CodeEditor.js +5022 -0
  78. package/build/extensions/CodeEditor.js.map +1 -0
  79. package/build/extensions/DocMaker.d.ts +27 -0
  80. package/build/extensions/DocMaker.js +327 -0
  81. package/build/extensions/DocMaker.js.map +1 -0
  82. package/build/extensions/GraphEditor.d.ts +276 -0
  83. package/build/extensions/GraphEditor.js +2770 -0
  84. package/build/extensions/GraphEditor.js.map +1 -0
  85. package/build/extensions/ImUi.d.ts +46 -0
  86. package/build/extensions/ImUi.js +227 -0
  87. package/build/extensions/ImUi.js.map +1 -0
  88. package/build/extensions/Timeline.d.ts +670 -0
  89. package/build/extensions/Timeline.js +3955 -0
  90. package/build/extensions/Timeline.js.map +1 -0
  91. package/build/extensions/VideoEditor.d.ts +128 -0
  92. package/build/extensions/VideoEditor.js +898 -0
  93. package/build/extensions/VideoEditor.js.map +1 -0
  94. package/build/extensions/index.d.ts +8 -0
  95. package/build/extensions/index.js +10 -0
  96. package/build/extensions/index.js.map +1 -0
  97. package/build/index.all.d.ts +2 -0
  98. package/build/index.css.d.ts +4 -0
  99. package/build/index.d.ts +56 -0
  100. package/build/lexgui.all.js +28498 -0
  101. package/build/lexgui.all.js.map +1 -0
  102. package/build/lexgui.all.min.js +1 -0
  103. package/build/lexgui.all.module.js +28422 -0
  104. package/build/lexgui.all.module.js.map +1 -0
  105. package/build/lexgui.all.module.min.js +1 -0
  106. package/build/lexgui.css +939 -346
  107. package/build/lexgui.js +13406 -17286
  108. package/build/lexgui.js.map +1 -0
  109. package/build/lexgui.min.css +3 -10
  110. package/build/lexgui.min.js +1 -1
  111. package/build/lexgui.module.js +12762 -16698
  112. package/build/lexgui.module.js.map +1 -0
  113. package/build/lexgui.module.min.js +1 -1
  114. package/changelog.md +170 -74
  115. package/demo.js +162 -48
  116. package/examples/all-components.html +45 -14
  117. package/examples/asset-view.html +110 -47
  118. package/examples/code-editor.html +5 -5
  119. package/examples/dialogs.html +3 -3
  120. package/examples/editor.html +27 -13
  121. package/examples/index.html +19 -14
  122. package/examples/node-graph.html +2 -2
  123. package/examples/previews/video-editor.png +0 -0
  124. package/examples/timeline.html +1 -1
  125. package/examples/video-editor.html +2 -2
  126. package/package.json +25 -9
  127. package/build/extensions/audio.js +0 -212
  128. package/build/extensions/codeeditor.js +0 -6319
  129. package/build/extensions/docmaker.js +0 -432
  130. package/build/extensions/imui.js +0 -325
  131. package/build/extensions/nodegraph.js +0 -3696
  132. package/build/extensions/timeline.js +0 -4636
  133. package/build/extensions/videoeditor.js +0 -953
  134. 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 };