Browse Source

Merge branch 'master' of https://github.com/BabylonJS/Babylon.js into master

David Catuhe 4 years ago
parent
commit
85f0c55759
20 changed files with 4118 additions and 3108 deletions
  1. 1 0
      dist/preview release/what's new.md
  2. 38 33
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx
  3. 85 50
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/anchorSvgPoint.tsx
  4. 1140 363
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx
  5. 43 16
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationGroupPropertyGridComponent.tsx
  6. 94 18
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationListTree.tsx
  7. 211 316
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx
  8. 78 35
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/controls.tsx
  9. 1204 1154
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss
  10. 294 272
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx
  11. 174 124
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar.tsx
  12. 93 86
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/keyframeSvgPoint.tsx
  13. 104 114
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/loadsnippet.tsx
  14. 22 25
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/notification.tsx
  15. 3 0
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx
  16. 155 163
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/saveSnippet.tsx
  17. 37 36
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/scale-label.tsx
  18. 114 98
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/svgDraggableArea.tsx
  19. 80 131
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/targetedAnimationPropertyGridComponent.tsx
  20. 148 74
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

+ 1 - 0
dist/preview release/what's new.md

@@ -330,6 +330,7 @@
 - Changed use of mousemove to pointermove in freeCameraMouseInput and flyCameraMouseInput to fix issue with Firefox ([PolygonalSun](https://github.com/PolygonalSun))
 
 ## Breaking changes
+
 - `FollowCamera.target` was renamed to `FollowCamera.meshTarget` to not be in conflict with `TargetCamera.target` ([Deltakosh](https://github.com/deltakosh))
 - `EffectRenderer.render` now takes a `RenderTargetTexture` or an `InternalTexture` as the output texture and only a single `EffectWrapper` for its first argument ([Popov72](https://github.com/Popov72))
 - Sound's `updateOptions` takes `options.length` and `options.offset` as seconds and not milliseconds ([RaananW](https://github.com/RaananW))

+ 38 - 33
inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx

@@ -15,11 +15,14 @@ interface IAddAnimationProps {
     onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
     setNotificationMessage: (message: string) => void;
     finishedUpdate: () => void;
-    addedNewAnimation: () => void;
+    addedNewAnimation: (animation: Animation) => void;
     fps: number;
     selectedToUpdate?: Animation | undefined;
 }
 
+/**
+ * Controls the creation of a new animation
+ */
 export class AddAnimation extends React.Component<
     IAddAnimationProps,
     {
@@ -47,17 +50,17 @@ export class AddAnimation extends React.Component<
         };
     }
 
-    componentWillReceiveProps(nextProps: IAddAnimationProps) {
-        if (nextProps.selectedToUpdate !== undefined && nextProps.selectedToUpdate !== this.props.selectedToUpdate) {
-            this.setState(this.setInitialState(nextProps.selectedToUpdate));
+    componentDidUpdate(prevProps: IAddAnimationProps, prevState: any) {
+        if (this.props.selectedToUpdate !== undefined && this.props.selectedToUpdate !== prevProps.selectedToUpdate) {
+            this.setState(this.setInitialState(this.props.selectedToUpdate));
         } else {
-            if (nextProps.isOpen === true && nextProps.isOpen !== this.props.isOpen) {
+            if (this.props.isOpen === true && this.props.isOpen !== prevProps.isOpen) {
                 this.setState(this.setInitialState());
             }
         }
     }
 
-    updateAnimation() {
+    updateAnimation = () => {
         if (this.props.selectedToUpdate !== undefined) {
             const oldNameValue = this.props.selectedToUpdate.name;
             this.props.selectedToUpdate.name = this.state.animationName;
@@ -73,7 +76,7 @@ export class AddAnimation extends React.Component<
 
             this.props.finishedUpdate();
         }
-    }
+    };
 
     getTypeAsString(type: number) {
         switch (type) {
@@ -96,7 +99,7 @@ export class AddAnimation extends React.Component<
         }
     }
 
-    addAnimation() {
+    addAnimation = () => {
         if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
             let matchTypeTargetProperty = this.state.animationTargetProperty.split(".");
             let animationDataType = this.state.animationType;
@@ -109,22 +112,22 @@ export class AddAnimation extends React.Component<
                 if (match) {
                     switch (match.constructor.name) {
                         case "Vector2":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR2 ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_VECTOR2;
                             break;
                         case "Vector3":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR3 ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_VECTOR3;
                             break;
                         case "Quaternion":
-                            animationDataType === Animation.ANIMATIONTYPE_QUATERNION ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_QUATERNION;
                             break;
                         case "Color3":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR3 ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_COLOR3;
                             break;
                         case "Color4":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR4 ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_COLOR4;
                             break;
                         case "Size":
-                            animationDataType === Animation.ANIMATIONTYPE_SIZE ? (matched = true) : (matched = false);
+                            matched = animationDataType === Animation.ANIMATIONTYPE_SIZE;
                             break;
                     }
                 } else {
@@ -162,7 +165,7 @@ export class AddAnimation extends React.Component<
                         const updatedCollection = [...this.props.entity.animations, animation];
                         this.raiseOnPropertyChanged(updatedCollection, store);
                         this.props.entity.animations = updatedCollection;
-                        this.props.addedNewAnimation();
+                        this.props.addedNewAnimation(animation);
                         //Cleaning form fields
                         this.setState({
                             animationName: "",
@@ -179,7 +182,7 @@ export class AddAnimation extends React.Component<
         } else {
             this.props.setNotificationMessage(`You need to provide a name and target property.`);
         }
-    }
+    };
 
     raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
         if (!this.props.onPropertyChangedObservable) {
@@ -207,49 +210,51 @@ export class AddAnimation extends React.Component<
         });
     }
 
-    handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
+    handlePathChange = (event: React.ChangeEvent<HTMLInputElement>) => {
         event.preventDefault();
-        this.setState({ animationName: event.target.value.trim() });
-    }
+        this.setState({ animationTargetPath: event.target.value.trim() });
+    };
 
-    handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
+    handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
         event.preventDefault();
-        this.setState({ animationTargetPath: event.target.value.trim() });
-    }
+        this.setState({ animationName: event.target.value.trim() });
+    };
 
-    handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    handleTypeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
         event.preventDefault();
         this.setState({ animationType: parseInt(event.target.value) });
-    }
+    };
 
-    handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
+    handlePropertyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
         event.preventDefault();
         this.setState({ animationTargetProperty: event.target.value });
-    }
+    };
 
-    handleLoopModeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    handleLoopModeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
         event.preventDefault();
         this.setState({ loopMode: parseInt(event.target.value) });
-    }
+    };
 
     render() {
+        const confirmLabel = this.state.isUpdating ? "Update" : "Create";
+        const confirmHandleOnClick = this.state.isUpdating ? this.updateAnimation : this.addAnimation;
         return (
             <div className="new-animation" style={{ display: this.props.isOpen ? "block" : "none" }}>
                 <div className="sub-content">
                     <div className="label-input">
                         <label>Display Name</label>
-                        <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
+                        <input type="text" value={this.state.animationName} onChange={this.handleNameChange}></input>
                     </div>
                     {this.state.isUpdating ? null : (
                         <div className="label-input">
                             <label>Property</label>
-                            <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
+                            <input type="text" value={this.state.animationTargetProperty} onChange={this.handlePropertyChange}></input>
                         </div>
                     )}
                     {this.state.isUpdating ? null : (
                         <div className="label-input">
                             <label>Type</label>
-                            <select onChange={(e) => this.handleTypeChange(e)} value={this.state.animationType}>
+                            <select onChange={this.handleTypeChange} value={this.state.animationType}>
                                 {/* <option value={Animation.ANIMATIONTYPE_COLOR3}>Color3</option>
                 <option value={Animation.ANIMATIONTYPE_COLOR4}>Color4</option> */}
                                 <option value={Animation.ANIMATIONTYPE_FLOAT}>Float</option>
@@ -263,14 +268,14 @@ export class AddAnimation extends React.Component<
                     )}
                     <div className="label-input">
                         <label>Loop Mode</label>
-                        <select onChange={(e) => this.handleLoopModeChange(e)} value={this.state.loopMode}>
+                        <select onChange={this.handleLoopModeChange} value={this.state.loopMode}>
                             <option value={Animation.ANIMATIONLOOPMODE_CYCLE}>Cycle</option>
                             <option value={Animation.ANIMATIONLOOPMODE_RELATIVE}>Relative</option>
                             <option value={Animation.ANIMATIONLOOPMODE_CONSTANT}>Constant</option>
                         </select>
                     </div>
                     <div className="confirm-buttons">
-                        <ButtonLineComponent label={this.state.isUpdating ? "Update" : "Create"} onClick={this.state.isUpdating ? () => this.updateAnimation() : () => this.addAnimation()} />
+                        <ButtonLineComponent label={confirmLabel} onClick={confirmHandleOnClick} />
                         {this.props.entity.animations?.length !== 0 ? <ButtonLineComponent label={"Cancel"} onClick={this.props.close} /> : null}
                     </div>
                 </div>

+ 85 - 50
inspector/src/components/actionTabs/tabs/propertyGrids/animations/anchorSvgPoint.tsx

@@ -1,57 +1,92 @@
-import * as React from 'react';
-import { Vector2 } from 'babylonjs/Maths/math.vector';
+import * as React from "react";
+import { Vector2 } from "babylonjs/Maths/math.vector";
 
 interface IAnchorSvgPointProps {
-  control: Vector2;
-  anchor: Vector2;
-  active: boolean;
-  type: string;
-  index: string;
-  selected: boolean;
-  selectControlPoint: (id: string) => void;
+    control: Vector2;
+    anchor: Vector2;
+    active: boolean;
+    type: string;
+    index: string;
+    selected: boolean;
+    selectControlPoint: (id: string) => void;
+    framesInCanvasView: { from: number; to: number };
 }
 
-export class AnchorSvgPoint extends React.Component<IAnchorSvgPointProps> {
-  constructor(props: IAnchorSvgPointProps) {
-    super(props);
-  }
+/**
+ * Renders the control point to a keyframe.
+ */
+export class AnchorSvgPoint extends React.Component<IAnchorSvgPointProps, { visiblePoint: Vector2 }> {
+    constructor(props: IAnchorSvgPointProps) {
+        super(props);
+        this.state = { visiblePoint: this.setVisiblePoint() };
+    }
 
-  select() {
-    this.props.selectControlPoint(this.props.type);
-  }
+    componentDidUpdate(prevProps: IAnchorSvgPointProps, prevState: any) {
+        if (prevProps.control !== this.props.control) {
+            this.setState({ visiblePoint: this.setVisiblePoint() });
+        }
+    }
 
-  render() {
-    return (
-      <>
-        <svg
-          x={this.props.control.x}
-          y={this.props.control.y}
-          style={{ overflow: 'visible' }}
-          onClick={() => this.select()}
-        >
-          <circle
-            type={this.props.type}
-            data-id={this.props.index}
-            className={`draggable control-point ${
-              this.props.active ? 'active' : ''
-            }`}
-            cx='0'
-            cy='0'
-            r='0.7%'
-            stroke='white'
-            strokeWidth={this.props.selected ? 0 : 0}
-            fill={this.props.active ? '#e9db1e' : 'white'}
-          />
-        </svg>
-        <line
-          className={`control-point ${this.props.active ? 'active' : ''}`}
-          x1={this.props.anchor.x}
-          y1={this.props.anchor.y}
-          x2={this.props.control.x}
-          y2={this.props.control.y}
-          strokeWidth='0.8%'
-        />
-      </>
-    );
-  }
+    select = () => {
+        this.props.selectControlPoint(this.props.type);
+    };
+
+    setVisiblePoint() {
+        const quarterDistance = (this.props.framesInCanvasView.to - this.props.framesInCanvasView.from) / 10;
+        const distanceOnFlat = Math.abs(this.props.anchor.x - this.props.control.x);
+        const currentDistance = Vector2.Distance(this.props.anchor, this.props.control);
+        const percentageChange = ((currentDistance - distanceOnFlat) * 100) / currentDistance;
+        const updateAmount = quarterDistance - (quarterDistance * percentageChange) / 100;
+        return Vector2.Lerp(this.props.anchor, this.props.control, updateAmount);
+    }
+
+    render() {
+        const visibleCircleClass = `draggable control-point ${this.props.active ? "active" : ""}`;
+        const nonVisibleCircleClass = `control-point ${this.props.active ? "active" : ""}`;
+        const strokeVisibleCircle = this.props.selected ? 1 : 0;
+        const visibleCircle = this.props.selected ? "#ffc017" : "black";
+        return (
+            <>
+                <line
+                    className={`control-point ${this.props.active ? "active" : ""}`}
+                    x1={this.props.anchor.x}
+                    y1={this.props.anchor.y}
+                    x2={this.state.visiblePoint.x}
+                    y2={this.state.visiblePoint.y}
+                    strokeWidth="0.8%"
+                />
+                <svg
+                    x={this.state.visiblePoint.x}
+                    y={this.state.visiblePoint.y}
+                    style={{ overflow: "visible" }}
+                    onClick={this.select}
+                >
+                    <circle
+                        type={this.props.type}
+                        data-id={this.props.index}
+                        className={visibleCircleClass}
+                        cx="0"
+                        cy="0"
+                        r="0.75%"
+                        stroke="aqua"
+                        strokeWidth={strokeVisibleCircle}
+                        fill={visibleCircle}
+                    />
+                </svg>
+                <svg x={this.props.control.x} y={this.props.control.y} style={{ overflow: "visible", display: "none" }}>
+                    <circle
+                        type={this.props.type}
+                        data-id={this.props.index}
+                        className={nonVisibleCircleClass}
+                        cx="0"
+                        cy="0"
+                        r="0.7%"
+                        stroke="white"
+                        strokeWidth={0}
+                        fill={"white"}
+                    />
+                </svg>
+            </>
+        );
+    }
 }

File diff suppressed because it is too large
+ 1140 - 363
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx


+ 43 - 16
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationGroupPropertyGridComponent.tsx

@@ -11,18 +11,21 @@ import { LineContainerComponent } from "../../../lineContainerComponent";
 import { TextLineComponent } from "../../../lines/textLineComponent";
 import { SliderLineComponent } from "../../../lines/sliderLineComponent";
 import { LockObject } from "../lockObject";
-import { GlobalState } from '../../../../globalState';
-import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
+import { GlobalState } from "../../../../globalState";
+import { TextInputLineComponent } from "../../../lines/textInputLineComponent";
 
 interface IAnimationGroupGridComponentProps {
     globalState: GlobalState;
-    animationGroup: AnimationGroup,
-    scene: Scene,
-    lockObject: LockObject,
-    onPropertyChangedObservable?: Observable<PropertyChangedEvent>
+    animationGroup: AnimationGroup;
+    scene: Scene;
+    lockObject: LockObject;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
 }
 
-export class AnimationGroupGridComponent extends React.Component<IAnimationGroupGridComponentProps, { playButtonText: string, currentFrame: number }> {
+export class AnimationGroupGridComponent extends React.Component<
+    IAnimationGroupGridComponentProps,
+    { playButtonText: string; currentFrame: number }
+> {
     private _onAnimationGroupPlayObserver: Nullable<Observer<AnimationGroup>>;
     private _onAnimationGroupPauseObserver: Nullable<Observer<AnimationGroup>>;
     private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
@@ -44,7 +47,6 @@ export class AnimationGroupGridComponent extends React.Component<IAnimationGroup
     }
 
     disconnect(animationGroup: AnimationGroup) {
-
         if (this._onAnimationGroupPlayObserver) {
             animationGroup.onAnimationGroupPlayObservable.remove(this._onAnimationGroupPlayObserver);
             this._onAnimationGroupPlayObserver = null;
@@ -105,7 +107,7 @@ export class AnimationGroupGridComponent extends React.Component<IAnimationGroup
             animationGroup.pause();
         } else {
             this.setState({ playButtonText: "Pause" });
-            this.props.scene.animationGroups.forEach(grp => grp.pause());
+            this.props.scene.animationGroups.forEach((grp) => grp.pause());
             animationGroup.play(true);
         }
     }
@@ -127,21 +129,46 @@ export class AnimationGroupGridComponent extends React.Component<IAnimationGroup
     render() {
         const animationGroup = this.props.animationGroup;
 
-        const playButtonText = animationGroup.isPlaying ? "Pause" : "Play"
+        const playButtonText = animationGroup.isPlaying ? "Pause" : "Play";
 
         return (
-            <div className="pane">                
+            <div className="pane">
                 <LineContainerComponent globalState={this.props.globalState} title="GENERAL">
                     <TextLineComponent label="Class" value={animationGroup.getClassName()} />
-                    <TextInputLineComponent lockObject={this.props.lockObject} label="Name" target={animationGroup} propertyName="name" onPropertyChangedObservable={this.props.onPropertyChangedObservable}/>
+                    <TextInputLineComponent
+                        lockObject={this.props.lockObject}
+                        label="Name"
+                        target={animationGroup}
+                        propertyName="name"
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable}
+                    />
                 </LineContainerComponent>
                 <LineContainerComponent globalState={this.props.globalState} title="CONTROLS">
                     <ButtonLineComponent label={playButtonText} onClick={() => this.playOrPause()} />
-                    <SliderLineComponent label="Speed ratio" minimum={0} maximum={10} step={0.1} target={animationGroup} propertyName="speedRatio" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                    <SliderLineComponent ref={this.timelineRef} label="Current frame" minimum={animationGroup.from} maximum={animationGroup.to} step={(animationGroup.to - animationGroup.from) / 1000.0} directValue={this.state.currentFrame} onInput={value => this.onCurrentFrameChange(value)} />
+                    <SliderLineComponent
+                        label="Speed ratio"
+                        minimum={0}
+                        maximum={10}
+                        step={0.1}
+                        target={animationGroup}
+                        propertyName="speedRatio"
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable}
+                    />
+                    <SliderLineComponent
+                        ref={this.timelineRef}
+                        label="Current frame"
+                        minimum={animationGroup.from}
+                        maximum={animationGroup.to}
+                        step={(animationGroup.to - animationGroup.from) / 1000.0}
+                        directValue={this.state.currentFrame}
+                        onInput={(value) => this.onCurrentFrameChange(value)}
+                    />
                 </LineContainerComponent>
                 <LineContainerComponent globalState={this.props.globalState} title="INFOS">
-                    <TextLineComponent label="Animation count" value={animationGroup.targetedAnimations.length.toString()} />
+                    <TextLineComponent
+                        label="Animation count"
+                        value={animationGroup.targetedAnimations.length.toString()}
+                    />
                     <TextLineComponent label="From" value={animationGroup.from.toFixed(2)} />
                     <TextLineComponent label="To" value={animationGroup.to.toFixed(2)} />
                     <TextLineComponent label="Unique ID" value={animationGroup.uniqueId.toString()} />
@@ -149,4 +176,4 @@ export class AnimationGroupGridComponent extends React.Component<IAnimationGroup
             </div>
         );
     }
-}
+}

+ 94 - 18
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationListTree.tsx

@@ -45,37 +45,92 @@ interface ItemCoordinate {
     coordinate: SelectedCoordinate;
 }
 
+/**
+ * Renders a list of current animations.
+ */
 export class AnimationListTree extends React.Component<
     IAnimationListTreeProps,
     {
         selectedCoordinate: SelectedCoordinate;
         selectedAnimation: number;
         animationList: Item[] | null;
+        animations: Nullable<Animation[]> | Animation;
     }
 > {
     constructor(props: IAnimationListTreeProps) {
         super(props);
 
+        const animations = this.props.entity instanceof TargetedAnimation ? (this.props.entity as TargetedAnimation).animation : (this.props.entity as IAnimatable).animations;
+
         this.state = {
             selectedCoordinate: 0,
             selectedAnimation: 0,
             animationList: this.generateList(),
+            animations: animations,
         };
     }
 
-    deleteAnimation() {
+    componentDidUpdate(prevProps: IAnimationListTreeProps) {
+        if (this.props.entity instanceof TargetedAnimation) {
+            if ((this.props.entity as TargetedAnimation).animation !== (prevProps.entity as TargetedAnimation).animation) {
+                this.setState({
+                    animationList: this.generateList(),
+                    animations: (this.props.entity as TargetedAnimation).animation,
+                });
+            }
+        } else {
+            if ((this.props.entity as IAnimatable).animations !== (prevProps.entity as IAnimatable).animations) {
+                this.setState({
+                    animationList: this.generateList(),
+                    animations: (this.props.entity as IAnimatable).animations,
+                });
+            }
+        }
+    }
+
+    deleteAnimation = () => {
         let currentSelected = this.props.selected;
         if (this.props.entity instanceof TargetedAnimation) {
             console.log("no animation remove allowed");
         } else {
             let animations = (this.props.entity as IAnimatable).animations;
             if (animations) {
-                let updatedAnimations = animations.filter((anim) => anim !== currentSelected);
-                (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
-                this.props.deselectAnimation();
-                this.setState({ animationList: this.generateList() });
+                if ((this.props.entity as IAnimatable).animations !== null) {
+                    let updatedAnimations = animations.filter((anim) => anim !== currentSelected);
+
+                    const store = (this.props.entity as IAnimatable).animations!;
+                    this.raiseOnPropertyChanged(updatedAnimations, store);
+                    (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
+                    if (updatedAnimations.length !== 0) {
+                        this.setState(
+                            {
+                                animationList: this.generateList(),
+                                animations: (this.props.entity as IAnimatable).animations,
+                            },
+                            () => {
+                                this.props.deselectAnimation();
+                            }
+                        );
+                    } else {
+                        this.props.deselectAnimation();
+                        this.props.empty();
+                    }
+                }
             }
         }
+    };
+
+    raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
+        if (!this.props.onPropertyChangedObservable) {
+            return;
+        }
+
+        this.props.onPropertyChangedObservable.notifyObservers({
+            object: this.props.entity,
+            property: "animations",
+            value: newValue,
+            initialValue: previousValue,
+        });
     }
 
     generateList() {
@@ -114,22 +169,35 @@ export class AnimationListTree extends React.Component<
     }
 
     coordinateItem(i: number, animation: Animation, coordinate: string, color: string, selectedCoordinate: SelectedCoordinate) {
+        const setSelectedCoordinate = () => this.setSelectedCoordinate(animation, selectedCoordinate, i);
+        const handleClass = `handle-indicator ${this.state.selectedCoordinate === selectedCoordinate && this.state.selectedAnimation === i ? "show" : "hide"}`;
         return (
-            <li key={`${i}_${coordinate}`} id={`${i}_${coordinate}`} className="property" style={{ color: color }} onClick={() => this.setSelectedCoordinate(animation, selectedCoordinate, i)}>
-                <div className={`handle-indicator ${this.state.selectedCoordinate === selectedCoordinate && this.state.selectedAnimation === i ? "show" : "hide"}`}></div>
+            <li key={`${i}_${coordinate}`} id={`${i}_${coordinate}`} className="property" style={{ color: color }} onClick={setSelectedCoordinate}>
+                <div className={handleClass}></div>
                 {animation.targetProperty} {coordinate.toUpperCase()}
             </li>
         );
     }
 
     typeAnimationItem(animation: Animation, i: number, childrenElements: ItemCoordinate[]) {
+        const editAnimation = () => this.props.editAnimation(animation);
+        const selectAnimation = () => this.props.selectAnimation(animation);
+        const toggle = () => this.toggleProperty(i);
         return (
             <li className={this.props.selected && this.props.selected.name === animation.name ? "property sub active" : "property sub"} key={i}>
-                <div className={`animation-arrow ${this.state.animationList && this.state.animationList[i].open ? "" : "flip"}`} onClick={() => this.toggleProperty(i)}></div>
-                <p onClick={() => this.props.selectAnimation(animation)}>{animation.targetProperty}</p>
-                <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={() => this.props.editAnimation(animation)} />
-                {!(this.props.entity instanceof TargetedAnimation) ? this.props.selected && this.props.selected.name === animation.name ? <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={() => this.deleteAnimation()} /> : <div className="spacer"></div> : null}
-                <ul className={`sub-list ${this.state.animationList && this.state.animationList[i].open ? "" : "hidden"}`}>{childrenElements.map((c) => this.coordinateItem(i, animation, c.id, c.color, c.coordinate))}</ul>
+                <div className={`animation-arrow ${this.state.animationList && this.state.animationList[i].open ? "" : "flip"}`} onClick={toggle}></div>
+                <p onClick={selectAnimation}>{animation.targetProperty}</p>
+                <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={editAnimation} />
+                {!((this.props.entity as TargetedAnimation).getClassName() === "TargetedAnimation") ? (
+                    this.props.selected && this.props.selected.name === animation.name ? (
+                        <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={this.deleteAnimation} />
+                    ) : (
+                        <div className="spacer"></div>
+                    )
+                ) : null}
+                <ul className={`sub-list ${this.state.animationList && this.state.animationList[i].open ? "" : "hidden"}`}>
+                    {childrenElements.map((c) => this.coordinateItem(i, animation, c.id, c.color, c.coordinate))}
+                </ul>
             </li>
         );
     }
@@ -137,12 +205,20 @@ export class AnimationListTree extends React.Component<
     setListItem(animation: Animation, i: number) {
         switch (animation.dataType) {
             case Animation.ANIMATIONTYPE_FLOAT:
+                const editAnimation = () => this.props.editAnimation(animation);
+                const selectAnimation = () => this.props.selectAnimation(animation, 0);
                 return (
-                    <li className={this.props.selected && this.props.selected.name === animation.name ? "property active" : "property"} key={i} onClick={() => this.props.selectAnimation(animation, 0)}>
+                    <li className={this.props.selected && this.props.selected.name === animation.name ? "property active" : "property"} key={i} onClick={selectAnimation}>
                         <div className={`animation-bullet`}></div>
                         <p>{animation.targetProperty}</p>
-                        <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={() => this.props.editAnimation(animation)} />
-                        {!(this.props.entity instanceof TargetedAnimation) ? this.props.selected && this.props.selected.name === animation.name ? <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={() => this.deleteAnimation()} /> : <div className="spacer"></div> : null}
+                        <IconButtonLineComponent tooltip="Options" icon="small animation-options" onClick={editAnimation} />
+                        {!(this.props.entity instanceof TargetedAnimation) ? (
+                            this.props.selected && this.props.selected.name === animation.name ? (
+                                <IconButtonLineComponent tooltip="Remove" icon="small animation-delete" onClick={this.deleteAnimation} />
+                            ) : (
+                                <div className="spacer"></div>
+                            )
+                        ) : null}
                     </li>
                 );
             case Animation.ANIMATIONTYPE_VECTOR2:
@@ -199,9 +275,9 @@ export class AnimationListTree extends React.Component<
             <div className="object-tree">
                 <ul>
                     {this.props.isTargetedAnimation
-                        ? this.setListItem((this.props.entity as TargetedAnimation).animation, 0)
-                        : (this.props.entity as IAnimatable).animations &&
-                          (this.props.entity as IAnimatable).animations?.map((animation, i) => {
+                        ? this.setListItem(this.state.animations as Animation, 0)
+                        : this.state.animations &&
+                          (this.state.animations as Animation[]).map((animation, i) => {
                               return this.setListItem(animation, i);
                           })}
                 </ul>

+ 211 - 316
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx

@@ -1,349 +1,244 @@
-import * as React from 'react';
-
-import { Observable, Observer } from 'babylonjs/Misc/observable';
-import { Scene } from 'babylonjs/scene';
-
-import { PropertyChangedEvent } from '../../../../propertyChangedEvent';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { LineContainerComponent } from '../../../lineContainerComponent';
-import { SliderLineComponent } from '../../../lines/sliderLineComponent';
-import { LockObject } from '../lockObject';
-import { GlobalState } from '../../../../globalState';
-import { Animation } from 'babylonjs/Animations/animation';
-import { Animatable } from 'babylonjs/Animations/animatable';
-import { AnimationPropertiesOverride } from 'babylonjs/Animations/animationPropertiesOverride';
-import { AnimationRange } from 'babylonjs/Animations/animationRange';
-import { CheckBoxLineComponent } from '../../../lines/checkBoxLineComponent';
-import { Nullable } from 'babylonjs/types';
-import { FloatLineComponent } from '../../../lines/floatLineComponent';
-import { TextLineComponent } from '../../../lines/textLineComponent';
-import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-import { AnimationCurveEditorComponent } from '../animations/animationCurveEditorComponent';
-import { PopupComponent } from '../../../../popupComponent';
+import * as React from "react";
+
+import { Observable, Observer } from "babylonjs/Misc/observable";
+import { Scene } from "babylonjs/scene";
+
+import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
+import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
+import { LineContainerComponent } from "../../../lineContainerComponent";
+import { SliderLineComponent } from "../../../lines/sliderLineComponent";
+import { LockObject } from "../lockObject";
+import { GlobalState } from "../../../../globalState";
+import { Animation } from "babylonjs/Animations/animation";
+import { Animatable } from "babylonjs/Animations/animatable";
+import { AnimationPropertiesOverride } from "babylonjs/Animations/animationPropertiesOverride";
+import { AnimationRange } from "babylonjs/Animations/animationRange";
+import { CheckBoxLineComponent } from "../../../lines/checkBoxLineComponent";
+import { Nullable } from "babylonjs/types";
+import { FloatLineComponent } from "../../../lines/floatLineComponent";
+import { TextLineComponent } from "../../../lines/textLineComponent";
+import { IAnimatable } from "babylonjs/Animations/animatable.interface";
+import { AnimationCurveEditorComponent } from "../animations/animationCurveEditorComponent";
+import { PopupComponent } from "../../../../popupComponent";
 
 interface IAnimationGridComponentProps {
-  globalState: GlobalState;
-  animatable: IAnimatable;
-  scene: Scene;
-  lockObject: LockObject;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    globalState: GlobalState;
+    animatable: IAnimatable;
+    scene: Scene;
+    lockObject: LockObject;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
 }
 
-export class AnimationGridComponent extends React.Component<
-  IAnimationGridComponentProps,
-  { currentFrame: number }
-> {
-  private _animations: Nullable<Animation[]> = null;
-  private _ranges: AnimationRange[];
-  private _mainAnimatable: Nullable<Animatable>;
-  private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
-  private _isPlaying = false;
-  private timelineRef: React.RefObject<SliderLineComponent>;
-  private _isCurveEditorOpen = false;
-  private _animationControl = {
-    from: 0,
-    to: 0,
-    loop: false,
-  };
+export class AnimationGridComponent extends React.Component<IAnimationGridComponentProps, { currentFrame: number }> {
+    private _animations: Nullable<Animation[]> = null;
+    private _ranges: AnimationRange[];
+    private _mainAnimatable: Nullable<Animatable>;
+    private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
+    private _isPlaying = false;
+    private timelineRef: React.RefObject<SliderLineComponent>;
+    private _isCurveEditorOpen = false;
+    private _animationControl = {
+        from: 0,
+        to: 0,
+        loop: false,
+    };
+
+    constructor(props: IAnimationGridComponentProps) {
+        super(props);
+
+        this.state = { currentFrame: 0 };
+
+        const animatableAsAny = this.props.animatable as any;
+
+        this._ranges = animatableAsAny.getAnimationRanges ? animatableAsAny.getAnimationRanges() : [];
+        if (animatableAsAny.getAnimatables) {
+            const animatables = animatableAsAny.getAnimatables();
+            this._animations = new Array<Animation>();
+
+            animatables.forEach((animatable: IAnimatable) => {
+                if (animatable.animations) {
+                    this._animations!.push(...animatable.animations);
+                }
+            });
+
+            if (animatableAsAny.animations) {
+                this._animations!.push(...animatableAsAny.animations);
+            }
 
-  constructor(props: IAnimationGridComponentProps) {
-    super(props);
+            // Extract from and to
+            if (this._animations && this._animations.length) {
+                this._animations.forEach((animation) => {
+                    let keys = animation.getKeys();
 
-    this.state = { currentFrame: 0 };
+                    if (keys && keys.length > 0) {
+                        if (keys[0].frame < this._animationControl.from) {
+                            this._animationControl.from = keys[0].frame;
+                        }
+                        const lastKeyIndex = keys.length - 1;
+                        if (keys[lastKeyIndex].frame > this._animationControl.to) {
+                            this._animationControl.to = keys[lastKeyIndex].frame;
+                        }
+                    }
+                });
+            }
+        }
 
-    const animatableAsAny = this.props.animatable as any;
+        this.timelineRef = React.createRef();
+    }
 
-    this._ranges = animatableAsAny.getAnimationRanges
-      ? animatableAsAny.getAnimationRanges()
-      : [];
-    if (animatableAsAny.getAnimatables) {
-      const animatables = animatableAsAny.getAnimatables();
-      this._animations = new Array<Animation>();
+    playOrPause() {
+        const animatable = this.props.animatable;
+        this._isPlaying = this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
 
-      animatables.forEach((animatable: IAnimatable) => {
-        if (animatable.animations) {
-          this._animations!.push(...animatable.animations);
+        if (this._isPlaying) {
+            this.props.scene.stopAnimation(this.props.animatable);
+            this._mainAnimatable = null;
+        } else {
+            this._mainAnimatable = this.props.scene.beginAnimation(this.props.animatable, this._animationControl.from, this._animationControl.to, this._animationControl.loop);
         }
-      });
-
-      if (animatableAsAny.animations) {
-        this._animations!.push(...animatableAsAny.animations);
-      }
-
-      // Extract from and to
-      if (this._animations && this._animations.length) {
-        this._animations.forEach((animation) => {
-          let keys = animation.getKeys();
+        this.forceUpdate();
+    }
 
-          if (keys && keys.length > 0) {
-            if (keys[0].frame < this._animationControl.from) {
-              this._animationControl.from = keys[0].frame;
+    componentDidMount() {
+        this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(() => {
+            if (!this._isPlaying || !this._mainAnimatable) {
+                return;
             }
-            const lastKeyIndex = keys.length - 1;
-            if (keys[lastKeyIndex].frame > this._animationControl.to) {
-              this._animationControl.to = keys[lastKeyIndex].frame;
-            }
-          }
+            this.setState({ currentFrame: this._mainAnimatable.masterFrame });
         });
-      }
     }
 
-    this.timelineRef = React.createRef();
-  }
-
-  playOrPause() {
-    const animatable = this.props.animatable;
-    this._isPlaying =
-      this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
-
-    if (this._isPlaying) {
-      this.props.scene.stopAnimation(this.props.animatable);
-      this._mainAnimatable = null;
-    } else {
-      this._mainAnimatable = this.props.scene.beginAnimation(
-        this.props.animatable,
-        this._animationControl.from,
-        this._animationControl.to,
-        this._animationControl.loop
-      );
+    componentWillUnmount() {
+        if (this._onBeforeRenderObserver) {
+            this.props.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver);
+            this._onBeforeRenderObserver = null;
+        }
     }
-    this.forceUpdate();
-  }
 
-  componentDidMount() {
-    this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(
-      () => {
-        if (!this._isPlaying || !this._mainAnimatable) {
-          return;
+    onCurrentFrameChange(value: number) {
+        if (!this._mainAnimatable) {
+            return;
         }
-        this.setState({ currentFrame: this._mainAnimatable.masterFrame });
-      }
-    );
-  }
 
-  componentWillUnmount() {
-    if (this._onBeforeRenderObserver) {
-      this.props.scene.onBeforeRenderObservable.remove(
-        this._onBeforeRenderObserver
-      );
-      this._onBeforeRenderObserver = null;
+        this._mainAnimatable.goToFrame(value);
+        this.setState({ currentFrame: value });
     }
-  }
 
-  onCurrentFrameChange(value: number) {
-    if (!this._mainAnimatable) {
-      return;
+    onChangeFromOrTo() {
+        this.playOrPause();
+        if (this._isPlaying) {
+            this.playOrPause();
+        }
     }
 
-    this._mainAnimatable.goToFrame(value);
-    this.setState({ currentFrame: value });
-  }
-
-  onChangeFromOrTo() {
-    this.playOrPause();
-    if (this._isPlaying) {
-      this.playOrPause();
+    onOpenAnimationCurveEditor() {
+        this._isCurveEditorOpen = true;
     }
-  }
-
-  onOpenAnimationCurveEditor() {
-    this._isCurveEditorOpen = true;
-  }
 
-  onCloseAnimationCurveEditor(window: Window | null) {
-    this._isCurveEditorOpen = false;
-    if (window !== null) {
-      window.close();
+    onCloseAnimationCurveEditor(window: Window | null) {
+        this._isCurveEditorOpen = false;
+        if (window !== null) {
+            window.close();
+        }
     }
-  }
-
-  render() {
-    const animatable = this.props.animatable;
-    const animatableAsAny = this.props.animatable as any;
-
-    let animatablesForTarget = this.props.scene.getAllAnimatablesByTarget(
-      animatable
-    );
-    this._isPlaying = animatablesForTarget.length > 0;
 
-    if (this._isPlaying && !this._mainAnimatable) {
-      this._mainAnimatable = animatablesForTarget[0];
-      if (this._mainAnimatable) {
-        this._animationControl.from = this._mainAnimatable.fromFrame;
-        this._animationControl.to = this._mainAnimatable.toFrame;
-        this._animationControl.loop = this._mainAnimatable.loopAnimation;
-      }
-    }
+    render() {
+        const animatable = this.props.animatable;
+        const animatableAsAny = this.props.animatable as any;
 
-    let animations = animatable.animations;
+        let animatablesForTarget = this.props.scene.getAllAnimatablesByTarget(animatable);
+        this._isPlaying = animatablesForTarget.length > 0;
 
-    return (
-      <div>
-        {this._ranges.length > 0 && (
-          <LineContainerComponent
-            globalState={this.props.globalState}
-            title='ANIMATION RANGES'
-          >
-            {this._ranges.map((range, i) => {
-              return (
-                <ButtonLineComponent
-                  key={range.name + i}
-                  label={range.name}
-                  onClick={() => {
-                    this._mainAnimatable = null;
-                    this.props.scene.beginAnimation(
-                      animatable,
-                      range.from,
-                      range.to,
-                      true
-                    );
-                  }}
-                />
-              );
-            })}
-          </LineContainerComponent>
-        )}
-        {animations && (
-          <>
-            <LineContainerComponent
-              globalState={this.props.globalState}
-              title='ANIMATIONS'
-            >
-              <TextLineComponent
-                label='Count'
-                value={animations.length.toString()}
-              />
-              <ButtonLineComponent
-                label='Edit'
-                onClick={() => this.onOpenAnimationCurveEditor()}
-              />
-              {animations.map((anim, i) => {
-                return (
-                  <TextLineComponent
-                    key={anim.targetProperty + i}
-                    label={'#' + i + ' >'}
-                    value={anim.targetProperty}
-                  />
-                );
-              })}
+        if (this._isPlaying && !this._mainAnimatable) {
+            this._mainAnimatable = animatablesForTarget[0];
+            if (this._mainAnimatable) {
+                this._animationControl.from = this._mainAnimatable.fromFrame;
+                this._animationControl.to = this._mainAnimatable.toFrame;
+                this._animationControl.loop = this._mainAnimatable.loopAnimation;
+            }
+        }
 
-              {this._isCurveEditorOpen && (
-                <PopupComponent
-                  id='curve-editor'
-                  title='Curve Animation Editor'
-                  size={{ width: 1024, height: 490 }}
-                  onOpen={(window: Window) => {}}
-                  onClose={(window: Window) =>
-                    this.onCloseAnimationCurveEditor(window)
-                  }
-                >
-                  <AnimationCurveEditorComponent
-                    scene={this.props.scene}
-                    entity={animatableAsAny}
-                    close={(event) =>
-                      this.onCloseAnimationCurveEditor(event.view)
-                    }
-                    lockObject={this.props.lockObject}
-                    playOrPause={() => this.playOrPause()}
-                    globalState={this.props.globalState}
-                  />
-                </PopupComponent>
-              )}
-            </LineContainerComponent>
-            {animations.length > 0 && (
-              <LineContainerComponent
-                globalState={this.props.globalState}
-                title='ANIMATION GENERAL CONTROL'
-              >
-                <FloatLineComponent
-                  lockObject={this.props.lockObject}
-                  isInteger={true}
-                  label='From'
-                  target={this._animationControl}
-                  propertyName='from'
-                  onChange={() => this.onChangeFromOrTo()}
-                />
-                <FloatLineComponent
-                  lockObject={this.props.lockObject}
-                  isInteger={true}
-                  label='To'
-                  target={this._animationControl}
-                  propertyName='to'
-                  onChange={() => this.onChangeFromOrTo()}
-                />
-                <CheckBoxLineComponent
-                  label='Loop'
-                  onSelect={(value) => (this._animationControl.loop = value)}
-                  isSelected={() => this._animationControl.loop}
-                />
-                {this._isPlaying && (
-                  <SliderLineComponent
-                    ref={this.timelineRef}
-                    label='Current frame'
-                    minimum={this._animationControl.from}
-                    maximum={this._animationControl.to}
-                    step={
-                      (this._animationControl.to -
-                        this._animationControl.from) /
-                      1000.0
-                    }
-                    directValue={this.state.currentFrame}
-                    onInput={(value) => this.onCurrentFrameChange(value)}
-                  />
+        let animations = animatable.animations;
+
+        return (
+            <div>
+                {this._ranges.length > 0 && (
+                    <LineContainerComponent globalState={this.props.globalState} title="ANIMATION RANGES">
+                        {this._ranges.map((range, i) => {
+                            return (
+                                <ButtonLineComponent
+                                    key={range.name + i}
+                                    label={range.name}
+                                    onClick={() => {
+                                        this._mainAnimatable = null;
+                                        this.props.scene.beginAnimation(animatable, range.from, range.to, true);
+                                    }}
+                                />
+                            );
+                        })}
+                    </LineContainerComponent>
                 )}
-                <ButtonLineComponent
-                  label={this._isPlaying ? 'Stop' : 'Play'}
-                  onClick={() => this.playOrPause()}
-                />
-                {(this._ranges.length > 0 ||
-                  (this._animations && this._animations.length > 0)) && (
-                  <>
-                    <CheckBoxLineComponent
-                      label='Enable override'
-                      onSelect={(value) => {
-                        if (value) {
-                          animatableAsAny.animationPropertiesOverride = new AnimationPropertiesOverride();
-                          animatableAsAny.animationPropertiesOverride.blendingSpeed = 0.05;
-                        } else {
-                          animatableAsAny.animationPropertiesOverride = null;
-                        }
-                        this.forceUpdate();
-                      }}
-                      isSelected={() =>
-                        animatableAsAny.animationPropertiesOverride != null
-                      }
-                      onValueChanged={() => this.forceUpdate()}
-                    />
-                    {animatableAsAny.animationPropertiesOverride != null && (
-                      <div>
-                        <CheckBoxLineComponent
-                          label='Enable blending'
-                          target={animatableAsAny.animationPropertiesOverride}
-                          propertyName='enableBlending'
-                          onPropertyChangedObservable={
-                            this.props.onPropertyChangedObservable
-                          }
-                        />
-                        <SliderLineComponent
-                          label='Blending speed'
-                          target={animatableAsAny.animationPropertiesOverride}
-                          propertyName='blendingSpeed'
-                          minimum={0}
-                          maximum={0.1}
-                          step={0.01}
-                          onPropertyChangedObservable={
-                            this.props.onPropertyChangedObservable
-                          }
-                        />
-                      </div>
-                    )}
-                  </>
+                {animations && (
+                    <>
+                        <LineContainerComponent globalState={this.props.globalState} title="ANIMATIONS">
+                            <TextLineComponent label="Count" value={animations.length.toString()} />
+                            <ButtonLineComponent label="Edit" onClick={() => this.onOpenAnimationCurveEditor()} />
+                            {animations.map((anim, i) => {
+                                return <TextLineComponent key={anim.targetProperty + i} label={"#" + i + " >"} value={anim.targetProperty} />;
+                            })}
+
+                            {this._isCurveEditorOpen && (
+                                <PopupComponent id="curve-editor" title="Curve Animation Editor" size={{ width: 1024, height: 512 }} onOpen={(window: Window) => {}} onClose={(window: Window) => this.onCloseAnimationCurveEditor(window)}>
+                                    <AnimationCurveEditorComponent scene={this.props.scene} entity={animatableAsAny} lockObject={this.props.lockObject} playOrPause={() => this.playOrPause()} globalState={this.props.globalState} />
+                                </PopupComponent>
+                            )}
+                        </LineContainerComponent>
+                        {animations.length > 0 && (
+                            <LineContainerComponent globalState={this.props.globalState} title="ANIMATION GENERAL CONTROL">
+                                <FloatLineComponent lockObject={this.props.lockObject} isInteger={true} label="From" target={this._animationControl} propertyName="from" onChange={() => this.onChangeFromOrTo()} />
+                                <FloatLineComponent lockObject={this.props.lockObject} isInteger={true} label="To" target={this._animationControl} propertyName="to" onChange={() => this.onChangeFromOrTo()} />
+                                <CheckBoxLineComponent label="Loop" onSelect={(value) => (this._animationControl.loop = value)} isSelected={() => this._animationControl.loop} />
+                                {this._isPlaying && (
+                                    <SliderLineComponent
+                                        ref={this.timelineRef}
+                                        label="Current frame"
+                                        minimum={this._animationControl.from}
+                                        maximum={this._animationControl.to}
+                                        step={(this._animationControl.to - this._animationControl.from) / 1000.0}
+                                        directValue={this.state.currentFrame}
+                                        onInput={(value) => this.onCurrentFrameChange(value)}
+                                    />
+                                )}
+                                <ButtonLineComponent label={this._isPlaying ? "Stop" : "Play"} onClick={() => this.playOrPause()} />
+                                {(this._ranges.length > 0 || (this._animations && this._animations.length > 0)) && (
+                                    <>
+                                        <CheckBoxLineComponent
+                                            label="Enable override"
+                                            onSelect={(value) => {
+                                                if (value) {
+                                                    animatableAsAny.animationPropertiesOverride = new AnimationPropertiesOverride();
+                                                    animatableAsAny.animationPropertiesOverride.blendingSpeed = 0.05;
+                                                } else {
+                                                    animatableAsAny.animationPropertiesOverride = null;
+                                                }
+                                                this.forceUpdate();
+                                            }}
+                                            isSelected={() => animatableAsAny.animationPropertiesOverride != null}
+                                            onValueChanged={() => this.forceUpdate()}
+                                        />
+                                        {animatableAsAny.animationPropertiesOverride != null && (
+                                            <div>
+                                                <CheckBoxLineComponent label="Enable blending" target={animatableAsAny.animationPropertiesOverride} propertyName="enableBlending" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                                                <SliderLineComponent label="Blending speed" target={animatableAsAny.animationPropertiesOverride} propertyName="blendingSpeed" minimum={0} maximum={0.1} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                                            </div>
+                                        )}
+                                    </>
+                                )}
+                            </LineContainerComponent>
+                        )}
+                    </>
                 )}
-              </LineContainerComponent>
-            )}
-          </>
-        )}
-      </div>
-    );
-  }
+            </div>
+        );
+    }
 }

+ 78 - 35
inspector/src/components/actionTabs/tabs/propertyGrids/animations/controls.tsx

@@ -7,12 +7,15 @@ interface IControlsProps {
     selected: IAnimationKey | null;
     currentFrame: number;
     onCurrentFrameChange: (frame: number) => void;
-    repositionCanvas: (frame: number) => void;
+    repositionCanvas: (keyframe: IAnimationKey) => void;
     playPause: (direction: number) => void;
     isPlaying: boolean;
     scrollable: React.RefObject<HTMLDivElement>;
 }
 
+/**
+ * The playback controls for the animation editor
+ */
 export class Controls extends React.Component<IControlsProps, { selected: IAnimationKey; playingType: string }> {
     readonly _sizeOfKeyframe: number = 5;
     constructor(props: IControlsProps) {
@@ -22,91 +25,131 @@ export class Controls extends React.Component<IControlsProps, { selected: IAnima
         }
     }
 
-    playBackwards() {
+    playBackwards = () => {
         this.setState({ playingType: "reverse" });
         this.props.playPause(-1);
-    }
+    };
 
-    play() {
+    play = () => {
         this.setState({ playingType: "forward" });
         this.props.playPause(1);
-    }
+    };
 
-    pause() {
+    pause = () => {
         if (this.props.isPlaying) {
             this.setState({ playingType: "" });
             this.props.playPause(0);
         }
-    }
+    };
 
-    moveToAnimationStart() {
-        const start = this.props.keyframes && this.props.keyframes[0].frame;
-        if (start !== undefined && typeof start === "number") {
-            this.props.onCurrentFrameChange(start);
-            this.props.repositionCanvas(start);
+    moveToAnimationStart = () => {
+        const startKeyframe = this.props.keyframes && this.props.keyframes[0];
+        if (startKeyframe !== null) {
+            if (typeof startKeyframe.frame === "number") {
+                this.props.onCurrentFrameChange(startKeyframe.frame);
+            }
         }
-    }
+    };
 
-    moveToAnimationEnd() {
-        const end = this.props.keyframes && this.props.keyframes[this.props.keyframes.length - 1].frame;
-        if (end !== undefined && typeof end === "number") {
-            this.props.onCurrentFrameChange(end);
-            this.props.repositionCanvas(end);
+    moveToAnimationEnd = () => {
+        const endKeyframe = this.props.keyframes && this.props.keyframes[this.props.keyframes.length - 1];
+        if (endKeyframe !== null) {
+            if (typeof endKeyframe.frame === "number") {
+                this.props.onCurrentFrameChange(endKeyframe.frame);
+            }
         }
-    }
+    };
 
-    nextKeyframe() {
+    nextKeyframe = () => {
         if (this.props.keyframes !== null) {
             let first = this.props.keyframes.find((kf) => kf.frame > this.props.currentFrame);
             if (first) {
                 this.props.onCurrentFrameChange(first.frame);
-                this.props.repositionCanvas(first.frame);
                 this.setState({ selected: first });
                 (this.props.scrollable.current as HTMLDivElement).scrollLeft = first.frame * this._sizeOfKeyframe;
             }
         }
-    }
+    };
 
-    previousKeyframe() {
+    previousKeyframe = () => {
         if (this.props.keyframes !== null) {
             let keyframes = [...this.props.keyframes];
             let first = keyframes.reverse().find((kf) => kf.frame < this.props.currentFrame);
             if (first) {
                 this.props.onCurrentFrameChange(first.frame);
-                this.props.repositionCanvas(first.frame);
                 this.setState({ selected: first });
                 (this.props.scrollable.current as HTMLDivElement).scrollLeft = -(first.frame * this._sizeOfKeyframe);
             }
         }
-    }
+    };
 
     render() {
         return (
             <div className="controls">
-                <IconButtonLineComponent tooltip="Animation Start" icon="animation-start" onClick={() => this.moveToAnimationStart()}></IconButtonLineComponent>
-                <IconButtonLineComponent tooltip="Previous Keyframe" icon="animation-lastkey" onClick={() => this.previousKeyframe()}></IconButtonLineComponent>
+                <IconButtonLineComponent
+                    tooltip="Animation Start"
+                    icon="animation-start"
+                    onClick={this.moveToAnimationStart}
+                ></IconButtonLineComponent>
+                <IconButtonLineComponent
+                    tooltip="Previous Keyframe"
+                    icon="animation-lastkey"
+                    onClick={this.previousKeyframe}
+                ></IconButtonLineComponent>
                 {this.props.isPlaying ? (
                     <div className="stop-container">
                         {this.state.playingType === "reverse" ? (
                             <>
-                                <IconButtonLineComponent tooltip="Pause" icon="animation-stop" onClick={() => this.pause()}></IconButtonLineComponent>
-                                <IconButtonLineComponent tooltip="Play Forward" icon="animation-playfwd" onClick={() => this.play()}></IconButtonLineComponent>
+                                <IconButtonLineComponent
+                                    tooltip="Pause"
+                                    icon="animation-stop"
+                                    onClick={this.pause}
+                                ></IconButtonLineComponent>
+                                <IconButtonLineComponent
+                                    tooltip="Play Forward"
+                                    icon="animation-playfwd"
+                                    onClick={this.play}
+                                ></IconButtonLineComponent>
                             </>
                         ) : (
                             <>
-                                <IconButtonLineComponent tooltip="Play Reverse" icon="animation-playrev" onClick={() => this.playBackwards()}></IconButtonLineComponent>
-                                <IconButtonLineComponent tooltip="Pause" icon="animation-stop" onClick={() => this.pause()}></IconButtonLineComponent>
+                                <IconButtonLineComponent
+                                    tooltip="Play Reverse"
+                                    icon="animation-playrev"
+                                    onClick={this.playBackwards}
+                                ></IconButtonLineComponent>
+                                <IconButtonLineComponent
+                                    tooltip="Pause"
+                                    icon="animation-stop"
+                                    onClick={this.pause}
+                                ></IconButtonLineComponent>
                             </>
                         )}
                     </div>
                 ) : (
                     <div className="stop-container">
-                        <IconButtonLineComponent tooltip="Play Reverse" icon="animation-playrev" onClick={() => this.playBackwards()}></IconButtonLineComponent>
-                        <IconButtonLineComponent tooltip="Play Forward" icon="animation-playfwd" onClick={() => this.play()}></IconButtonLineComponent>
+                        <IconButtonLineComponent
+                            tooltip="Play Reverse"
+                            icon="animation-playrev"
+                            onClick={this.playBackwards}
+                        ></IconButtonLineComponent>
+                        <IconButtonLineComponent
+                            tooltip="Play Forward"
+                            icon="animation-playfwd"
+                            onClick={this.play}
+                        ></IconButtonLineComponent>
                     </div>
                 )}
-                <IconButtonLineComponent tooltip="Next Keyframe" icon="animation-nextkey" onClick={() => this.nextKeyframe()}></IconButtonLineComponent>
-                <IconButtonLineComponent tooltip="Animation End" icon="animation-end" onClick={() => this.moveToAnimationEnd()}></IconButtonLineComponent>
+                <IconButtonLineComponent
+                    tooltip="Next Keyframe"
+                    icon="animation-nextkey"
+                    onClick={this.nextKeyframe}
+                ></IconButtonLineComponent>
+                <IconButtonLineComponent
+                    tooltip="Animation End"
+                    icon="animation-end"
+                    onClick={this.moveToAnimationEnd}
+                ></IconButtonLineComponent>
             </div>
         );
     }

File diff suppressed because it is too large
+ 1204 - 1154
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss


+ 294 - 272
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -1,299 +1,321 @@
-import * as React from 'react';
-
-import { Observable } from 'babylonjs/Misc/observable';
-import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
-import { Animation } from 'babylonjs/Animations/animation';
-import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
-import { NumericInputComponent } from '../../../lines/numericInputComponent';
-import { AddAnimation } from './addAnimation';
-import { AnimationListTree, SelectedCoordinate } from './animationListTree';
-import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
-import { LoadSnippet } from './loadsnippet';
-import { SaveSnippet } from './saveSnippet';
-import { LockObject } from '../lockObject';
-import { GlobalState } from '../../../../globalState';
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Animation } from "babylonjs/Animations/animation";
+import { IconButtonLineComponent } from "../../../lines/iconButtonLineComponent";
+import { NumericInputComponent } from "../../../lines/numericInputComponent";
+import { AddAnimation } from "./addAnimation";
+import { AnimationListTree, SelectedCoordinate } from "./animationListTree";
+import { IAnimatable } from "babylonjs/Animations/animatable.interface";
+import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+import { LoadSnippet } from "./loadsnippet";
+import { SaveSnippet } from "./saveSnippet";
+import { LockObject } from "../lockObject";
+import { GlobalState } from "../../../../globalState";
 
 interface IEditorControlsProps {
-  isTargetedAnimation: boolean;
-  entity: IAnimatable | TargetedAnimation;
-  selected: Animation | null;
-  lockObject: LockObject;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-  setNotificationMessage: (message: string) => void;
-  selectAnimation: (selected: Animation, axis?: SelectedCoordinate) => void;
-  setFps: (fps: number) => void;
-  setIsLooping: () => void;
-  globalState: GlobalState;
-  snippetServer: string;
-  deselectAnimation: () => void;
-  fps: number;
+    isTargetedAnimation: boolean;
+    entity: IAnimatable | TargetedAnimation;
+    selected: Animation | null;
+    lockObject: LockObject;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    setNotificationMessage: (message: string) => void;
+    selectAnimation: (selected: Animation, axis?: SelectedCoordinate) => void;
+    setFps: (fps: number) => void;
+    setIsLooping: () => void;
+    globalState: GlobalState;
+    snippetServer: string;
+    deselectAnimation: () => void;
+    fps: number;
 }
 
+/**
+ * Renders the Curve Editor controls to create, save, remove, load and edit animations
+ */
 export class EditorControls extends React.Component<
-  IEditorControlsProps,
-  {
-    isAnimationTabOpen: boolean;
-    isEditTabOpen: boolean;
-    isLoadTabOpen: boolean;
-    isSaveTabOpen: boolean;
-    isLoopActive: boolean;
-    animationsCount: number;
-    framesPerSecond: number;
-    snippetId: string;
-    selected: Animation | undefined;
-  }
+    IEditorControlsProps,
+    {
+        isAnimationTabOpen: boolean;
+        isEditTabOpen: boolean;
+        isLoadTabOpen: boolean;
+        isSaveTabOpen: boolean;
+        isLoopActive: boolean;
+        animationsCount: number;
+        framesPerSecond: number;
+        snippetId: string;
+        selected: Animation | undefined;
+    }
 > {
-  constructor(props: IEditorControlsProps) {
-    super(props);
-    let count = this.props.isTargetedAnimation
-      ? 1
-      : (this.props.entity as IAnimatable).animations?.length ?? 0;
-    this.state = {
-      isAnimationTabOpen: count === 0 ? true : false,
-      isEditTabOpen: count === 0 ? false : true,
-      isSaveTabOpen: false,
-      isLoadTabOpen: false,
-      isLoopActive: true,
-      animationsCount: count,
-      framesPerSecond: this.props.fps,
-      snippetId: '',
-      selected: undefined,
-    };
-  }
+    constructor(props: IEditorControlsProps) {
+        super(props);
+        let count = this.props.isTargetedAnimation ? 1 : (this.props.entity as IAnimatable).animations?.length ?? 0;
+        this.state = {
+            isAnimationTabOpen: count === 0 ? true : false,
+            isEditTabOpen: count === 0 ? false : true,
+            isSaveTabOpen: false,
+            isLoadTabOpen: false,
+            isLoopActive: true,
+            animationsCount: count,
+            framesPerSecond: this.props.fps,
+            snippetId: "",
+            selected: undefined,
+        };
+    }
 
-  componentWillReceiveProps(newProps: IEditorControlsProps) {
-    if (newProps.fps !== this.props.fps) {
-      this.setState({ framesPerSecond: newProps.fps });
+    componentDidUpdate(prevProps: IEditorControlsProps) {
+        if (this.props.fps !== prevProps.fps) {
+            this.setState({ framesPerSecond: this.props.fps });
+        }
     }
-  }
 
-  animationAdded() {
-    this.setState({
-      animationsCount: this.recountAnimations(),
-      isEditTabOpen: true,
-      isAnimationTabOpen: false,
-    });
-  }
+    onAnimationAdded = (animation: Animation) => {
+        this.setState({
+            animationsCount: this.recountAnimations(),
+            isEditTabOpen: true,
+            isAnimationTabOpen: false,
+        });
+        this.props.selectAnimation(animation, undefined);
+    };
 
-  finishedUpdate() {
-    this.setState({
-      isEditTabOpen: true,
-      isAnimationTabOpen: false,
-      selected: undefined,
-    });
-  }
+    finishedUpdate = () => {
+        this.setState({
+            isEditTabOpen: true,
+            isAnimationTabOpen: false,
+            selected: undefined,
+        });
+    };
 
-  recountAnimations() {
-    return (this.props.entity as IAnimatable).animations?.length ?? 0;
-  }
+    recountAnimations() {
+        return (this.props.entity as IAnimatable).animations?.length ?? 0;
+    }
 
-  changeLoopBehavior() {
-    this.setState({
-      isLoopActive: !this.state.isLoopActive,
-    });
-    this.props.setIsLooping();
-  }
+    changeLoopBehavior = () => {
+        this.setState({
+            isLoopActive: !this.state.isLoopActive,
+        });
+        this.props.setIsLooping();
+    };
 
-  handleTabs(tab: number) {
-    let state = {
-      isAnimationTabOpen: true,
-      isLoadTabOpen: false,
-      isSaveTabOpen: false,
-      isEditTabOpen: false,
+    handleFirstTab = () => {
+        this.handleTabs(0);
+    };
+    handleSecondTab = () => {
+        this.handleTabs(1);
+    };
+    handleThirdTab = () => {
+        this.handleTabs(2);
+    };
+    handleFourthTab = () => {
+        this.handleTabs(3);
     };
 
-    switch (tab) {
-      case 0:
-        state = {
-          isAnimationTabOpen: true,
-          isLoadTabOpen: false,
-          isSaveTabOpen: false,
-          isEditTabOpen: false,
+    handleTabs(tab: number) {
+        let state = {
+            isAnimationTabOpen: true,
+            isLoadTabOpen: false,
+            isSaveTabOpen: false,
+            isEditTabOpen: false,
         };
-        break;
-      case 1:
-        state = {
-          isAnimationTabOpen: false,
-          isLoadTabOpen: true,
-          isSaveTabOpen: false,
-          isEditTabOpen: false,
-        };
-        break;
-      case 2:
-        state = {
-          isAnimationTabOpen: false,
-          isLoadTabOpen: false,
-          isSaveTabOpen: true,
-          isEditTabOpen: false,
-        };
-        break;
-      case 3:
-        state = {
-          isAnimationTabOpen: false,
-          isLoadTabOpen: false,
-          isSaveTabOpen: false,
-          isEditTabOpen: true,
-        };
-        break;
-    }
 
-    this.setState(state);
-  }
+        switch (tab) {
+            case 0:
+                state = {
+                    isAnimationTabOpen: true,
+                    isLoadTabOpen: false,
+                    isSaveTabOpen: false,
+                    isEditTabOpen: false,
+                };
+                break;
+            case 1:
+                state = {
+                    isAnimationTabOpen: false,
+                    isLoadTabOpen: true,
+                    isSaveTabOpen: false,
+                    isEditTabOpen: false,
+                };
+                break;
+            case 2:
+                state = {
+                    isAnimationTabOpen: false,
+                    isLoadTabOpen: false,
+                    isSaveTabOpen: true,
+                    isEditTabOpen: false,
+                };
+                break;
+            case 3:
+                state = {
+                    isAnimationTabOpen: false,
+                    isLoadTabOpen: false,
+                    isSaveTabOpen: false,
+                    isEditTabOpen: true,
+                };
+                break;
+        }
 
-  handleChangeFps(fps: number) {
-    this.props.setFps(fps);
-    this.setState({ framesPerSecond: fps });
-    if (this.props.selected) {
-      this.props.selected.framePerSecond = fps;
+        this.setState(state);
     }
-  }
 
-  emptiedList() {
-    this.setState({
-      animationsCount: this.recountAnimations(),
-      isEditTabOpen: false,
-      isAnimationTabOpen: true,
-    });
-  }
-
-  animationsLoaded(numberOfAnimations: number) {
-    this.setState({
-      animationsCount: numberOfAnimations,
-      isEditTabOpen: true,
-      isAnimationTabOpen: false,
-      isLoadTabOpen: false,
-      isSaveTabOpen: false,
-    });
-  }
+    handleChangeFps = (fps: number) => {
+        this.props.setFps(fps);
+        this.setState({ framesPerSecond: fps });
+        if (this.props.selected) {
+            this.props.selected.framePerSecond = fps;
+        }
+    };
 
-  editAnimation(selected: Animation) {
-    this.setState({
-      selected: selected,
-      isEditTabOpen: false,
-      isAnimationTabOpen: true,
-      isLoadTabOpen: false,
-      isSaveTabOpen: false,
-    });
-  }
+    /**
+     * Cleans the list when has been emptied
+     */
+    onEmptiedList = () => {
+        this.setState({
+            animationsCount: this.recountAnimations(),
+            isEditTabOpen: false,
+            isAnimationTabOpen: true,
+        });
+    };
 
-  render() {
-    return (
-      <div className='animation-list'>
-        <div className='controls-header'>
-          {this.props.isTargetedAnimation ? null : (
-            <IconButtonLineComponent
-              active={this.state.isAnimationTabOpen}
-              tooltip='Add Animation'
-              icon='medium add-animation'
-              onClick={() => this.handleTabs(0)}
-            ></IconButtonLineComponent>
-          )}
-          <IconButtonLineComponent
-            active={this.state.isLoadTabOpen}
-            tooltip='Load Animation'
-            icon='medium load'
-            onClick={() => this.handleTabs(1)}
-          ></IconButtonLineComponent>
-          {this.state.animationsCount === 0 ? null : (
-            <IconButtonLineComponent
-              active={this.state.isSaveTabOpen}
-              tooltip='Save Animation'
-              icon='medium save'
-              onClick={() => this.handleTabs(2)}
-            ></IconButtonLineComponent>
-          )}
-          {this.state.animationsCount === 0 ? null : (
-            <IconButtonLineComponent
-              active={this.state.isEditTabOpen}
-              tooltip='Edit Animations'
-              icon='medium animation-edit'
-              onClick={() => this.handleTabs(3)}
-            ></IconButtonLineComponent>
-          )}
-          {this.state.isEditTabOpen ? (
-            <div className='input-fps'>
-              <NumericInputComponent
-                label={''}
-                precision={0}
-                value={this.state.framesPerSecond}
-                onChange={(framesPerSecond: number) =>
-                  this.handleChangeFps(framesPerSecond)
-                }
-              />
-              <p>fps</p>
-            </div>
-          ) : null}
-          {this.state.isEditTabOpen ? (
-            <IconButtonLineComponent
-              tooltip='Loop/Unloop'
-              icon={`medium ${
-                this.state.isLoopActive
-                  ? 'loop-active last'
-                  : 'loop-inactive last'
-              }`}
-              onClick={() => this.changeLoopBehavior()}
-            ></IconButtonLineComponent>
-          ) : null}
-        </div>
-        {this.props.isTargetedAnimation ? null : (
-          <AddAnimation
-            isOpen={this.state.isAnimationTabOpen}
-            close={() => {
-              this.setState({ isAnimationTabOpen: false, isEditTabOpen: true });
-            }}
-            entity={this.props.entity as IAnimatable}
-            setNotificationMessage={(message: string) => {
-              this.props.setNotificationMessage(message);
-            }}
-            addedNewAnimation={() => this.animationAdded()}
-            onPropertyChangedObservable={this.props.onPropertyChangedObservable}
-            fps={this.state.framesPerSecond}
-            selectedToUpdate={this.state.selected}
-            finishedUpdate={() => this.finishedUpdate()}
-          />
-        )}
+    /**
+     * When animations have been reloaded update tabs
+     */
+    animationsLoaded = (numberOfAnimations: number) => {
+        this.setState({
+            animationsCount: numberOfAnimations,
+            isEditTabOpen: true,
+            isAnimationTabOpen: false,
+            isLoadTabOpen: false,
+            isSaveTabOpen: false,
+        });
 
-        {this.state.isLoadTabOpen ? (
-          <LoadSnippet
-            animationsLoaded={(numberOfAnimations: number) =>
-              this.animationsLoaded(numberOfAnimations)
+        if (this.props.entity instanceof TargetedAnimation) {
+            const animation = (this.props.entity as TargetedAnimation).animation;
+            this.props.selectAnimation(animation);
+        } else {
+            const animations = (this.props.entity as IAnimatable).animations;
+            if (animations !== null) {
+                this.props.selectAnimation(animations[0]);
             }
-            lockObject={this.props.lockObject}
-            animations={[]}
-            snippetServer={this.props.snippetServer}
-            globalState={this.props.globalState}
-            setSnippetId={(id: string) => this.setState({ snippetId: id })}
-            entity={this.props.entity}
-            setNotificationMessage={this.props.setNotificationMessage}
-          />
-        ) : null}
+        }
+    };
 
-        {this.state.isSaveTabOpen ? (
-          <SaveSnippet
-            lockObject={this.props.lockObject}
-            animations={(this.props.entity as IAnimatable).animations}
-            snippetServer={this.props.snippetServer}
-            globalState={this.props.globalState}
-            snippetId={this.state.snippetId}
-          />
-        ) : null}
+    editAnimation = (selected: Animation) => {
+        this.setState({
+            selected: selected,
+            isEditTabOpen: false,
+            isAnimationTabOpen: true,
+            isLoadTabOpen: false,
+            isSaveTabOpen: false,
+        });
+    };
 
-        {this.state.isEditTabOpen ? (
-          <AnimationListTree
-            deselectAnimation={() => this.props.deselectAnimation()}
-            isTargetedAnimation={this.props.isTargetedAnimation}
-            entity={this.props.entity}
-            selected={this.props.selected}
-            onPropertyChangedObservable={this.props.onPropertyChangedObservable}
-            empty={() => this.emptiedList()}
-            selectAnimation={this.props.selectAnimation}
-            editAnimation={(selected: Animation) =>
-              this.editAnimation(selected)
-            }
-          />
-        ) : null}
-      </div>
-    );
-  }
+    setSnippetId = (id: string) => {
+        this.setState({ snippetId: id });
+    };
+
+     /**
+     * Marks animation tab closed and hides the tab
+     */
+    onCloseAddAnimation = () => {
+        this.setState({ isAnimationTabOpen: false, isEditTabOpen: true });
+    };
+
+    render() {
+        return (
+            <div className="animation-list">
+                <div className="controls-header">
+                    {this.props.isTargetedAnimation ? null : (
+                        <IconButtonLineComponent
+                            active={this.state.isAnimationTabOpen}
+                            tooltip="Add Animation"
+                            icon="medium add-animation"
+                            onClick={this.handleFirstTab}></IconButtonLineComponent>
+                    )}
+                    <IconButtonLineComponent
+                        active={this.state.isLoadTabOpen}
+                        tooltip="Load Animation"
+                        icon="medium load"
+                        onClick={this.handleSecondTab}></IconButtonLineComponent>
+                    {this.state.animationsCount === 0 ? null : (
+                        <IconButtonLineComponent
+                            active={this.state.isSaveTabOpen}
+                            tooltip="Save Animation"
+                            icon="medium save"
+                            onClick={this.handleThirdTab}></IconButtonLineComponent>
+                    )}
+                    {this.state.animationsCount === 0 ? null : (
+                        <IconButtonLineComponent
+                            active={this.state.isEditTabOpen}
+                            tooltip="Edit Animations"
+                            icon="medium animation-edit"
+                            onClick={this.handleFourthTab}></IconButtonLineComponent>
+                    )}
+                    {this.state.isEditTabOpen ? (
+                        <div className="input-fps">
+                            <NumericInputComponent
+                                label={""}
+                                precision={0}
+                                value={this.state.framesPerSecond}
+                                onChange={this.handleChangeFps}
+                            />
+                            <p>fps</p>
+                        </div>
+                    ) : null}
+                    {this.state.isEditTabOpen ? (
+                        <IconButtonLineComponent
+                            tooltip="Loop/Unloop"
+                            icon={`medium ${this.state.isLoopActive ? "loop-active last" : "loop-inactive last"}`}
+                            onClick={this.changeLoopBehavior}></IconButtonLineComponent>
+                    ) : null}
+                </div>
+                {this.props.isTargetedAnimation ? null : (
+                    <AddAnimation
+                        isOpen={this.state.isAnimationTabOpen}
+                        close={this.onCloseAddAnimation}
+                        entity={this.props.entity as IAnimatable}
+                        setNotificationMessage={this.props.setNotificationMessage}
+                        addedNewAnimation={this.onAnimationAdded}
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable}
+                        fps={this.state.framesPerSecond}
+                        selectedToUpdate={this.state.selected}
+                        finishedUpdate={this.finishedUpdate}
+                    />
+                )}
+
+                {this.state.isLoadTabOpen ? (
+                    <LoadSnippet
+                        animationsLoaded={this.animationsLoaded}
+                        lockObject={this.props.lockObject}
+                        animations={[]}
+                        snippetServer={this.props.snippetServer}
+                        globalState={this.props.globalState}
+                        setSnippetId={this.setSnippetId}
+                        entity={this.props.entity}
+                        setNotificationMessage={this.props.setNotificationMessage}
+                    />
+                ) : null}
+
+                {this.state.isSaveTabOpen ? (
+                    <SaveSnippet
+                        lockObject={this.props.lockObject}
+                        animations={(this.props.entity as IAnimatable).animations}
+                        snippetServer={this.props.snippetServer}
+                        globalState={this.props.globalState}
+                        snippetId={this.state.snippetId}
+                    />
+                ) : null}
+
+                {this.state.isEditTabOpen ? (
+                    <AnimationListTree
+                        deselectAnimation={this.props.deselectAnimation}
+                        isTargetedAnimation={this.props.isTargetedAnimation}
+                        entity={this.props.entity}
+                        selected={this.props.selected}
+                        onPropertyChangedObservable={this.props.onPropertyChangedObservable}
+                        empty={this.onEmptiedList}
+                        selectAnimation={this.props.selectAnimation}
+                        editAnimation={this.editAnimation}
+                    />
+                ) : null}
+            </div>
+        );
+    }
 }

+ 174 - 124
inspector/src/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar.tsx

@@ -1,132 +1,182 @@
-import * as React from 'react';
-import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
-import { IActionableKeyFrame } from './animationCurveEditorComponent';
+import * as React from "react";
+import { IconButtonLineComponent } from "../../../lines/iconButtonLineComponent";
+import { IActionableKeyFrame } from "./animationCurveEditorComponent";
 
 interface IGraphActionsBarProps {
-  addKeyframe: () => void;
-  removeKeyframe: () => void;
-  handleValueChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
-  handleFrameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
-  flatTangent: () => void;
-  brokeTangents: () => void;
-  setLerpMode: () => void;
-  brokenMode: boolean;
-  lerpMode: boolean;
-  actionableKeyframe: IActionableKeyFrame;
-  title: string;
-  close: (event: any) => void;
-  enabled: boolean;
-  setKeyframeValue: () => void;
+    addKeyframe: () => void;
+    removeKeyframe: () => void;
+    frameSelectedKeyframes: () => void;
+    handleValueChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    handleFrameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+    flatTangent: () => void;
+    brokeTangents: () => void;
+    setLerpToActiveControlPoint: () => void;
+    brokenMode: boolean;
+    lerpMode: boolean;
+    actionableKeyframe: IActionableKeyFrame;
+    title: string;
+    enabled: boolean;
+    setKeyframeValue: (actionableKeyframe: IActionableKeyFrame) => void;
+    frameRange: { min: number | undefined; max: number | undefined };
 }
 
-export class GraphActionsBar extends React.Component<IGraphActionsBarProps> {
-  private _frameInput: React.RefObject<HTMLInputElement>;
-  private _valueInput: React.RefObject<HTMLInputElement>;
-  constructor(props: IGraphActionsBarProps) {
-    super(props);
-    this._frameInput = React.createRef();
-    this._valueInput = React.createRef();
-  }
-
-  componentDidMount() {
-    this._frameInput.current?.addEventListener(
-      'keyup',
-      this.isEnterKeyUp.bind(this)
-    );
-    this._valueInput.current?.addEventListener(
-      'keyup',
-      this.isEnterKeyUp.bind(this)
-    );
-  }
-
-  componentWillUnmount() {
-    this._frameInput.current?.removeEventListener(
-      'keyup',
-      this.isEnterKeyUp.bind(this)
-    );
-    this._valueInput.current?.removeEventListener(
-      'keyup',
-      this.isEnterKeyUp.bind(this)
-    );
-  }
-
-  isEnterKeyUp(event: KeyboardEvent) {
-    event.preventDefault();
-
-    if (event.key === 'Enter') {
-      this.props.setKeyframeValue();
+/**
+ * Has the buttons and actions for the Canvas Graph.
+ * Handles input change and actions (flat, broken mode, set linear control points)
+ */
+export class GraphActionsBar extends React.Component<
+    IGraphActionsBarProps,
+    { frame: string; value: string; min: number | undefined; max: number | undefined }
+> {
+    private _frameInput: React.RefObject<HTMLInputElement>;
+    private _valueInput: React.RefObject<HTMLInputElement>;
+    constructor(props: IGraphActionsBarProps) {
+        super(props);
+        this._frameInput = React.createRef();
+        this._valueInput = React.createRef();
+        const { frame, value } = this.selectedKeyframeChanged(this.props.actionableKeyframe);
+        this.state = { frame, value, min: this.props.frameRange.min, max: this.props.frameRange.max };
     }
-  }
 
-  onBlur(event: React.FocusEvent<HTMLInputElement>) {
-    event.preventDefault();
-    if (event.target.value !== '') {
-      this.props.setKeyframeValue();
+    componentDidMount() {
+        this._frameInput.current?.addEventListener("keyup", this.isEnterKeyUp.bind(this));
+        this._valueInput.current?.addEventListener("keyup", this.isEnterKeyUp.bind(this));
+    }
+
+    componentDidUpdate(prevProps: IGraphActionsBarProps, prevState: any) {
+        if (prevProps.actionableKeyframe !== this.props.actionableKeyframe) {
+            const { frame, value } = this.selectedKeyframeChanged(this.props.actionableKeyframe);
+            this.setState({ frame, value });
+        }
+
+        if (
+            prevProps.frameRange.min !== this.props.frameRange.min ||
+            prevProps.frameRange.max !== this.props.frameRange.max
+        ) {
+            this.setState({ min: this.props.frameRange.min, max: this.props.frameRange.max });
+        }
+    }
+
+    selectedKeyframeChanged(keyframe: IActionableKeyFrame) {
+        let frame = "";
+        if (typeof keyframe.frame === "number") {
+            frame = keyframe.frame.toString();
+        }
+        let value = "";
+        if (typeof keyframe.value === "number") {
+            value = keyframe.value.toFixed(3);
+        }
+        return { frame, value };
+    }
+
+    componentWillUnmount() {
+        this._frameInput.current?.removeEventListener("keyup", this.isEnterKeyUp.bind(this));
+        this._valueInput.current?.removeEventListener("keyup", this.isEnterKeyUp.bind(this));
+    }
+
+    isEnterKeyUp(event: KeyboardEvent) {
+        event.preventDefault();
+
+        if (event.key === "Enter") {
+            const actionableKeyframe: IActionableKeyFrame = { frame: this.getFrame(), value: this.getValue() };
+            this.props.setKeyframeValue(actionableKeyframe);
+        }
+    }
+
+    onBlur = (event: React.FocusEvent<HTMLInputElement>) => {
+        event.preventDefault();
+        if (event.target.value !== "") {
+            const actionableKeyframe: IActionableKeyFrame = { frame: this.getFrame(), value: this.getValue() };
+            this.props.setKeyframeValue(actionableKeyframe);
+        }
+    };
+
+    getFrame() {
+        let frame;
+        if (this.state.frame === "") {
+            frame = "";
+        } else {
+            frame = parseInt(this.state.frame);
+        }
+
+        return frame;
+    }
+
+    getValue() {
+        let value;
+        if (this.state.value !== "") {
+            value = parseFloat(this.state.value);
+        } else {
+            value = "";
+        }
+        return value;
+    }
+
+    handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        e.preventDefault();
+        this.setState({ value: e.target.value });
+    };
+
+    handleFrameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        e.preventDefault();
+        this.setState({ frame: e.target.value });
+    };
+
+    render() {
+        return (
+            <div className="actions-wrapper">
+                <div className="title-container">
+                    <div className="icon babylon-logo"></div>
+                    <div className="title">{this.props.title}</div>
+                </div>
+                <div className={`buttons-container ${this.props.enabled ? "pointer-events-enabled" : "pointer-events-disabled"}`}>
+                    <div className="action-input frame-input">
+                        <input
+                            ref={this._frameInput}
+                            type="number"
+                            onChange={this.handleFrameChange}
+                            value={this.state.frame}
+                            max={this.state.max}
+                            min={this.state.min}
+                            step="1"
+                            disabled={this.props.actionableKeyframe.frame === undefined}
+                            onBlur={this.onBlur}
+                        />
+                    </div>
+                    <div className="action-input">
+                        <input
+                            ref={this._valueInput}
+                            type="number"
+                            value={this.state.value}
+                            onChange={this.handleValueChange}
+                            step="0.01"
+                            disabled={this.props.actionableKeyframe.value === undefined}
+                            onBlur={this.onBlur}
+                        />
+                    </div>
+                    <IconButtonLineComponent tooltip={"Add Keyframe"} icon="new-key" onClick={this.props.addKeyframe} />
+                    <IconButtonLineComponent
+                        tooltip={"Frame selected keyframes"}
+                        icon="frame"
+                        onClick={this.props.frameSelectedKeyframes}
+                    />
+                    <IconButtonLineComponent
+                        tooltip={this.props.brokenMode ? "Flat selected control point" : "Flat control points"}
+                        icon="flat-tangent"
+                        onClick={this.props.flatTangent}
+                    />
+                    <IconButtonLineComponent
+                        tooltip={this.props.brokenMode ? "Broken Mode On" : "Broken Mode Off"}
+                        icon={this.props.brokenMode ? "break-tangent" : "unify-tangent"}
+                        onClick={this.props.brokeTangents}
+                    />
+                    <IconButtonLineComponent
+                        tooltip={"Linear"}
+                        icon="linear-tangent"
+                        onClick={this.props.setLerpToActiveControlPoint}
+                    />
+                </div>
+            </div>
+        );
     }
-  }
-
-  render() {
-    return (
-      <div className='actions-wrapper'>
-        <div className='title-container'>
-          <div className='icon babylon-logo'></div>
-          <div className='title'>{this.props.title}</div>
-        </div>
-        <div
-          className='buttons-container'
-          style={{ pointerEvents: this.props.enabled ? 'all' : 'none' }}
-        >
-          <div className='action-input frame-input'>
-            <input
-              ref={this._frameInput}
-              type='number'
-              onChange={this.props.handleFrameChange}
-              value={this.props.actionableKeyframe.frame?.toString() || ''}
-              step='1'
-              disabled={this.props.actionableKeyframe.frame === undefined}
-              onBlur={(e) => this.onBlur(e)}
-            />
-          </div>
-          <div className='action-input'>
-            <input
-              ref={this._valueInput}
-              type='number'
-              value={this.props.actionableKeyframe.value || ''}
-              onChange={this.props.handleValueChange}
-              step='0.01'
-              disabled={this.props.actionableKeyframe.value === undefined}
-              onBlur={(e) => this.onBlur(e)}
-            />
-          </div>
-          <IconButtonLineComponent
-            tooltip={'Add Keyframe'}
-            icon='new-key'
-            onClick={this.props.addKeyframe}
-          />
-          <IconButtonLineComponent
-            tooltip={'Frame selected keyframes'}
-            icon='frame'
-            onClick={this.props.removeKeyframe}
-          />
-          <IconButtonLineComponent
-            tooltip={'Flat Tangents'}
-            icon='flat-tangent'
-            onClick={this.props.flatTangent}
-          />
-          <IconButtonLineComponent
-            tooltip={
-              this.props.brokenMode ? 'Broken Mode On' : 'Broken Mode Off'
-            }
-            icon={this.props.brokenMode ? 'break-tangent' : 'unify-tangent'}
-            onClick={this.props.brokeTangents}
-          />
-          <IconButtonLineComponent
-            tooltip={this.props.lerpMode ? 'Lerp On' : 'lerp Off'}
-            icon='linear-tangent'
-            onClick={this.props.setLerpMode}
-          />
-        </div>
-      </div>
-    );
-  }
 }

+ 93 - 86
inspector/src/components/actionTabs/tabs/propertyGrids/animations/keyframeSvgPoint.tsx

@@ -1,101 +1,108 @@
-import * as React from 'react';
-import { Vector2 } from 'babylonjs/Maths/math.vector';
-import { AnchorSvgPoint } from './anchorSvgPoint';
+import * as React from "react";
+import { Vector2 } from "babylonjs/Maths/math.vector";
+import { AnchorSvgPoint } from "./anchorSvgPoint";
 
-const keyInactive = require('./assets/keyInactiveIcon.svg') as string;
-//const keyActive = require("./assets/keyActiveIcon.svg") as string; uncomment when setting active multiselect
-const keySelected = require('./assets/keySelectedIcon.svg') as string;
+const keyInactive = require("./assets/keyInactiveIcon.svg") as string;
+const keySelected = require("./assets/keySelectedIcon.svg") as string;
 
 export interface IKeyframeSvgPoint {
-  keyframePoint: Vector2;
-  rightControlPoint: Vector2 | null;
-  leftControlPoint: Vector2 | null;
-  id: string;
-  selected: boolean;
-  isLeftActive: boolean;
-  isRightActive: boolean;
-  curveId?: ICurveMetaData;
+    keyframePoint: Vector2;
+    rightControlPoint: Vector2 | null;
+    leftControlPoint: Vector2 | null;
+    id: string;
+    selected: boolean;
+    isLeftActive: boolean;
+    isRightActive: boolean;
+    curveId?: ICurveMetaData;
 }
 
 export interface ICurveMetaData {
-  id: number;
-  animationName: string;
-  property: string;
+    id: number;
+    animationName: string;
+    property: string;
 }
 
 interface IKeyframeSvgPointProps {
-  keyframePoint: Vector2;
-  leftControlPoint: Vector2 | null;
-  rightControlPoint: Vector2 | null;
-  id: string;
-  selected: boolean;
-  selectKeyframe: (id: string, multiselect: boolean) => void;
-  selectedControlPoint: (type: string, id: string) => void;
-  isLeftActive: boolean;
-  isRightActive: boolean;
+    keyframePoint: Vector2;
+    leftControlPoint: Vector2 | null;
+    rightControlPoint: Vector2 | null;
+    id: string;
+    selected: boolean;
+    selectKeyframe: (id: string, multiselect: boolean) => void;
+    selectedControlPoint: (type: string, id: string) => void;
+    isLeftActive: boolean;
+    isRightActive: boolean;
+    framesInCanvasView: { from: number; to: number };
 }
 
+/**
+ * Renders the Keyframe as an SVG Element for the Canvas component.
+ * Holds the two control points to generate the proper curve.
+ */
 export class KeyframeSvgPoint extends React.Component<IKeyframeSvgPointProps> {
-  constructor(props: IKeyframeSvgPointProps) {
-    super(props);
-  }
-
-  select(e: React.MouseEvent<SVGImageElement>) {
-    e.preventDefault();
-    let multiSelect = false;
-    if (e.buttons === 0 && e.ctrlKey) {
-      multiSelect = true;
+    constructor(props: IKeyframeSvgPointProps) {
+        super(props);
     }
-    this.props.selectKeyframe(this.props.id, multiSelect);
-  }
 
-  render() {
-    return (
-      <>
-        <svg
-          className='draggable'
-          x={this.props.keyframePoint.x}
-          y={this.props.keyframePoint.y}
-          style={{ overflow: 'visible', cursor: 'pointer' }}
-        >
-          <image
-            data-id={this.props.id}
-            className='draggable'
-            x='-1'
-            y='-1.5'
-            width='3'
-            height='3'
-            href={this.props.selected ? keySelected : keyInactive}
-            onClick={(e) => this.select(e)}
-          />
-        </svg>
-        {this.props.leftControlPoint && (
-          <AnchorSvgPoint
-            type='left'
-            index={this.props.id}
-            control={this.props.leftControlPoint}
-            anchor={this.props.keyframePoint}
-            active={this.props.selected}
-            selected={this.props.isLeftActive}
-            selectControlPoint={(type: string) =>
-              this.props.selectedControlPoint(type, this.props.id)
-            }
-          />
-        )}
-        {this.props.rightControlPoint && (
-          <AnchorSvgPoint
-            type='right'
-            index={this.props.id}
-            control={this.props.rightControlPoint}
-            anchor={this.props.keyframePoint}
-            active={this.props.selected}
-            selected={this.props.isRightActive}
-            selectControlPoint={(type: string) =>
-              this.props.selectedControlPoint(type, this.props.id)
-            }
-          />
-        )}
-      </>
-    );
-  }
+    select = (e: React.MouseEvent<SVGImageElement>) => {
+        e.preventDefault();
+        let multiSelect = false;
+        if (e.buttons === 0 && e.ctrlKey) {
+            multiSelect = true;
+        }
+        this.props.selectKeyframe(this.props.id, multiSelect);
+    };
+
+    selectedControlPointId = (type: string) => {
+        this.props.selectedControlPoint(type, this.props.id);
+    };
+
+    render() {
+        const svgImageIcon = this.props.selected ? keySelected : keyInactive;
+        return (
+            <>
+                <svg
+                    className="draggable"
+                    x={this.props.keyframePoint.x}
+                    y={this.props.keyframePoint.y}
+                    style={{ overflow: "visible", cursor: "pointer" }}
+                >
+                    <image
+                        data-id={this.props.id}
+                        className="draggable"
+                        x="-1"
+                        y="-1.5"
+                        width="3"
+                        height="3"
+                        href={svgImageIcon}
+                        onClick={this.select}
+                    />
+                </svg>
+                {this.props.leftControlPoint && (
+                    <AnchorSvgPoint
+                        type="left"
+                        index={this.props.id}
+                        control={this.props.leftControlPoint}
+                        anchor={this.props.keyframePoint}
+                        active={this.props.selected}
+                        selected={this.props.isLeftActive}
+                        selectControlPoint={this.selectedControlPointId}
+                        framesInCanvasView={this.props.framesInCanvasView}
+                    />
+                )}
+                {this.props.rightControlPoint && (
+                    <AnchorSvgPoint
+                        type="right"
+                        index={this.props.id}
+                        control={this.props.rightControlPoint}
+                        anchor={this.props.keyframePoint}
+                        active={this.props.selected}
+                        selected={this.props.isRightActive}
+                        selectControlPoint={this.selectedControlPointId}
+                        framesInCanvasView={this.props.framesInCanvasView}
+                    />
+                )}
+            </>
+        );
+    }
 }

+ 104 - 114
inspector/src/components/actionTabs/tabs/propertyGrids/animations/loadsnippet.tsx

@@ -1,126 +1,116 @@
-import * as React from 'react';
-import { Observable } from 'babylonjs/Misc/observable';
-import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
-import { Animation } from 'babylonjs/Animations/animation';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { FileButtonLineComponent } from '../../../lines/fileButtonLineComponent';
-import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
-import { LockObject } from '../lockObject';
-import { Tools } from 'babylonjs/Misc/tools';
-import { GlobalState } from '../../../../globalState';
-import { ReadFileError } from 'babylonjs/Misc/fileTools';
-import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
-import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Animation } from "babylonjs/Animations/animation";
+import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
+import { FileButtonLineComponent } from "../../../lines/fileButtonLineComponent";
+import { TextInputLineComponent } from "../../../lines/textInputLineComponent";
+import { LockObject } from "../lockObject";
+import { Tools } from "babylonjs/Misc/tools";
+import { GlobalState } from "../../../../globalState";
+import { ReadFileError } from "babylonjs/Misc/fileTools";
+import { IAnimatable } from "babylonjs/Animations/animatable.interface";
+import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
 
 interface ILoadSnippetProps {
-  animations: Animation[];
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-  lockObject: LockObject;
-  globalState: GlobalState;
-  snippetServer: string;
-  setSnippetId: (id: string) => void;
-  entity: IAnimatable | TargetedAnimation;
-  setNotificationMessage: (message: string) => void;
-  animationsLoaded: (numberOfAnimations: number) => void;
+    animations: Animation[];
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    lockObject: LockObject;
+    globalState: GlobalState;
+    snippetServer: string;
+    setSnippetId: (id: string) => void;
+    entity: IAnimatable | TargetedAnimation;
+    setNotificationMessage: (message: string) => void;
+    animationsLoaded: (numberOfAnimations: number) => void;
 }
 
-export class LoadSnippet extends React.Component<
-  ILoadSnippetProps,
-  { snippetId: string }
-> {
-  private _serverAddress: string;
-  constructor(props: ILoadSnippetProps) {
-    super(props);
-    this._serverAddress = this.props.snippetServer;
-    this.state = { snippetId: '' };
-  }
+/**
+ * Loads animation locally or from the Babylon.js Snippet Server
+ */
+export class LoadSnippet extends React.Component<ILoadSnippetProps, { snippetId: string }> {
+    private _serverAddress: string;
+    constructor(props: ILoadSnippetProps) {
+        super(props);
+        this._serverAddress = this.props.snippetServer;
+        this.state = { snippetId: "" };
+    }
 
-  change(value: string) {
-    this.setState({ snippetId: value });
-    this.props.setSnippetId(value);
-  }
+    change = (value: string) => {
+        this.setState({ snippetId: value });
+        this.props.setSnippetId(value);
+    };
 
-  loadFromFile(file: File) {
-    Tools.ReadFile(
-      file,
-      (data) => {
-        let decoder = new TextDecoder('utf-8');
-        let jsonObject = JSON.parse(decoder.decode(data));
-        var result: Animation[] = [];
+    loadFromFile = (file: File) => {
+        Tools.ReadFile(
+            file,
+            (data) => {
+                let decoder = new TextDecoder("utf-8");
+                let jsonObject = JSON.parse(decoder.decode(data));
+                var result: Animation[] = [];
 
-        for (var i in jsonObject) {
-          result.push(Animation.Parse(jsonObject[i]));
-        }
+                for (var i in jsonObject) {
+                    result.push(Animation.Parse(jsonObject[i]));
+                }
 
-        if (this.props.entity) {
-          (this.props.entity as IAnimatable).animations = result;
-          var e = new PropertyChangedEvent();
-          e.object = this.props.entity;
-          e.property = 'animations';
-          e.value = (this.props.entity as IAnimatable).animations;
-          this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
-          this.props.animationsLoaded(result.length);
-        }
-      },
-      undefined,
-      true,
-      (error: ReadFileError) => {
-        console.log(error.message);
-      }
-    );
-  }
+                if (this.props.entity) {
+                    (this.props.entity as IAnimatable).animations = result;
+                    var e = new PropertyChangedEvent();
+                    e.object = this.props.entity;
+                    e.property = "animations";
+                    e.value = (this.props.entity as IAnimatable).animations;
+                    this.props.globalState.onPropertyChangedObservable.notifyObservers(e);
+                    this.props.animationsLoaded(result.length);
+                }
+            },
+            undefined,
+            true,
+            (error: ReadFileError) => {
+                console.log(error.message);
+            }
+        );
+    };
 
-  loadFromSnippet() {
-    if (this.state.snippetId !== '') {
-      //How to dispose() previous animations;
-      //How to notify observers
-      Animation.CreateFromSnippetAsync(this.state.snippetId)
-        .then((newAnimations) => {
-          // Explore how observers are notified from snippet
-          if (newAnimations instanceof Array) {
-            (this.props.entity as IAnimatable).animations = newAnimations;
-          }
+    loadFromSnippet = () => {
+        if (this.state.snippetId !== "") {
+            //Notify observers
+            Animation.CreateFromSnippetAsync(this.state.snippetId)
+                .then((newAnimations) => {
+                    // Explore how observers are notified from snippet
+                    if (newAnimations instanceof Array) {
+                        (this.props.entity as IAnimatable).animations = newAnimations;
+                    }
 
-          if (newAnimations instanceof Animation) {
-            (this.props.entity as IAnimatable).animations?.push(newAnimations);
-          }
-        })
-        .catch((err) => {
-          this.props.setNotificationMessage(
-            `Unable to load your animations: ${err}`
-          );
-        });
-    } else {
-      this.props.setNotificationMessage(`You need to add an snippet id`);
-    }
-  }
+                    if (newAnimations instanceof Animation) {
+                        (this.props.entity as IAnimatable).animations?.push(newAnimations);
+                    }
+                })
+                .catch((err) => {
+                    this.props.setNotificationMessage(`Unable to load your animations: ${err}`);
+                });
+        } else {
+            this.props.setNotificationMessage(`You need to add an snippet id`);
+        }
+    };
 
-  render() {
-    return (
-      <div className='load-container'>
-        <TextInputLineComponent
-          label='Snippet Id'
-          lockObject={this.props.lockObject}
-          value={this.state.snippetId}
-          onChange={(value: string) => this.change(value)}
-        />
-        <ButtonLineComponent
-          label='Load from snippet server'
-          onClick={() => this.loadFromSnippet()}
-        />
-        <div className='load-browse'>
-          <p>Local File</p>
-          <FileButtonLineComponent
-            label='Load'
-            onClick={(file) => this.loadFromFile(file)}
-            accept='.json'
-          />
-        </div>
-        <div className='load-server'>
-          <p>Snippet Server: </p>&nbsp;
-          <p> {this._serverAddress ?? '-'}</p>
-        </div>
-      </div>
-    );
-  }
+    render() {
+        return (
+            <div className="load-container">
+                <TextInputLineComponent
+                    label="Snippet Id"
+                    lockObject={this.props.lockObject}
+                    value={this.state.snippetId}
+                    onChange={this.change}
+                />
+                <ButtonLineComponent label="Load from snippet server" onClick={this.loadFromSnippet} />
+                <div className="load-browse">
+                    <p>Local File</p>
+                    <FileButtonLineComponent label="Load" onClick={this.loadFromFile} accept=".json" />
+                </div>
+                <div className="load-server">
+                    <p>Snippet Server: </p>&nbsp;
+                    <p> {this._serverAddress ?? "-"}</p>
+                </div>
+            </div>
+        );
+    }
 }

+ 22 - 25
inspector/src/components/actionTabs/tabs/propertyGrids/animations/notification.tsx

@@ -1,32 +1,29 @@
-import * as React from 'react';
+import * as React from "react";
 
 interface IPlayheadProps {
-  message: string;
-  open: boolean;
-  close: () => void;
+    message: string;
+    open: boolean;
+    close: () => void;
 }
 
+/**
+ * Renders the notification for the user
+ */
 export class Notification extends React.Component<IPlayheadProps> {
-  constructor(props: IPlayheadProps) {
-    super(props);
-  }
+    constructor(props: IPlayheadProps) {
+        super(props);
+    }
 
-  render() {
-    return (
-      <div
-        className='notification-area'
-        style={{ display: this.props.open ? 'block' : 'none' }}>
-        <div className='alert alert-error'>
-          <button
-            type='button'
-            className='close'
-            data-dismiss='alert'
-            onClick={this.props.close}>
-            &times;
-          </button>
-          {this.props.message}
-        </div>
-      </div>
-    );
-  }
+    render() {
+        return (
+            <div className="notification-area" style={{ display: this.props.open ? "block" : "none" }}>
+                <div className="alert alert-error">
+                    <button type="button" className="close" data-dismiss="alert" onClick={this.props.close}>
+                        &times;
+                    </button>
+                    {this.props.message}
+                </div>
+            </div>
+        );
+    }
 }

+ 3 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx

@@ -6,6 +6,9 @@ interface IPlayheadProps {
   onCurrentFrameChange: (frame: number) => void;
 }
 
+/**
+ * Renders the Playhead
+ */
 export class Playhead extends React.Component<IPlayheadProps> {
   private _direction: number;
   private _active: boolean;

+ 155 - 163
inspector/src/components/actionTabs/tabs/propertyGrids/animations/saveSnippet.tsx

@@ -1,182 +1,174 @@
-import * as React from 'react';
-import { Observable } from 'babylonjs/Misc/observable';
-import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { Tools } from 'babylonjs/Misc/tools';
-import { Animation } from 'babylonjs/Animations/animation';
-import { LockObject } from '../lockObject';
-import { Nullable } from 'babylonjs/types';
-import { GlobalState } from '../../../../globalState';
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
+import { Tools } from "babylonjs/Misc/tools";
+import { Animation } from "babylonjs/Animations/animation";
+import { LockObject } from "../lockObject";
+import { Nullable } from "babylonjs/types";
+import { GlobalState } from "../../../../globalState";
 
 interface ISaveSnippetProps {
-  animations: Nullable<Animation[]>;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-  lockObject: LockObject;
-  globalState: GlobalState;
-  snippetServer: string;
-  snippetId: string;
+    animations: Nullable<Animation[]>;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    lockObject: LockObject;
+    globalState: GlobalState;
+    snippetServer: string;
+    snippetId: string;
 }
 
 export interface Snippet {
-  url: string;
-  id: string;
+    url: string;
+    id: string;
 }
 interface SelectedAnimation {
-  id: string;
-  name: string;
-  index: number;
-  selected: boolean;
+    id: string;
+    name: string;
+    index: number;
+    selected: boolean;
 }
 
-export class SaveSnippet extends React.Component<
-  ISaveSnippetProps,
-  { selectedAnimations: SelectedAnimation[] }
-> {
-  constructor(props: ISaveSnippetProps) {
-    super(props);
-    let animList = this.props.animations?.map((animation, i) => {
-      return {
-        id: `${animation.name}_${animation.targetProperty}`,
-        name: animation.name,
-        index: i,
-        selected: false,
-      };
-    });
-    this.state = { selectedAnimations: animList ?? [] };
-  }
-
-  handleCheckboxChange(e: React.ChangeEvent<HTMLInputElement>) {
-    e.preventDefault();
-
-    let index = parseInt(e.target.id.replace('save_', ''));
-
-    let updated = this.state.selectedAnimations?.map((item) => {
-      if (item.index === index) {
-        item.selected = !item.selected;
-      }
-      return item;
-    });
-
-    this.setState({ selectedAnimations: updated });
-  }
-
-  stringifySelectedAnimations() {
-    const content: string[] = [];
-    this.state.selectedAnimations.forEach((animation) => {
-      if (animation.selected) {
-        const selected =
-          this.props.animations && this.props.animations[animation.index];
-        if (selected) {
-          content.push(selected.serialize());
-        }
-      }
-    });
-    return JSON.stringify(content);
-  }
-
-  saveToFile() {
-    const content = this.stringifySelectedAnimations();
-    Tools.Download(new Blob([content]), 'animations.json');
-  }
-
-  saveToSnippet() {
-    if (this.props.snippetId !== '') {
-      let serverId = this.props.snippetId;
-      const serverUrl = this.props.snippetServer;
-      const content = this.stringifySelectedAnimations();
-
-      var xmlHttp = new XMLHttpRequest();
-      xmlHttp.onreadystatechange = () => {
-        if (xmlHttp.readyState == 4) {
-          if (xmlHttp.status == 200) {
-            var snippet = JSON.parse(xmlHttp.responseText);
-            const oldId = serverId;
-            serverId = snippet.id;
-            if (snippet.version && snippet.version != '0') {
-              serverId += '#' + snippet.version;
-            }
-            this.forceUpdate();
-            if (navigator.clipboard) {
-              navigator.clipboard.writeText(serverId);
-            }
+/**
+ * Saves the animation snippet to the Babylon.js site or downloads the animation file locally
+ */
+export class SaveSnippet extends React.Component<ISaveSnippetProps, { selectedAnimations: SelectedAnimation[] }> {
+    constructor(props: ISaveSnippetProps) {
+        super(props);
+        let animList = this.props.animations?.map((animation, i) => {
+            return {
+                id: `${animation.name}_${animation.targetProperty}`,
+                name: animation.name,
+                index: i,
+                selected: false,
+            };
+        });
+        this.state = { selectedAnimations: animList ?? [] };
+    }
+
+    handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        e.preventDefault();
 
-            let windowAsAny = window as any;
+        let index = parseInt(e.target.id.replace("save_", ""));
 
-            if (windowAsAny.Playground && oldId) {
-              windowAsAny.Playground.onRequestCodeChangeObservable.notifyObservers(
-                {
-                  regex: new RegExp(oldId, 'g'),
-                  replace: serverId,
+        let updated = this.state.selectedAnimations?.map((item) => {
+            if (item.index === index) {
+                item.selected = !item.selected;
+            }
+            return item;
+        });
+
+        this.setState({ selectedAnimations: updated });
+    };
+
+    stringifySelectedAnimations() {
+        const content: string[] = [];
+        this.state.selectedAnimations.forEach((animation) => {
+            if (animation.selected) {
+                const selected = this.props.animations && this.props.animations[animation.index];
+                if (selected) {
+                    content.push(selected.serialize());
                 }
-              );
             }
+        });
+        return JSON.stringify(content);
+    }
 
-            alert(
-              'Animations saved with ID: ' +
-                serverId +
-                ' (please note that the id was also saved to your clipboard)'
-            );
-          } else {
-            alert('Unable to save your animations');
-          }
-        }
-      };
+    saveToFile = () => {
+        const content = this.stringifySelectedAnimations();
+        Tools.Download(new Blob([content]), "animations.json");
+    };
+
+    saveToSnippet = () => {
+        if (this.props.snippetId !== "") {
+            let serverId = this.props.snippetId;
+            const serverUrl = this.props.snippetServer;
+            const content = this.stringifySelectedAnimations();
+
+            var xmlHttp = new XMLHttpRequest();
+            xmlHttp.onreadystatechange = () => {
+                if (xmlHttp.readyState == 4) {
+                    if (xmlHttp.status == 200) {
+                        var snippet = JSON.parse(xmlHttp.responseText);
+                        const oldId = serverId;
+                        serverId = snippet.id;
+                        if (snippet.version && snippet.version != "0") {
+                            serverId += "#" + snippet.version;
+                        }
+                        this.forceUpdate();
+                        if (navigator.clipboard) {
+                            navigator.clipboard.writeText(serverId);
+                        }
+
+                        let windowAsAny = window as any;
+
+                        if (windowAsAny.Playground && oldId) {
+                            windowAsAny.Playground.onRequestCodeChangeObservable.notifyObservers({
+                                regex: new RegExp(oldId, "g"),
+                                replace: serverId,
+                            });
+                        }
+
+                        alert(
+                            "Animations saved with ID: " +
+                                serverId +
+                                " (please note that the id was also saved to your clipboard)"
+                        );
+                    } else {
+                        alert("Unable to save your animations");
+                    }
+                }
+            };
 
-      xmlHttp.open('POST', serverUrl + (serverId ? '/' + serverId : ''), true);
-      xmlHttp.setRequestHeader('Content-Type', 'application/json');
+            xmlHttp.open("POST", serverUrl + (serverId ? "/" + serverId : ""), true);
+            xmlHttp.setRequestHeader("Content-Type", "application/json");
 
-      var dataToSend = {
-        payload: JSON.stringify({
-          animations: content,
-        }),
-        name: '',
-        description: '',
-        tags: '',
-      };
+            var dataToSend = {
+                payload: JSON.stringify({
+                    animations: content,
+                }),
+                name: "",
+                description: "",
+                tags: "",
+            };
 
-      xmlHttp.send(JSON.stringify(dataToSend));
+            xmlHttp.send(JSON.stringify(dataToSend));
+        }
+    };
+
+    render() {
+        return (
+            <div className="save-container">
+                <div className="item-list">
+                    <ul>
+                        {this.props.animations?.map((animation, i) => {
+                            return (
+                                <li key={i}>
+                                    <div>
+                                        <label>
+                                            <input
+                                                id={`save_${i}`}
+                                                name={`save_${animation?.name}`}
+                                                type="checkbox"
+                                                checked={this.state.selectedAnimations[i].selected}
+                                                onChange={this.handleCheckboxChange}
+                                            />
+                                            {animation?.name}
+                                        </label>
+                                    </div>
+                                </li>
+                            );
+                        })}
+                    </ul>
+                </div>
+                <div className="save-buttons">
+                    {this.props.snippetId && <ButtonLineComponent label="Save to snippet server" onClick={this.saveToSnippet} />}
+                    <ButtonLineComponent label="Save" onClick={this.saveToFile} />
+                </div>
+                <div className="save-server">
+                    <p>Snippet Server: </p>&nbsp;
+                    <p> {this.props.snippetServer ?? "-"}</p>
+                </div>
+            </div>
+        );
     }
-  }
-
-  render() {
-    return (
-      <div className='save-container'>
-        <div className='item-list'>
-          <ul>
-            {this.props.animations?.map((animation, i) => {
-              return (
-                <li key={i}>
-                  <div>
-                    <label>
-                      <input
-                        id={`save_${i}`}
-                        name={`save_${animation?.name}`}
-                        type='checkbox'
-                        checked={this.state.selectedAnimations[i].selected}
-                        onChange={(e) => this.handleCheckboxChange(e)}
-                      />
-                      {animation?.name}
-                    </label>
-                  </div>
-                </li>
-              );
-            })}
-          </ul>
-        </div>
-        <div className='save-buttons'>
-          {this.props.snippetId !== '' ? (
-            <ButtonLineComponent
-              label='Save to snippet server'
-              onClick={() => this.saveToSnippet()}
-            />
-          ) : null}
-          <ButtonLineComponent label='Save' onClick={() => this.saveToFile()} />
-        </div>
-        <div className='save-server'>
-          <p>Snippet Server: </p>&nbsp;
-          <p> {this.props.snippetServer ?? '-'}</p>
-        </div>
-      </div>
-    );
-  }
 }

+ 37 - 36
inspector/src/components/actionTabs/tabs/propertyGrids/animations/scale-label.tsx

@@ -1,44 +1,45 @@
-import * as React from 'react';
-import { CurveScale } from './animationCurveEditorComponent';
+import * as React from "react";
+import { CurveScale } from "./animationCurveEditorComponent";
 
 interface ISwitchButtonProps {
-  current: CurveScale;
-  action?: (event: CurveScale) => void;
+    current: CurveScale;
+    action?: (event: CurveScale) => void;
 }
 
-export class ScaleLabel extends React.Component<
-  ISwitchButtonProps,
-  { current: CurveScale }
-> {
-  constructor(props: ISwitchButtonProps) {
-    super(props);
-    this.state = { current: this.props.current };
-  }
+/**
+ * Displays the current scale
+ */
+export class ScaleLabel extends React.Component<ISwitchButtonProps, { current: CurveScale }> {
+    constructor(props: ISwitchButtonProps) {
+        super(props);
+        this.state = { current: this.props.current };
+    }
 
-  renderLabel(scale: CurveScale) {
-    switch (scale) {
-      case CurveScale.default:
-        return '';
-      case CurveScale.degrees:
-        return 'DEG';
-      case CurveScale.float:
-        return 'FLT';
-      case CurveScale.integers:
-        return 'INT';
-      case CurveScale.radians:
-        return 'RAD';
+    renderLabel(scale: CurveScale) {
+        switch (scale) {
+            case CurveScale.default:
+                return "";
+            case CurveScale.degrees:
+                return "DEG";
+            case CurveScale.float:
+                return "FLT";
+            case CurveScale.integers:
+                return "INT";
+            case CurveScale.radians:
+                return "RAD";
+        }
     }
-  }
 
-  render() {
-    return (
-      <div
-        className='switch-button'
-        onClick={() =>
-          this.props.action && this.props.action(this.state.current)
-        }>
-        <p>{this.renderLabel(this.state.current)}</p>
-      </div>
-    );
-  }
+    onClickHandle = () => {
+        this.props.action && this.props.action(this.state.current);
+    };
+
+    render() {
+        const label = this.renderLabel(this.state.current);
+        return (
+            <div className="switch-button" onClick={this.onClickHandle}>
+                <p>{label}</p>
+            </div>
+        );
+    }
 }

+ 114 - 98
inspector/src/components/actionTabs/tabs/propertyGrids/animations/svgDraggableArea.tsx

@@ -1,25 +1,30 @@
 import * as React from "react";
 import { Vector2 } from "babylonjs/Maths/math.vector";
-import { KeyframeSvgPoint, IKeyframeSvgPoint } from "./keyframeSvgPoint";
+import { IKeyframeSvgPoint } from "./keyframeSvgPoint";
 
 interface ISvgDraggableAreaProps {
     keyframeSvgPoints: IKeyframeSvgPoint[];
     updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void;
     scale: number;
     viewBoxScale: number;
-    selectKeyframe: (id: string, multiselect: boolean) => void;
-    selectedControlPoint: (type: string, id: string) => void;
     deselectKeyframes: () => void;
     removeSelectedKeyframes: (points: IKeyframeSvgPoint[]) => void;
     panningY: (panningY: number) => void;
     panningX: (panningX: number) => void;
     setCurrentFrame: (direction: number) => void;
-    positionCanvas?: number;
+    positionCanvas?: Vector2;
     repositionCanvas?: boolean;
     canvasPositionEnded: () => void;
     resetActionableKeyframe: () => void;
+    framesInCanvasView: { from: number; to: number };
+    framesResized: number;
 }
 
+/**
+ * The SvgDraggableArea is a wrapper for SVG Canvas the interaction
+ *
+ * Here we control the drag and key behavior for the SVG components. 
+ */
 export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, { panX: number; panY: number }> {
     private _active: boolean;
     private _isCurrentPointControl: string;
@@ -31,6 +36,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
     private _playheadSelected: boolean;
     private _movedX: number;
     private _movedY: number;
+    private _isControlKeyPress: boolean;
     readonly _dragBuffer: number;
     readonly _draggingMultiplier: number;
 
@@ -45,8 +51,9 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         this._playheadSelected = false;
         this._movedX = 0;
         this._movedY = 0;
-        this._dragBuffer = 4;
-        this._draggingMultiplier = 3;
+        this._dragBuffer = 3;
+        this._draggingMultiplier = 10;
+        this._isControlKeyPress = false;
 
         this.state = { panX: 0, panY: 0 };
     }
@@ -59,78 +66,110 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         }, 500);
     }
 
-    componentWillReceiveProps(newProps: ISvgDraggableAreaProps) {
-        if (newProps.positionCanvas !== this.props.positionCanvas && newProps.positionCanvas !== undefined && newProps.repositionCanvas) {
-            this.setState({ panX: newProps.positionCanvas }, () => {
-                this.props.canvasPositionEnded();
-            });
+    componentDidUpdate(prevProps: ISvgDraggableAreaProps) {
+        if (
+            this.props.positionCanvas !== prevProps.positionCanvas &&
+            this.props.positionCanvas !== undefined &&
+            this.props.repositionCanvas
+        ) {
+            this.setState(
+                {
+                    panX: this.props.positionCanvas.x,
+                    panY: this.props.positionCanvas.y,
+                },
+                () => {
+                    this.props.canvasPositionEnded();
+                }
+            );
         }
     }
 
-    dragStart(e: React.TouchEvent<SVGSVGElement>): void;
-    dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragStart(e: any): void {
+    dragStart = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         e.preventDefault();
-        if (e.target.classList.contains("draggable")) {
+        if ((e.target as SVGSVGElement).classList.contains("draggable")) {
             this._active = true;
-            this._currentPointId = e.target.getAttribute("data-id");
+            const dataId = (e.target as SVGSVGElement).getAttribute("data-id");
+            if (dataId !== null) {
+                this._currentPointId = dataId;
+            }
 
-            if (e.target.classList.contains("control-point")) {
-                this._isCurrentPointControl = e.target.getAttribute("type");
+            if ((e.target as SVGSVGElement).classList.contains("control-point")) {
+                const type = (e.target as SVGSVGElement).getAttribute("type");
+                if (type !== null) {
+                    this._isCurrentPointControl = type;
+                }
             }
         }
 
-        if (e.target.classList.contains("svg-playhead")) {
+        if ((e.target as SVGSVGElement).classList.contains("svg-playhead")) {
             this._active = true;
             this._playheadSelected = true;
             this._playheadDrag = e.clientX - e.currentTarget.getBoundingClientRect().left;
         }
 
-        if (e.target.classList.contains("pannable")) {
-            this._active = true;
-            this._panStart.set(e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top);
+        if ((e.target as SVGSVGElement).classList.contains("pannable")) {
+            if (this._isControlKeyPress) {
+                this._active = true;
+                this._panStart.set(
+                    e.clientX - e.currentTarget.getBoundingClientRect().left,
+                    e.clientY - e.currentTarget.getBoundingClientRect().top
+                );
+            }
         }
-    }
+    };
 
-    drag(e: React.TouchEvent<SVGSVGElement>): void;
-    drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    drag(e: any): void {
+    drag = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         if (this._active) {
             e.preventDefault();
 
             var coord = this.getMousePosition(e);
 
             if (coord !== undefined) {
-                if (e.target.classList.contains("pannable")) {
-                    if (this._panStart.x !== 0 && this._panStart.y !== 0) {
-                        this._panStop.set(e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top);
-                        this.panDirection();
+                // Handles the canvas panning
+                if ((e.target as SVGSVGElement).classList.contains("pannable")) {
+                    if (this._isControlKeyPress) {
+                        if (this._panStart.x !== 0 && this._panStart.y !== 0) {
+                            this._panStop.set(
+                                e.clientX - e.currentTarget.getBoundingClientRect().left,
+                                e.clientY - e.currentTarget.getBoundingClientRect().top
+                            );
+                            this.panDirection();
+                        }
                     }
                 }
-                if (e.currentTarget.classList.contains("linear") && this._playheadDrag !== 0 && this._playheadSelected) {
+                // Handles the playhead dragging
+                if (
+                    e.currentTarget.classList.contains("linear") &&
+                    this._playheadDrag !== 0 &&
+                    this._playheadSelected
+                ) {
                     const moving = e.clientX - e.currentTarget.getBoundingClientRect().left;
 
-                    const distance = moving - this._playheadDrag;
                     const draggableAreaWidth = e.currentTarget.clientWidth;
-                    const framesInCavas = 20;
-                    const unit = draggableAreaWidth / framesInCavas;
 
-                    if (Math.abs(distance) >= unit / 1.25) {
-                        this.props.setCurrentFrame(Math.sign(distance));
-                        this._playheadDrag = this._playheadDrag + distance;
-                    }
+                    const initialFrame = this.props.framesInCanvasView.from;
+
+                    const lastFrame = this.props.framesInCanvasView.to;
+
+                    const framesInCanvas = lastFrame - initialFrame;
+
+                    const unit = draggableAreaWidth / framesInCanvas;
+
+                    const newFrame = Math.round(moving / unit) + initialFrame;
+                    this.props.setCurrentFrame(newFrame);
                 } else {
+                    // Handles the control point dragging
                     var newPoints = [...this.props.keyframeSvgPoints];
-
                     let point = newPoints.find((kf) => kf.id === this._currentPointId);
                     if (point) {
-                        // Check for NaN values here.
                         if (this._isCurrentPointControl === "left") {
                             point.leftControlPoint = coord;
                             point.isLeftActive = true;
+                            point.isRightActive = false;
                         } else if (this._isCurrentPointControl === "right") {
                             point.rightControlPoint = coord;
                             point.isRightActive = true;
+                            point.isLeftActive = false;
                         } else {
                             point.keyframePoint = coord;
                             point.isRightActive = false;
@@ -141,11 +180,9 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
                 }
             }
         }
-    }
+    };
 
-    dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
-    dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragEnd(e: any): void {
+    dragEnd = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         e.preventDefault();
         this._active = false;
         this._currentPointId = "";
@@ -156,15 +193,9 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         this._playheadSelected = false;
         this._movedX = 0;
         this._movedY = 0;
-    }
-
-    getMousePosition(e: React.TouchEvent<SVGSVGElement>): Vector2 | undefined;
-    getMousePosition(e: React.MouseEvent<SVGSVGElement, MouseEvent>): Vector2 | undefined;
-    getMousePosition(e: any): Vector2 | undefined {
-        if (e.touches) {
-            e = e.touches[0];
-        }
+    };
 
+    getMousePosition = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): Vector2 | undefined => {
         if (this._draggableArea.current) {
             var svg = this._draggableArea.current as SVGSVGElement;
             var CTM = svg.getScreenCTM();
@@ -176,8 +207,12 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         } else {
             return undefined;
         }
-    }
+    };
 
+    /**
+    * Handles the canvas panning direction and sets the X and Y values to move the
+    * SVG canvas
+    */
     panDirection() {
         let directionX = 1;
         if (this._movedX < this._panStop.x) {
@@ -193,17 +228,17 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
             directionY = 1; //bottom
         }
 
-        const bufferX = Math.abs(this._movedX - this._panStop.x);
-        const bufferY = Math.abs(this._movedY - this._panStop.y);
+        const bufferX = this._movedX === 0 ? 1 : Math.abs(this._movedX - this._panStop.x);
+        const bufferY = this._movedY === 0 ? 1 : Math.abs(this._movedY - this._panStop.y);
 
         let xMulti = 0;
-        if (bufferX > this._dragBuffer) {
-            xMulti = this._draggingMultiplier;
+        if (bufferX >= this._dragBuffer) {
+            xMulti = Math.round(Math.abs(bufferX - this._dragBuffer) / 2.5);
         }
 
         let yMulti = 0;
-        if (bufferY > this._dragBuffer) {
-            yMulti = this._draggingMultiplier;
+        if (bufferY >= this._dragBuffer) {
+            yMulti = Math.round(Math.abs(bufferY - this._dragBuffer) / 2.5);
         }
 
         this._movedX = this._panStop.x;
@@ -223,15 +258,22 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
 
     keyDown(e: KeyboardEvent) {
         e.preventDefault();
-        if (e.keyCode === 17) {
+        if (e.keyCode === 17 || e.keyCode === 32) {
             this._draggableArea.current?.style.setProperty("cursor", "grab");
+            this._isControlKeyPress = true;
         }
     }
 
     keyUp(e: KeyboardEvent) {
         e.preventDefault();
-        if (e.keyCode === 17) {
+        if (e.keyCode === 17 || e.keyCode === 32) {
             this._draggableArea.current?.style.setProperty("cursor", "initial");
+            this._isControlKeyPress = false;
+            this._active = false;
+            this._panStart.set(0, 0);
+            this._panStop.set(0, 0);
+            this._movedX = 0;
+            this._movedY = 0;
         }
 
         if (e.keyCode === 8 || e.keyCode === 46) {
@@ -240,7 +282,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
         }
     }
 
-    focus(e: React.MouseEvent<SVGSVGElement>) {
+    focus = (e: React.MouseEvent<SVGSVGElement>) => {
         e.preventDefault();
         this._draggableArea.current?.focus();
 
@@ -251,7 +293,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
 
             this.props.resetActionableKeyframe();
         }
-    }
+    };
 
     isNotControlPointActive() {
         const activeControlPoints = this.props.keyframeSvgPoints.filter((x) => x.isLeftActive || x.isRightActive);
@@ -263,49 +305,23 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, {
     }
 
     render() {
+        const viewBoxScaling = `${this.props.positionCanvas?.x} ${this.props.positionCanvas?.y} ${Math.round(
+            this.props.scale * 200
+        )} ${Math.round(this.props.scale * 100)}`;
         return (
             <>
                 <svg
-                    style={{
-                        width: 30,
-                        height: 364,
-                        position: "absolute",
-                        zIndex: 1,
-                        pointerEvents: "none",
-                    }}
-                >
-                    <rect x="0" y="0" width="38px" height="100%" fill="#ffffff1c"></rect>
-                </svg>
-                <svg
                     className="linear pannable"
                     ref={this._draggableArea}
                     tabIndex={0}
-                    onMouseMove={(e) => this.drag(e)}
-                    onTouchMove={(e) => this.drag(e)}
-                    onTouchStart={(e) => this.dragStart(e)}
-                    onTouchEnd={(e) => this.dragEnd(e)}
-                    onMouseDown={(e) => this.dragStart(e)}
-                    onMouseUp={(e) => this.dragEnd(e)}
-                    onMouseLeave={(e) => this.dragEnd(e)}
-                    onClick={(e) => this.focus(e)}
-                    viewBox={`${this.state.panX} ${this.state.panY} ${Math.round(this.props.scale * 200)} ${Math.round(this.props.scale * 100)}`}
+                    onMouseMove={this.drag}
+                    onMouseDown={this.dragStart}
+                    onMouseUp={this.dragEnd}
+                    onMouseLeave={this.dragEnd}
+                    onClick={this.focus}
+                    viewBox={viewBoxScaling}
                 >
                     {this.props.children}
-
-                    {this.props.keyframeSvgPoints.map((keyframe, i) => (
-                        <KeyframeSvgPoint
-                            key={`${keyframe.id}_${i}`}
-                            id={keyframe.id}
-                            keyframePoint={keyframe.keyframePoint}
-                            leftControlPoint={keyframe.leftControlPoint}
-                            rightControlPoint={keyframe.rightControlPoint}
-                            isLeftActive={keyframe.isLeftActive}
-                            isRightActive={keyframe.isRightActive}
-                            selected={keyframe.selected}
-                            selectedControlPoint={(type: string, id: string) => this.props.selectedControlPoint(type, id)}
-                            selectKeyframe={(id: string, multiselect: boolean) => this.props.selectKeyframe(id, multiselect)}
-                        />
-                    ))}
                 </svg>
             </>
         );

+ 80 - 131
inspector/src/components/actionTabs/tabs/propertyGrids/animations/targetedAnimationPropertyGridComponent.tsx

@@ -1,145 +1,94 @@
-import * as React from 'react';
-
-import { Observable } from 'babylonjs/Misc/observable';
-import { TargetedAnimation } from 'babylonjs/Animations/animationGroup';
-import { Scene } from 'babylonjs/scene';
-
-import { PropertyChangedEvent } from '../../../../propertyChangedEvent';
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { LineContainerComponent } from '../../../lineContainerComponent';
-import { TextLineComponent } from '../../../lines/textLineComponent';
-import { LockObject } from '../lockObject';
-import { GlobalState } from '../../../../globalState';
-import { TextInputLineComponent } from '../../../lines/textInputLineComponent';
-import { PopupComponent } from '../../../../popupComponent';
-import { AnimationCurveEditorComponent } from '../animations/animationCurveEditorComponent';
-import { AnimationGroup } from 'babylonjs/Animations/animationGroup';
+import * as React from "react";
+import { Observable } from "babylonjs/Misc/observable";
+import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+import { Scene } from "babylonjs/scene";
+import { PropertyChangedEvent } from "../../../../propertyChangedEvent";
+import { ButtonLineComponent } from "../../../lines/buttonLineComponent";
+import { LineContainerComponent } from "../../../lineContainerComponent";
+import { TextLineComponent } from "../../../lines/textLineComponent";
+import { LockObject } from "../lockObject";
+import { GlobalState } from "../../../../globalState";
+import { TextInputLineComponent } from "../../../lines/textInputLineComponent";
+import { PopupComponent } from "../../../../popupComponent";
+import { AnimationCurveEditorComponent } from "../animations/animationCurveEditorComponent";
+import { AnimationGroup } from "babylonjs/Animations/animationGroup";
 
 interface ITargetedAnimationGridComponentProps {
-  globalState: GlobalState;
-  targetedAnimation: TargetedAnimation;
-  scene: Scene;
-  lockObject: LockObject;
-  onSelectionChangedObservable?: Observable<any>;
-  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+    globalState: GlobalState;
+    targetedAnimation: TargetedAnimation;
+    scene: Scene;
+    lockObject: LockObject;
+    onSelectionChangedObservable?: Observable<any>;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
 }
 
-export class TargetedAnimationGridComponent extends React.Component<
-  ITargetedAnimationGridComponentProps
-> {
-  private _isCurveEditorOpen: boolean;
-  private _animationGroup: AnimationGroup | undefined;
-  constructor(props: ITargetedAnimationGridComponentProps) {
-    super(props);
-    this._animationGroup = this.props.scene.animationGroups.find((ag) => {
-      let ta = ag.targetedAnimations.find(
-        (ta) => ta === this.props.targetedAnimation
-      );
-      return ta !== undefined;
-    });
-  }
+export class TargetedAnimationGridComponent extends React.Component<ITargetedAnimationGridComponentProps> {
+    private _isCurveEditorOpen: boolean;
+    private _animationGroup: AnimationGroup | undefined;
+    constructor(props: ITargetedAnimationGridComponentProps) {
+        super(props);
+        this._animationGroup = this.props.scene.animationGroups.find((ag) => {
+            let ta = ag.targetedAnimations.find((ta) => ta === this.props.targetedAnimation);
+            return ta !== undefined;
+        });
+    }
 
-  onOpenAnimationCurveEditor() {
-    this._isCurveEditorOpen = true;
-  }
+    onOpenAnimationCurveEditor = () => {
+        this._isCurveEditorOpen = true;
+    };
 
-  onCloseAnimationCurveEditor(window: Window | null) {
-    this._isCurveEditorOpen = false;
-    if (window !== null) {
-      window.close();
-    }
-  }
+    onCloseAnimationCurveEditor = (window: Window | null) => {
+        this._isCurveEditorOpen = false;
+        if (window !== null) {
+            window.close();
+        }
+    };
 
-  playOrPause() {
-    if (this._animationGroup) {
-      if (this._animationGroup.isPlaying) {
-        this._animationGroup.stop();
-      } else {
-        this._animationGroup.start();
-      }
-      this.forceUpdate();
-    }
-  }
+    playOrPause = () => {
+        if (this._animationGroup) {
+            if (this._animationGroup.isPlaying) {
+                this._animationGroup.stop();
+            } else {
+                this._animationGroup.start();
+            }
+            this.forceUpdate();
+        }
+    };
 
-  deleteAnimation() {
-    if (this._animationGroup) {
-      let index = this._animationGroup.targetedAnimations.indexOf(
-        this.props.targetedAnimation
-      );
+    deleteAnimation = () => {
+        if (this._animationGroup) {
+            let index = this._animationGroup.targetedAnimations.indexOf(this.props.targetedAnimation);
 
-      if (index > -1) {
-        this._animationGroup.targetedAnimations.splice(index, 1);
-        this.props.onSelectionChangedObservable?.notifyObservers(null);
+            if (index > -1) {
+                this._animationGroup.targetedAnimations.splice(index, 1);
+                this.props.onSelectionChangedObservable?.notifyObservers(null);
 
-        if (this._animationGroup.isPlaying) {
-          this._animationGroup.stop();
-          this._animationGroup.start();
+                if (this._animationGroup.isPlaying) {
+                    this._animationGroup.stop();
+                    this._animationGroup.start();
+                }
+            }
         }
-      }
-    }
-  }
+    };
 
-  render() {
-    const targetedAnimation = this.props.targetedAnimation;
+    render() {
+        const targetedAnimation = this.props.targetedAnimation;
 
-    return (
-      <div className='pane'>
-        <LineContainerComponent
-          globalState={this.props.globalState}
-          title='GENERAL'
-        >
-          <TextLineComponent
-            label='Class'
-            value={targetedAnimation.getClassName()}
-          />
-          <TextInputLineComponent
-            lockObject={this.props.lockObject}
-            label='Name'
-            target={targetedAnimation.animation}
-            propertyName='name'
-            onPropertyChangedObservable={this.props.onPropertyChangedObservable}
-          />
-          {targetedAnimation.target.name && (
-            <TextLineComponent
-              label='Target'
-              value={targetedAnimation.target.name}
-              onLink={() =>
-                this.props.globalState.onSelectionChangedObservable.notifyObservers(
-                  targetedAnimation
-                )
-              }
-            />
-          )}
-          <ButtonLineComponent
-            label='Edit animation'
-            onClick={() => this.onOpenAnimationCurveEditor()}
-          />
-          {this._isCurveEditorOpen && (
-            <PopupComponent
-              id='curve-editor'
-              title='Curve Animation Editor'
-              size={{ width: 1024, height: 512 }}
-              onOpen={(window: Window) => {}}
-              onClose={(window: Window) =>
-                this.onCloseAnimationCurveEditor(window)
-              }
-            >
-              <AnimationCurveEditorComponent
-                scene={this.props.scene}
-                entity={targetedAnimation as any}
-                playOrPause={() => this.playOrPause()}
-                lockObject={this.props.lockObject}
-                globalState={this.props.globalState}
-                close={(event) => this.onCloseAnimationCurveEditor(event.view)}
-              />
-            </PopupComponent>
-          )}
-          <ButtonLineComponent
-            label='Dispose'
-            onClick={() => this.deleteAnimation()}
-          />
-        </LineContainerComponent>
-      </div>
-    );
-  }
+        return (
+            <div className="pane">
+                <LineContainerComponent globalState={this.props.globalState} title="GENERAL">
+                    <TextLineComponent label="Class" value={targetedAnimation.getClassName()} />
+                    <TextInputLineComponent lockObject={this.props.lockObject} label="Name" target={targetedAnimation.animation} propertyName="name" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    {targetedAnimation.target.name && <TextLineComponent label="Target" value={targetedAnimation.target.name} onLink={() => this.props.globalState.onSelectionChangedObservable.notifyObservers(targetedAnimation)} />}
+                    <ButtonLineComponent label="Edit animation" onClick={this.onOpenAnimationCurveEditor} />
+                    {this._isCurveEditorOpen && (
+                        <PopupComponent id="curve-editor" title="Curve Animation Editor" size={{ width: 1024, height: 512 }} onOpen={(window: Window) => {}} onClose={this.onCloseAnimationCurveEditor}>
+                            <AnimationCurveEditorComponent scene={this.props.scene} entity={targetedAnimation as any} playOrPause={this.playOrPause} lockObject={this.props.lockObject} globalState={this.props.globalState} />
+                        </PopupComponent>
+                    )}
+                    <ButtonLineComponent label="Dispose" onClick={this.deleteAnimation} />
+                </LineContainerComponent>
+            </div>
+        );
+    }
 }

+ 148 - 74
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -13,9 +13,16 @@ interface ITimelineProps {
     isPlaying: boolean;
     animationLimit: number;
     fps: number;
-    repositionCanvas: (frame: number) => void;
+    repositionCanvas: (keyframe: IAnimationKey) => void;
+    resizeWindowProportion: number;
 }
 
+/**
+ * The Timeline for the curve editor
+ *
+ * Has a scrollbar that can be resized and move to left and right. 
+ * The timeline does not affect the Canvas but only the frame container.
+ */
 export class Timeline extends React.Component<
     ITimelineProps,
     {
@@ -28,14 +35,17 @@ export class Timeline extends React.Component<
         limitValue: number;
     }
 > {
+    // Div Elements to display the timeline
     private _scrollable: React.RefObject<HTMLDivElement>;
     private _scrollbarHandle: React.RefObject<HTMLDivElement>;
     private _scrollContainer: React.RefObject<HTMLDivElement>;
     private _inputAnimationLimit: React.RefObject<HTMLInputElement>;
+    // Direction of drag and resize of timeline
     private _direction: number;
     private _scrolling: boolean;
     private _shiftX: number;
     private _active: string = "";
+    // Margin of scrollbar and container
     readonly _marginScrollbar: number;
 
     constructor(props: ITimelineProps) {
@@ -50,7 +60,9 @@ export class Timeline extends React.Component<
         this._shiftX = 0;
         this._marginScrollbar = 3;
 
+        // Limit as Int because is related to Frames.
         const limit = Math.round(this.props.animationLimit / 2);
+        const scrollWidth = this.calculateScrollWidth(0, limit);
 
         if (this.props.selected !== null) {
             this.state = {
@@ -58,7 +70,7 @@ export class Timeline extends React.Component<
                 activeKeyframe: null,
                 start: 0,
                 end: limit,
-                scrollWidth: this.calculateScrollWidth(0, limit),
+                scrollWidth: scrollWidth,
                 selectionLength: this.range(0, limit),
                 limitValue: this.props.animationLimit,
             };
@@ -66,20 +78,32 @@ export class Timeline extends React.Component<
     }
 
     componentDidMount() {
-        this.setState({
-            scrollWidth: this.calculateScrollWidth(this.state.start, this.state.end),
-        });
+        setTimeout(() => {
+            this.setState({
+                scrollWidth: this.calculateScrollWidth(this.state.start, this.state.end),
+            });
+        }, 0);
 
         this._inputAnimationLimit.current?.addEventListener("keyup", this.isEnterKeyUp.bind(this));
     }
 
+    componentDidUpdate(prevProps: ITimelineProps) {
+        if (prevProps.animationLimit !== this.props.animationLimit) {
+            this.setState({ limitValue: this.props.animationLimit });
+        }
+        if (prevProps.resizeWindowProportion !== this.props.resizeWindowProportion) {
+            if (this.state.scrollWidth !== undefined) {
+                this.setState({ scrollWidth: this.calculateScrollWidth(this.state.start, this.state.end) });
+            }
+        }
+    }
+
     componentWillUnmount() {
         this._inputAnimationLimit.current?.removeEventListener("keyup", this.isEnterKeyUp.bind(this));
     }
 
     isEnterKeyUp(event: KeyboardEvent) {
         event.preventDefault();
-
         if (event.key === "Enter") {
             this.setControlState();
         }
@@ -104,12 +128,18 @@ export class Timeline extends React.Component<
                     scrollWidth: this.calculateScrollWidth(0, newEnd),
                 });
                 if (this._scrollbarHandle.current && this._scrollContainer.current) {
-                    this._scrollbarHandle.current.style.left = `${this._scrollContainer.current.getBoundingClientRect().left + this._marginScrollbar}px`;
+                    this._scrollbarHandle.current.style.left = `${
+                        this._scrollContainer.current.getBoundingClientRect().left + this._marginScrollbar
+                    }px`;
                 }
             }
         );
     }
 
+    /**
+    * @param {number} start Frame from which the scrollbar should begin.
+    * @param {number} end Last frame for the timeline.
+    */
     calculateScrollWidth(start: number, end: number) {
         if (this._scrollContainer.current && this.props.animationLimit !== 0) {
             const containerMarginLeftRight = this._marginScrollbar * 2;
@@ -141,17 +171,21 @@ export class Timeline extends React.Component<
         }
     }
 
-    setCurrentFrame(event: React.MouseEvent<HTMLDivElement>) {
+    setCurrentFrame = (event: React.MouseEvent<HTMLDivElement>) => {
         event.preventDefault();
         if (this._scrollable.current) {
-            const containerWidth = this._scrollable.current?.clientWidth;
-            const unit = Math.round(containerWidth / this.state.selectionLength.length);
-            const frame = Math.round((event.clientX - 233) / unit) + this.state.start;
+            this._scrollable.current.focus();
+            const containerWidth = this._scrollable.current?.clientWidth - 20;
+            const framesOnView = this.state.selectionLength.length;
+            const unit = containerWidth / framesOnView;
+            const frame = Math.round((event.clientX - 230) / unit) + this.state.start;
             this.props.onCurrentFrameChange(frame);
-            this.props.repositionCanvas(frame);
         }
-    }
+    };
 
+    /**
+    * Handles the change of number of frames available in the timeline.
+    */
     handleLimitChange(event: React.ChangeEvent<HTMLInputElement>) {
         event.preventDefault();
         let newLimit = parseInt(event.target.value);
@@ -163,20 +197,16 @@ export class Timeline extends React.Component<
         });
     }
 
-    dragStart(e: React.TouchEvent<SVGSVGElement>): void;
-    dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragStart(e: any): void {
+    dragStart = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         e.preventDefault();
-        this.setState({ activeKeyframe: parseInt(e.target.id.replace("kf_", "")) });
+        this.setState({ activeKeyframe: parseInt((e.target as SVGSVGElement).id.replace("kf_", "")) });
         this._direction = e.clientX;
-    }
+    };
 
-    drag(e: React.TouchEvent<SVGSVGElement>): void;
-    drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    drag(e: any): void {
+    drag = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         e.preventDefault();
         if (this.props.keyframes) {
-            if (this.state.activeKeyframe === parseInt(e.target.id.replace("kf_", ""))) {
+            if (this.state.activeKeyframe === parseInt((e.target as SVGSVGElement).id.replace("kf_", ""))) {
                 let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
                 if (this._direction > e.clientX) {
                     let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
@@ -193,8 +223,11 @@ export class Timeline extends React.Component<
                 this.props.dragKeyframe(updatedKeyframe.frame, this.state.activeKeyframe);
             }
         }
-    }
+    };
 
+    /**
+    * Check if the frame is being used as a Keyframe by the animation
+    */
     isFrameBeingUsed(frame: number, direction: number) {
         let used = this.props.keyframes?.find((kf) => kf.frame === frame);
         if (used) {
@@ -205,42 +238,38 @@ export class Timeline extends React.Component<
         }
     }
 
-    dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
-    dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragEnd(e: any): void {
+    dragEnd = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
         e.preventDefault();
         this._direction = 0;
         this.setState({ activeKeyframe: null });
-    }
+    };
 
-    scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
-    scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
-    scrollDragStart(e: any) {
+    scrollDragStart = (e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
         e.preventDefault();
-        if (e.target.className === "scrollbar") {
-            if ((e.target.class = "scrollbar") && this._scrollbarHandle.current) {
+        this._scrollContainer.current && this._scrollContainer.current.focus();
+
+        if ((e.target as HTMLDivElement).className === "scrollbar") {
+            if (this._scrollbarHandle.current) {
                 this._scrolling = true;
                 this._shiftX = e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left;
                 this._scrollbarHandle.current.style.left = e.pageX - this._shiftX + "px";
             }
         }
 
-        if (e.target.className === "left-draggable" && this._scrollbarHandle.current) {
+        if ((e.target as HTMLDivElement).className === "left-draggable" && this._scrollbarHandle.current) {
             this._active = "leftDraggable";
-            this._shiftX = e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left;
+            this._shiftX = e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left - 3;
         }
 
-        if (e.target.className === "right-draggable" && this._scrollbarHandle.current) {
+        if ((e.target as HTMLDivElement).className === "right-draggable" && this._scrollbarHandle.current) {
             this._active = "rightDraggable";
-            this._shiftX = e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left;
+            this._shiftX = e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left + 3;
         }
-    }
+    };
 
-    scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
-    scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
-    scrollDrag(e: any) {
+    scrollDrag = (e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
         e.preventDefault();
-        if (e.target.className === "scrollbar") {
+        if ((e.target as HTMLDivElement).className === "scrollbar") {
             this.moveScrollbar(e.pageX);
         }
 
@@ -251,17 +280,20 @@ export class Timeline extends React.Component<
         if (this._active === "rightDraggable") {
             this.resizeScrollbarRight(e.clientX);
         }
-    }
+    };
 
-    scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
-    scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
-    scrollDragEnd(e: any) {
+    scrollDragEnd = (e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
         e.preventDefault();
         this._scrolling = false;
         this._active = "";
         this._shiftX = 0;
-    }
+    };
 
+    /**
+    * Sets the start, end and selection length of the scrollbar. This will control the width and
+    * height of the scrollbar as well as the number of frames available
+    * @param {number} pageX Controls the X axis of the scrollbar movement.
+    */
     moveScrollbar(pageX: number) {
         if (this._scrolling && this._scrollbarHandle.current && this._scrollContainer.current) {
             const moved = pageX - this._shiftX;
@@ -288,6 +320,9 @@ export class Timeline extends React.Component<
         }
     }
 
+    /**
+    * Controls the resizing of the scrollbar from the right handle
+    */
     resizeScrollbarRight(clientX: number) {
         if (this._scrollContainer.current && this._scrollbarHandle.current) {
             const moving = clientX - this._scrollContainer.current.getBoundingClientRect().left;
@@ -315,6 +350,9 @@ export class Timeline extends React.Component<
         }
     }
 
+    /**
+    * Controls the resizing of the scrollbar from the left handle
+    */
     resizeScrollbarLeft(clientX: number) {
         if (this._scrollContainer.current && this._scrollbarHandle.current) {
             const moving = clientX - this._scrollContainer.current.getBoundingClientRect().left;
@@ -332,7 +370,8 @@ export class Timeline extends React.Component<
             }
 
             if (!(framesTo >= this.state.end - 20)) {
-                let toleft = framesTo * unit + this._scrollContainer.current.getBoundingClientRect().left + this._marginScrollbar * 2;
+                let toleft =
+                    framesTo * unit + this._scrollContainer.current.getBoundingClientRect().left + this._marginScrollbar * 2;
                 if (this._scrollbarHandle.current) {
                     this._scrollbarHandle.current.style.left = toleft + "px";
                 }
@@ -345,6 +384,9 @@ export class Timeline extends React.Component<
         }
     }
 
+    /**
+    * Returns array with the expected length between two numbers
+    */
     range(start: number, end: number) {
         return Array.from({ length: end - start }, (_, i) => start + i * 1);
     }
@@ -365,6 +407,8 @@ export class Timeline extends React.Component<
         }
     }
 
+    dragDomFalse = () => false;
+
     render() {
         return (
             <>
@@ -380,34 +424,51 @@ export class Timeline extends React.Component<
                         scrollable={this._scrollable}
                     />
                     <div className="timeline-wrapper">
-                        <div ref={this._scrollable} className="display-line" onClick={(e) => this.setCurrentFrame(e)}>
+                        <div ref={this._scrollable} className="display-line" onClick={this.setCurrentFrame} tabIndex={50}>
                             <svg
                                 style={{
                                     width: "100%",
                                     height: 40,
                                     backgroundColor: "#222222",
                                 }}
-                                onMouseMove={(e) => this.drag(e)}
-                                onTouchMove={(e) => this.drag(e)}
-                                onTouchStart={(e) => this.dragStart(e)}
-                                onTouchEnd={(e) => this.dragEnd(e)}
-                                onMouseDown={(e) => this.dragStart(e)}
-                                onMouseUp={(e) => this.dragEnd(e)}
-                                onMouseLeave={(e) => this.dragEnd(e)}
-                                onDragStart={() => false}
+                                onMouseMove={this.drag}
+                                onMouseDown={this.dragStart}
+                                onMouseUp={this.dragEnd}
+                                onMouseLeave={this.dragEnd}
                             >
                                 {this.state.selectionLength.map((frame, i) => {
                                     return (
                                         <svg key={`tl_${frame}`}>
                                             {
                                                 <>
-                                                    <text x={(i * 100) / this.state.selectionLength.length + "%"} y="18" style={{ fontSize: 10, fill: "#555555" }}>
-                                                        {frame}
-                                                    </text>
-                                                    <line x1={(i * 100) / this.state.selectionLength.length + "%"} y1="22" x2={(i * 100) / this.state.selectionLength.length + "%"} y2="40" style={{ stroke: "#555555", strokeWidth: 0.5 }} />
-
+                                                    {frame % Math.round(this.state.selectionLength.length / 20) === 0 ? (
+                                                        <>
+                                                            <text
+                                                                x={(i * 100) / this.state.selectionLength.length + "%"}
+                                                                y="18"
+                                                                style={{ fontSize: 10, fill: "#555555" }}
+                                                            >
+                                                                {frame}
+                                                            </text>
+                                                            <line
+                                                                x1={(i * 100) / this.state.selectionLength.length + "%"}
+                                                                y1="22"
+                                                                x2={(i * 100) / this.state.selectionLength.length + "%"}
+                                                                y2="40"
+                                                                style={{ stroke: "#555555", strokeWidth: 0.5 }}
+                                                            />
+                                                        </>
+                                                    ) : null}
                                                     {this.getCurrentFrame(frame) ? (
-                                                        <svg x={this._scrollable.current ? this._scrollable.current.clientWidth / this.state.selectionLength.length / 2 : 1}>
+                                                        <svg
+                                                            x={
+                                                                this._scrollable.current
+                                                                    ? this._scrollable.current.clientWidth /
+                                                                      this.state.selectionLength.length /
+                                                                      2
+                                                                    : 1
+                                                            }
+                                                        >
                                                             <line
                                                                 x1={(i * 100) / this.state.selectionLength.length + "%"}
                                                                 y1="0"
@@ -415,7 +476,10 @@ export class Timeline extends React.Component<
                                                                 y2="40"
                                                                 style={{
                                                                     stroke: "rgba(18, 80, 107, 0.26)",
-                                                                    strokeWidth: this._scrollable.current ? this._scrollable.current.clientWidth / this.state.selectionLength.length : 1,
+                                                                    strokeWidth: this._scrollable.current
+                                                                        ? this._scrollable.current.clientWidth /
+                                                                          this.state.selectionLength.length
+                                                                        : 1,
                                                                 }}
                                                             />
                                                         </svg>
@@ -423,7 +487,14 @@ export class Timeline extends React.Component<
 
                                                     {this.getKeyframe(frame) ? (
                                                         <svg key={`kf_${i}`} tabIndex={i + 40}>
-                                                            <line id={`kf_${i.toString()}`} x1={(i * 100) / this.state.selectionLength.length + "%"} y1="0" x2={(i * 100) / this.state.selectionLength.length + "%"} y2="40" style={{ stroke: "#ffc017", strokeWidth: 1 }} />
+                                                            <line
+                                                                id={`kf_${i.toString()}`}
+                                                                x1={(i * 100) / this.state.selectionLength.length + "%"}
+                                                                y1="0"
+                                                                x2={(i * 100) / this.state.selectionLength.length + "%"}
+                                                                y2="40"
+                                                                style={{ stroke: "#ffc017", strokeWidth: 1 }}
+                                                            />
                                                         </svg>
                                                     ) : null}
                                                 </>
@@ -436,16 +507,13 @@ export class Timeline extends React.Component<
 
                         <div
                             className="timeline-scroll-handle"
-                            onMouseMove={(e) => this.scrollDrag(e)}
-                            onTouchMove={(e) => this.scrollDrag(e)}
-                            onTouchStart={(e) => this.scrollDragStart(e)}
-                            onTouchEnd={(e) => this.scrollDragEnd(e)}
-                            onMouseDown={(e) => this.scrollDragStart(e)}
-                            onMouseUp={(e) => this.scrollDragEnd(e)}
-                            onMouseLeave={(e) => this.scrollDragEnd(e)}
-                            onDragStart={() => false}
+                            onMouseMove={this.scrollDrag}
+                            onMouseDown={this.scrollDragStart}
+                            onMouseUp={this.scrollDragEnd}
+                            onMouseLeave={this.scrollDragEnd}
+                            onDragStart={this.dragDomFalse}
                         >
-                            <div className="scroll-handle" ref={this._scrollContainer}>
+                            <div className="scroll-handle" ref={this._scrollContainer} tabIndex={60}>
                                 <div className="handle" ref={this._scrollbarHandle} style={{ width: this.state.scrollWidth }}>
                                     <div className="left-grabber">
                                         <div className="left-draggable">
@@ -470,7 +538,13 @@ export class Timeline extends React.Component<
                         </div>
 
                         <div className="input-frame">
-                            <input ref={this._inputAnimationLimit} type="number" value={this.state.limitValue} onChange={(e) => this.handleLimitChange(e)} onBlur={(e) => this.onInputBlur(e)}></input>
+                            <input
+                                ref={this._inputAnimationLimit}
+                                type="number"
+                                value={this.state.limitValue}
+                                onChange={(e) => this.handleLimitChange(e)}
+                                onBlur={(e) => this.onInputBlur(e)}
+                            ></input>
                         </div>
                     </div>
                 </div>