lexgui 0.7.9 → 0.7.11

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.
@@ -1299,6 +1299,10 @@ class CodeEditor
1299
1299
  this.codeArea.root.style.height = `calc(100% - ${ this._fullVerticalOffset }px)`;
1300
1300
  }, 50 );
1301
1301
 
1302
+ if( options.callback )
1303
+ {
1304
+ options.callback.call( this, this );
1305
+ }
1302
1306
  });
1303
1307
 
1304
1308
  window.editor = this;
@@ -6,6 +6,9 @@ if(!LX) {
6
6
 
7
7
  LX.extensions.push( 'Timeline' );
8
8
 
9
+ LX.registerIcon("TimelineLock", '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path fill="none" d="M7 11V7a4 4 0 0 1 9 0v4 M5,11h13 a2 2 0 0 1 2 2 v7 a2 2 0 0 1 -2 2 h-13 a2 2 0 0 1 -2 -2 v-7 a2 2 0 0 1 2 -2 M12 16 v2"/></svg>' );
10
+ LX.registerIcon("TimelineLockOpen", '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path fill="none" d="M14 11V7a4 4 0 0 1 9 0v2 M3,11h13 a2 2 0 0 1 2 2 v7 a2 2 0 0 1 -2 2 h-13 a2 2 0 0 1 -2 -2 v-7 a2 2 0 0 1 2 -2 M8 17 h3"/></svg>' );
11
+
9
12
  /**
10
13
  * @class Timeline
11
14
  * @description Agnostic timeline, do not impose any timeline content. Renders to a canvas
@@ -64,7 +67,8 @@ class Timeline {
64
67
  this.pixelsPerSecond = 300;
65
68
  this.secondsPerPixel = 1 / this.pixelsPerSecond;
66
69
  this.animationClip = this.instantiateAnimationClip();
67
- this.trackHeight = 25;
70
+
71
+ this.trackHeight = 32;
68
72
  this.timeSeparators = [0.01, 0.1, 0.5, 1, 5];
69
73
 
70
74
  this.boxSelection = false;
@@ -301,7 +305,7 @@ class Timeline {
301
305
 
302
306
  const panel = this.leftPanel;
303
307
 
304
- panel.sameLine( 2 );
308
+ panel.sameLine();
305
309
  let titleComponent = panel.addTitle( "Tracks", { style: { background: "none"}, className: "fg-secondary text-lg px-4"} );
306
310
  let title = titleComponent.root;
307
311
 
@@ -340,10 +344,20 @@ class Timeline {
340
344
  this.setTrackState( e.node.trackData.trackIdx, e.value );
341
345
  }
342
346
  break;
343
- case LX.TreeEvent.NODE_CARETCHANGED:
344
- break;
347
+ }
348
+
349
+ if ( this.onTrackTreeEvent ){
350
+ this.onTrackTreeEvent(e);
345
351
  }
346
352
  }});
353
+
354
+ const that = this;
355
+ this.trackTreesComponent.innerTree._refresh = this.trackTreesComponent.innerTree.refresh;
356
+ this.trackTreesComponent.innerTree.refresh = function( newData, selectedId ){
357
+ this._refresh( newData, selectedId );
358
+ that.setTrackHeight( that.trackHeight );
359
+ }
360
+
347
361
  // setting a name in the addTree function adds an undesired node
348
362
  this.trackTreesComponent.name = "tracksTrees";
349
363
  p.components[this.trackTreesComponent.name] = this.trackTreesComponent;
@@ -362,12 +376,33 @@ class Timeline {
362
376
  });
363
377
 
364
378
  this.trackTreesPanel.root.scrollTop = this.currentScrollInPixels;
379
+ this.setTrackHeight( this.trackHeight );
365
380
 
366
381
  if( this.leftPanel.parent.root.classList.contains("hidden") || !this.root.parentElement ){
367
382
  return;
368
383
  }
369
384
 
370
385
  this.resizeCanvas();
386
+
387
+ this.setScroll( this.currentScroll ); // avoid scroll bugs
388
+
389
+ }
390
+
391
+ setTrackHeight( trackHeight ){
392
+ // ul list has a "gap" of 0.25rem. Compute pixel count of 0.25 rem
393
+ const gapSize = parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.25;
394
+
395
+ this.trackHeight = trackHeight = Math.max(gapSize, trackHeight);
396
+
397
+ if ( !this.trackTreesComponent ){
398
+ return;
399
+ }
400
+
401
+ trackHeight -= gapSize;
402
+ const tracks = this.trackTreesComponent.root.querySelector("ul").children;
403
+ for( let i = 0; i < tracks.length; ++i ){
404
+ tracks[i].style.height = trackHeight + "px";
405
+ }
371
406
  }
372
407
 
373
408
  /**
@@ -583,11 +618,6 @@ class Timeline {
583
618
  const scrollableHeight = this.trackTreesComponent.root.scrollHeight;
584
619
  // tree has gaps of 0.25rem (4px) inbetween entries but not in the beginning nor ending. Move half gap upwards.
585
620
  const treeOffset = this.lastTrackTreesComponentOffset = this.trackTreesComponent.innerTree.domEl.offsetTop - this.canvas.offsetTop -2;
586
-
587
- if ( this.trackTreesPanel.root.scrollHeight > 0 ){
588
- const ul = this.trackTreesComponent.innerTree.domEl.children[0];
589
- this.trackHeight = ul.children.length < 1 ? 25 : ((ul.offsetHeight+4) / ul.children.length);
590
- }
591
621
 
592
622
  //zoom
593
623
  let startTime = this.visualOriginTime; //seconds
@@ -1306,25 +1336,12 @@ class Timeline {
1306
1336
  const track = this.selectedItems[ i ];
1307
1337
  treeTracks.push({'trackData': track, 'id': track.id, 'skipVisibility': this.skipVisibility, visible: track.active, 'children':[], actions : this.skipLock ? null : [{
1308
1338
  'name':'Lock edition',
1309
- 'icon': (track.locked ? 'Lock' : 'LockOpen'),
1310
- 'swap': (track.locked ? 'LockOpen' : 'Lock'),
1311
- 'callback': (node, el) => {
1312
- let value = el.classList.contains('Lock');
1313
-
1314
- if(value) {
1315
- el.title = 'Lock edition';
1316
- el.classList.remove('Lock');
1317
- el.classList.add('LockOpen');
1318
- }
1319
- else {
1320
- el.title = 'Unlock edition';
1321
- el.classList.remove('LockOpen');
1322
- el.classList.add('Lock');
1323
- }
1324
-
1325
- node.trackData.locked = !value;
1339
+ 'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
1340
+ 'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
1341
+ 'callback': (node, swapValue, event) => {
1342
+ node.trackData.locked = !node.trackData.locked;
1326
1343
  if(this.onLockTrack){
1327
- this.onLockTrack(el, node.trackData, node)
1344
+ this.onLockTrack(node.trackData, node);
1328
1345
  }
1329
1346
  }
1330
1347
  }]});
@@ -1418,6 +1435,9 @@ class KeyFramesTimeline extends Timeline {
1418
1435
  this.defaultCurves = true; // whn a track with dim == 1 has no curves attribute, defaultCurves will be used instead. If true, track is rendered using curves
1419
1436
  this.defaultCurvesRange = [0,1]; // whn a track with dim == 1 has no curves attribute, defaultCurves will be used instead. If true, track is rendered using curves
1420
1437
 
1438
+ this.keyframeSize = this.trackHeight * 0.5; // height of keyframe
1439
+ this.keyframeSizeHovered = this.trackHeight * 0.5 + 5;
1440
+
1421
1441
  if(this.animationClip) {
1422
1442
  this.setAnimationClip(this.animationClip);
1423
1443
  }
@@ -1437,25 +1457,12 @@ class KeyFramesTimeline extends Timeline {
1437
1457
  const track = itemTracks[j];
1438
1458
  nodes.push({'trackData': track, 'id': track.id, 'skipVisibility': this.skipVisibility, visible: track.active, 'children':[], actions : this.skipLock ? null : [{
1439
1459
  'name':'Lock edition',
1440
- 'icon': (track.locked ? 'Lock' : 'LockOpen'),
1441
- 'swap': (track.locked ? 'LockOpen' : 'Lock'),
1442
- 'callback': (node, el) => {
1443
- let value = el.classList.contains('Lock');
1444
-
1445
- if(value) {
1446
- el.title = 'Lock edition';
1447
- el.classList.remove('Lock');
1448
- el.classList.add('LockOpen');
1449
- }
1450
- else {
1451
- el.title = 'Unlock edition';
1452
- el.classList.remove('LockOpen');
1453
- el.classList.add('Lock');
1454
- }
1455
-
1456
- node.trackData.locked = !value;
1460
+ 'icon': (track.locked ? 'TimelineLock' : 'TimelineLockOpen'),
1461
+ 'swap': (track.locked ? 'TimelineLockOpen' : 'TimelineLock'),
1462
+ 'callback': (node, swapValue, event) => {
1463
+ node.trackData.locked = !node.trackData.locked;
1457
1464
  if(this.onLockTrack){
1458
- this.onLockTrack(el, node.trackData, node)
1465
+ this.onLockTrack(node.trackData, node);
1459
1466
  }
1460
1467
  }
1461
1468
  }]});
@@ -1646,7 +1653,6 @@ class KeyFramesTimeline extends Timeline {
1646
1653
  }
1647
1654
  }
1648
1655
  }
1649
-
1650
1656
 
1651
1657
  this.updateLeftPanel();
1652
1658
 
@@ -1751,6 +1757,16 @@ class KeyFramesTimeline extends Timeline {
1751
1757
  return null;
1752
1758
  }
1753
1759
 
1760
+ /**
1761
+ *
1762
+ * @param {number} size pixels, height of keyframe
1763
+ * @param {number} sizeHovered optional, size in pixels when hovered
1764
+ */
1765
+ setKeyframeSize( size, sizeHovered = null ){
1766
+ this.keyframeSizeHovered = sizeHovered ?? size;
1767
+ this.keyframeSize = size;
1768
+ }
1769
+
1754
1770
  onMouseUp( e, time ) {
1755
1771
 
1756
1772
  let track = e.track;
@@ -1760,7 +1776,8 @@ class KeyFramesTimeline extends Timeline {
1760
1776
  if(e.shiftKey) {
1761
1777
  // Manual multiple selection
1762
1778
  if(!discard && track) {
1763
- const keyFrameIdx = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * 5 );
1779
+ const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
1780
+ const keyFrameIdx = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
1764
1781
  if ( keyFrameIdx > -1 ){
1765
1782
  track.selected[keyFrameIdx] ?
1766
1783
  this.deselectKeyFrame(track.trackIdx, keyFrameIdx) :
@@ -1794,7 +1811,8 @@ class KeyFramesTimeline extends Timeline {
1794
1811
  this.deselectAllKeyFrames();
1795
1812
  }
1796
1813
  if (track){
1797
- const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * 5 );
1814
+ const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
1815
+ const keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
1798
1816
  if( keyFrameIndex > -1 ) {
1799
1817
  this.processSelectionKeyFrame( track.trackIdx, keyFrameIndex, false ); // Settings this as multiple so time is not being set
1800
1818
  }
@@ -1811,7 +1829,7 @@ class KeyFramesTimeline extends Timeline {
1811
1829
  let localY = e.localY;
1812
1830
  let track = e.track;
1813
1831
 
1814
- if(e.ctrlKey && this.lastKeyFramesSelected.length) { // move keyframes
1832
+ if( (e.ctrlKey || e.altKey) && this.lastKeyFramesSelected.length) { // move keyframes
1815
1833
  this.movingKeys = true;
1816
1834
  this.canvas.style.cursor = "grab";
1817
1835
  this.canvas.classList.add('grabbing');
@@ -1819,31 +1837,28 @@ class KeyFramesTimeline extends Timeline {
1819
1837
  // Set pre-move state
1820
1838
  this.moveKeyMinTime = Infinity;
1821
1839
  const tracks = this.animationClip.tracks;
1822
- for(let selectedKey of this.lastKeyFramesSelected) {
1840
+ let lastTrackIdx = -1;
1841
+ for(let selectedKey of this.lastKeyFramesSelected) { // WARNING assumes lasKeyFramesSelected is sorted, so all keyframes of the same track are grouped
1823
1842
  let [trackIdx, keyIndex, keyTime] = selectedKey;
1824
1843
  const track = tracks[trackIdx];
1844
+
1845
+ selectedKey[2] = track.times[keyIndex]; // update original time just in case
1825
1846
 
1826
- // save track states only once
1827
- if (this.moveKeyMinTime < Infinity){
1828
- let state = this.historyUndo[this.historyUndo.length-1];
1829
- let s = 0;
1830
- for( s = 0; s < state.length; ++s){
1831
- if ( state[s].trackIdx == track.trackIdx ){ break; }
1832
- }
1833
- if( s == state.length ){
1847
+ if ( lastTrackIdx != trackIdx ){
1848
+ // save track states only once
1849
+ if (this.moveKeyMinTime < Infinity){
1834
1850
  this.saveState(track.trackIdx, true);
1851
+ }else{
1852
+ this.saveState(track.trackIdx, false);
1835
1853
  }
1836
- }else{
1837
- this.saveState(track.trackIdx, false);
1854
+ this.moveKeyMinTime = Math.min( this.moveKeyMinTime, selectedKey[2] );
1855
+ lastTrackIdx = trackIdx;
1838
1856
  }
1839
1857
 
1840
- selectedKey[2] = track.times[keyIndex]; // update original time just in case
1841
- this.moveKeyMinTime = Math.min( this.moveKeyMinTime, selectedKey[2] );
1842
1858
  }
1843
1859
 
1844
1860
  this.timeBeforeMove = this.xToTime( localX );
1845
- }
1846
- else if( e.altKey ){ // if only altkey, do not grab timeline
1861
+
1847
1862
  this.grabbing = false;
1848
1863
  this.grabbingTimeBar = false;
1849
1864
  }
@@ -1918,46 +1933,45 @@ class KeyFramesTimeline extends Timeline {
1918
1933
  }
1919
1934
  }
1920
1935
  }
1921
- if ( !e.altKey || !(e.buttons & 0x01) ){
1922
- return;
1923
- }
1924
- }
1925
-
1926
- // Track.dim == 1: move keyframes vertically (change values instead of time)
1927
- // RELIES ON SORTED ARRAY OF lastKeyFramesSelected
1928
- if ( e.altKey && e.buttons & 0x01 ){
1929
- const tracks = this.animationClip.tracks;
1930
- let lastTrackChanged = -1;
1931
- for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
1932
- const [trackIdx, keyIndex, originalKeyTime] = this.lastKeyFramesSelected[i];
1933
- track = tracks[trackIdx];
1934
- if(track.locked || track.dim != 1 || !track.curves){
1935
- continue;
1936
+
1937
+ // Track.dim == 1: move keyframes vertically (change values instead of time)
1938
+ // RELIES ON SORTED ARRAY OF lastKeyFramesSelected
1939
+ if ( e.altKey && e.buttons & 0x01 ){
1940
+ const tracks = this.animationClip.tracks;
1941
+ let lastTrackChanged = -1;
1942
+ for( let i = 0; i < this.lastKeyFramesSelected.length; ++i ){
1943
+ const [trackIdx, keyIndex, originalKeyTime] = this.lastKeyFramesSelected[i];
1944
+ track = tracks[trackIdx];
1945
+ if(track.locked || track.dim != 1 || !track.curves){
1946
+ continue;
1947
+ }
1948
+
1949
+ let value = track.values[keyIndex];
1950
+ let delta = e.deltay * this.keyValuePerPixel * (track.curvesRange[1]-track.curvesRange[0]);
1951
+ track.values[keyIndex] = Math.max(track.curvesRange[0], Math.min(track.curvesRange[1], value - delta)); // invert delta because of screen y
1952
+ track.edited[keyIndex] = true;
1953
+
1954
+ if ( this.onUpdateTrack && track.trackIdx != lastTrackChanged && lastTrackChanged > -1){ // do it only once all keyframes of the same track have been modified
1955
+ this.onUpdateTrack( [track.trackIdx] );
1956
+ }
1957
+ lastTrackChanged = track.trackIdx;
1936
1958
  }
1937
-
1938
- let value = track.values[keyIndex];
1939
- let delta = e.deltay * this.keyValuePerPixel * (track.curvesRange[1]-track.curvesRange[0]);
1940
- track.values[keyIndex] = Math.max(track.curvesRange[0], Math.min(track.curvesRange[1], value - delta)); // invert delta because of screen y
1941
- track.edited[keyIndex] = true;
1942
-
1943
- if ( this.onUpdateTrack && track.trackIdx != lastTrackChanged && lastTrackChanged > -1){ // do it only once all keyframes of the same track have been modified
1959
+ if( this.onUpdateTrack && lastTrackChanged > -1 ){ // do the last update, once the last track has been processed
1944
1960
  this.onUpdateTrack( [track.trackIdx] );
1945
1961
  }
1946
- lastTrackChanged = track.trackIdx;
1947
- }
1948
- if( this.onUpdateTrack && lastTrackChanged > -1 ){ // do the last update, once the last track has been processed
1949
- this.onUpdateTrack( [track.trackIdx] );
1962
+ return;
1950
1963
  }
1951
- return;
1952
1964
  }
1953
1965
 
1966
+
1954
1967
  if( this.grabbing && e.button != 2) {
1955
1968
 
1956
1969
  }
1957
1970
  else if(track) {
1958
1971
 
1959
1972
  this.unHoverAll();
1960
- let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * 5 );
1973
+ const thresholdPixels = this.keyframeSize * 0.5; // radius of circle (curves) or rotated square (keyframes)
1974
+ let keyFrameIndex = this.getCurrentKeyFrame( track, this.xToTime( localX ), this.secondsPerPixel * thresholdPixels );
1961
1975
  if(keyFrameIndex > -1 ) {
1962
1976
  if(track && track.locked)
1963
1977
  return;
@@ -2026,7 +2040,7 @@ class KeyFramesTimeline extends Timeline {
2026
2040
  if ( !e.track ){ return; }
2027
2041
  const values = new Float32Array( e.track.dim );
2028
2042
  values.fill(0);
2029
- this.addKeyFrames( e.track, values, [this.currentTime] );
2043
+ this.addKeyFrames( e.track.trackIdx, values, [this.currentTime] );
2030
2044
  }
2031
2045
  }
2032
2046
  );
@@ -2075,9 +2089,21 @@ class KeyFramesTimeline extends Timeline {
2075
2089
  const visibleElements = this.getVisibleItems();
2076
2090
 
2077
2091
  let offset = scrollY;
2092
+
2093
+ // compute track from which to start rendering (avoid rendering unseen tracks)
2094
+ let startElIdx = 0;
2095
+ if ( offset < -this.lastTrackTreesComponentOffset ){ // offset 0 = (0 of canvas) + track-Tree-Offset. This renders tracks under the time zone
2096
+ startElIdx = Math.floor( -(offset + this.lastTrackTreesComponentOffset) / this.trackHeight ); // how many tracks to skip
2097
+ offset += startElIdx * this.trackHeight;
2098
+ }
2099
+
2078
2100
  ctx.translate(0, offset);
2079
2101
 
2080
- for(let t = 0; t < visibleElements.length; t++) {
2102
+ // compute track to end rendering (avoid rendering unseen tracks)
2103
+ let endElIdx = startElIdx + Math.ceil( ( ctx.canvas.height - this.lastTrackTreesComponentOffset - offset ) / this.trackHeight );
2104
+ endElIdx = endElIdx > visibleElements.length ? visibleElements.length : endElIdx;
2105
+
2106
+ for(let t = startElIdx; t < endElIdx; t++) {
2081
2107
  const track = visibleElements[t].treeData.trackData;
2082
2108
 
2083
2109
  if (track){
@@ -2088,7 +2114,6 @@ class KeyFramesTimeline extends Timeline {
2088
2114
  }
2089
2115
  }
2090
2116
 
2091
- offset += trackHeight;
2092
2117
  ctx.translate(0, trackHeight);
2093
2118
  }
2094
2119
 
@@ -2115,6 +2140,8 @@ class KeyFramesTimeline extends Timeline {
2115
2140
  const keyframes = track.times;
2116
2141
  const startTime = this.visualTimeRange[0];
2117
2142
  const endTime = this.visualTimeRange[1] + 0.0000001;
2143
+ const defaultPointSize = this.keyframeSize / Math.SQRT2; // pythagoras with equal sides h2 = c2 + c2 = 2 * c2
2144
+ const hoverPointSize = this.keyframeSizeHovered / Math.SQRT2;
2118
2145
 
2119
2146
  for(let j = 0; j < keyframes.length; ++j)
2120
2147
  {
@@ -2124,7 +2151,7 @@ class KeyFramesTimeline extends Timeline {
2124
2151
  }
2125
2152
 
2126
2153
  let keyframePosX = this.timeToX( time );
2127
- let size = trackHeight * 0.3;
2154
+ let size = defaultPointSize;
2128
2155
 
2129
2156
  if(!this.active || track.active == false) {
2130
2157
  ctx.fillStyle = Timeline.KEYFRAME_COLOR_INACTIVE;
@@ -2133,7 +2160,7 @@ class KeyFramesTimeline extends Timeline {
2133
2160
  ctx.fillStyle = Timeline.KEYFRAME_COLOR_LOCK;
2134
2161
  }
2135
2162
  else if(track.hovered[j]) {
2136
- size = trackHeight * 0.45;
2163
+ size = hoverPointSize;
2137
2164
  ctx.fillStyle = Timeline.KEYFRAME_COLOR_HOVERED;
2138
2165
  }
2139
2166
  else if(track.selected[j]) {
@@ -2166,8 +2193,8 @@ class KeyFramesTimeline extends Timeline {
2166
2193
  ctx.globalAlpha = 1;
2167
2194
  const keyframes = track.times;
2168
2195
  const values = track.values;
2169
- const defaultPointSize = 5;
2170
- const hoverPointSize = 7;
2196
+ const defaultPointSize = this.keyframeSize * 0.5; // radius
2197
+ const hoverPointSize = this.keyframeSizeHovered * 0.5; // radius
2171
2198
  const valueRange = track.curvesRange; //[min, max]
2172
2199
  const displayRange = trackHeight - defaultPointSize * 2;
2173
2200
  const startTime = this.visualTimeRange[0];
@@ -2179,7 +2206,7 @@ class KeyFramesTimeline extends Timeline {
2179
2206
  if ( keyframes.length > 1){
2180
2207
  let startPosX = this.timeToX( keyframes[0] );
2181
2208
  let startValue = values[0];
2182
- startValue = ((startValue - valueRange[0]) / (valueRange[1] - valueRange[0])) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2209
+ startValue = LX.clamp((startValue - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2183
2210
  ctx.moveTo( startPosX, startValue );
2184
2211
 
2185
2212
  for(let j = 1; j < keyframes.length; ++j){
@@ -2187,7 +2214,7 @@ class KeyFramesTimeline extends Timeline {
2187
2214
  let time = keyframes[j];
2188
2215
  let keyframePosX = this.timeToX( time );
2189
2216
  let value = values[j];
2190
- value = ((value - valueRange[0]) / (valueRange[1] - valueRange[0])) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2217
+ value = LX.clamp((value - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2191
2218
 
2192
2219
  if( time < startTime ){
2193
2220
  ctx.moveTo( keyframePosX, value );
@@ -2199,7 +2226,7 @@ class KeyFramesTimeline extends Timeline {
2199
2226
  let dt = keyframePosX - lastKeyframePosX;
2200
2227
  if ( dt > 0 ){
2201
2228
  let lastValue = values[j-1];
2202
- lastValue = ((lastValue - valueRange[0]) / (valueRange[1] - valueRange[0])) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2229
+ lastValue = LX.clamp((lastValue - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) * (-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2203
2230
  let f = (this.timeToX( endTime ) - lastKeyframePosX) / dt;
2204
2231
  ctx.lineTo( lastKeyframePosX + dt * f, lastValue * (1-f) + value * f );
2205
2232
  }
@@ -2239,7 +2266,7 @@ class KeyFramesTimeline extends Timeline {
2239
2266
  ctx.fillStyle = Timeline.KEYFRAME_COLOR
2240
2267
 
2241
2268
  let value = values[j];
2242
- value = ((value - valueRange[0]) / (valueRange[1] - valueRange[0])) *(-displayRange) + (trackHeight - defaultPointSize); // normalize and offset
2269
+ value = LX.clamp((value - valueRange[0]) / (valueRange[1] - valueRange[0]), 0,1) *(-displayRange) + (trackHeight - defaultPointSize); // normalize, clamp and offset
2243
2270
 
2244
2271
  ctx.beginPath();
2245
2272
  ctx.arc( keyframePosX, value, size, 0, Math.PI * 2);