sunrize 1.10.3 → 1.11.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.
@@ -1,18 +1,3088 @@
1
- "use strict"
1
+ "use strict";
2
2
 
3
3
  const
4
- Interface = require ("../Application/Interface")
4
+ $ = require ("jquery"),
5
+ electron = require ("electron"),
6
+ capitalize = require ("capitalize"),
7
+ X3D = require ("../X3D"),
8
+ Interface = require ("../Application/Interface"),
9
+ Splitter = require ("../Controls/Splitter"),
10
+ NodeList = require ("./NodeList"),
11
+ MemberList = require ("./AnimationMemberList"),
12
+ Editor = require ("../Undo/Editor"),
13
+ _ = require ("../Application/GetText");
14
+
15
+ require ("../Bits/Validate");
5
16
 
6
17
  module .exports = class AnimationEditor extends Interface
7
18
  {
8
19
  constructor (element)
9
20
  {
10
- super (`Sunrize.AnimationEditor.${element .attr ("id")}.`)
21
+ super (`Sunrize.AnimationEditor.${element .attr ("id")}.`);
22
+
23
+ this .animationEditor = element;
24
+
25
+ this .verticalSplitter = $("<div></div>")
26
+ .attr ("id", "animation-editor-content")
27
+ .addClass (["animation-editor-content", "vertical-splitter"])
28
+ .appendTo (this .animationEditor)
29
+ .on ("mouseleave", () => this .requestDrawTimeline ());
30
+
31
+ this .verticalSplitterLeft = $("<div></div>")
32
+ .addClass ("vertical-splitter-left")
33
+ .css ("width", "30%")
34
+ .appendTo (this .verticalSplitter)
35
+ .on ("mouseleave", () => this .requestDrawTimeline ());
36
+
37
+ this .timelineElement = $("<div></div>")
38
+ .attr ("tabindex", 0)
39
+ .addClass (["timeline", "vertical-splitter-right"])
40
+ .css ("width", "70%")
41
+ .on ("mouseleave", () => this .clearPointer ())
42
+ .on ("mousedown", event => this .on_mousedown (event))
43
+ .on ("mouseup", event => this .on_mouseup (event))
44
+ .on ("mousemove", event => this .on_mousemove (event))
45
+ .on ("wheel", event => this .on_wheel (event))
46
+ .on ("keydown", event => this .on_keydown (event))
47
+ .appendTo (this .verticalSplitter);
48
+
49
+ this .scrollbarElement = $("<div></div>")
50
+ .addClass ("scrollbar")
51
+ .css ("width", "100%")
52
+ .on ("mousedown", event => this .on_mousedown_scrollbar (event))
53
+ .on ("mouseup", event => this .on_mouseup_scrollbar (event))
54
+ .on ("mousemove", event => this .on_mousemove_scrollbar (event))
55
+ .appendTo (this .timelineElement);
56
+
57
+ this .vSplitter = new Splitter (this .verticalSplitter, "vertical");
58
+
59
+ // Toolbar
60
+
61
+ this .toolbar = $("<div></div>")
62
+ .attr ("id", "animation-editor-toolbar")
63
+ .addClass (["animation-editor-toolbar", "toolbar", "horizontal-toolbar"])
64
+ .appendTo (this .animationEditor);
65
+
66
+ this .createAnimationIcon = $("<span></span>")
67
+ .addClass (["material-symbols-outlined", "disabled"])
68
+ .attr ("title", _("Create animation."))
69
+ .text ("animation")
70
+ .appendTo (this .toolbar)
71
+ .on ("click", () => this .createAnimation ());
72
+
73
+ $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
74
+
75
+ this .addMembersIcon = $("<span></span>")
76
+ .addClass ("material-icons")
77
+ .attr ("title", _("Add member(s) to animation."))
78
+ .text ("add")
79
+ .appendTo (this .toolbar)
80
+ .on ("click", () => this .addMembers ());
81
+
82
+ $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
83
+
84
+ this .cutFrameIcon = $("<span></span>")
85
+ .addClass ("material-icons")
86
+ .attr ("title", _("Cut selected keyframes."))
87
+ .text ("content_cut")
88
+ .appendTo (this .toolbar)
89
+ .on ("click", () => this .cutKeyframes ());
90
+
91
+ this .copyFrameIcon = $("<span></span>")
92
+ .addClass ("material-icons")
93
+ .attr ("title", _("Copy selected keyframes."))
94
+ .text ("content_copy")
95
+ .appendTo (this .toolbar)
96
+ .on ("click", () => this .copyKeyframes ());
97
+
98
+ this .pasteFrameIcon = $("<span></span>")
99
+ .addClass ("material-icons")
100
+ .attr ("title", _("Paste keyframes at current frame."))
101
+ .text ("content_paste")
102
+ .appendTo (this .toolbar)
103
+ .on ("click", () => this .pasteKeyframes ());
104
+
105
+ $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
106
+
107
+ this .firstFrameIcon = $("<span></span>")
108
+ .addClass ("material-icons")
109
+ .attr ("title", _("Go to first frame."))
110
+ .text ("first_page")
111
+ .appendTo (this .toolbar)
112
+ .on ("click", () => this .firstFrame ());
113
+
114
+ this .toggleAnimationIcon = $("<span></span>")
115
+ .addClass ("material-icons")
116
+ .attr ("title", _("Start animation."))
117
+ .text ("play_arrow")
118
+ .appendTo (this .toolbar)
119
+ .on ("click", () => this .toggleAnimation ());
120
+
121
+ this .lastFrameIcon = $("<span></span>")
122
+ .addClass ("material-icons")
123
+ .attr ("title", _("Go to last frame."))
124
+ .text ("last_page")
125
+ .appendTo (this .toolbar)
126
+ .on ("click", () => this .lastFrame ());
127
+
128
+ this .loopIcon = $("<span></span>")
129
+ .addClass ("material-icons")
130
+ .attr ("title", _("Loop animation."))
131
+ .text ("loop")
132
+ .appendTo (this .toolbar)
133
+ .on ("click", () => this .toggleLoop ());
134
+
135
+ this .frameInput = $("<input></input>")
136
+ .addClass ("input")
137
+ .attr ("type", "number")
138
+ .attr ("step", 1)
139
+ .attr ("min", 0)
140
+ .attr ("max", 0)
141
+ .attr ("title", _("Current frame."))
142
+ .css ("width", "55px")
143
+ .appendTo (this .toolbar)
144
+ .on ("change input", () => this .setCurrentFrame (this .getCurrentFrame ()));
145
+
146
+ this .propertiesIcon = $("<span></span>")
147
+ .addClass ("material-icons")
148
+ .attr ("title", _("Edit animation properties."))
149
+ .text ("access_time")
150
+ .appendTo (this .toolbar)
151
+ .on ("click", () => this .showProperties ());
152
+
153
+ $("<span></span>") .addClass ("separator") .appendTo (this .toolbar);
154
+
155
+ this .keyTypeElement = $("<select></select>")
156
+ .addClass ("select")
157
+ .attr ("title", _("Select keyframe type."))
158
+ .append ($("<option></option>") .text ("CONSTANT"))
159
+ .append ($("<option></option>") .text ("LINEAR") .attr ("selected", ""))
160
+ .append ($("<option></option>") .text ("SPLINE"))
161
+ .append ($("<option></option>") .text ("SPLIT"))
162
+ .append ($("<option></option>") .text ("MIXED") .hide ())
163
+ .appendTo (this .toolbar)
164
+ .on ("change", () => this .setKeyType ());
165
+
166
+ this .timeElement = $("<span></span>")
167
+ .addClass (["text", "right"])
168
+ .attr ("title", _("Current frame time (hours:minutes:seconds:frames)."))
169
+ .css ("top", "7px")
170
+ .css ("margin-right", "6px")
171
+ .text (this .formatFrames (0, 10))
172
+ .appendTo (this .toolbar);
173
+
174
+ // Navigation toolbar
175
+
176
+ this .navigation = $("<div></div>")
177
+ .attr ("id", "animation-editor-navigation")
178
+ .addClass (["animation-editor-navigation", "toolbar", "vertical-toolbar"])
179
+ .appendTo (this .animationEditor);
180
+
181
+ this .zoomOutIcon = $("<span></span>")
182
+ .addClass ("material-icons")
183
+ .attr ("title", _("Zoom timeline out."))
184
+ .css ("transform", "scale(1.4)")
185
+ .css ("margin-bottom", "15px")
186
+ .text ("zoom_out")
187
+ .appendTo (this .navigation)
188
+ .on ("click", () => this .zoomOut ());
189
+
190
+ this .zoomInIcon = $("<span></span>")
191
+ .addClass ("material-icons")
192
+ .attr ("title", _("Zoom timeline in."))
193
+ .css ("transform", "scale(1.4)")
194
+ .css ("margin-bottom", "15px")
195
+ .text ("zoom_in")
196
+ .appendTo (this .navigation)
197
+ .on ("click", () => this .zoomIn ());
198
+
199
+ this .zoomFitIcon = $("<span></span>")
200
+ .addClass ("material-icons")
201
+ .attr ("title", _("Zoom timeline to fit in window."))
202
+ .css ("transform", "scale(1.4)")
203
+ .css ("margin-bottom", "15px")
204
+ .text ("fit_screen")
205
+ .appendTo (this .navigation)
206
+ .on ("click", () => this .zoomFit ());
207
+
208
+ this .zoom100Icon = $("<span></span>")
209
+ .addClass ("material-icons")
210
+ .attr ("title", _("Default timeline zoom."))
211
+ .css ("transform", "scale(1.4)")
212
+ .css ("margin-bottom", "15px")
213
+ .text ("1x_mobiledata")
214
+ .appendTo (this .navigation)
215
+ .on ("click", () => this .zoom100 ());
216
+
217
+ // Animations List
218
+
219
+ this .nodeListElement = $("<div></div>")
220
+ .addClass (["alternating", "node-list"])
221
+ .appendTo (this .verticalSplitterLeft);
222
+
223
+ this .membersListElement = $("<div></div>")
224
+ .addClass ("node-list")
225
+ .appendTo (this .verticalSplitterLeft)
226
+ .on ("scroll mousemove", () => this .drawTimeline ());
227
+
228
+ this .animationName = $("<input></input>")
229
+ .addClass ("node-name")
230
+ .attr ("title", _("Rename animation."))
231
+ .attr ("placeholder", _("Enter animation name."))
232
+ .appendTo (this .verticalSplitterLeft)
233
+ .validate (Editor .Id, () =>
234
+ {
235
+ electron .shell .beep ();
236
+ this .highlight ();
237
+ })
238
+ .on ("keydown", event => this .renameAnimation (event));
239
+
240
+ // Tracks
241
+
242
+ this .tracks = $("<canvas></canvas>")
243
+ .addClass ("tracks")
244
+ .prependTo (this .animationEditor);
245
+
246
+ this .tracksResizer = new ResizeObserver (() => this .resizeTimeline ());
247
+ this .tracksResizer .observe (this .timelineElement [0]);
248
+
249
+ // Lists
250
+
251
+ this .memberList = new MemberList (this, this .membersListElement);
252
+
253
+ this .nodeList = new NodeList (this .nodeListElement,
254
+ {
255
+ filter: node => this .isAnimation (node),
256
+ callback: animation => this .setAnimation (animation),
257
+ });
258
+
259
+ // Selection
260
+
261
+ const selection = require ("../Application/Selection");
262
+
263
+ selection .addInterest (this, () => this .setSelection (selection));
264
+
265
+ this .setSelection (selection);
266
+
267
+ // Setup
268
+
269
+ this .setup ();
270
+ }
271
+
272
+ configure ()
273
+ {
274
+ this .config .file .setDefaultValues ({
275
+ scaleKeyframes: true,
276
+ keyType: "LINEAR",
277
+ });
278
+
279
+ this .keyTypeElement .val (this .config .file .keyType);
280
+ }
281
+
282
+ colorScheme (shouldUseDarkColors)
283
+ {
284
+ this .requestDrawTimeline ();
285
+ }
286
+
287
+ isAnimation (node)
288
+ {
289
+ if (!node .getType () .includes (X3D .X3DConstants .Group))
290
+ return false;
291
+
292
+ if (!node .hasMetaData ("Animation/duration"))
293
+ return false;
294
+
295
+ if (!node ._children .find (node => node .getValue () .getType () .includes (X3D .X3DConstants .TimeSensor)))
296
+ return false;
297
+
298
+ return true;
299
+ }
300
+
301
+ setAnimation (animation)
302
+ {
303
+ // Remove
304
+
305
+ this .setPickedKeyframes ([ ]);
306
+ this .setSelectedKeyframes ([ ]);
307
+ this .setSelectionRange (0, 0);
308
+
309
+ this .animation ?._children .removeInterest ("updateMemberList", this);
310
+ this .animation ?.name_changed .removeInterest ("updateAnimationName", this);
311
+
312
+ if (this .timeSensor)
313
+ {
314
+ this .timeSensor ._loop .removeInterest ("set_loop", this);
315
+ this .timeSensor ._isActive .removeInterest ("set_active", this);
316
+ this .timeSensor ._fraction_changed .removeInterest ("set_fraction", this);
317
+
318
+ this .timeSensor ._evenLive = false;
319
+ this .timeSensor ._range = [0, 0, 1];
320
+
321
+ if (this .timeSensor ._loop .getValue () && this .timeSensor ._isActive .getValue ())
322
+ {
323
+ this .timeSensor ._stopTime = 0;
324
+ this .timeSensor ._startTime = 0;
325
+ }
326
+ else
327
+ {
328
+ this .timeSensor ._startTime = 0;
329
+ this .timeSensor ._stopTime = 1;
330
+ }
331
+
332
+ for (const interpolator of this .interpolators)
333
+ interpolator ._set_fraction = 0;
334
+ }
335
+
336
+ // Set
337
+
338
+ this .animation = animation;
339
+
340
+ // Add
341
+
342
+ this .enableIcons (this .animation);
343
+
344
+ if (this .animation)
345
+ {
346
+ // TimeSensor
347
+
348
+ this .timeSensor = this .animation ._children
349
+ .find (node => node .getValue () .getType () .includes (X3D .X3DConstants .TimeSensor)) .getValue ();
350
+
351
+ this .timeSensor ._loop .addInterest ("set_loop", this);
352
+ this .timeSensor ._isActive .addInterest ("set_active", this);
353
+ this .timeSensor ._fraction_changed .addInterest ("set_fraction", this);
354
+
355
+ this .timeSensor ._evenLive = true;
356
+ this .timeSensor ._range = [0, 0, 1];
357
+
358
+ this .set_loop (this .timeSensor ._loop);
359
+ this .set_active (this .timeSensor ._isActive);
360
+
361
+ this .updateRange ();
362
+
363
+ // Show Member List
364
+
365
+ this .animation ._children .addInterest ("updateMemberList", this);
366
+
367
+ this .nodeListElement .hide ();
368
+ this .membersListElement .show ();
369
+ this .memberList .setAnimation (this .animation, this .timeSensor);
370
+
371
+ this .updateMemberList ();
372
+
373
+ // Animation Name
374
+
375
+ this .animationName .removeAttr ("disabled");
376
+
377
+ this .animation .name_changed .addInterest ("updateAnimationName", this);
378
+
379
+ this .updateAnimationName ();
380
+
381
+ // Timeline
382
+
383
+ this .frameInput .attr ("max", this .getDuration ());
384
+ }
385
+ else
386
+ {
387
+ // Show Animations List
388
+
389
+ this .updateMemberList ();
390
+
391
+ this .membersListElement .hide ();
392
+ this .nodeListElement .show ();
393
+
394
+ // Animation Name
395
+
396
+ this .animationName .val ("");
397
+ this .animationName .attr ("disabled", "");
398
+
399
+ // Timeline
400
+
401
+ this .frameInput .attr ("max", 0);
402
+ }
403
+
404
+ // Timeline
405
+
406
+ this .setSelection (require ("../Application/Selection"));
407
+ this .zoomFit ();
408
+ this .setCurrentFrame (0);
409
+ this .requestDrawTimeline ();
410
+ }
411
+
412
+ enableIcons (enabled)
413
+ {
414
+ $([
415
+ this .addMembersIcon,
416
+ this .cutFrameIcon,
417
+ this .copyFrameIcon,
418
+ this .pasteFrameIcon,
419
+ this .firstFrameIcon,
420
+ this .toggleAnimationIcon,
421
+ this .lastFrameIcon,
422
+ this .loopIcon,
423
+ this .frameInput,
424
+ this .propertiesIcon,
425
+ this .keyTypeElement,
426
+ this .timeElement,
427
+ ]
428
+ .flatMap (object => [... object]))
429
+ .removeClass (enabled ? "disabled" : [ ])
430
+ .addClass (enabled ? [ ] : "disabled");
431
+ }
432
+
433
+ setSelection (selection)
434
+ {
435
+ if (this .isGroupingNodeLike (selection .nodes .at (-1)))
436
+ this .createAnimationIcon .removeClass ("disabled");
437
+ else
438
+ this .createAnimationIcon .addClass ("disabled");
439
+
440
+ if (!this .animation)
441
+ return;
442
+
443
+ if (selection .nodes .at (-1))
444
+ this .addMembersIcon .removeClass ("disabled");
445
+ else
446
+ this .addMembersIcon .addClass ("disabled");
447
+ }
448
+
449
+ #groupingNodes = new Set ([
450
+ X3D .X3DConstants .X3DLayerNode,
451
+ X3D .X3DConstants .X3DGroupingNode,
452
+ X3D .X3DConstants .ViewpointGroup,
453
+ ]);
454
+
455
+ isGroupingNodeLike (node)
456
+ {
457
+ if (!node)
458
+ return true; // X3DScene
459
+
460
+ if (node .getType () .some (type => this .#groupingNodes .has (type)))
461
+ return true;
462
+
463
+ return false;
464
+ }
465
+
466
+ createAnimation ()
467
+ {
468
+ Editor .undoManager .beginUndo (_("Add Animation"));
469
+
470
+ const
471
+ selection = require ("../Application/Selection"),
472
+ group = selection .nodes .at (-1),
473
+ executionContext = group ?.getExecutionContext () ?? this .browser .currentScene,
474
+ node = group ?? executionContext,
475
+ field = group ?._children ?? executionContext ._rootNodes;
476
+
477
+ Editor .addComponent (executionContext .getLocalScene (), "Grouping");
478
+ Editor .addComponent (executionContext .getLocalScene (), "Time");
479
+
480
+ const
481
+ animation = executionContext .createNode ("Group", false),
482
+ timeSensor = executionContext .createNode ("TimeSensor", false);
483
+
484
+ animation ._children .push (timeSensor);
485
+ timeSensor ._description = "New Animation";
486
+
487
+ timeSensor .setup ();
488
+ animation .setup ();
489
+
490
+ executionContext .addNamedNode (executionContext .getUniqueName ("NewAnimation"), animation);
491
+ executionContext .addNamedNode (executionContext .getUniqueName ("NewAnimationTimer"), timeSensor);
492
+
493
+ animation .setMetaData ("Animation/duration", new X3D .SFInt32 (10));
494
+ animation .setMetaData ("Animation/frameRate", new X3D .SFInt32 (10));
495
+
496
+ Editor .insertValueIntoArray (executionContext, node, field, 0, animation);
497
+
498
+ Editor .undoManager .endUndo ();
499
+
500
+ // Wait until NodeList knows animation, to have it restored after reload.
501
+ setTimeout (() => this .nodeList .setNode (animation));
502
+ }
503
+
504
+ resizeAnimation (newDuration, newFrameRate, scaleKeyframes)
505
+ {
506
+ this .config .file .scaleKeyframes = scaleKeyframes;
11
507
 
12
- this .animationEditor = element
508
+ const
509
+ duration = this .getDuration (),
510
+ frameRate = this .getFrameRate ();
511
+
512
+ if (newDuration === duration && newFrameRate === frameRate)
513
+ return;
514
+
515
+ if (newDuration < 1)
516
+ return;
517
+
518
+ Editor .undoManager .beginUndo (_("Resize Animation"));
519
+
520
+ const
521
+ timeSensor = this .timeSensor,
522
+ executionContext = timeSensor .getExecutionContext ()
523
+
524
+ Editor .setFieldValue (executionContext, timeSensor, timeSensor ._cycleInterval, newDuration / newFrameRate);
525
+
526
+ Editor .setNodeMetaData (this .animation, "Animation/duration", new X3D .SFInt32 (newDuration));
527
+ Editor .setNodeMetaData (this .animation, "Animation/frameRate", new X3D .SFInt32 (newFrameRate));
528
+
529
+ if (scaleKeyframes)
530
+ {
531
+ const scale = newDuration / duration;
532
+
533
+ for (const interpolator of this .interpolators)
534
+ {
535
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ())
536
+ .map (value => value * scale);
537
+
538
+ Editor .setNodeMetaData (interpolator, "Interpolator/key", key);
539
+ }
540
+
541
+ this .setCurrentFrame (Math .floor (this .getCurrentFrame () * scale));
542
+ }
543
+ else
544
+ {
545
+ // Remove keyframes greater than duration.
546
+
547
+ for (const interpolator of this .interpolators)
548
+ {
549
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
550
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
551
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
552
+
553
+ const index = X3D .Algorithm .upperBound (key, 0, key .length, newDuration);
554
+
555
+ Editor .setNodeMetaData (interpolator, "Interpolator/key", key .slice (0, index));
556
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", keyValue .slice (0, index));
557
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType .slice (0, index));
558
+ }
559
+
560
+ this .setCurrentFrame (Math .min (this .getCurrentFrame (), newDuration));
561
+ }
562
+
563
+ this .updateInterpolators ()
564
+ this .registerZoomFit ();
565
+
566
+ Editor .undoManager .endUndo ();
567
+
568
+ this .frameInput .attr ("max", newDuration);
569
+ }
570
+
571
+ closeAnimation ()
572
+ {
573
+ this .nodeList .setNode (null);
574
+ }
575
+
576
+ updateAnimationName ()
577
+ {
578
+ const name = this .animation .getDisplayName ();
579
+
580
+ this .animationName .val (name);
581
+ this .memberList .setAnimationName (name);
582
+ }
583
+
584
+ renameAnimation (event)
585
+ {
586
+ if (event .key !== "Enter")
587
+ return;
588
+
589
+ const getDescription = (animation) =>
590
+ {
591
+ return animation .getDisplayName ()
592
+ .replace (/(\d+)/g, " $1")
593
+ .replace (/([A-Z]+[a-z\d ]+)/g, " $1")
594
+ .replace (/([A-Z][a-z]+)/g, " $1")
595
+ .replace (/\s+/g, " ")
596
+ .trim ();
597
+ }
598
+
599
+ Editor .undoManager .beginUndo (_("Rename Animation"));
600
+
601
+ const { animation, timeSensor } = this;
602
+ const executionContext = animation .getExecutionContext ();
603
+ const name = this .animationName .val ();
604
+ const oldDescription = getDescription (animation);
605
+
606
+ Editor .updateNamedNode (executionContext, executionContext .getUniqueName (`${name}`), animation);
607
+ Editor .updateNamedNode (executionContext, executionContext .getUniqueName (`${name}Timer`), timeSensor);
608
+
609
+ // Don't update description if manually set.
610
+ if (!timeSensor ._description .getValue () || timeSensor ._description .getValue () === oldDescription)
611
+ Editor .setFieldValue (executionContext, timeSensor, timeSensor ._description, getDescription (animation));
612
+
613
+ for (const interpolator of this .interpolators)
614
+ {
615
+ const name = this .getInterpolatorName (interpolator);
616
+
617
+ if (!name)
618
+ continue;
619
+
620
+ Editor .updateNamedNode (executionContext, executionContext .getUniqueName (name), interpolator);
621
+ }
622
+
623
+ Editor .undoManager .endUndo ();
624
+ }
625
+
626
+ // Members Handling
627
+
628
+ addMembers ()
629
+ {
630
+ const selection = require ("../Application/Selection");
631
+
632
+ this .memberList .addNodes (selection .nodes);
633
+
634
+ this .requestDrawTimeline ();
635
+ }
636
+
637
+ removeMembers (nodes)
638
+ {
639
+ const
640
+ animation = this .animation,
641
+ executionContext = animation .getExecutionContext ();
642
+
643
+ Editor .undoManager .beginUndo (_("Remove Member from »%s«"), animation .getDisplayName ());
644
+
645
+ for (const node of nodes)
646
+ {
647
+ const
648
+ interpolators = Array .from (node .getFields (), field => this .fields .get (field)),
649
+ children = animation ._children .filter (node => !interpolators .includes (node .getValue ()));
650
+
651
+ Editor .setFieldValue (executionContext, animation, animation ._children, children);
652
+ }
653
+
654
+ this .registerRequestDrawTimeline ();
655
+
656
+ Editor .undoManager .endUndo ();
657
+
658
+ // Update member list.
659
+
660
+ this .updateMembers ();
661
+ this .memberList .removeNodes (nodes);
662
+
663
+ // Prevent losing members without interpolator.
664
+
665
+ this .#changing = true;
666
+
667
+ this .browser .nextFrame () .then (() => this .#changing = false);
668
+ }
669
+
670
+ #interpolatorTypes = new Set ([
671
+ X3D .X3DConstants .BooleanSequencer,
672
+ X3D .X3DConstants .IntegerSequencer,
673
+ X3D .X3DConstants .ColorInterpolator,
674
+ X3D .X3DConstants .ScalarInterpolator,
675
+ X3D .X3DConstants .OrientationInterpolator,
676
+ X3D .X3DConstants .PositionInterpolator2D,
677
+ X3D .X3DConstants .PositionInterpolator,
678
+ X3D .X3DConstants .CoordinateInterpolator2D,
679
+ X3D .X3DConstants .CoordinateInterpolator,
680
+ X3D .X3DConstants .NormalInterpolator,
681
+ ]);
682
+
683
+ members = new Set ();
684
+ fields = new Map (); // [field, interpolator]
685
+ interpolators = new Set ();
686
+
687
+ updateMembers ()
688
+ {
689
+ for (const interpolator of this .interpolators)
690
+ interpolator ._value_changed .removeRouteCallback (this);
691
+
692
+ this .members .clear ();
693
+ this .fields .clear ();
694
+ this .interpolators .clear ();
695
+
696
+ for (const node of this .animation ?._children ?? [ ])
697
+ {
698
+ const interpolator = node .getValue ();
699
+
700
+ if (!interpolator .getType () .some (type => this .#interpolatorTypes .has (type)))
701
+ continue;
702
+
703
+ this .interpolators .add (interpolator);
704
+
705
+ for (const route of interpolator ._value_changed .getOutputRoutes ())
706
+ {
707
+ const
708
+ node = route .getDestinationNode (),
709
+ field = node .getField (route .getDestinationField ());
710
+
711
+ this .members .add (node);
712
+ this .fields .set (field, interpolator);
713
+ }
714
+ }
715
+
716
+ for (const interpolator of this .interpolators)
717
+ interpolator ._value_changed .addRouteCallback (this, () => this .updateMemberList ());
718
+ }
719
+
720
+ updateMemberList ()
721
+ {
722
+ if (this .#changing)
723
+ return;
724
+
725
+ this .updateMembers ();
726
+
727
+ this .memberList .saveScrollbars ();
728
+ this .memberList .clearNodes ();
729
+ this .memberList .addNodes (Array .from (this .members));
730
+ this .memberList .restoreScrollbars ();
731
+
732
+ this .requestDrawTimeline ();
733
+ }
734
+
735
+ // Interpolators
736
+
737
+ #interpolatorTypeNames = new Map ([
738
+ [X3D .X3DConstants .SFBool, "BooleanSequencer"],
739
+ [X3D .X3DConstants .SFInt32, "IntegerSequencer"],
740
+ [X3D .X3DConstants .SFColor, "ColorInterpolator"],
741
+ [X3D .X3DConstants .SFFloat, "ScalarInterpolator"],
742
+ [X3D .X3DConstants .SFRotation, "OrientationInterpolator"],
743
+ [X3D .X3DConstants .SFVec2f, "PositionInterpolator2D"],
744
+ [X3D .X3DConstants .SFVec3f, "PositionInterpolator"],
745
+ [X3D .X3DConstants .MFVec2f, "CoordinateInterpolator2D"],
746
+ [X3D .X3DConstants .MFVec3f, "CoordinateInterpolator"],
747
+ // NormalInterpolator
748
+ ]);
749
+
750
+ #components = new Map ([
751
+ [X3D .X3DConstants .BooleanSequencer, 1],
752
+ [X3D .X3DConstants .IntegerSequencer, 1],
753
+ [X3D .X3DConstants .ColorInterpolator, 3],
754
+ [X3D .X3DConstants .ScalarInterpolator, 1],
755
+ [X3D .X3DConstants .OrientationInterpolator, 4],
756
+ [X3D .X3DConstants .PositionInterpolator2D, 2],
757
+ [X3D .X3DConstants .PositionInterpolator, 3],
758
+ [X3D .X3DConstants .CoordinateInterpolator2D, 2],
759
+ [X3D .X3DConstants .CoordinateInterpolator, 3],
760
+ [X3D .X3DConstants .NormalInterpolator, 3],
761
+ ]);
762
+
763
+ getKeyType ()
764
+ {
765
+ return this .keyTypeElement .val ();
766
+ }
767
+
768
+ setKeyType ()
769
+ {
770
+ const value = this .getKeyType ();
771
+
772
+ this .config .file .keyType = value;
773
+
774
+ // Update interpolators.
775
+
776
+ const keyframes = this .getSelectedKeyframes ();
777
+
778
+ if (keyframes .length)
779
+ {
780
+ Editor .undoManager .beginUndo (_("Change Key Type of Selected Keyframes"));
781
+
782
+ for (const { field, interpolator, index } of keyframes)
783
+ {
784
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
785
+
786
+ keyType [index] = this .restrictKeyType (field, interpolator, value);
787
+
788
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType);
789
+ }
790
+
791
+ for (const interpolator of new Set (keyframes .map (({ interpolator }) => interpolator)))
792
+ this .updateInterpolator (interpolator);
793
+
794
+ Editor .undoManager .endUndo ();
795
+ }
796
+ }
797
+
798
+ updateKeyType ()
799
+ {
800
+ if (this .getSelectedKeyframes () .length)
801
+ {
802
+ const keyTypes = {
803
+ CONSTANT: 0,
804
+ LINEAR: 0,
805
+ SPLINE: 0,
806
+ SPLIT: 0,
807
+ };
808
+
809
+ for (const { interpolator, index } of this .getSelectedKeyframes ())
810
+ {
811
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
812
+
813
+ ++ keyTypes [keyType [index]];
814
+ }
815
+
816
+ const keyType = Object .entries (keyTypes)
817
+ .find (([key, value]) => value === this .getSelectedKeyframes () .length);
818
+
819
+ this .keyTypeElement .val (keyType ?.[0] ?? "MIXED");
820
+ }
821
+ else
822
+ {
823
+ this .keyTypeElement .val (this .config .file .keyType);
824
+ }
825
+ }
826
+
827
+ restrictKeyType (field, interpolator, keyType)
828
+ {
829
+ switch (field .getType ())
830
+ {
831
+ case X3D .X3DConstants .SFBool:
832
+ case X3D .X3DConstants .SFInt32:
833
+ {
834
+ return "CONSTANT";
835
+ }
836
+ case X3D .X3DConstants .SFColor:
837
+ {
838
+ if (keyType .match (/^(?:SPLINE|SPLIT)$/))
839
+ return "LINEAR";
840
+
841
+ return keyType;
842
+ }
843
+ case X3D .X3DConstants .MFVec3f:
844
+ {
845
+ if (keyType .match (/^(?:SPLINE|SPLIT)$/))
846
+ {
847
+ if (interpolator instanceof X3D .NormalInterpolator)
848
+ return "LINEAR";
849
+ }
850
+
851
+ return keyType;
852
+ }
853
+ default:
854
+ {
855
+ return keyType;
856
+ }
857
+ }
858
+ }
859
+
860
+ addKeyframes (keyframes)
861
+ {
862
+ // Create interpolators.
863
+
864
+ const count = keyframes .reduce ((p, { field }) => p + !this .fields .has (field), 0);
865
+
866
+ if (count === 1)
867
+ Editor .undoManager .beginUndo (_("Add Interpolator to »%s«"), this .animation .getDisplayName ());
868
+ else
869
+ Editor .undoManager .beginUndo (_("Add Interpolators to »%s«"), this .animation .getDisplayName ());
870
+
871
+ for (const { node, field, typeName } of keyframes)
872
+ this .getInterpolator (node, field, typeName)
873
+
874
+ Editor .undoManager .endUndo ();
875
+
876
+ // Add keyframes.
877
+
878
+ if (keyframes .length === 1)
879
+ Editor .undoManager .beginUndo (_("Add Keyframe to »%s«"), this .animation .getDisplayName ());
880
+ else
881
+ Editor .undoManager .beginUndo (_("Add Keyframes to »%s«"), this .animation .getDisplayName ());
882
+
883
+ for (const { node, field, typeName } of keyframes)
884
+ this .addKeyframe (node, field, typeName);
885
+
886
+ Editor .undoManager .endUndo ();
887
+ }
888
+
889
+ addKeyframe (node, field, typeName)
890
+ {
891
+ Editor .undoManager .beginUndo (_("Add Keyframe to »%s«"), this .animation .getDisplayName ());
892
+
893
+ const
894
+ interpolator = this .getInterpolator (node, field, typeName),
895
+ frame = this .getCurrentFrame (),
896
+ type = this .restrictKeyType (field, interpolator, this .getKeyType ());
897
+
898
+ switch (field .getType ())
899
+ {
900
+ case X3D .X3DConstants .SFBool:
901
+ case X3D .X3DConstants .SFInt32:
902
+ case X3D .X3DConstants .SFColor:
903
+ case X3D .X3DConstants .SFFloat:
904
+ case X3D .X3DConstants .SFRotation:
905
+ case X3D .X3DConstants .SFVec2f:
906
+ case X3D .X3DConstants .SFVec3f:
907
+ {
908
+ this .addKeyframeToInterpolator (interpolator, frame, type, field);
909
+ break;
910
+ }
911
+ case X3D .X3DConstants .MFVec2f:
912
+ case X3D .X3DConstants .MFVec3f:
913
+ {
914
+ if (field .length === 0)
915
+ break;
916
+
917
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ());
918
+
919
+ if (keySize .getValue () !== 0 && keySize .getValue () !== field .length)
920
+ {
921
+ this .showArraySizeErrorDialog (keySize .getValue ());
922
+ break;
923
+ }
924
+
925
+ keySize .setValue (field .length);
926
+
927
+ Editor .setNodeMetaData (interpolator, "Interpolator/keySize", keySize);
928
+
929
+ const value = Array .from (field) .flatMap (value => Array .from (value));
930
+
931
+ this .addKeyframeToInterpolator (interpolator, frame, type, value);
932
+ break;
933
+ }
934
+ }
935
+
936
+ this .updateInterpolator (interpolator);
937
+
938
+ Editor .undoManager .endUndo ();
939
+ }
13
940
 
14
- this .animationEditor .text ("Animation Editor")
941
+ #changing = false;
942
+
943
+ getInterpolator (node, field, typeName)
944
+ {
945
+ if (this .fields .has (field))
946
+ return this .fields .get (field);
947
+
948
+ typeName ??= this .#interpolatorTypeNames .get (field .getType ());
949
+
950
+ Editor .undoManager .beginUndo (_("Add Interpolator"));
951
+
952
+ const executionContext = this .animation .getExecutionContext ();
953
+
954
+ if (typeName .includes ("Sequencer"))
955
+ Editor .addComponent (executionContext .getLocalScene (), "EventUtilities");
956
+ else if (typeName .includes ("Interpolator"))
957
+ Editor .addComponent (executionContext .getLocalScene (), "Interpolation");
958
+
959
+ const interpolator = executionContext .createNode (typeName, false);
960
+
961
+ interpolator .setup ();
962
+
963
+ this .fields .set (field, interpolator);
964
+ this .interpolators .add (interpolator);
965
+
966
+ Editor .appendValueToArray (executionContext, this .animation, this .animation ._children, interpolator);
967
+ Editor .addRoute (executionContext, this .timeSensor, "fraction_changed", interpolator, "set_fraction");
968
+ Editor .addRoute (executionContext, interpolator, "value_changed", node, field .getName ());
969
+
970
+ const name = this .getInterpolatorName (interpolator);
971
+
972
+ Editor .updateNamedNode (executionContext, executionContext .getUniqueName (name), interpolator);
973
+
974
+ Editor .undoManager .endUndo ();
975
+
976
+ // Prevent losing members without interpolator.
977
+
978
+ this .#changing = true;
979
+
980
+ this .browser .nextFrame () .then (() => this .#changing = false);
981
+
982
+ return interpolator;
983
+ }
984
+
985
+ getInterpolatorName (interpolator)
986
+ {
987
+ const route = Array .from (interpolator ._value_changed .getOutputRoutes ()) [0];
988
+
989
+ if (!route)
990
+ return;
991
+
992
+ const
993
+ destinationNode = route .getDestinationNode (),
994
+ destinationField = route .getDestinationField (),
995
+ nodeName = destinationNode .getDisplayName () || destinationNode .getTypeName (),
996
+ fieldName = capitalize (destinationField .replace (/^set_|_changed$/g, ""), true),
997
+ typeName = interpolator .getTypeName () .match (/(Sequencer|Interpolator)$/) [1];
998
+
999
+ return `${nodeName}${fieldName}${typeName}`;
1000
+ }
1001
+
1002
+ removeInterpolator (node, field)
1003
+ {
1004
+ const
1005
+ animation = this .animation,
1006
+ executionContext = animation .getExecutionContext (),
1007
+ interpolator = this .fields .get (field),
1008
+ children = animation ._children .filter (node => node .getValue () !== interpolator);
1009
+
1010
+ Editor .undoManager .beginUndo (_("Remove Interpolator from »%s«"), animation .getDisplayName ());
1011
+
1012
+ Editor .setFieldValue (executionContext, animation, animation ._children, children);
1013
+
1014
+ this .registerRequestDrawTimeline ();
1015
+
1016
+ Editor .undoManager .endUndo ();
1017
+ }
1018
+
1019
+ updateInterpolators ()
1020
+ {
1021
+ Editor .undoManager .beginUndo (_("Update Interpolators"));
1022
+
1023
+ for (const interpolator of this .interpolators)
1024
+ this .updateInterpolator (interpolator)
1025
+
1026
+ Editor .undoManager .endUndo ();
1027
+ }
1028
+
1029
+ updateInterpolator (interpolator)
1030
+ {
1031
+ Editor .undoManager .beginUndo (_("Update Interpolator"));
1032
+
1033
+ switch (interpolator .getType () .at (-1))
1034
+ {
1035
+ case X3D .X3DConstants .BooleanSequencer:
1036
+ case X3D .X3DConstants .IntegerSequencer:
1037
+ {
1038
+ this .updateSequencer (interpolator);
1039
+ break;
1040
+ }
1041
+ case X3D .X3DConstants .ColorInterpolator:
1042
+ case X3D .X3DConstants .ScalarInterpolator:
1043
+ case X3D .X3DConstants .OrientationInterpolator:
1044
+ case X3D .X3DConstants .PositionInterpolator2D:
1045
+ case X3D .X3DConstants .PositionInterpolator:
1046
+ {
1047
+ this .updateScalarInterpolator (interpolator);
1048
+ break;
1049
+ }
1050
+ case X3D .X3DConstants .CoordinateInterpolator2D:
1051
+ case X3D .X3DConstants .CoordinateInterpolator:
1052
+ case X3D .X3DConstants .NormalInterpolator:
1053
+ {
1054
+ this .updateArrayInterpolator (interpolator);
1055
+ break;
1056
+ }
1057
+ }
1058
+
1059
+ interpolator ._set_fraction .addEvent ();
1060
+
1061
+ Editor .undoManager .endUndo ();
1062
+ }
1063
+
1064
+ updateSequencer (interpolator)
1065
+ {
1066
+ this .resizeInterpolator (interpolator);
1067
+
1068
+ const components = this .#components .get (interpolator .getType () .at (-1));
1069
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1070
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1071
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1072
+
1073
+ keyValue .length = key .length * components;
1074
+ keyType .length = key .length;
1075
+
1076
+ const size = key .length;
1077
+ const duration = this .getDuration ();
1078
+ const keys = [ ];
1079
+ const keyValues = [ ];
1080
+
1081
+ let i = 0; // index in key
1082
+ let iN = 0; // index in meta data keyValue
1083
+
1084
+ while (i < size)
1085
+ {
1086
+ if (key [i] < 0 || key [i] > duration)
1087
+ continue;
1088
+
1089
+ const fraction = key [i] / duration;
1090
+ const value = keyValue [iN];
1091
+
1092
+ keys .push (fraction);
1093
+ keyValues .push (value);
1094
+
1095
+ ++ i;
1096
+ iN += components;
1097
+ }
1098
+
1099
+ const executionContext = interpolator .getExecutionContext ();
1100
+
1101
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys);
1102
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues);
1103
+
1104
+ this .registerRequestDrawTimeline ();
1105
+ }
1106
+
1107
+ #vectors = new Map ([
1108
+ [2, X3D .Vector2],
1109
+ [3, X3D .Vector3],
1110
+ [4, X3D .Vector4],
1111
+ ]);
1112
+
1113
+ updateScalarInterpolator (interpolator)
1114
+ {
1115
+ this .resizeInterpolator (interpolator);
1116
+
1117
+ const components = this .#components .get (interpolator .getType () .at (-1));
1118
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1119
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1120
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1121
+
1122
+ keyValue .length = key .length * components;
1123
+ keyType .length = key .length;
1124
+
1125
+ const size = key .length;
1126
+ const duration = this .getDuration ();
1127
+ const keys = [ ];
1128
+ const keyValues = [ ];
1129
+
1130
+ let i = 0; // index in key
1131
+ let iN = 0; // index in meta data keyValue
1132
+
1133
+ while (i < size)
1134
+ {
1135
+ if (key [i] < 0 || key [i] > duration)
1136
+ {
1137
+ ++ i;
1138
+ continue;
1139
+ }
1140
+
1141
+ const value = this .getValue (keyValue, iN, components);
1142
+ const fraction = key [i] / duration;
1143
+
1144
+ let iT = i;
1145
+
1146
+ if (keyType [iT] === "SPLIT" && iT + 1 < size)
1147
+ ++ iT;
1148
+
1149
+ switch (keyType [iT])
1150
+ {
1151
+ case "CONSTANT":
1152
+ {
1153
+ keys .push (fraction);
1154
+ keyValues .push (... value);
1155
+
1156
+ if (key [i] < duration)
1157
+ {
1158
+ const nextFraction = i === size - 1 ? 1 : key [i + 1] / duration;
1159
+
1160
+ keys .push (nextFraction);
1161
+ keyValues .push (... value);
1162
+ }
1163
+
1164
+ break;
1165
+ }
1166
+ case "LINEAR":
1167
+ case "SPLIT":
1168
+ {
1169
+ keys .push (fraction);
1170
+ keyValues .push (... value);
1171
+ break;
1172
+ }
1173
+ case "SPLINE":
1174
+ {
1175
+ const currentKeys = new X3D .MFFloat ();
1176
+
1177
+ const currentKeyValues = interpolator instanceof X3D .OrientationInterpolator
1178
+ ? new X3D .MFRotation ()
1179
+ : components === 1 ? new X3D .MFFloat () : new X3D [`MFVec${components}f`] ();
1180
+
1181
+ const currentKeyVelocities = currentKeyValues .create ();
1182
+ const Vector = this .#vectors .get (components);
1183
+
1184
+ for (; i < size; ++ i, iN += components)
1185
+ {
1186
+ let value = this .getValue (keyValue, iN, components);
1187
+
1188
+ if (interpolator instanceof X3D .ColorInterpolator)
1189
+ value = new X3D .Color3 (... value) .getHSV ();
1190
+
1191
+ currentKeys .push (key [i]);
1192
+ currentKeyValues .push (components === 1 ? value [0] : new Vector (... value));
1193
+
1194
+ if (currentKeys .length === 1)
1195
+ continue;
1196
+
1197
+ if (keyType [i] !== "SPLINE")
1198
+ break;
1199
+ }
1200
+
1201
+ if (currentKeys .length < 2)
1202
+ {
1203
+ // This can happen if only the last frame is of type SPLINE.
1204
+
1205
+ keys .push (fraction);
1206
+ keyValues .push (... value);
1207
+ break;
1208
+ }
1209
+
1210
+ // currentKeyVelocities .length = currentKeys .length;
1211
+
1212
+ const closed = currentKeys .at (0) === 0
1213
+ && currentKeys .at (-1) === duration
1214
+ && (components === 1
1215
+ ? currentKeyValues .at (0) === currentKeyValues .at (-1)
1216
+ : currentKeyValues .at (0) .equals (currentKeyValues .at (-1)));
1217
+
1218
+ const normalizeVelocity = false;
1219
+
1220
+ const spline = interpolator instanceof X3D .OrientationInterpolator
1221
+ ? new X3D .SquadInterpolator ()
1222
+ : new X3D [`CatmullRomSplineInterpolator${components}`] ();
1223
+
1224
+ spline .generate (closed,
1225
+ currentKeys,
1226
+ currentKeyValues,
1227
+ currentKeyVelocities,
1228
+ normalizeVelocity);
1229
+
1230
+ const length = currentKeys .length - 1;
1231
+
1232
+ for (let k = 0; k < length; ++ k)
1233
+ {
1234
+ const frames = currentKeys [k + 1] - currentKeys [k];
1235
+ const fraction = currentKeys [k] / duration;
1236
+ const distance = frames / duration;
1237
+ const framesN = frames + (k + 1 === length && i === key .length);
1238
+
1239
+ for (let f = 0; f < framesN; ++ f)
1240
+ {
1241
+ const weight = f / frames;
1242
+
1243
+ let value = spline .interpolate (k, k + 1, weight, currentKeyValues);
1244
+
1245
+ if (interpolator instanceof X3D .ColorInterpolator)
1246
+ value = new X3D .Color3 () .setHSV (... value);
1247
+
1248
+ keys .push (fraction + weight * distance);
1249
+ keyValues .push (... (components === 1 ? [value] : value));
1250
+ }
1251
+ }
1252
+
1253
+ if (i + 1 !== size)
1254
+ {
1255
+ i -= 1;
1256
+ iN -= components;
1257
+ }
1258
+
1259
+ break;
1260
+ }
1261
+ }
1262
+
1263
+ i += 1;
1264
+ iN += components;
1265
+ }
1266
+
1267
+ const executionContext = interpolator .getExecutionContext ();
1268
+
1269
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys);
1270
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues);
1271
+
1272
+ this .registerRequestDrawTimeline ();
1273
+ }
1274
+
1275
+ updateArrayInterpolator (interpolator)
1276
+ {
1277
+ this .resizeInterpolator (interpolator);
1278
+
1279
+ const components = this .#components .get (interpolator .getType () .at (-1));
1280
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1281
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1282
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1283
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ());
1284
+
1285
+ keyValue .length = key .length * components * keySize;
1286
+ keyType .length = key .length;
1287
+
1288
+ const size = key .length;
1289
+ const duration = this .getDuration ();
1290
+ const keys = [ ];
1291
+ const keyValues = [ ];
1292
+
1293
+ let i = 0; // index in key
1294
+ let iN = 0; // index in meta data keyValue
1295
+
1296
+ while (i < size)
1297
+ {
1298
+ if (key [i] < 0 && key [i] > duration)
1299
+ {
1300
+ ++ i;
1301
+ continue;
1302
+ }
1303
+
1304
+ const fraction = key [i] / duration;
1305
+
1306
+ let iT = i;
1307
+
1308
+ if (keyType [iT] === "SPLIT" && iT + 1 < size)
1309
+ ++ iT;
1310
+
1311
+ switch (keyType [iT])
1312
+ {
1313
+ case "CONSTANT":
1314
+ {
1315
+ const length = components * keySize;
1316
+
1317
+ keys .push (fraction);
1318
+
1319
+ for (let a = 0; a < length; a += components)
1320
+ keyValues .push (... this .getValue (keyValue, iN + a, components));
1321
+
1322
+ if (key [i] < duration)
1323
+ {
1324
+ const nextFraction = (i === size - 1 ? 1 : key [i + 1] / duration);
1325
+
1326
+ keys .push (nextFraction);
1327
+
1328
+ for (let a = 0; a < length; a += components)
1329
+ keyValues .push (... this .getValue (keyValue, iN + a, components));
1330
+ }
1331
+
1332
+ break;
1333
+ }
1334
+ case "LINEAR":
1335
+ case "SPLIT":
1336
+ {
1337
+ const length = components * keySize;
1338
+
1339
+ keys .push (fraction);
1340
+
1341
+ for (let a = 0; a < length; a += components)
1342
+ keyValues .push (... this .getValue (keyValue, iN + a, components));
1343
+
1344
+ break;
1345
+ }
1346
+ case "SPLINE":
1347
+ {
1348
+ const first = keyValues .length;
1349
+
1350
+ // Generate key.
1351
+
1352
+ const currentKeys = interpolator ._key .create ();
1353
+
1354
+ for (; i < size; ++ i)
1355
+ {
1356
+ currentKeys .push (key [i]);
1357
+
1358
+ if (currentKeys .length === 1)
1359
+ continue;
1360
+
1361
+ if (keyType [i] !== "SPLINE")
1362
+ break;
1363
+ }
1364
+
1365
+ if (currentKeys .length < 2)
1366
+ {
1367
+ // This can happen if only the last frame is of type SPLINE.
1368
+
1369
+ const length = components * keySize;
1370
+
1371
+ keys .push (fraction);
1372
+
1373
+ for (let a = 0; a < length; a += components)
1374
+ keyValues .push (... this .getValue (keyValue, iN + a, components));
1375
+
1376
+ break;
1377
+ }
1378
+
1379
+ const length = currentKeys .length - 1;
1380
+
1381
+ for (let k = 0; k < length; ++ k)
1382
+ {
1383
+ const frames = currentKeys [k + 1] - currentKeys [k];
1384
+ const fraction = currentKeys [k] / duration;
1385
+ const distance = frames / duration;
1386
+ const framesN = k + 1 === length && i === key .length ? frames + 1 : frames;
1387
+
1388
+ for (let f = 0; f < framesN; ++ f)
1389
+ {
1390
+ const weight = f / frames;
1391
+
1392
+ keys .push (fraction + weight * distance);
1393
+ }
1394
+ }
1395
+
1396
+ // Generate keyValue.
1397
+
1398
+ for (let a = 0; a < keySize; ++ a)
1399
+ {
1400
+ const currentKeyValues = interpolator ._keyValue .create ();
1401
+ const currentKeyVelocities = interpolator ._keyValue .create ();
1402
+ const Vector = this .#vectors .get (components);
1403
+
1404
+ for (let i = 0, aiN = iN + a * components; i < currentKeys .length; ++ i, aiN += components * keySize)
1405
+ currentKeyValues .push (new Vector (... this .getValue (keyValue, aiN, components)));
1406
+
1407
+ // currentKeyVelocities .length = currentKeys .length;
1408
+
1409
+ const closed = currentKeys .at (0) === 0
1410
+ && currentKeys .at (-1) === duration
1411
+ && currentKeyValues .at (0) .equals (currentKeyValues .at (-1));
1412
+
1413
+ const normalizeVelocity = false;
1414
+
1415
+ const spline = new X3D [`CatmullRomSplineInterpolator${components}`] ();
1416
+
1417
+ spline .generate (closed,
1418
+ currentKeys,
1419
+ currentKeyValues,
1420
+ currentKeyVelocities,
1421
+ normalizeVelocity);
1422
+
1423
+ const length = currentKeys .length - 1;
1424
+
1425
+ let totalFrames = 0;
1426
+
1427
+ for (let k = 0; k < length; ++ k)
1428
+ {
1429
+ const frames = currentKeys [k + 1] - currentKeys [k];
1430
+ const framesN = frames + (k + 1 === length && i === key .length);
1431
+
1432
+ for (let f = 0; f < framesN; ++ f)
1433
+ {
1434
+ const weight = f / frames;
1435
+ const value = spline .interpolate (k, k + 1, weight, currentKeyValues);
1436
+ const index = first + (a + (totalFrames + f) * keySize) * components;
1437
+
1438
+ if (index >= keyValues .length)
1439
+ keyValues .length = index + 1;
1440
+
1441
+ keyValues .splice (index, components, ... value);
1442
+ }
1443
+
1444
+ totalFrames += frames;
1445
+ }
1446
+ }
1447
+
1448
+ if (i + 1 !== size)
1449
+ i -= 1;
1450
+
1451
+ iN += components * keySize * (currentKeys .length - 2);
1452
+ break;
1453
+ }
1454
+ }
1455
+
1456
+ i += 1;
1457
+ iN += components * keySize;
1458
+ }
1459
+
1460
+ const executionContext = interpolator .getExecutionContext ();
1461
+
1462
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._key, keys);
1463
+ Editor .setFieldValue (executionContext, interpolator, interpolator ._keyValue, keyValues);
1464
+
1465
+ this .registerRequestDrawTimeline ();
1466
+ }
1467
+
1468
+ getValue (keyValue, index, components)
1469
+ {
1470
+ const value = [ ];
1471
+
1472
+ for (let i = 0; i < components; ++ i)
1473
+ value .push (keyValue [index + i]);
1474
+
1475
+ return value;
1476
+ }
1477
+
1478
+ resizeInterpolator (interpolator)
1479
+ {
1480
+ const components = this .#components .get (interpolator .getType () .at (-1));
1481
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1482
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1483
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1484
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 (1));
1485
+ const size = X3D .Algorithm .upperBound (key, 0, key .length, this .getDuration ());
1486
+ const sizeN = size * components * keySize;
1487
+
1488
+ // Remove frames greater than duration.
1489
+
1490
+ key .length = size;
1491
+ keyValue .length = sizeN;
1492
+ keyType .length = size;
1493
+
1494
+ Editor .setNodeMetaData (interpolator, "Interpolator/key", key);
1495
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", keyValue);
1496
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType);
1497
+
1498
+ if (key .length === 0)
1499
+ Editor .removeNodeMetaData (interpolator, "Interpolator/keySize", new X3D .SFInt32 ());
1500
+
1501
+ this .registerRequestDrawTimeline ();
1502
+ }
1503
+
1504
+ addKeyframeToInterpolator (interpolator, frame, type, value)
1505
+ {
1506
+ const components = this .#components .get (interpolator .getType () .at (-1));
1507
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1508
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1509
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1510
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 (1));
1511
+ const index = X3D .Algorithm .lowerBound (key, 0, key .length, frame);
1512
+ const indexN = index * components * keySize;
1513
+
1514
+ keyValue .length = key .length * components * keySize;
1515
+ keyType .length = key .length;
1516
+
1517
+ const deleteCountN = index === key .length || frame === key [index]
1518
+ ? components * keySize // update
1519
+ : 0; // insert
1520
+
1521
+ key .splice (index, deleteCountN ? 1 : 0, frame);
1522
+ keyType .splice (index, deleteCountN ? 1 : 0, type);
1523
+
1524
+ // Use slice and concat instead of splice to support very large arrays.
1525
+
1526
+ const
1527
+ before = keyValue .slice (0, indexN),
1528
+ after = keyValue .slice (indexN + deleteCountN),
1529
+ insert = new X3D .MFDouble ();
1530
+
1531
+ insert .setValue (components === 1 ? [value] : Array .from (value));
1532
+
1533
+ const newKeyValue = before .concat (insert) .concat (after);
1534
+
1535
+ Editor .setNodeMetaData (interpolator, "Interpolator/key", key);
1536
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", newKeyValue);
1537
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType);
1538
+
1539
+ this .registerRequestDrawTimeline ();
1540
+
1541
+ return index;
1542
+ }
1543
+
1544
+ removeKeyframes (keyframes)
1545
+ {
1546
+ Editor .undoManager .beginUndo (_("Delete Keyframes"));
1547
+
1548
+ // Sort keyframes in descending order.
1549
+ keyframes .sort (({ index: a }, { index: b }) => b - a);
1550
+
1551
+ for (const { interpolator, index } of keyframes)
1552
+ this .removeKeyframeFromInterpolator (interpolator, index);
1553
+
1554
+ for (const interpolator of new Set (keyframes .map (({ interpolator }) => interpolator)))
1555
+ this .updateInterpolator (interpolator);
1556
+
1557
+ Editor .undoManager .endUndo ();
1558
+ }
1559
+
1560
+ removeKeyframeFromInterpolator (interpolator, index)
1561
+ {
1562
+ const components = this .#components .get (interpolator .getType () .at (-1));
1563
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1564
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1565
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1566
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 (1));
1567
+ const indexN = index * components * keySize;
1568
+
1569
+ keyValue .length = key .length * components * keySize;
1570
+ keyType .length = key .length;
1571
+
1572
+ const deleteCountN = components * keySize;
1573
+
1574
+ const frame = key .splice (index, 1);
1575
+ const frameKeyType = keyType .splice (index, 1);
1576
+ const frameValue = keyValue .splice (indexN, deleteCountN);
1577
+
1578
+ Editor .setNodeMetaData (interpolator, "Interpolator/key", key);
1579
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyValue", keyValue);
1580
+ Editor .setNodeMetaData (interpolator, "Interpolator/keyType", keyType);
1581
+
1582
+ if (!key .length)
1583
+ Editor .removeNodeMetaData (interpolator, "Interpolator/keySize", new X3D .SFInt32 ());
1584
+
1585
+ this .registerRequestDrawTimeline ();
1586
+
1587
+ return { interpolator, index, frame: frame [0], keyType: frameKeyType [0], value: Array .from (frameValue) };
1588
+ }
1589
+
1590
+ cutKeyframes ()
1591
+ {
1592
+ switch (this .getSelectedKeyframes () .length)
1593
+ {
1594
+ case 0:
1595
+ return;
1596
+ case 1:
1597
+ Editor .undoManager .beginUndo (_("Cut Keyframe"));
1598
+ break;
1599
+ default:
1600
+ Editor .undoManager .beginUndo (_("Cut Keyframes"));
1601
+ break;
1602
+ }
1603
+
1604
+ this .copyKeyframes ();
1605
+ this .removeKeyframes (this .getSelectedKeyframes ());
1606
+ this .registerClearSelectedKeyframes ();
1607
+
1608
+ Editor .undoManager .endUndo ();
1609
+ }
1610
+
1611
+ copyKeyframes ()
1612
+ {
1613
+ const string = JSON .stringify ({
1614
+ "sunrize-keyframes": this .getSelectedKeyframes () .map (({ field, interpolator, index }) =>
1615
+ {
1616
+ const components = this .#components .get (interpolator .getType () .at (-1));
1617
+ const key = interpolator .getMetaData ("Interpolator/key", new X3D .MFInt32 ());
1618
+ const keyValue = interpolator .getMetaData ("Interpolator/keyValue", new X3D .MFDouble ());
1619
+ const keyType = interpolator .getMetaData ("Interpolator/keyType", new X3D .MFString ());
1620
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 (1));
1621
+ const indexN = index * components * keySize;
1622
+ const countN = components * keySize;
1623
+
1624
+ return {
1625
+ field: field .getId (),
1626
+ frame: key [index],
1627
+ type: keyType [index],
1628
+ value: Array .from (keyValue .slice (indexN, indexN + countN)),
1629
+ };
1630
+ }),
1631
+ });
1632
+
1633
+ navigator .clipboard .writeText (string);
1634
+ }
1635
+
1636
+ async pasteKeyframes ()
1637
+ {
1638
+ const
1639
+ string = await navigator .clipboard .readText (),
1640
+ json = $.try (() => JSON .parse (string));
1641
+
1642
+ if (!json)
1643
+ return;
1644
+
1645
+ const keyframes = json ["sunrize-keyframes"];
1646
+
1647
+ if (!keyframes)
1648
+ return;
1649
+
1650
+ try
1651
+ {
1652
+ Editor .undoManager .beginUndo (_("Paste Keyframes"));
1653
+
1654
+ const
1655
+ currentFrame = this .getCurrentFrame (),
1656
+ firstFrame = keyframes .reduce ((p, c) => Math .min (p, c .frame), Number .POSITIVE_INFINITY),
1657
+ selectedKeyframes = [ ];
1658
+
1659
+ for (const { field: id, frame, type, value } of keyframes)
1660
+ {
1661
+ for (const field of this .fields .keys ())
1662
+ {
1663
+ if (field .getId () !== id)
1664
+ continue;
1665
+
1666
+ const interpolator = this .fields .get (field);
1667
+ const newFrame = frame - firstFrame + currentFrame;
1668
+
1669
+ if (newFrame > this .getDuration ())
1670
+ continue;
1671
+
1672
+ if (interpolator ._value_changed instanceof X3D .X3DArrayField)
1673
+ {
1674
+ const components = this .#components .get (interpolator .getType () .at (-1));
1675
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ());
1676
+
1677
+ if (keySize .getValue () === 0)
1678
+ {
1679
+ keySize .setValue (value .length / components);
1680
+
1681
+ Editor .setNodeMetaData (interpolator, "Interpolator/keySize", keySize);
1682
+ }
1683
+
1684
+ const countN = components * keySize;
1685
+
1686
+ if (value .length !== countN)
1687
+ continue;
1688
+ }
1689
+
1690
+ const index = this .addKeyframeToInterpolator (interpolator, newFrame, type, value);
1691
+
1692
+ selectedKeyframes .push ({ field, interpolator, index });
1693
+ }
1694
+ }
1695
+
1696
+ for (const interpolator of new Set (selectedKeyframes .map (({ interpolator }) => interpolator)))
1697
+ this .updateInterpolator (interpolator);
1698
+
1699
+ this .registerClearSelectedKeyframes ();
1700
+ this .setSelectedKeyframes (selectedKeyframes);
1701
+ this .setSelectionRange (0, 0);
1702
+ this .registerRequestDrawTimeline ();
1703
+ }
1704
+ catch (error)
1705
+ {
1706
+ console .error (error);
1707
+ }
1708
+ finally
1709
+ {
1710
+ Editor .undoManager .endUndo ();
1711
+ }
1712
+ }
1713
+
1714
+ deleteKeyframes ()
1715
+ {
1716
+ switch (this .getSelectedKeyframes () .length)
1717
+ {
1718
+ case 0:
1719
+ return;
1720
+ case 1:
1721
+ Editor .undoManager .beginUndo (_("Delete Keyframe"));
1722
+ break;
1723
+ default:
1724
+ Editor .undoManager .beginUndo (_("Delete Keyframes"));
1725
+ break;
1726
+ }
1727
+
1728
+ this .removeKeyframes (this .getSelectedKeyframes ());
1729
+ this .registerClearSelectedKeyframes ();
1730
+
1731
+ Editor .undoManager .endUndo ();
1732
+ }
1733
+
1734
+ moveKeyframes (keyframes, distance)
1735
+ {
1736
+ switch (keyframes .length)
1737
+ {
1738
+ case 0:
1739
+ return;
1740
+ case 1:
1741
+ Editor .undoManager .beginUndo (_("Move Keyframe"));
1742
+ break;
1743
+ default:
1744
+ Editor .undoManager .beginUndo (_("Move Keyframes"));
1745
+ break;
1746
+ }
1747
+
1748
+ const
1749
+ removed = [ ],
1750
+ added = [ ];
1751
+
1752
+ // Sort keyframes in descending order.
1753
+ keyframes = keyframes
1754
+ .sort (({ index: a }, { index: b }) => b - a)
1755
+ .map (keyframe => Object .assign (keyframe, { keyframe }));
1756
+
1757
+ for (const { interpolator, index, keyframe } of keyframes)
1758
+ removed .push (Object .assign (this .removeKeyframeFromInterpolator (interpolator, index), { keyframe }));
1759
+
1760
+ // Sort keyframes in ascending order.
1761
+ removed .sort (({ index: a }, { index: b }) => a - b);
1762
+
1763
+ for (const { interpolator, frame, keyType, value, keyframe } of removed)
1764
+ {
1765
+ const newFrame = frame + distance;
1766
+
1767
+ if (newFrame < 0 || newFrame > this .getDuration ())
1768
+ continue;
1769
+
1770
+ if (interpolator ._value_changed instanceof X3D .X3DArrayField)
1771
+ {
1772
+ const components = this .#components .get (interpolator .getType () .at (-1));
1773
+ const keySize = interpolator .getMetaData ("Interpolator/keySize", new X3D .SFInt32 ());
1774
+
1775
+ if (keySize .getValue () === 0)
1776
+ {
1777
+ keySize .setValue (value .length / components);
1778
+
1779
+ Editor .setNodeMetaData (interpolator, "Interpolator/keySize", keySize);
1780
+ }
1781
+ }
1782
+
1783
+ const index = this .addKeyframeToInterpolator (interpolator, newFrame, keyType, value);
1784
+
1785
+ added .push (Object .assign (keyframe, { index }));
1786
+ }
1787
+
1788
+ for (const interpolator of new Set (keyframes .map (({ interpolator }) => interpolator)))
1789
+ this .updateInterpolator (interpolator);
1790
+
1791
+ this .registerClearSelectedKeyframes ();
1792
+ this .setSelectedKeyframes (added);
1793
+ this .setSelectionRange (0, 0);
1794
+
1795
+ Editor .undoManager .endUndo ();
1796
+ }
1797
+
1798
+ registerRequestDrawTimeline ()
1799
+ {
1800
+ Editor .undoManager .beginUndo (_("Request Draw Tracks"));
1801
+
1802
+ this .requestDrawTimeline ();
1803
+
1804
+ Editor .undoManager .registerUndo (() =>
1805
+ {
1806
+ this .registerRequestDrawTimeline ();
1807
+ });
1808
+
1809
+ Editor .undoManager .endUndo ();
1810
+ }
1811
+
1812
+ showArraySizeErrorDialog (keySize)
1813
+ {
1814
+ console .error (_(`The key size has changed!`));
1815
+ console .error (_(`The number of values must remain consistent throughout the animation. Set size is ${keySize}.`));
1816
+ }
1817
+
1818
+ // Player
1819
+
1820
+ firstFrame ()
1821
+ {
1822
+ const selectionRange = this .getSelectionRange ();
1823
+
1824
+ if (selectionRange [0] === selectionRange [1])
1825
+ this .setCurrentFrame (0);
1826
+ else
1827
+ this .setCurrentFrame (selectionRange [0]);
1828
+ }
1829
+
1830
+ lastFrame ()
1831
+ {
1832
+ const selectionRange = this .getSelectionRange ();
1833
+
1834
+ if (selectionRange [0] === selectionRange [1])
1835
+ this .setCurrentFrame (this .getDuration ());
1836
+ else
1837
+ this .setCurrentFrame (selectionRange [1]);
1838
+ }
1839
+
1840
+ previousFrame ()
1841
+ {
1842
+ const selectionRange = this .getSelectionRange ();
1843
+
1844
+ if (selectionRange [0] === selectionRange [1])
1845
+ {
1846
+ if (this .getCurrentFrame () === 0)
1847
+ this .lastFrame ();
1848
+ else
1849
+ this .setCurrentFrame (Math .max (this .getCurrentFrame () - 1, 0));
1850
+ }
1851
+ else
1852
+ {
1853
+ if (this .getCurrentFrame () <= selectionRange [0])
1854
+ this .lastFrame ();
1855
+ else
1856
+ this .setCurrentFrame (Math .max (this .getCurrentFrame () - 1, 0));
1857
+ }
1858
+ }
1859
+
1860
+ nextFrame ()
1861
+ {
1862
+ const selectionRange = this .getSelectionRange ();
1863
+
1864
+ if (selectionRange [0] === selectionRange [1])
1865
+ {
1866
+ if (this .getCurrentFrame () === this .getDuration ())
1867
+ this .firstFrame ()
1868
+ else
1869
+ this .setCurrentFrame (Math .min (this .getCurrentFrame () + 1, this .getDuration ()));
1870
+ }
1871
+ else
1872
+ {
1873
+ if (this .getCurrentFrame () >= selectionRange [1])
1874
+ this .firstFrame ()
1875
+ else
1876
+ this .setCurrentFrame (Math .min (this .getCurrentFrame () + 1, this .getDuration ()));
1877
+ }
1878
+ }
1879
+
1880
+ toggleAnimation ()
1881
+ {
1882
+ require ("../Application/Window") .requestAutoSave ();
1883
+
1884
+ this .timeSensor ._stopTime = Date .now () / 1000;
1885
+
1886
+ if (this .timeSensor ._isActive .getValue ())
1887
+ return;
1888
+
1889
+ this .timeSensor ._evenLive = true;
1890
+ this .timeSensor ._startTime = Date .now () / 1000;
1891
+ }
1892
+
1893
+ toggleLoop ()
1894
+ {
1895
+ const node = this .timeSensor;
1896
+
1897
+ Editor .setFieldValue (this .browser .currentScene, node, node ._loop, !node ._loop .getValue ());
1898
+
1899
+ if (node ._loop .getValue () && node ._startTime .getValue () >= node ._stopTime .getValue ())
1900
+ node ._evenLive = true;
1901
+ }
1902
+
1903
+ showProperties ()
1904
+ {
1905
+ require ("../Controls/AnimationPropertiesPopover");
1906
+
1907
+ this .propertiesIcon .animationPropertiesPopover (this);
1908
+ }
1909
+
1910
+ updateRange ()
1911
+ {
1912
+ if (!this .timeSensor)
1913
+ return;
1914
+
1915
+ const selectionRange = this .getSelectionRange ();
1916
+
1917
+ if (selectionRange [0] === selectionRange [1])
1918
+ {
1919
+ this .timeSensor ._range [1] = 0;
1920
+ this .timeSensor ._range [2] = 1;
1921
+ }
1922
+ else
1923
+ {
1924
+ const duration = this .getDuration ();
1925
+
1926
+ this .timeSensor ._range [1] = selectionRange [0] / duration;
1927
+ this .timeSensor ._range [2] = selectionRange [1] / duration;
1928
+ }
1929
+ }
1930
+
1931
+ formatFrames (frame, framesPerSecond)
1932
+ {
1933
+ let time = Math .floor (frame);
1934
+
1935
+ const frames = String (time % framesPerSecond) .padStart (2, "0");
1936
+ time /= framesPerSecond;
1937
+ time = Math .floor (time);
1938
+
1939
+ const seconds = String (time % 60) .padStart (2, "0");
1940
+ time /= 60;
1941
+ time = Math .floor (time);
1942
+
1943
+
1944
+ const minutes = String (time % 60) .padStart (2, "0");
1945
+ time /= 60;
1946
+ time = Math .floor (time);
1947
+
1948
+ const hours = String (time) .padStart (2, "0");
1949
+
1950
+ return `${hours}:${minutes}:${seconds}:${frames}`;
1951
+ }
1952
+
1953
+ set_loop (loop)
1954
+ {
1955
+ if (loop .getValue ())
1956
+ this .loopIcon .addClass ("active");
1957
+ else
1958
+ this .loopIcon .removeClass ("active");
1959
+ }
1960
+
1961
+ set_active (active)
1962
+ {
1963
+ if (!active .getValue ())
1964
+ this .timeSensor ._range [0] = this .getCurrentFrame () / this .getDuration ();
1965
+
1966
+ this .toggleAnimationIcon .text (active .getValue () ? "pause" : "play_arrow");
1967
+ }
1968
+
1969
+ set_fraction (fraction)
1970
+ {
1971
+ const frame = Math .round (this .getDuration () * fraction .getValue ());
1972
+
1973
+ this .frameInput .val (frame);
1974
+ this .timeElement .text (this .formatFrames (frame, this .getFrameRate ()));
1975
+
1976
+ this .requestDrawTimeline ();
1977
+ }
1978
+
1979
+ // Navigation Function Handlers
1980
+
1981
+ on_keydown (event)
1982
+ {
1983
+ // console .log (event .key);
1984
+
1985
+ switch (event .key)
1986
+ {
1987
+ case " ":
1988
+ {
1989
+ this .toggleAnimation ();
1990
+
1991
+ event .preventDefault ();
1992
+ event .stopPropagation ();
1993
+ break;
1994
+ }
1995
+ case "ArrowLeft":
1996
+ {
1997
+ this .previousFrame ();
1998
+
1999
+ event .preventDefault ();
2000
+ event .stopPropagation ();
2001
+ break;
2002
+ }
2003
+ case "ArrowRight":
2004
+ {
2005
+ this .nextFrame ();
2006
+
2007
+ event .preventDefault ();
2008
+ event .stopPropagation ();
2009
+ break;
2010
+ }
2011
+ case "ArrowDown":
2012
+ {
2013
+ this .firstFrame ();
2014
+
2015
+ event .preventDefault ();
2016
+ event .stopPropagation ();
2017
+ break;
2018
+ }
2019
+ case "ArrowUp":
2020
+ {
2021
+ this .lastFrame ();
2022
+
2023
+ event .preventDefault ();
2024
+ event .stopPropagation ();
2025
+ break;
2026
+ }
2027
+ case "-":
2028
+ {
2029
+ this .zoomOut ();
2030
+
2031
+ event .preventDefault ();
2032
+ event .stopPropagation ();
2033
+ break;
2034
+ }
2035
+ case "+":
2036
+ {
2037
+ this .zoomIn ();
2038
+
2039
+ event .preventDefault ();
2040
+ event .stopPropagation ();
2041
+ break;
2042
+ }
2043
+ case "0":
2044
+ {
2045
+ this .zoomFit ();
2046
+
2047
+ event .preventDefault ();
2048
+ event .stopPropagation ();
2049
+ break;
2050
+ }
2051
+ case "1":
2052
+ {
2053
+ this .zoom100 ();
2054
+
2055
+ event .preventDefault ();
2056
+ event .stopPropagation ();
2057
+ break;
2058
+ }
2059
+ case "a":
2060
+ {
2061
+ if (event .metaKey || event .ctrlKey)
2062
+ {
2063
+ if (event .shiftKey)
2064
+ {
2065
+ this .setSelectedKeyframes ([ ]);
2066
+ this .setSelectionRange (0, 0);
2067
+ }
2068
+ else
2069
+ {
2070
+ this .setSelectionRange (0, this .getDuration ());
2071
+ }
2072
+
2073
+ event .preventDefault ();
2074
+ event .stopPropagation ();
2075
+ }
2076
+
2077
+ break;
2078
+ }
2079
+ case "x":
2080
+ {
2081
+ if (event .metaKey || event .ctrlKey)
2082
+ {
2083
+ this .cutKeyframes ();
2084
+
2085
+ event .preventDefault ();
2086
+ event .stopPropagation ();
2087
+ }
2088
+
2089
+ break;
2090
+ }
2091
+ case "c":
2092
+ {
2093
+ if (event .metaKey || event .ctrlKey)
2094
+ {
2095
+ this .copyKeyframes ();
2096
+
2097
+ event .preventDefault ();
2098
+ event .stopPropagation ();
2099
+ }
2100
+
2101
+ break;
2102
+ }
2103
+ case "v":
2104
+ {
2105
+ if (event .metaKey || event .ctrlKey)
2106
+ {
2107
+ this .pasteKeyframes ();
2108
+
2109
+ event .preventDefault ();
2110
+ event .stopPropagation ();
2111
+ }
2112
+
2113
+ break;
2114
+ }
2115
+ case "Backspace":
2116
+ {
2117
+ this .deleteKeyframes ();
2118
+
2119
+ event .preventDefault ();
2120
+ event .stopPropagation ();
2121
+ break;
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ zoomOut ()
2127
+ {
2128
+ const x = this .getPointerFromFrame (this .getCurrentFrame ());
2129
+
2130
+ this .zoom ("out", x, this .SCROLL_FACTOR);
2131
+ }
2132
+
2133
+ zoomIn ()
2134
+ {
2135
+ const x = this .getPointerFromFrame (this .getCurrentFrame ());
2136
+
2137
+ this .zoom ("in", x, this .SCROLL_FACTOR);
2138
+ }
2139
+
2140
+ zoom (direction, position, factor)
2141
+ {
2142
+ const fromFrame = (position - this .getTranslation ()) / this .getScale ();
2143
+
2144
+ switch (direction)
2145
+ {
2146
+ case "out": // Move backwards.
2147
+ {
2148
+ this .setScale (this .getScale () / factor);
2149
+ break;
2150
+ }
2151
+ case "in": // Move forwards.
2152
+ {
2153
+ this .setScale (this .getScale () * factor);
2154
+ break;
2155
+ }
2156
+ }
2157
+
2158
+ const toFrame = (position - this .getTranslation ()) / this .getScale ();
2159
+ const offset = (toFrame - fromFrame) * this .getScale ();
2160
+
2161
+ this .setTranslation (this .getTranslation () + offset);
2162
+ }
2163
+
2164
+ zoomFit ()
2165
+ {
2166
+ this .setScale (this .getFitScale ());
2167
+ this .setTranslation (0);
2168
+ }
2169
+
2170
+ registerZoomFit ()
2171
+ {
2172
+ Editor .undoManager .beginUndo (_("Zoom Fit"));
2173
+
2174
+ setTimeout (() => this .zoomFit ());
2175
+
2176
+ Editor .undoManager .registerUndo (() =>
2177
+ {
2178
+ this .registerZoomFit ();
2179
+ });
2180
+
2181
+ Editor .undoManager .endUndo ();
2182
+ }
2183
+
2184
+ zoom100 ()
2185
+ {
2186
+ const
2187
+ frame = this .getCurrentFrame (),
2188
+ x = this .getPointerFromFrame (frame);
2189
+
2190
+ this .setScale (this .DEFAULT_SCALE);
2191
+ this .setTranslation (x - frame * this .DEFAULT_SCALE);
2192
+ }
2193
+
2194
+ // Timeline Properties
2195
+
2196
+ TIMELINE_PADDING = 15; // in pixels
2197
+ FRAME_SIZE = 7; // in pixels
2198
+ DEFAULT_SCALE = 16; // in pixels
2199
+ MIN_SCALE = 128; // in pixels
2200
+ SCROLL_FACTOR = 1 + 1 / 16; // something nice
2201
+ WHEEL_SCROLL_FACTOR = 1 + 1 / 30; // something nice
2202
+
2203
+ translation = 0;
2204
+ scale = 1;
2205
+
2206
+ getCurrentFrame ()
2207
+ {
2208
+ return X3D .Algorithm .clamp (Math .round (this .frameInput .val ()), 0, this .getDuration ());
2209
+ }
2210
+
2211
+ setCurrentFrame (frame)
2212
+ {
2213
+ // Update interpolator fraction.
2214
+
2215
+ this .frameInput .val (frame);
2216
+ this .timeElement .text (this .formatFrames (frame, this .getFrameRate ()));
2217
+
2218
+ const fraction = frame / this .getDuration ();
2219
+
2220
+ for (const interpolator of this .interpolators)
2221
+ interpolator ._set_fraction = fraction;
2222
+
2223
+ if (this .timeSensor)
2224
+ this .timeSensor ._range [0] = fraction;
2225
+
2226
+ this .requestDrawTimeline ();
2227
+ }
2228
+
2229
+ #defaultInteger = new X3D .SFInt32 ();
2230
+
2231
+ getDuration ()
2232
+ {
2233
+ this .#defaultInteger .setValue (10);
2234
+
2235
+ return Math .max (this .animation ?.getMetaData ("Animation/duration", this .#defaultInteger) ?? 10, 1);
2236
+ }
2237
+
2238
+ getFrameRate ()
2239
+ {
2240
+ this .#defaultInteger .setValue (10);
2241
+
2242
+ return Math .max (this .animation ?.getMetaData ("Animation/frameRate", this .#defaultInteger) ?? 10, 1);
2243
+ }
2244
+
2245
+ getTranslation ()
2246
+ {
2247
+ return this .translation;
2248
+ }
2249
+
2250
+ setTranslation (translation)
2251
+ {
2252
+ const width = this .getWidth ();
2253
+ const max = width - (this .getDuration () * this .getScale ());
2254
+
2255
+ translation = Math .max (translation, max);
2256
+ translation = Math .min (translation, 0);
2257
+
2258
+ this .translation = translation;
2259
+
2260
+ this .updateScrollbar ();
2261
+ this .requestDrawTimeline ();
2262
+ }
2263
+
2264
+ getScale ()
2265
+ {
2266
+ return this .scale;
2267
+ }
2268
+
2269
+ setScale (scale)
2270
+ {
2271
+ this .scale = Math .max (Math .min (scale, this .MIN_SCALE), this .getFitScale ());
2272
+
2273
+ this .updateScrollbar ();
2274
+ this .requestDrawTimeline ();
2275
+ }
2276
+
2277
+ getFitScale ()
2278
+ {
2279
+ return this .getWidth () / this .getDuration ();
2280
+ }
2281
+
2282
+ /**
2283
+ *
2284
+ * @returns {number} start of timeline area
2285
+ */
2286
+ getLeft ()
2287
+ {
2288
+ return Math .floor (this .tracks .width () - this .getWidth () - this .TIMELINE_PADDING);
2289
+ }
2290
+
2291
+ /**
2292
+ *
2293
+ * @returns {number} width of timeline area
2294
+ */
2295
+ getWidth ()
2296
+ {
2297
+ return Math .floor (this .timelineElement .width () - this .TIMELINE_PADDING * 2);
2298
+ }
2299
+
2300
+ // Update Tracks
2301
+
2302
+ #button;
2303
+
2304
+ on_mousedown (event)
2305
+ {
2306
+ $(document)
2307
+ .on ("mousemove.AnimationEditor", event => this .updatePointer (event))
2308
+ .on ("mouseup.AnimationEditor", event => this .on_mouseup (event))
2309
+ .on ("mousemove.AnimationEditor", event => this .on_mousemove (event));
2310
+
2311
+ this .#button = event .button;
2312
+
2313
+ switch (this .#button)
2314
+ {
2315
+ case 0:
2316
+ {
2317
+ this .updatePointer (event);
2318
+ this .addAutoScroll ();
2319
+
2320
+ const pickedKeyframes = this .pickKeyframes ();
2321
+
2322
+ if (!pickedKeyframes .length)
2323
+ this .setCurrentFrame (this .getFrameFromPointer (this .pointer .x));
2324
+
2325
+ this .startMovingFrame = this .getFrameFromPointer (this .pointer .x);
2326
+
2327
+ if (event .shiftKey && pickedKeyframes .length)
2328
+ {
2329
+ this .togglePickedKeyframes (pickedKeyframes);
2330
+ }
2331
+ else if (event .shiftKey)
2332
+ {
2333
+ this .setPickedKeyframes ([ ]);
2334
+ this .setSelectedKeyframes ([ ]);
2335
+ this .expandSelectionRange (this .getCurrentFrame ());
2336
+ }
2337
+ else
2338
+ {
2339
+ if (!pickedKeyframes .length || !pickedKeyframes .every (p => this .getSelectedKeyframes () .some (s => this .equalKeyframe (p, s))))
2340
+ {
2341
+ this .setPickedKeyframes (pickedKeyframes);
2342
+ this .setSelectedKeyframes (pickedKeyframes);
2343
+ this .setSelectionRange (this .getCurrentFrame (), this .getCurrentFrame ());
2344
+ }
2345
+ else
2346
+ {
2347
+ this .setPickedKeyframes (this .getSelectedKeyframes ());
2348
+ }
2349
+ }
2350
+
2351
+ this .timeSensor ._pauseTime = Date .now () / 1000;
2352
+ break;
2353
+ }
2354
+ }
2355
+ }
2356
+
2357
+ on_mouseup ()
2358
+ {
2359
+ $(document) .off (".AnimationEditor");
2360
+
2361
+ this .removeAutoScroll ();
2362
+
2363
+ if (this .#movingKeyframesOffset)
2364
+ this .moveKeyframes (this .getSelectedKeyframes (), this .#movingKeyframesOffset);
2365
+
2366
+ this .#button = undefined;
2367
+ this .#movingKeyframesOffset = 0;
2368
+
2369
+ this .timeSensor ._resumeTime = Date .now () / 1000;
2370
+ }
2371
+
2372
+ on_mousemove (event)
2373
+ {
2374
+ switch (this .#button)
2375
+ {
2376
+ case undefined:
2377
+ {
2378
+ this .updatePointer (event);
2379
+ this .updateCursor ();
2380
+ break;
2381
+ }
2382
+ case 0:
2383
+ {
2384
+ this .updatePointer (event);
2385
+ this .moveOrSelectKeyframes (event);
2386
+ break;
2387
+ }
2388
+ }
2389
+ }
2390
+
2391
+ on_wheel (event)
2392
+ {
2393
+ const deltaY = event .originalEvent .deltaY;
2394
+
2395
+ this .updatePointer (event);
2396
+ this .zoom (deltaY > 0 ? "out" : "in", this .pointer .x, this .WHEEL_SCROLL_FACTOR);
2397
+ }
2398
+
2399
+ updateCursor ()
2400
+ {
2401
+ if (this .pickKeyframes () .length)
2402
+ this .timelineElement .addClass ("pointer");
2403
+ else
2404
+ this .timelineElement .removeClass ("pointer");
2405
+ }
2406
+
2407
+ pointer = new X3D .Vector2 (-1, -1);
2408
+
2409
+ clearPointer ()
2410
+ {
2411
+ this .pointer .set (-1, -1);
2412
+
2413
+ this .requestDrawTimeline ();
2414
+ }
2415
+
2416
+ updatePointer (event)
2417
+ {
2418
+ const offset = this .tracks .offset ();
2419
+
2420
+ const x = event .pageX - offset .left - this .getLeft ();
2421
+ const y = event .pageY - offset .top;
2422
+
2423
+ this .pointer .set (x, y);
2424
+
2425
+ this .requestDrawTimeline ();
2426
+ }
2427
+
2428
+ getFrameFromPointer (pointerX)
2429
+ {
2430
+ const frame = Math .round ((pointerX - this .getTranslation ()) / this .getScale ());
2431
+
2432
+ return X3D .Algorithm .clamp (frame, 0, this .getDuration ());
2433
+ }
2434
+
2435
+ getPointerFromFrame (frame)
2436
+ {
2437
+ return frame * this .getScale () + this .getTranslation ();
2438
+ }
2439
+
2440
+ pickKeyframes ()
2441
+ {
2442
+ const
2443
+ width = this .getWidth (),
2444
+ translation = this .getTranslation (),
2445
+ scale = this .getScale (),
2446
+ trackOffsets = this .memberList .getTrackOffsets (),
2447
+ firstFrame = Math .max (0, Math .floor (-translation / scale)),
2448
+ lastFrame = Math .min (this .getDuration (), Math .ceil ((width - translation) / scale)) + 1,
2449
+ keyframes = [ ];
2450
+
2451
+ for (const { item, bottom } of trackOffsets .values ())
2452
+ {
2453
+ switch (item .attr ("type"))
2454
+ {
2455
+ case "main":
2456
+ {
2457
+ for (const field of this .fields .keys ())
2458
+ this .pickKeyframe (field, firstFrame, lastFrame, bottom - this .TRACK_PADDING, keyframes);
2459
+
2460
+ break;
2461
+ }
2462
+ case "node":
2463
+ {
2464
+ const node = item .data ("node");
2465
+
2466
+ for (const field of node .getFields ())
2467
+ this .pickKeyframe (field, firstFrame, lastFrame, bottom - this .TRACK_PADDING, keyframes);
2468
+
2469
+ break;
2470
+ }
2471
+ case "field":
2472
+ {
2473
+ this .pickKeyframe (item .data ("field"), firstFrame, lastFrame, bottom - this .TRACK_PADDING, keyframes);
2474
+ break;
2475
+ }
2476
+ }
2477
+ }
2478
+
2479
+ return keyframes;
2480
+ }
2481
+
2482
+ #frameBox = new X3D .Box2 ();
2483
+ #frameSize = new X3D .Vector2 (this .FRAME_SIZE, this .FRAME_SIZE);
2484
+ #frameCenter = new X3D .Vector2 ();
2485
+
2486
+ pickKeyframe (field, firstFrame, lastFrame, bottom, keyframes)
2487
+ {
2488
+ const interpolator = this .fields .get (field);
2489
+
2490
+ if (!interpolator)
2491
+ return;
2492
+
2493
+ this .#defaultIntegers .length = 0;
2494
+
2495
+ const
2496
+ translation = this .getTranslation (),
2497
+ scale = this .getScale ();
2498
+
2499
+ const
2500
+ key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers),
2501
+ first = X3D .Algorithm .lowerBound (key, 0, key .length, firstFrame),
2502
+ last = X3D .Algorithm .upperBound (key, 0, key .length, lastFrame);
2503
+
2504
+ for (let index = first; index < last; ++ index)
2505
+ {
2506
+ const frame = key [index];
2507
+ const x = Math .floor (this .getPointerFromFrame (frame)) + 0.5;
2508
+ const y = Math .floor (bottom - this .FRAME_SIZE / 2) + 0.5;
2509
+
2510
+ this .#frameBox .set (this .#frameSize, this .#frameCenter .set (x, y));
2511
+
2512
+ if (this .#frameBox .containsPoint (this .pointer))
2513
+ keyframes .push ({ field, interpolator, index });
2514
+ }
2515
+ }
2516
+
2517
+ #pickedKeyframes = [ ];
2518
+ #selectedKeyframes = [ ];
2519
+ #movingKeyframesOffset = 0;
2520
+
2521
+ getPickedKeyframes ()
2522
+ {
2523
+ return this .#pickedKeyframes;
2524
+ }
2525
+
2526
+ setPickedKeyframes (pickedKeyframes)
2527
+ {
2528
+ this .#pickedKeyframes = pickedKeyframes .slice ();
2529
+ }
2530
+
2531
+ togglePickedKeyframes (pickedKeyframes)
2532
+ {
2533
+ // Picked Keyframes
2534
+ {
2535
+ const add = pickedKeyframes .filter (n => this .getPickedKeyframes () .every (o => !this .equalKeyframe (n, o)));
2536
+
2537
+ this .setPickedKeyframes (this .getPickedKeyframes ()
2538
+ .filter (o => !pickedKeyframes .some (n => this .equalKeyframe (n, o)))
2539
+ .concat (add));
2540
+ }
2541
+
2542
+ // Selected Keyframes
2543
+ {
2544
+ const add = pickedKeyframes .filter (n => this .getSelectedKeyframes () .every (o => !this .equalKeyframe (n, o)));
2545
+
2546
+ this .setSelectedKeyframes (this .getSelectedKeyframes ()
2547
+ .filter (o => !pickedKeyframes .some (n => this .equalKeyframe (n, o)))
2548
+ .concat (add));
2549
+ }
2550
+ }
2551
+
2552
+ equalKeyframe (a, b)
2553
+ {
2554
+ return a .field === b .field && a .index === b .index;
2555
+ }
2556
+
2557
+ getSelectedKeyframes ()
2558
+ {
2559
+ return this .#selectedKeyframes;
2560
+ }
2561
+
2562
+ setSelectedKeyframes (selectedKeyframes)
2563
+ {
2564
+ this .#selectedKeyframes = selectedKeyframes .slice ();
2565
+
2566
+ this .updateKeyType ();
2567
+ }
2568
+
2569
+ registerClearSelectedKeyframes ()
2570
+ {
2571
+ Editor .undoManager .beginUndo (_("Clear Selected Keyframes"));
2572
+
2573
+ this .setPickedKeyframes ([ ]);
2574
+ this .setSelectedKeyframes ([ ]);
2575
+
2576
+ Editor .undoManager .registerUndo (() =>
2577
+ {
2578
+ this .registerClearSelectedKeyframes ();
2579
+ });
2580
+
2581
+ this .registerRequestDrawTimeline ();
2582
+
2583
+ Editor .undoManager .endUndo ();
2584
+ }
2585
+
2586
+ #autoScrollId;
2587
+
2588
+ addAutoScroll ()
2589
+ {
2590
+ this .#autoScrollId = setInterval (() => this .autoScroll (), 100);
2591
+ }
2592
+
2593
+ removeAutoScroll ()
2594
+ {
2595
+ clearInterval (this .#autoScrollId);
2596
+ }
2597
+
2598
+ autoScroll ()
2599
+ {
2600
+ // Autoscroll area.
2601
+
2602
+ const width = this .getWidth ();
2603
+
2604
+ if (this .pointer .x < 0)
2605
+ {
2606
+ this .setTranslation (this .getTranslation () - this .pointer .x);
2607
+ this .moveOrSelectKeyframes ();
2608
+ }
2609
+ else if (this .pointer .x > width)
2610
+ {
2611
+ this .setTranslation (this .getTranslation () - (this .pointer .x - width));
2612
+ this .moveOrSelectKeyframes ();
2613
+ }
2614
+ }
2615
+
2616
+ #selectionRange = [0, 0];
2617
+
2618
+ getSelectionRange ()
2619
+ {
2620
+ const [a, b] = this .#selectionRange;
2621
+
2622
+ if (a < b)
2623
+ return [a, b];
2624
+
2625
+ return [b, a];
2626
+ }
2627
+
2628
+ setSelectionRange (start, end)
2629
+ {
2630
+ this .#selectionRange = [start, end];
2631
+
2632
+ this .selectKeyframesInRange ();
2633
+ this .updateRange ();
2634
+ this .requestDrawTimeline ();
2635
+ }
2636
+
2637
+ expandSelectionRange (frame)
2638
+ {
2639
+ const
2640
+ selectionRange = this .getSelectionRange (),
2641
+ middle = (selectionRange [0] + selectionRange [1]) / 2;
2642
+
2643
+ if (frame < middle)
2644
+ this .setSelectionRange (frame, selectionRange [1]);
2645
+ else if (frame > middle)
2646
+ this .setSelectionRange (selectionRange [0], frame);
2647
+ }
2648
+
2649
+ selectKeyframesInRange ()
2650
+ {
2651
+ const selectionRange = this .getSelectionRange ();
2652
+
2653
+ if (selectionRange [0] === selectionRange [1])
2654
+ return;
2655
+
2656
+ const selectedKeyframes = [ ];
2657
+
2658
+ for (const [field, interpolator] of this .fields)
2659
+ {
2660
+ const
2661
+ key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers),
2662
+ first = X3D .Algorithm .lowerBound (key, 0, key .length, selectionRange [0]),
2663
+ last = X3D .Algorithm .upperBound (key, 0, key .length, selectionRange [1]);
2664
+
2665
+ for (let index = first; index < last; ++ index)
2666
+ selectedKeyframes .push ({ field, interpolator, index });
2667
+ }
2668
+
2669
+ this .setSelectedKeyframes (selectedKeyframes);
2670
+ this .requestDrawTimeline ();
2671
+ }
2672
+
2673
+ moveOrSelectKeyframes (event)
2674
+ {
2675
+ // Move keyframes or select range.
2676
+
2677
+ if (this .getPickedKeyframes () .length)
2678
+ {
2679
+ this .#movingKeyframesOffset = this .getFrameFromPointer (this .pointer .x) - this .startMovingFrame;
2680
+ }
2681
+ else
2682
+ {
2683
+ // Select range.
2684
+
2685
+ this .setCurrentFrame (this .getFrameFromPointer (this .pointer .x));
2686
+
2687
+ if (event ?.shiftKey)
2688
+ this .expandSelectionRange (this .getCurrentFrame ());
2689
+ else
2690
+ this .setSelectionRange (this .#selectionRange [0], this .getCurrentFrame ());
2691
+ }
2692
+ }
2693
+
2694
+ /* Scrollbar Handling */
2695
+
2696
+ MIN_SCROLLBAR_SCALE = 0.05; // in fractions
2697
+
2698
+ #scrollButton;
2699
+ #scrollStart;
2700
+ #scrollLeft;
2701
+
2702
+ on_mousedown_scrollbar (event)
2703
+ {
2704
+ $(document)
2705
+ .on ("mouseup.AnimationEditorScrollbar", event => this .on_mouseup_scrollbar (event))
2706
+ .on ("mousemove.AnimationEditorScrollbar", event => this .on_mousemove_scrollbar (event));
2707
+
2708
+ this .#scrollButton = event .button;
2709
+ this .#scrollStart = event .pageX;
2710
+ this .#scrollLeft = parseFloat (this .scrollbarElement .css ("left"));
2711
+
2712
+ event .preventDefault ();
2713
+ event .stopPropagation ();
2714
+ }
2715
+
2716
+ on_mouseup_scrollbar (event)
2717
+ {
2718
+ $(document) .off ("mouseup.AnimationEditorScrollbar");
2719
+
2720
+ this .#scrollButton = undefined;
2721
+ }
2722
+
2723
+ on_mousemove_scrollbar (event)
2724
+ {
2725
+ if (this .#scrollButton === undefined)
2726
+ return;
2727
+
2728
+ const
2729
+ scale = this .getScale (),
2730
+ duration = this .getDuration (),
2731
+ width = this .getWidth (),
2732
+ visibleFrames = Math .min (width / scale, duration),
2733
+ scrollbarTranslation = event .pageX - this .#scrollStart,
2734
+ scrollbarWidth = this .timelineElement .width () - this .scrollbarElement .width (),
2735
+ scrollbarLeft = X3D .Algorithm .clamp (this .#scrollLeft + scrollbarTranslation, 0, scrollbarWidth),
2736
+ translation = -scrollbarLeft / scrollbarWidth * (duration - visibleFrames) * scale;
2737
+
2738
+ if (scrollbarWidth)
2739
+ this .setTranslation (translation);
2740
+
2741
+ event .preventDefault ();
2742
+ event .stopPropagation ();
2743
+ }
2744
+
2745
+ updateScrollbar ()
2746
+ {
2747
+ const translation = this .getTranslation ();
2748
+ const scale = this .getScale ();
2749
+ const duration = this .getDuration ();
2750
+ const width = this .getWidth ();
2751
+ const firstFrame = Math .max (0, -translation / scale);
2752
+ const visibleFrames = Math .min (width / scale, duration);
2753
+ const scrollbarScale = X3D .Algorithm .clamp (this .getFitScale () / scale, this .MIN_SCROLLBAR_SCALE, 1);
2754
+ const scrollbarWidth = this .timelineElement .width () - this .timelineElement .width () * scrollbarScale;
2755
+ const scrollbarLeft = Math .max (firstFrame / (duration - visibleFrames) * scrollbarWidth, 0);
2756
+
2757
+ if (duration === visibleFrames)
2758
+ {
2759
+ this .scrollbarElement
2760
+ .css ("left", 0)
2761
+ .css ("width", "100%");
2762
+ }
2763
+ else
2764
+ {
2765
+ this .scrollbarElement
2766
+ .css ("left", scrollbarLeft)
2767
+ .css ("width", `${scrollbarScale * 100}%`);
2768
+ }
2769
+ }
2770
+
2771
+ /* Timeline Draw Handling */
2772
+
2773
+ resizeTimeline ()
2774
+ {
2775
+ const
2776
+ tracksWidth = this .tracks .width (),
2777
+ tracksHeight = this .tracks .height ();
2778
+
2779
+ this .tracks
2780
+ .prop ("width", tracksWidth)
2781
+ .prop ("height", tracksHeight);
2782
+
2783
+ this .timelineClip = new Path2D ();
2784
+ this .timelineClip .rect (this .getLeft () - this .FRAME_SIZE, 0, this .getWidth () + this .FRAME_SIZE * 2, tracksHeight);
2785
+
2786
+ this .drawTimeline ();
2787
+ }
2788
+
2789
+ #updateTracksId = undefined;
2790
+
2791
+ requestDrawTimeline ()
2792
+ {
2793
+ clearTimeout (this .#updateTracksId);
2794
+
2795
+ this .#updateTracksId = setTimeout (() => this .drawTimeline ());
2796
+ }
2797
+
2798
+ #style = window .getComputedStyle ($("body") [0]);
2799
+
2800
+ TRACK_PADDING = 8;
2801
+
2802
+ drawTimeline ()
2803
+ {
2804
+ const
2805
+ context = this .tracks [0] .getContext ("2d"),
2806
+ tracksWidth = this .tracks .width (),
2807
+ tracksHeight = this .tracks .height ();
2808
+
2809
+ context .clearRect (0, 0, tracksWidth, tracksHeight);
2810
+
2811
+ if (!this .animation)
2812
+ return;
2813
+
2814
+ const
2815
+ left = this .getLeft (),
2816
+ width = this .getWidth (),
2817
+ translation = this .getTranslation (),
2818
+ scale = this .getScale (),
2819
+ trackOffsets = this .memberList .getTrackOffsets (),
2820
+ firstFrame = Math .max (0, Math .floor (-translation / scale)),
2821
+ lastFrame = Math .min (this .getDuration (), Math .ceil ((width - translation) / scale)) + 1;
2822
+
2823
+ const [frameStep, frameFactor] = this .getFrameParams ();
2824
+
2825
+ const
2826
+ blue = this .#style .getPropertyValue ("--system-blue"),
2827
+ indigo = this .#style .getPropertyValue ("--system-indigo"),
2828
+ orange = this .#style .getPropertyValue ("--system-orange"),
2829
+ brown = this .#style .getPropertyValue ("--system-brown"),
2830
+ red = this .#style .getPropertyValue ("--system-red"),
2831
+ range = this .#style .getPropertyValue ("--selection-range"),
2832
+ tint1 = this .#style .getPropertyValue ("--tint-color1"),
2833
+ tint2 = this .#style .getPropertyValue ("--tint-color2");
2834
+
2835
+ // Draw selection range.
2836
+
2837
+ context .save ();
2838
+ context .clip (this .timelineClip);
2839
+
2840
+ const selectionRange = this .getSelectionRange ();
2841
+
2842
+ if (selectionRange [0] !== selectionRange [1])
2843
+ {
2844
+ const minFrame = X3D .Algorithm .clamp (selectionRange [0], firstFrame, lastFrame - 1);
2845
+ const maxFrame = X3D .Algorithm .clamp (selectionRange [1], firstFrame, lastFrame - 1);
2846
+ const x0 = left + minFrame * scale + translation;
2847
+ const x1 = left + maxFrame * scale + translation;
2848
+
2849
+ context .fillStyle = range;
2850
+
2851
+ context .fillRect (Math .min (x0, x1) - 1, 0, Math .abs (x1 - x0) + 3, tracksHeight);
2852
+ }
2853
+
2854
+ context .restore ();
2855
+
2856
+ // Draw all tracks.
2857
+
2858
+ for (const [i, { item, top, bottom, height }] of trackOffsets .entries ())
2859
+ {
2860
+ // Track
2861
+
2862
+ const odd = item .data ("i") % 2;
2863
+
2864
+ if (odd || i === 0)
2865
+ {
2866
+ // Draw a line below last field.
2867
+
2868
+ if (trackOffsets [i + 1] ?.item .hasClass ("node") ?? true)
2869
+ {
2870
+ context .fillStyle = tint2;
2871
+
2872
+ context .fillRect (0, bottom - 1, tracksWidth, 1);
2873
+ }
2874
+ }
2875
+ else if (item .hasClass ("field"))
2876
+ {
2877
+ // Draw a bar.
2878
+
2879
+ context .fillStyle = tint1;
2880
+
2881
+ context .fillRect (0, top, tracksWidth, height);
2882
+ }
2883
+
2884
+ // Highlight track on hover.
2885
+
2886
+ const hover = this .pointer .y > top && this .pointer .y < bottom;
2887
+
2888
+ if (hover)
2889
+ item .addClass ("hover-track");
2890
+ else
2891
+ item .removeClass ("hover-track");
2892
+
2893
+ if (item .is (".hover, .hover-tracks") || hover)
2894
+ {
2895
+ context .fillStyle = tint2;
2896
+
2897
+ context .fillRect (0, top, tracksWidth, height);
2898
+ }
2899
+
2900
+ // Frames
2901
+
2902
+ context .save ();
2903
+ context .clip (this .timelineClip);
2904
+
2905
+ // Draw vertical lines.
2906
+
2907
+ context .strokeStyle = item .hasClass ("main") ? indigo : blue;
2908
+ context .lineWidth = item .is (".main, .node") ? 3 : 1;
2909
+
2910
+ for (let frame = firstFrame - (firstFrame % frameStep); frame < lastFrame; frame += frameStep)
2911
+ {
2912
+ const s = frame % frameFactor; // size (large or small)
2913
+ const y = Math .floor (top + height * (s ? 0.75 : 0.5));
2914
+ const x = Math .floor (left + this .getPointerFromFrame (frame));
2915
+
2916
+ context .beginPath ();
2917
+ context .moveTo (x + 0.5, y - this .TRACK_PADDING);
2918
+ context .lineTo (x + 0.5, bottom - this .TRACK_PADDING);
2919
+ context .stroke ();
2920
+ }
2921
+
2922
+ // Draw keyframes.
2923
+
2924
+ switch (item .attr ("type"))
2925
+ {
2926
+ case "main":
2927
+ {
2928
+ for (const field of this .fields .keys ())
2929
+ this .drawKeyframes (context, field, firstFrame, lastFrame, bottom - this .TRACK_PADDING, brown);
2930
+
2931
+ break;
2932
+ }
2933
+ case "node":
2934
+ {
2935
+ const node = item .data ("node");
2936
+
2937
+ for (const field of node .getFields ())
2938
+ this .drawKeyframes (context, field, firstFrame, lastFrame, bottom - this .TRACK_PADDING, brown);
2939
+
2940
+ break;
2941
+ }
2942
+ case "field":
2943
+ {
2944
+ this .drawKeyframes (context, item .data ("field"), firstFrame, lastFrame, bottom - this .TRACK_PADDING, orange);
2945
+ break;
2946
+ }
2947
+ }
2948
+
2949
+ // Draw selected keyframes.
2950
+
2951
+ switch (item .attr ("type"))
2952
+ {
2953
+ case "main":
2954
+ {
2955
+ const allSelected = Array .from (this .fields .keys ())
2956
+ .every (field => this .getSelectedKeyframes () .some (keyframe => field === keyframe .field));
2957
+
2958
+ if (allSelected)
2959
+ {
2960
+ for (const field of this .fields .keys ())
2961
+ {
2962
+ this .drawSelectedKeyframes (context, field, bottom - this .TRACK_PADDING, red);
2963
+ break;
2964
+ }
2965
+ }
2966
+
2967
+ break;
2968
+ }
2969
+ case "node":
2970
+ {
2971
+ const
2972
+ node = item .data ("node"),
2973
+ fields = node .getFields () .filter (field => this .fields .has (field));
2974
+
2975
+ const allSelected = fields
2976
+ .every (field => this .getSelectedKeyframes () .some (keyframe => field === keyframe .field));
2977
+
2978
+ if (allSelected)
2979
+ {
2980
+ for (const field of fields)
2981
+ {
2982
+ this .drawSelectedKeyframes (context, field, bottom - this .TRACK_PADDING, red);
2983
+ break;
2984
+ }
2985
+ }
2986
+
2987
+ break;
2988
+ }
2989
+ case "field":
2990
+ {
2991
+ this .drawSelectedKeyframes (context, item .data ("field"), bottom - this .TRACK_PADDING, red);
2992
+ break;
2993
+ }
2994
+ }
2995
+
2996
+ context .restore ();
2997
+ }
2998
+
2999
+ // Draw current frame cursor.
3000
+
3001
+ context .save ();
3002
+ context .clip (this .timelineClip);
3003
+
3004
+ const frame = this .getCurrentFrame ();
3005
+ const x = Math .floor (left + this .getPointerFromFrame (frame));
3006
+
3007
+ context .fillStyle = blue;
3008
+
3009
+ context .fillRect (x - 1, 0, 3, tracksHeight);
3010
+
3011
+ context .restore ();
3012
+ }
3013
+
3014
+ #defaultIntegers = new X3D .MFInt32 ();
3015
+
3016
+ drawKeyframes (context, field, firstFrame, lastFrame, bottom, color)
3017
+ {
3018
+ const interpolator = this .fields .get (field);
3019
+
3020
+ if (!interpolator)
3021
+ return;
3022
+
3023
+ this .#defaultIntegers .length = 0;
3024
+
3025
+ const
3026
+ left = this .getLeft (),
3027
+ translation = this .getTranslation (),
3028
+ scale = this .getScale ();
3029
+
3030
+ const
3031
+ key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers),
3032
+ first = X3D .Algorithm .lowerBound (key, 0, key .length, firstFrame),
3033
+ last = X3D .Algorithm .upperBound (key, 0, key .length, lastFrame);
3034
+
3035
+ for (let index = first; index < last; ++ index)
3036
+ {
3037
+ const frame = key [index];
3038
+ const x = Math .floor (left + this .getPointerFromFrame (frame));
3039
+ const x1 = x - (this .FRAME_SIZE / 2) + 0.5;
3040
+
3041
+ context .fillStyle = color;
3042
+
3043
+ context .fillRect (x1, bottom - this .FRAME_SIZE, this .FRAME_SIZE, this .FRAME_SIZE);
3044
+ }
3045
+ }
3046
+
3047
+ drawSelectedKeyframes (context, currentField, bottom, selectedColor)
3048
+ {
3049
+ const
3050
+ left = this .getLeft (),
3051
+ translation = this .getTranslation (),
3052
+ scale = this .getScale ();
3053
+
3054
+ for (const { field, interpolator, index } of this .getSelectedKeyframes ())
3055
+ {
3056
+ if (field !== currentField)
3057
+ continue;
3058
+
3059
+ this .#defaultIntegers .length = 0;
3060
+
3061
+ const key = interpolator .getMetaData ("Interpolator/key", this .#defaultIntegers);
3062
+ const frame = key [index] + this .#movingKeyframesOffset;
3063
+ const x = Math .floor (left + this .getPointerFromFrame (frame));
3064
+ const x1 = x - (this .FRAME_SIZE / 2) + 0.5;
3065
+
3066
+ context .fillStyle = selectedColor;
3067
+
3068
+ context .fillRect (x1, bottom - this .FRAME_SIZE, this .FRAME_SIZE, this .FRAME_SIZE);
3069
+ }
3070
+ }
3071
+
3072
+ /**
3073
+ * Params for scaling steps of timeline.
3074
+ */
3075
+ #params = Array .from ({ length: 7 }, (_, i) => Math .pow (10, i))
3076
+ .map (n => [5 / n, [n * 10, n * 50]])
3077
+ .reverse ();
3078
+
3079
+ getFrameParams ()
3080
+ {
3081
+ const index = X3D .Algorithm .upperBound (this .#params, 0, this .#params .length, this .getScale (), (a, b) =>
3082
+ {
3083
+ return a < b [0];
3084
+ });
15
3085
 
16
- this .setup ()
3086
+ return this .#params [index] ?.[1] ?? [1, 5];
17
3087
  }
18
3088
  }