浏览代码

Adding frame playback (#8397)

* dragging

* playing

* whats new

* remove unused function

* remove unused library

Co-authored-by: Alejandro Toledo <alex@pixelspace.com>
Alejandro Toledo Martinez 5 年之前
父节点
当前提交
984cd86037

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

@@ -8,7 +8,7 @@
 - Added HDR texture filtering tools to the sandbox ([Sebavan](https://github.com/sebavan/))
 - Reflection probes can now be used to give accurate shading with PBR ([CraigFeldpsar](https://github.com/craigfeldspar) and ([Sebavan](https://github.com/sebavan/)))
 - Added editing of PBR materials, Post processes and Particle fragment shaders in the node material editor ([Popov72](https://github.com/Popov72))
-- Added Curve editor to manage entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
+- Added Curve editor to manage selected entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
 - Added support in `ShadowGenerator` for fast fake soft transparent shadows ([Popov72](https://github.com/Popov72))
 - Added support for **thin instances** for faster mesh instances. [Doc](https://doc.babylonjs.com/how_to/how_to_use_thininstances) ([Popov72](https://github.com/Popov72))
 

+ 21 - 15
inspector/src/components/actionTabs/lines/iconButtonLineComponent.tsx

@@ -1,21 +1,27 @@
-import * as React from "react";
+import * as React from 'react';
 
 export interface IIconButtonLineComponentProps {
-    icon: string;
-    onClick: () => void;
-    tooltip: string;
-    active?: boolean;
+  icon: string;
+  onClick: () => void;
+  tooltip: string;
+  active?: boolean;
 }
 
-export class IconButtonLineComponent extends React.Component<IIconButtonLineComponentProps> {
-    constructor(props: IIconButtonLineComponentProps) {
-        super(props);
-    }
+export class IconButtonLineComponent extends React.Component<
+  IIconButtonLineComponentProps
+> {
+  constructor(props: IIconButtonLineComponentProps) {
+    super(props);
+  }
 
-    render() {
-
-        return (
-            <div style={{backgroundColor: this.props.active ? '#111111' : 'transparent'}} title={this.props.tooltip} className={`icon ${this.props.icon}`} onClick={() => this.props.onClick()} />
-        );
-    }
+  render() {
+    return (
+      <div
+        style={{ backgroundColor: this.props.active ? '#111111' : '' }}
+        title={this.props.tooltip}
+        className={`icon ${this.props.icon}`}
+        onClick={() => this.props.onClick()}
+      />
+    );
+  }
 }

+ 346 - 253
inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx

@@ -1,8 +1,7 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
-import { Observable } from "babylonjs/Misc/observable";
-import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Observable } from 'babylonjs/Misc/observable';
+import { PropertyChangedEvent } from '../../../../../components/propertyChangedEvent';
 import { Animation } from 'babylonjs/Animations/animation';
 import { Vector2, Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
 import { Size } from 'babylonjs/Maths/math.size';
@@ -10,277 +9,371 @@ import { Color3, Color4 } from 'babylonjs/Maths/math.color';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 
 interface IAddAnimationProps {
-   isOpen: boolean;
-   close: () => void;
-   entity: IAnimatable;
-   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
-   setNotificationMessage: (message: string) => void;
-   changed: () => void;
+  isOpen: boolean;
+  close: () => void;
+  entity: IAnimatable;
+  onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+  setNotificationMessage: (message: string) => void;
+  changed: () => void;
 }
 
-export class AddAnimation extends React.Component<IAddAnimationProps, {animationName: string, animationTargetProperty: string, animationType:string, loopMode: number, animationTargetPath:string}>{ 
-    constructor(props: IAddAnimationProps) {
-        super(props);
-        this.state = { animationName: "", animationTargetPath: "", animationType: "Float", loopMode: Animation.ANIMATIONLOOPMODE_CYCLE, animationTargetProperty: ""}
-    }
-
-    getAnimationTypeofChange(selected: string) {
-        let dataType = 0;
-        switch (selected) {
-            case "Float":
-                dataType = Animation.ANIMATIONTYPE_FLOAT;
-                break;
-            case "Quaternion":
-                dataType = Animation.ANIMATIONTYPE_QUATERNION;
-                break;
-            case "Vector3":
-                dataType = Animation.ANIMATIONTYPE_VECTOR3;
-                break;
-            case "Vector2":
-                dataType = Animation.ANIMATIONTYPE_VECTOR2;
-                break;
-            case "Size":
-                dataType = Animation.ANIMATIONTYPE_SIZE;
-                break;
-            case "Color3":
-                dataType = Animation.ANIMATIONTYPE_COLOR3;
-                break;
-            case "Color4":
-                dataType = Animation.ANIMATIONTYPE_COLOR4;
-                break;
-        }
-
-        return dataType;
+export class AddAnimation extends React.Component<
+  IAddAnimationProps,
+  {
+    animationName: string;
+    animationTargetProperty: string;
+    animationType: string;
+    loopMode: number;
+    animationTargetPath: string;
+  }
+> {
+  constructor(props: IAddAnimationProps) {
+    super(props);
+    this.state = {
+      animationName: '',
+      animationTargetPath: '',
+      animationType: 'Float',
+      loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
+      animationTargetProperty: '',
+    };
+  }
 
+  getAnimationTypeofChange(selected: string) {
+    let dataType = 0;
+    switch (selected) {
+      case 'Float':
+        dataType = Animation.ANIMATIONTYPE_FLOAT;
+        break;
+      case 'Quaternion':
+        dataType = Animation.ANIMATIONTYPE_QUATERNION;
+        break;
+      case 'Vector3':
+        dataType = Animation.ANIMATIONTYPE_VECTOR3;
+        break;
+      case 'Vector2':
+        dataType = Animation.ANIMATIONTYPE_VECTOR2;
+        break;
+      case 'Size':
+        dataType = Animation.ANIMATIONTYPE_SIZE;
+        break;
+      case 'Color3':
+        dataType = Animation.ANIMATIONTYPE_COLOR3;
+        break;
+      case 'Color4':
+        dataType = Animation.ANIMATIONTYPE_COLOR4;
+        break;
     }
 
-    addAnimation() {
-        if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
-
-            let matchTypeTargetProperty = this.state.animationTargetProperty.split('.');
-            let animationDataType = this.getAnimationTypeofChange(this.state.animationType);
-            let matched = false;
-
-            if (matchTypeTargetProperty.length === 1) {
-                let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
+    return dataType;
+  }
 
-                if (match) {
-                    switch (match.constructor.name) {
-                        case "Vector2":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR2 ? matched = true : matched = false;
-                            break;
-                        case "Vector3":
-                            animationDataType === Animation.ANIMATIONTYPE_VECTOR3 ? matched = true : matched = false;
-                            break;
-                        case "Quaternion":
-                            animationDataType === Animation.ANIMATIONTYPE_QUATERNION ? matched = true : matched = false;
-                            break;
-                        case "Color3":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR3 ? matched = true : matched = false;
-                            break;
-                        case "Color4":
-                            animationDataType === Animation.ANIMATIONTYPE_COLOR4 ? matched = true : matched = false;
-                            break;
-                        case "Size":
-                            animationDataType === Animation.ANIMATIONTYPE_SIZE ? matched = true : matched = false;
-                            break;
-                        default: console.log("not recognized");
-                            break;
-                    }
-                } else {
-                   this.props.setNotificationMessage(`The selected entity doesn't have a ${matchTypeTargetProperty[0]} property`)
-                }
-            } else if (matchTypeTargetProperty.length > 1) {
-                let match = (this.props.entity as any)[matchTypeTargetProperty[0]][matchTypeTargetProperty[1]];
-                if (typeof match === "number") {
-                    animationDataType === Animation.ANIMATIONTYPE_FLOAT ? matched = true : matched = false;
-                }
-            }
+  addAnimation() {
+    if (
+      this.state.animationName != '' &&
+      this.state.animationTargetProperty != ''
+    ) {
+      let matchTypeTargetProperty = this.state.animationTargetProperty.split(
+        '.'
+      );
+      let animationDataType = this.getAnimationTypeofChange(
+        this.state.animationType
+      );
+      let matched = false;
 
-            if (matched) {
+      if (matchTypeTargetProperty.length === 1) {
+        let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
 
-                let startValue;
-                let endValue;
-                let outTangent;
-                let inTangent;
-                // Default start and end values for new animations
-                switch (animationDataType) {
-                    case Animation.ANIMATIONTYPE_FLOAT:
-                        startValue = 1;
-                        endValue = 1;
-                        outTangent = 0;
-                        inTangent = 0;
-                        break;
-                    case Animation.ANIMATIONTYPE_VECTOR2:
-                        startValue = new Vector2(1, 1);
-                        endValue = new Vector2(1, 1);
-                        outTangent = Vector2.Zero();
-                        inTangent = Vector2.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_VECTOR3:
-                        startValue = new Vector3(1, 1, 1);
-                        endValue = new Vector3(1, 1, 1);
-                        outTangent = Vector3.Zero();
-                        inTangent = Vector3.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_QUATERNION:
-                        startValue = new Quaternion(1, 1, 1, 1);
-                        endValue = new Quaternion(1, 1, 1, 1);
-                        outTangent = Quaternion.Zero();
-                        inTangent = Quaternion.Zero();
-                        break;
-                    case Animation.ANIMATIONTYPE_COLOR3:
-                        startValue = new Color3(1, 1, 1);
-                        endValue = new Color3(1, 1, 1);
-                        outTangent = new Color3(0, 0, 0);
-                        inTangent = new Color3(0, 0, 0);
-                        break;
-                    case Animation.ANIMATIONTYPE_COLOR4:
-                        startValue = new Color4(1, 1, 1, 1);
-                        endValue = new Color4(1, 1, 1, 1);
-                        outTangent = new Color4(0, 0, 0, 0);
-                        inTangent = new Color4(0, 0, 0, 0);
-                        break;
-                    case Animation.ANIMATIONTYPE_SIZE:
-                        startValue = new Size(1, 1);
-                        endValue = new Size(1, 1);
-                        outTangent = Size.Zero();
-                        inTangent = Size.Zero();
-                        break;
-                    default: console.log("not recognized");
-                        break;
-                }
+        if (match) {
+          switch (match.constructor.name) {
+            case 'Vector2':
+              animationDataType === Animation.ANIMATIONTYPE_VECTOR2
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Vector3':
+              animationDataType === Animation.ANIMATIONTYPE_VECTOR3
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Quaternion':
+              animationDataType === Animation.ANIMATIONTYPE_QUATERNION
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Color3':
+              animationDataType === Animation.ANIMATIONTYPE_COLOR3
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Color4':
+              animationDataType === Animation.ANIMATIONTYPE_COLOR4
+                ? (matched = true)
+                : (matched = false);
+              break;
+            case 'Size':
+              animationDataType === Animation.ANIMATIONTYPE_SIZE
+                ? (matched = true)
+                : (matched = false);
+              break;
+            default:
+              console.log('not recognized');
+              break;
+          }
+        } else {
+          this.props.setNotificationMessage(
+            `The selected entity doesn't have a ${matchTypeTargetProperty[0]} property`
+          );
+        }
+      } else if (matchTypeTargetProperty.length > 1) {
+        let matchProp = (this.props.entity as any)[matchTypeTargetProperty[0]];
+        if (matchProp) {
+          let match = matchProp[matchTypeTargetProperty[1]];
+          if (typeof match === 'number') {
+            animationDataType === Animation.ANIMATIONTYPE_FLOAT
+              ? (matched = true)
+              : (matched = false);
+          }
+        }
+      }
 
-                let alreadyAnimatedProperty = (this.props.entity as IAnimatable).animations?.find(anim =>
-                    anim.targetProperty === this.state.animationTargetProperty
-                    , this);
+      if (matched) {
+        let startValue;
+        let endValue;
+        let outTangent;
+        let inTangent;
+        // Default start and end values for new animations
+        switch (animationDataType) {
+          case Animation.ANIMATIONTYPE_FLOAT:
+            startValue = 1;
+            endValue = 1;
+            outTangent = 0;
+            inTangent = 0;
+            break;
+          case Animation.ANIMATIONTYPE_VECTOR2:
+            startValue = new Vector2(1, 1);
+            endValue = new Vector2(1, 1);
+            outTangent = Vector2.Zero();
+            inTangent = Vector2.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_VECTOR3:
+            startValue = new Vector3(1, 1, 1);
+            endValue = new Vector3(1, 1, 1);
+            outTangent = Vector3.Zero();
+            inTangent = Vector3.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_QUATERNION:
+            startValue = new Quaternion(1, 1, 1, 1);
+            endValue = new Quaternion(1, 1, 1, 1);
+            outTangent = Quaternion.Zero();
+            inTangent = Quaternion.Zero();
+            break;
+          case Animation.ANIMATIONTYPE_COLOR3:
+            startValue = new Color3(1, 1, 1);
+            endValue = new Color3(1, 1, 1);
+            outTangent = new Color3(0, 0, 0);
+            inTangent = new Color3(0, 0, 0);
+            break;
+          case Animation.ANIMATIONTYPE_COLOR4:
+            startValue = new Color4(1, 1, 1, 1);
+            endValue = new Color4(1, 1, 1, 1);
+            outTangent = new Color4(0, 0, 0, 0);
+            inTangent = new Color4(0, 0, 0, 0);
+            break;
+          case Animation.ANIMATIONTYPE_SIZE:
+            startValue = new Size(1, 1);
+            endValue = new Size(1, 1);
+            outTangent = Size.Zero();
+            inTangent = Size.Zero();
+            break;
+          default:
+            console.log('not recognized');
+            break;
+        }
 
-                let alreadyAnimationName = (this.props.entity as IAnimatable).animations?.find(anim =>
-                    anim.name === this.state.animationName
-                    , this);
+        let alreadyAnimatedProperty = (this.props
+          .entity as IAnimatable).animations?.find(
+          (anim) => anim.targetProperty === this.state.animationTargetProperty,
+          this
+        );
 
-                if (alreadyAnimatedProperty) {
-                    this.props.setNotificationMessage(`The property "${this.state.animationTargetProperty}" already has an animation`);
-                } else if (alreadyAnimationName) {
-                    this.props.setNotificationMessage(`There is already an animation with the name: "${this.state.animationName}"`);
-                } else {
+        let alreadyAnimationName = (this.props
+          .entity as IAnimatable).animations?.find(
+          (anim) => anim.name === this.state.animationName,
+          this
+        );
 
-                    let animation = new Animation(this.state.animationName, this.state.animationTargetProperty, 30, animationDataType);
+        if (alreadyAnimatedProperty) {
+          this.props.setNotificationMessage(
+            `The property "${this.state.animationTargetProperty}" already has an animation`
+          );
+        } else if (alreadyAnimationName) {
+          this.props.setNotificationMessage(
+            `There is already an animation with the name: "${this.state.animationName}"`
+          );
+        } else {
+          let animation = new Animation(
+            this.state.animationName,
+            this.state.animationTargetProperty,
+            30,
+            animationDataType
+          );
 
-                    // Start with two keyframes
-                    var keys = [];
-                    keys.push({
-                        frame: 0,
-                        value: startValue,
-                        outTangent: outTangent
-                    });
+          // Start with two keyframes
+          var keys = [];
+          keys.push({
+            frame: 0,
+            value: startValue,
+            outTangent: outTangent,
+          });
 
-                    keys.push({
-                        inTangent: inTangent,
-                        frame: 100,
-                        value: endValue
-                    });
+          keys.push({
+            inTangent: inTangent,
+            frame: 100,
+            value: endValue,
+          });
 
-                    animation.setKeys(keys);
+          animation.setKeys(keys);
 
-                    if (this.props.entity.animations){
-                        const store = this.props.entity.animations;
-                        const updatedCollection = [...this.props.entity.animations, animation]
-                        this.raiseOnPropertyChanged(updatedCollection, store);
-                        this.props.entity.animations = updatedCollection;
-                        this.props.changed();
-                        this.props.close();
-                        //Cleaning form fields
-                        this.setState({ animationName: "", animationTargetPath: "", animationType: "Float", loopMode: Animation.ANIMATIONLOOPMODE_CYCLE, animationTargetProperty: ""});
-                    }   
-                }
-            } else {
-                this.props.setNotificationMessage(`The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type`);
-            }
-        } else {
-            this.props.setNotificationMessage(`You need to provide a name and target property.`);
+          if (this.props.entity.animations) {
+            const store = this.props.entity.animations;
+            const updatedCollection = [
+              ...this.props.entity.animations,
+              animation,
+            ];
+            this.raiseOnPropertyChanged(updatedCollection, store);
+            this.props.entity.animations = updatedCollection;
+            this.props.changed();
+            this.props.close();
+            //Cleaning form fields
+            this.setState({
+              animationName: '',
+              animationTargetPath: '',
+              animationType: 'Float',
+              loopMode: Animation.ANIMATIONLOOPMODE_CYCLE,
+              animationTargetProperty: '',
+            });
+          }
         }
+      } else {
+        this.props.setNotificationMessage(
+          `The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type`
+        );
+      }
+    } else {
+      this.props.setNotificationMessage(
+        `You need to provide a name and target property.`
+      );
     }
+  }
 
-    raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
-        if (!this.props.onPropertyChangedObservable) {
-            return;
-        }
-
-        this.props.onPropertyChangedObservable.notifyObservers({
-            object: this.props.entity,
-            property: 'animations',
-            value: newValue,
-            initialValue: previousValue
-        });
+  raiseOnPropertyChanged(newValue: Animation[], previousValue: Animation[]) {
+    if (!this.props.onPropertyChangedObservable) {
+      return;
     }
 
-    handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationName: event.target.value.trim() });
-    }
-    
-    handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationTargetPath: event.target.value.trim() });
-    }
+    this.props.onPropertyChangedObservable.notifyObservers({
+      object: this.props.entity,
+      property: 'animations',
+      value: newValue,
+      initialValue: previousValue,
+    });
+  }
 
-    handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
-        event.preventDefault();
-        this.setState({ animationType: event.target.value });
-    }
+  handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationName: event.target.value.trim() });
+  }
 
-    handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
-        event.preventDefault();
-        this.setState({ animationTargetProperty: event.target.value });
-    }
+  handlePathChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationTargetPath: event.target.value.trim() });
+  }
 
-    handleLoopModeChange(event: React.ChangeEvent<HTMLSelectElement>) {
-        event.preventDefault();
-        this.setState({ loopMode: parseInt(event.target.value) });
-    }
-     
-    render() { 
-       return (
-        <div className="new-animation" style={{ display: this.props.isOpen ? "block" : "none" }}>
-            <div className="sub-content">
-            <div className="label-input">
-                <label>Target Path</label>
-                <input type="text" value={this.state.animationTargetPath} onChange={(e) => this.handlePathChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Display Name</label>
-                <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Property</label>
-                <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
-            </div>
-            <div className="label-input">
-                <label>Type</label>
-                <select onChange={(e) => this.handleTypeChange(e)} value={this.state.animationType}>
-                    <option value="Float">Float</option>
-                    <option value="Vector3">Vector3</option>
-                    <option value="Vector2">Vector2</option>
-                    <option value="Quaternion">Quaternion</option>
-                    <option value="Color3">Color3</option>
-                    <option value="Color4">Color4</option>
-                    <option value="Size">Size</option>
-                </select>
-            </div>
-            <div className="label-input">
-                <label>Loop Mode</label>
-                <select onChange={(e) => this.handleLoopModeChange(e)} 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={"Create"} onClick={() => this.addAnimation()} />
-            </div>
-            </div>
+  handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    event.preventDefault();
+    this.setState({ animationType: event.target.value });
+  }
+
+  handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
+    event.preventDefault();
+    this.setState({ animationTargetProperty: event.target.value });
+  }
+
+  handleLoopModeChange(event: React.ChangeEvent<HTMLSelectElement>) {
+    event.preventDefault();
+    this.setState({ loopMode: parseInt(event.target.value) });
+  }
+
+  render() {
+    return (
+      <div
+        className='new-animation'
+        style={{ display: this.props.isOpen ? 'block' : 'none' }}
+      >
+        <div className='sub-content'>
+          <div className='label-input'>
+            <label>Target Path</label>
+            <input
+              type='text'
+              value={this.state.animationTargetPath}
+              onChange={(e) => this.handlePathChange(e)}
+              disabled
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Display Name</label>
+            <input
+              type='text'
+              value={this.state.animationName}
+              onChange={(e) => this.handleNameChange(e)}
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Property</label>
+            <input
+              type='text'
+              value={this.state.animationTargetProperty}
+              onChange={(e) => this.handlePropertyChange(e)}
+            ></input>
+          </div>
+          <div className='label-input'>
+            <label>Type</label>
+            <select
+              onChange={(e) => this.handleTypeChange(e)}
+              value={this.state.animationType}
+            >
+              <option value='Float'>Float</option>
+              <option value='Vector3'>Vector3</option>
+              <option value='Vector2'>Vector2</option>
+              <option value='Quaternion'>Quaternion</option>
+              <option value='Color3'>Color3</option>
+              <option value='Color4'>Color4</option>
+              <option value='Size'>Size</option>
+            </select>
+          </div>
+          <div className='label-input'>
+            <label>Loop Mode</label>
+            <select
+              onChange={(e) => this.handleLoopModeChange(e)}
+              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={'Create'}
+              onClick={() => this.addAnimation()}
+            />
+          </div>
         </div>
-        )
-    }
-} 
+      </div>
+    );
+  }
+}

文件差异内容过多而无法显示
+ 1380 - 1033
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx


+ 300 - 209
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationPropertyGridComponent.tsx

@@ -1,13 +1,13 @@
-import * as React from "react";
+import * as React from 'react';
 
-import { Observable, Observer } from "babylonjs/Misc/observable";
-import { Scene } from "babylonjs/scene";
+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 { 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';
@@ -22,238 +22,329 @@ import { AnimationCurveEditorComponent } from '../animations/animationCurveEdito
 import { PopupComponent } from '../animations/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
-    }
-
-    constructor(props: IAnimationGridComponentProps) {
-        super(props);
-
-        this.state = { currentFrame: 0 };
-
-        const animatableAsAny = this.props.animatable as any;
+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,
+  };
 
-        this._ranges = animatableAsAny.getAnimationRanges ? animatableAsAny.getAnimationRanges() : [];
-        if (animatableAsAny.getAnimatables) {
-            const animatables = animatableAsAny.getAnimatables();
-            this._animations = new Array<Animation>();
+  constructor(props: IAnimationGridComponentProps) {
+    super(props);
 
-            animatables.forEach((animatable: IAnimatable) => {
-                if (animatable.animations) {
-                    this._animations!.push(...animatable.animations);
-                }
-            });
+    this.state = { currentFrame: 0 };
 
-            if (animatableAsAny.animations) {
-                this._animations!.push(...animatableAsAny.animations);
-            }
+    const animatableAsAny = this.props.animatable as any;
 
-            // Extract from and to
-            if (this._animations && this._animations.length) {
-                this._animations.forEach(animation => {
-                    let keys = animation.getKeys();
+    this._ranges = animatableAsAny.getAnimationRanges
+      ? animatableAsAny.getAnimationRanges()
+      : [];
+    if (animatableAsAny.getAnimatables) {
+      const animatables = animatableAsAny.getAnimatables();
+      this._animations = new Array<Animation>();
 
-                    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;
-                        }
-                    }
-                });
-            }
+      animatables.forEach((animatable: IAnimatable) => {
+        if (animatable.animations) {
+          this._animations!.push(...animatable.animations);
         }
+      });
 
-        this.timelineRef = React.createRef();
-    }
+      if (animatableAsAny.animations) {
+        this._animations!.push(...animatableAsAny.animations);
+      }
 
-    playOrPause() {
-        const animatable = this.props.animatable;
-        this._isPlaying = this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
+      // Extract from and to
+      if (this._animations && this._animations.length) {
+        this._animations.forEach((animation) => {
+          let keys = animation.getKeys();
 
-        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);
-        }
-        this.forceUpdate();
-    }
-
-    componentDidMount() {
-        this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(() => {
-            if (!this._isPlaying || !this._mainAnimatable) {
-                return;
+          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;
             }
-            this.setState({ currentFrame: this._mainAnimatable.masterFrame });
+          }
         });
+      }
     }
 
-    componentWillUnmount() {
-        if (this._onBeforeRenderObserver) {
-            this.props.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver);
-            this._onBeforeRenderObserver = null;
-        }
-    }
+    this.timelineRef = React.createRef();
+  }
 
-    onCurrentFrameChange(value: number) {
-        if (!this._mainAnimatable) {
-            return;
-        }
+  playOrPause() {
+    const animatable = this.props.animatable;
+    this._isPlaying =
+      this.props.scene.getAllAnimatablesByTarget(animatable).length > 0;
 
-        this._mainAnimatable.goToFrame(value);
-        this.setState({ currentFrame: value });
+    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
+      );
     }
+    this.forceUpdate();
+  }
 
-    onChangeFromOrTo() {
-        this.playOrPause();
-        if (this._isPlaying) {
-            this.playOrPause();
+  componentDidMount() {
+    this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(
+      () => {
+        if (!this._isPlaying || !this._mainAnimatable) {
+          return;
         }
+        this.setState({ currentFrame: this._mainAnimatable.masterFrame });
+      }
+    );
+  }
+
+  componentWillUnmount() {
+    if (this._onBeforeRenderObserver) {
+      this.props.scene.onBeforeRenderObservable.remove(
+        this._onBeforeRenderObserver
+      );
+      this._onBeforeRenderObserver = null;
     }
+  }
 
-    onOpenAnimationCurveEditor() {
-        this._isCurveEditorOpen = true;
+  onCurrentFrameChange(value: number) {
+    if (!this._mainAnimatable) {
+      return;
     }
 
-    onCloseAnimationCurveEditor(window: Window | null) {
-        this._isCurveEditorOpen = false;
-        if (window === null) {
-            console.log("Window already closed");
-        } else {
-            window.close();
-        }
+    this._mainAnimatable.goToFrame(value);
+    this.setState({ currentFrame: value });
+  }
+
+  onChangeFromOrTo() {
+    this.playOrPause();
+    if (this._isPlaying) {
+      this.playOrPause();
     }
+  }
 
-    render() {
-        const animatable = this.props.animatable;
-        const animatableAsAny = this.props.animatable as any;
+  onOpenAnimationCurveEditor() {
+    this._isCurveEditorOpen = true;
+  }
 
-        let animatablesForTarget = this.props.scene.getAllAnimatablesByTarget(animatable);
-        this._isPlaying = animatablesForTarget.length > 0;
+  onCloseAnimationCurveEditor(window: Window | null) {
+    this._isCurveEditorOpen = false;
+    if (window === null) {
+      console.log('Window already closed');
+    } else {
+      window.close();
+    }
+  }
 
-        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;
+      }
+    }
 
-                            {
+    let animations = animatable.animations;
 
-                                this._isCurveEditorOpen && <PopupComponent
-                                    id="curve-editor"
-                                    title="Curve Animation Editor"
-                                    size={{ width: 1024, height: 490 }}
-                                    onOpen={(window: Window) => { }}
-                                    onClose={(window: Window) => this.onCloseAnimationCurveEditor(window)}>
+    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}
+                  />
+                );
+              })}
 
-                                    <AnimationCurveEditorComponent
-                                        scene={this.props.scene}
-                                        entity={animatableAsAny}
-                                        close={(event) => this.onCloseAnimationCurveEditor(event.view)}
-                                        lockObject={this.props.lockObject}
-                                        playOrPause={() => this.playOrPause()} />
-                                </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>
+              {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()}
+                  />
+                </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;
                         }
-                    </>
-                }
-            </div>
-        );
-    }
-}
+                        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>
+            )}
+          </>
+        )}
+      </div>
+    );
+  }
+}

+ 132 - 55
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss

@@ -1,3 +1,7 @@
+body {
+  background-color: rgb(51, 51, 51);
+}
+
 #animation-curve-editor {
   font-family: acumin-pro-condensed;
 
@@ -17,14 +21,14 @@
       height: 20px;
     }
     &.babylon-logo {
-      background-image: url("./assets/babylonLogo.svg");
+      background-image: url('./assets/babylonLogo.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
     }
 
     &.close {
-      background-image: url("./assets/closeWindowIcon.svg");
+      background-image: url('./assets/closeWindowIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -33,61 +37,103 @@
     }
 
     &.auto-tangent {
-      background-image: url("./assets/autoTangentIcon.svg");
+      background-image: url('./assets/autoTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.break-tangent {
-      background-image: url("./assets/breakTangentIcon.svg");
+      background-image: url('./assets/breakTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.flat-tangent {
-      background-image: url("./assets/flatTangentIcon.svg");
+      background-image: url('./assets/flatTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.frame {
-      background-image: url("./assets/frameIcon.svg");
+      background-image: url('./assets/frameIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.linear-tangent {
-      background-image: url("./assets/linearTangentIcon.svg");
+      background-image: url('./assets/linearTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.unify-tangent {
-      background-image: url("./assets/unifyTangentIcon.svg");
+      background-image: url('./assets/unifyTangentIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
       cursor: pointer;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.add-animation {
-      background-image: url("./assets/addAnimationIcon.svg");
+      background-image: url('./assets/addAnimationIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -96,7 +142,7 @@
     }
 
     &.animation-bullet {
-      background-image: url("./assets/animationBulletIcon.svg");
+      background-image: url('./assets/animationBulletIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -104,7 +150,7 @@
     }
 
     &.animation-delete {
-      background-image: url("./assets/animationDeleteIcon.svg");
+      background-image: url('./assets/animationDeleteIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -113,7 +159,7 @@
     }
 
     &.animation-edit {
-      background-image: url("./assets/editIcon.svg");
+      background-image: url('./assets/editIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -121,7 +167,7 @@
     }
 
     &.animation-end {
-      background-image: url("./assets/animationEndIcon.svg");
+      background-image: url('./assets/animationEndIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -132,7 +178,7 @@
     }
 
     &.animation-lastkey {
-      background-image: url("./assets/animationLastKeyIcon.svg");
+      background-image: url('./assets/animationLastKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -140,12 +186,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationLastKeyHoverIcon.svg");
+        background-image: url('./assets/animationLastKeyHoverIcon.svg');
       }
     }
 
     &.animation-nextkey {
-      background-image: url("./assets/animationNextKeyIcon.svg");
+      background-image: url('./assets/animationNextKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -153,12 +199,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationNextKeyHoverIcon.svg");
+        background-image: url('./assets/animationNextKeyHoverIcon.svg');
       }
     }
 
     &.animation-options {
-      background-image: url("./assets/animationOptionsIcon.svg");
+      background-image: url('./assets/animationOptionsIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -171,7 +217,7 @@
     }
 
     &.animation-playfwd {
-      background-image: url("./assets/animationPlayFwdIcon.svg");
+      background-image: url('./assets/animationPlayFwdIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -181,12 +227,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationPlayFwdHoverIcon.svg");
+        background-image: url('./assets/animationPlayFwdHoverIcon.svg');
       }
     }
 
     &.animation-playrev {
-      background-image: url("./assets/animationPlayRevIcon.svg");
+      background-image: url('./assets/animationPlayRevIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -196,12 +242,12 @@
       cursor: pointer;
       background-position: center;
       &:hover {
-        background-image: url("./assets/animationPlayRevHoverIcon.svg");
+        background-image: url('./assets/animationPlayRevHoverIcon.svg');
       }
     }
 
     &.animation-start {
-      background-image: url("./assets/animationStartIcon.svg");
+      background-image: url('./assets/animationStartIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -213,7 +259,7 @@
     }
 
     &.animation-stop {
-      background-image: url("./assets/animationStopIcon.svg");
+      background-image: url('./assets/animationStopIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -222,10 +268,11 @@
       cursor: pointer;
       background-position: center;
       width: 20px;
+      margin-left: 10px;
     }
 
     &.animation-triangle {
-      background-image: url("./assets/animationTriangleIcon.svg");
+      background-image: url('./assets/animationTriangleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -233,7 +280,7 @@
     }
 
     &.key-active {
-      background-image: url("./assets/keyActiveIcon.svg");
+      background-image: url('./assets/keyActiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -241,7 +288,7 @@
     }
 
     &.key-inactive {
-      background-image: url("./assets/keyInactiveIcon.svg");
+      background-image: url('./assets/keyInactiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -249,7 +296,7 @@
     }
 
     &.key-selected {
-      background-image: url("./assets/keySelectedIcon.svg");
+      background-image: url('./assets/keySelectedIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -257,7 +304,7 @@
     }
 
     &.loop-active {
-      background-image: url("./assets/loopActiveIcon.svg");
+      background-image: url('./assets/loopActiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -266,7 +313,7 @@
     }
 
     &.loop-inactive {
-      background-image: url("./assets/loopInactiveIcon.svg");
+      background-image: url('./assets/loopInactiveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -275,7 +322,7 @@
     }
 
     &.move {
-      background-image: url("./assets/moveIcon.svg");
+      background-image: url('./assets/moveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -283,7 +330,7 @@
     }
 
     &.save {
-      background-image: url("./assets/saveIcon.svg");
+      background-image: url('./assets/saveIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -292,7 +339,7 @@
     }
 
     &.load {
-      background-image: url("./assets/loadIcon.svg");
+      background-image: url('./assets/loadIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -301,7 +348,7 @@
     }
 
     &.checked {
-      background-image: url("./assets/checkboxCheckedIcon.svg");
+      background-image: url('./assets/checkboxCheckedIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -309,7 +356,7 @@
     }
 
     &.unchecked {
-      background-image: url("./assets/checkboxDefaultIcon.svg");
+      background-image: url('./assets/checkboxDefaultIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -317,15 +364,22 @@
     }
 
     &.new-key {
-      background-image: url("./assets/newKeyIcon.svg");
+      background-image: url('./assets/newKeyIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
       color: white;
+      &:hover {
+        background-color: #888888;
+      }
+
+      &:active {
+        background-color: #555555;
+      }
     }
 
     &.scale {
-      background-image: url("./assets/scaleIcon.svg");
+      background-image: url('./assets/scaleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -333,7 +387,7 @@
     }
 
     &.scrollbar-handle {
-      background-image: url("./assets/scrollbarHandleIcon.svg");
+      background-image: url('./assets/scrollbarHandleIcon.svg');
       background-repeat: no-repeat;
       background-color: transparent;
       background-size: contain;
@@ -423,6 +477,7 @@
 
     .buttons-container {
       display: flex;
+      padding-left: 10px;
     }
 
     .action-input {
@@ -440,6 +495,9 @@
         border: none;
         background-color: black;
         padding: 6px;
+        &:focus {
+          outline: solid 1px #ccc;
+        }
       }
     }
   }
@@ -539,6 +597,12 @@
               height: 20px;
               background-color: #666666;
               justify-content: space-between;
+              position: absolute;
+
+              .scrollbar {
+                cursor: pointer;
+                width: 100%;
+              }
 
               .left-grabber,
               .right-grabber {
@@ -649,7 +713,7 @@
 
       .load-container {
         flex-direction: column;
-        height: 387px;
+        height: 377px;
         padding-top: 10px;
 
         .load-server {
@@ -658,7 +722,7 @@
           background-color: #222222;
           padding-left: 10px;
           height: 20px;
-          margin-top: 287px;
+          margin-top: 277px;
           p {
             margin: 0px;
           }
@@ -729,11 +793,11 @@
 
       .save-container {
         flex-direction: column;
-        height: 387px;
+        height: 377px;
         padding-top: 10px;
 
         .item-list {
-          height: 327px;
+          height: 317px;
           ul {
             list-style: none;
             padding-left: 10px;
@@ -830,7 +894,7 @@
         display: block;
         position: absolute;
         background-color: #111111;
-        height: 367px;
+        height: 377px;
         z-index: 10;
 
         .sub-header {
@@ -885,7 +949,7 @@
               height: auto;
             }
             &:before {
-              content: "";
+              content: '';
               background: none;
             }
             height: 20px;
@@ -906,7 +970,7 @@
             }
 
             .animation-bullet {
-              background-image: url("./assets/animationBulletIcon.svg");
+              background-image: url('./assets/animationBulletIcon.svg');
               background-repeat: no-repeat;
               background-color: transparent;
               background-size: contain;
@@ -920,7 +984,7 @@
 
             .animation-arrow {
               width: 30px;
-              background-image: url("./assets/animationTriangleIcon.svg");
+              background-image: url('./assets/animationTriangleIcon.svg');
               background-repeat: no-repeat;
               background-color: transparent;
               background-size: 10px;
@@ -942,7 +1006,7 @@
 
               &.show {
                 display: block;
-                background-image: url("./assets/keySelectedIcon.svg");
+                background-image: url('./assets/keySelectedIcon.svg');
                 background-repeat: no-repeat;
                 background-color: transparent;
                 background-size: 10px;
@@ -952,7 +1016,7 @@
               }
 
               &.hide {
-                display: none;
+                display: block;
               }
             }
 
@@ -986,8 +1050,7 @@
           font-size: 10px;
           &:focus {
             border-radius: 0px;
-            outline-style: auto;
-            outline-color: lightgrey;
+            outline: 1px solid #ccc;
           }
           font-family: acumin-pro-condensed;
         }
@@ -1001,8 +1064,7 @@
           color: white;
           &:focus {
             border-radius: 0px;
-            outline-style: auto;
-            outline-color: lightgrey;
+            outline: 1px solid #ccc;
           }
           font-family: acumin-pro-condensed;
         }
@@ -1045,7 +1107,7 @@
         }
         text {
           fill: #555555;
-          font-family: "acumin-pro-condensed";
+          font-family: 'acumin-pro-condensed';
         }
 
         .control-point {
@@ -1059,15 +1121,29 @@
         }
       }
 
-      .playhead-wrapper {
+      .playhead-container {
         position: relative;
+      }
+
+      .playhead-wrapper {
         left: -13px;
         bottom: 366px;
+        position: relative;
+      }
+
+      .playhead-scrollable {
+        width: 100px;
+        height: 33px;
+        position: absolute;
+        top: 335px;
+        left: -39px;
       }
 
       .playhead-handle {
         position: relative;
         top: 340px;
+        width: 22px;
+        height: 30px;
         .playhead {
           width: 22px;
           background-color: transparent;
@@ -1076,6 +1152,7 @@
           font-size: 12px;
           position: absolute;
           top: 1px;
+          cursor: pointer;
         }
 
         .playhead-circle {
@@ -1104,7 +1181,7 @@
     align-items: center;
     justify-items: stretch;
 
-    input[type="file"] {
+    input[type='file'] {
       display: none;
     }
 

+ 29 - 28
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -1,17 +1,17 @@
-import * as React from "react";
+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 { 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';
 
 interface IEditorControlsProps {
   isTargetedAnimation: boolean;
@@ -52,6 +52,7 @@ export class EditorControls extends React.Component<
   }
 
   animationAdded() {
+    // select recently created animation/first coordinate...
     this.setState({
       animationsCount: this.recountAnimations(),
       isEditTabOpen: true,
@@ -123,40 +124,40 @@ export class EditorControls extends React.Component<
 
   render() {
     return (
-      <div className="animation-list">
-        <div className="controls-header">
+      <div className='animation-list'>
+        <div className='controls-header'>
           {this.props.isTargetedAnimation ? null : (
             <IconButtonLineComponent
               active={this.state.isAnimationTabOpen}
-              tooltip="Add Animation"
-              icon="medium add-animation"
+              tooltip='Add Animation'
+              icon='medium add-animation'
               onClick={() => this.handleTabs(0)}
             ></IconButtonLineComponent>
           )}
           <IconButtonLineComponent
             active={this.state.isLoadTabOpen}
-            tooltip="Load Animation"
-            icon="medium load"
+            tooltip='Load Animation'
+            icon='medium load'
             onClick={() => this.handleTabs(1)}
           ></IconButtonLineComponent>
           <IconButtonLineComponent
             active={this.state.isSaveTabOpen}
-            tooltip="Save Animation"
-            icon="medium save"
+            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"
+              tooltip='Edit Animations'
+              icon='medium animation-edit'
               onClick={() => this.handleTabs(3)}
             ></IconButtonLineComponent>
           )}
           {this.state.isEditTabOpen ? (
-            <div className="input-fps">
+            <div className='input-fps'>
               <NumericInputComponent
-                label={""}
+                label={''}
                 precision={0}
                 value={this.state.framesPerSecond}
                 onChange={(framesPerSecond: number) =>
@@ -168,11 +169,11 @@ export class EditorControls extends React.Component<
           ) : null}
           {this.state.isEditTabOpen ? (
             <IconButtonLineComponent
-              tooltip="Loop/Unloop"
+              tooltip='Loop/Unloop'
               icon={`medium ${
                 this.state.isLoopActive
-                  ? "loop-active last"
-                  : "loop-inactive last"
+                  ? 'loop-active last'
+                  : 'loop-inactive last'
               }`}
               onClick={() => {
                 this.setState({ isLoopActive: !this.state.isLoopActive });

+ 85 - 47
inspector/src/components/actionTabs/tabs/propertyGrids/animations/graphActionsBar.tsx

@@ -1,52 +1,90 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
 
 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;
-   currentValue: number;
-   currentFrame: number;
-   title: string;
-   close: (event: any) => void;
-   enabled: boolean;
+  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;
+  currentValue: number;
+  currentFrame: number;
+  title: string;
+  close: (event: any) => void;
+  enabled: boolean;
 }
 
-export class GraphActionsBar extends React.Component<IGraphActionsBarProps>{ 
-    constructor(props: IGraphActionsBarProps) {
-        super(props);
-    }
-     
-    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={{display: this.props.enabled ? 'flex' : 'none'}}>
-               <div className="action-input">
-               <input type="number" value={this.props.currentFrame} onChange={this.props.handleFrameChange} step="1"/>
-               </div>
-               <div className="action-input">
-               <input type="number" value={this.props.currentValue.toFixed(3)} onChange={this.props.handleValueChange} step="0.001"/>
-               </div>
-              <IconButtonLineComponent tooltip={"Add Keyframe"} icon="new-key" onClick={this.props.addKeyframe} />
-              <IconButtonLineComponent tooltip={"Remove Keyframe"} 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 className="icon close" onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => this.props.close(event)}>
-              </div>
-           </div>
-        )
-    }
-} 
+export class GraphActionsBar extends React.Component<IGraphActionsBarProps> {
+  constructor(props: IGraphActionsBarProps) {
+    super(props);
+  }
+
+  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={{ display: this.props.enabled ? 'flex' : 'none' }}
+        >
+          <div className='action-input'>
+            <input
+              type='number'
+              value={this.props.currentFrame}
+              onChange={this.props.handleFrameChange}
+              step='1'
+            />
+          </div>
+          <div className='action-input'>
+            <input
+              type='number'
+              value={this.props.currentValue.toFixed(3)}
+              onChange={this.props.handleValueChange}
+              step='0.001'
+            />
+          </div>
+          <IconButtonLineComponent
+            tooltip={'Add Keyframe'}
+            icon='new-key'
+            onClick={this.props.addKeyframe}
+          />
+          <IconButtonLineComponent
+            tooltip={'Remove Keyframe'}
+            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
+          className='icon close'
+          onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
+            this.props.close(event)
+          }
+        ></div>
+      </div>
+    );
+  }
+}

+ 72 - 19
inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx

@@ -1,27 +1,80 @@
-
-import * as React from "react";
+import * as React from 'react';
 
 interface IPlayheadProps {
-    frame: number;
-    offset: number;
+  frame: number;
+  offset: number;
+  onCurrentFrameChange: (frame: number) => void;
 }
 
-export class Playhead extends React.Component<IPlayheadProps>{
-    constructor(props: IPlayheadProps) {
-        super(props);
-    }
+export class Playhead extends React.Component<IPlayheadProps> {
+  private _direction: number;
+  private _active: boolean;
+  constructor(props: IPlayheadProps) {
+    super(props);
+  }
+
+  dragStart(e: React.TouchEvent<HTMLDivElement>): void;
+  dragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  dragStart(e: any) {
+    e.preventDefault();
+    this._direction = e.clientX;
+    this._active = true;
+  }
+
+  drag(e: React.TouchEvent<HTMLDivElement>): void;
+  drag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  drag(e: any) {
+    e.preventDefault();
+    if (this._active) {
+      let moved = e.pageX - this._direction;
 
-    render() {
-        return (
-            <div className="playhead-wrapper" id="playhead" style={{ left: `calc(${this.props.frame * (this.props.offset)}px - 13px)` }}>
-                <div className="playhead-line"></div>
-                <div className="playhead-handle">
-                    <div className="playhead-circle"></div>
-                    <div className="playhead">{this.props.frame}</div>
-                </div>
-            </div>
-        )
+      let framesToMove = Math.round(Math.abs(moved) / 2);
+      console.log(framesToMove);
+      if (Math.sign(moved) === -1) {
+        this.props.onCurrentFrameChange(this.props.frame - 1);
+      } else {
+        this.props.onCurrentFrameChange(this.props.frame + 1);
+      }
     }
-}
+  }
+
+  dragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+  dragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  dragEnd(e: any) {
+    e.preventDefault();
+    this._direction = 0;
+    this._active = false;
+  }
 
+  calculateMove() {
+    return `calc(${this.props.frame * this.props.offset}px - 13px)`;
+  }
 
+  render() {
+    return (
+      <div
+        className='playhead-wrapper'
+        id='playhead'
+        style={{
+          left: this.calculateMove(),
+        }}
+      >
+        <div className='playhead-line'></div>
+        <div
+          className='playhead-handle'
+          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}
+        >
+          <div className='playhead-circle'></div>
+          <div className='playhead'>{this.props.frame}</div>
+        </div>
+      </div>
+    );
+  }
+}

+ 290 - 184
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -1,215 +1,321 @@
-
-import * as React from "react";
+import * as React from 'react';
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
 import { Controls } from './controls';
 
 interface ITimelineProps {
-    keyframes: IAnimationKey[] | null;
-    selected: IAnimationKey | null;
-    currentFrame: number;
-    onCurrentFrameChange: (frame: number) => void;
-    dragKeyframe: (frame: number, index: number) => void;
-    playPause: (direction: number) => void;
-    isPlaying: boolean;
+  keyframes: IAnimationKey[] | null;
+  selected: IAnimationKey | null;
+  currentFrame: number;
+  onCurrentFrameChange: (frame: number) => void;
+  dragKeyframe: (frame: number, index: number) => void;
+  playPause: (direction: number) => void;
+  isPlaying: boolean;
 }
 
-export class Timeline extends React.Component<ITimelineProps, { selected: IAnimationKey, activeKeyframe: number | null }>{
-    readonly _frames: object[] = Array(300).fill({});
-    private _scrollable: React.RefObject<HTMLDivElement>;
-    private _direction: number;
-    constructor(props: ITimelineProps) {
-        super(props);
-        if (this.props.selected !== null) {
-            this.state = { selected: this.props.selected, activeKeyframe: null };
-        }
-        this._scrollable = React.createRef();
-        this._direction = 0;
+export class Timeline extends React.Component<
+  ITimelineProps,
+  { selected: IAnimationKey; activeKeyframe: number | null }
+> {
+  readonly _frames: object[] = Array(300).fill({});
+  private _scrollable: React.RefObject<HTMLDivElement>;
+  private _scrollbarHandle: React.RefObject<HTMLDivElement>;
+  private _direction: number;
+  private _scrolling: boolean;
+  private _shiftX: number;
+  constructor(props: ITimelineProps) {
+    super(props);
+    if (this.props.selected !== null) {
+      this.state = { selected: this.props.selected, activeKeyframe: null };
     }
+    this._scrollable = React.createRef();
+    this._scrollbarHandle = React.createRef();
+    this._direction = 0;
+    this._scrolling = false;
+    this._shiftX = 0;
+  }
 
-    playBackwards(event: React.MouseEvent<HTMLDivElement>) {
-        this.props.playPause(-1);
-    }
+  playBackwards(event: React.MouseEvent<HTMLDivElement>) {
+    this.props.playPause(-1);
+  }
 
-    play(event: React.MouseEvent<HTMLDivElement>) {
-        this.props.playPause(1);
-    }
+  play(event: React.MouseEvent<HTMLDivElement>) {
+    this.props.playPause(1);
+  }
 
-    pause(event: React.MouseEvent<HTMLDivElement>) {
-        if (this.props.isPlaying) {
-            this.props.playPause(1);
-        }
+  pause(event: React.MouseEvent<HTMLDivElement>) {
+    if (this.props.isPlaying) {
+      this.props.playPause(1);
     }
+  }
 
-    handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
-        this.props.onCurrentFrameChange(parseInt(event.target.value));
-        event.preventDefault();
+  handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
+    this.props.onCurrentFrameChange(parseInt(event.target.value));
+    event.preventDefault();
+  }
+
+  nextFrame(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    this.props.onCurrentFrameChange(this.props.currentFrame + 1);
+    (this._scrollable.current as HTMLDivElement).scrollLeft =
+      this.props.currentFrame * 5;
+  }
+
+  previousFrame(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    if (this.props.currentFrame !== 0) {
+      this.props.onCurrentFrameChange(this.props.currentFrame - 1);
+      (this._scrollable.current as HTMLDivElement).scrollLeft = -(
+        this.props.currentFrame * 5
+      );
     }
+  }
 
-    nextFrame(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        this.props.onCurrentFrameChange(this.props.currentFrame + 1);
-        (this._scrollable.current as HTMLDivElement).scrollLeft = this.props.currentFrame * 5;
+  nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    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.setState({ selected: first });
+        (this._scrollable.current as HTMLDivElement).scrollLeft =
+          first.frame * 5;
+      }
     }
+  }
 
-    previousFrame(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        if (this.props.currentFrame !== 0) {
-            this.props.onCurrentFrameChange(this.props.currentFrame - 1);
-            (this._scrollable.current as HTMLDivElement).scrollLeft = -(this.props.currentFrame * 5);
-        }
+  previousKeyframe(event: React.MouseEvent<HTMLDivElement>) {
+    event.preventDefault();
+    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.setState({ selected: first });
+        (this._scrollable.current as HTMLDivElement).scrollLeft = -(
+          first.frame * 5
+        );
+      }
     }
+  }
 
-    nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        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.setState({ selected: first });
-                (this._scrollable.current as HTMLDivElement).scrollLeft = first.frame * 5;
-            }
+  dragStart(e: React.TouchEvent<SVGSVGElement>): void;
+  dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragStart(e: any): void {
+    e.preventDefault();
+    this.setState({ activeKeyframe: parseInt(e.target.id.replace('kf_', '')) });
+    this._direction = e.clientX;
+  }
+
+  drag(e: React.TouchEvent<SVGSVGElement>): void;
+  drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  drag(e: any): void {
+    e.preventDefault();
+    if (this.props.keyframes) {
+      if (
+        this.state.activeKeyframe === parseInt(e.target.id.replace('kf_', ''))
+      ) {
+        let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
+        if (this._direction > e.clientX) {
+          console.log(`Dragging left ${this.state.activeKeyframe}`);
+          let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
+          if (used) {
+            updatedKeyframe.frame = used;
+          }
+        } else {
+          console.log(`Dragging Right ${this.state.activeKeyframe}`);
+          let used = this.isFrameBeingUsed(updatedKeyframe.frame + 1, 1);
+          if (used) {
+            updatedKeyframe.frame = used;
+          }
         }
+
+        this.props.dragKeyframe(
+          updatedKeyframe.frame,
+          this.state.activeKeyframe
+        );
+      }
     }
+  }
 
-    previousKeyframe(event: React.MouseEvent<HTMLDivElement>) {
-        event.preventDefault();
-        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.setState({ selected: first });
-                (this._scrollable.current as HTMLDivElement).scrollLeft = -(first.frame * 5);
-            }
-        }
+  isFrameBeingUsed(frame: number, direction: number) {
+    let used = this.props.keyframes?.find((kf) => kf.frame === frame);
+    if (used) {
+      this.isFrameBeingUsed(used.frame + direction, direction);
+      return false;
+    } else {
+      return frame;
     }
+  }
 
-    dragStart(e: React.TouchEvent<SVGSVGElement>): void;
-    dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragStart(e: any): void {
-        e.preventDefault();
-        this.setState({ activeKeyframe: parseInt(e.target.id.replace('kf_', '')) });
-        this._direction = e.clientX;
+  dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
+  dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
+  dragEnd(e: any): 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) {
+    e.preventDefault();
+    if ((e.target.class = 'scrollbar') && 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';
     }
+  }
 
-    drag(e: React.TouchEvent<SVGSVGElement>): void;
-    drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    drag(e: any): void {
-        e.preventDefault();
-        if (this.props.keyframes) {
-            if (this.state.activeKeyframe === parseInt(e.target.id.replace('kf_', ''))) {
-                let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
-                if (this._direction > e.clientX) {
-                    console.log(`Dragging left ${this.state.activeKeyframe}`);
-                    let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
-                    if (used) {
-                        updatedKeyframe.frame = used
-                    }
-                } else {
-                    console.log(`Dragging Right ${this.state.activeKeyframe}`)
-                    let used = this.isFrameBeingUsed(updatedKeyframe.frame + 1, 1);
-                    if (used) {
-                        updatedKeyframe.frame = used
-                    }
-                }
-
-                this.props.dragKeyframe(updatedKeyframe.frame, this.state.activeKeyframe);
-
-            }
-        }
+  scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
+  scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  scrollDrag(e: any) {
+    e.preventDefault();
+    if (this._scrolling && this._scrollbarHandle.current) {
+      let moved = e.pageX - this._shiftX;
+      if (moved > 233 && moved < 630) {
+        this._scrollbarHandle.current.style.left = moved + 'px';
+        (this._scrollable.current as HTMLDivElement).scrollLeft = moved + 10;
+      }
     }
+  }
 
-    isFrameBeingUsed(frame: number, direction: number) {
-        let used = this.props.keyframes?.find(kf => kf.frame === frame);
-        if (used) {
-            this.isFrameBeingUsed(used.frame + direction, direction);
-            return false;
-        } else {
-            return frame;
-        }
-    }
+  scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
+  scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
+  scrollDragEnd(e: any) {
+    e.preventDefault();
+    this._scrolling = false;
+    this._shiftX = 0;
+  }
 
-    dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
-    dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
-    dragEnd(e: any): void {
-        e.preventDefault();
-        this._direction = 0;
-        this.setState({ activeKeyframe: null })
-    }
+  render() {
+    return (
+      <>
+        <div className='timeline'>
+          <Controls
+            keyframes={this.props.keyframes}
+            selected={this.props.selected}
+            currentFrame={this.props.currentFrame}
+            onCurrentFrameChange={this.props.onCurrentFrameChange}
+            playPause={this.props.playPause}
+            isPlaying={this.props.isPlaying}
+            scrollable={this._scrollable}
+          />
+          <div className='timeline-wrapper'>
+            <div ref={this._scrollable} className='display-line'>
+              <svg
+                viewBox='0 0 2010 40'
+                style={{ width: 2000, 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}
+              >
+                <line
+                  x1={this.props.currentFrame * 10}
+                  y1='0'
+                  x2={this.props.currentFrame * 10}
+                  y2='40'
+                  style={{ stroke: '#12506b', strokeWidth: 6 }}
+                />
+                {this.props.keyframes &&
+                  this.props.keyframes.map((kf, i) => {
+                    return (
+                      <svg
+                        key={`kf_${i}`}
+                        style={{ cursor: 'pointer' }}
+                        tabIndex={i + 40}
+                      >
+                        <line
+                          id={`kf_${i.toString()}`}
+                          x1={kf.frame * 10}
+                          y1='0'
+                          x2={kf.frame * 10}
+                          y2='40'
+                          style={{ stroke: 'red', strokeWidth: 6 }}
+                        />
+                      </svg>
+                    );
+                  })}
+                {this._frames.map((frame, i) => {
+                  return (
+                    <svg key={`tl_${i}`}>
+                      {i % 5 === 0 ? (
+                        <>
+                          <text
+                            x={i * 5 - 3}
+                            y='18'
+                            style={{ fontSize: 10, fill: '#555555' }}
+                          >
+                            {i}
+                          </text>
+                          <line
+                            x1={i * 5}
+                            y1='22'
+                            x2={i * 5}
+                            y2='40'
+                            style={{ stroke: '#555555', strokeWidth: 0.5 }}
+                          />
+                        </>
+                      ) : null}
+                    </svg>
+                  );
+                })}
+              </svg>
+            </div>
+
+            <div className='timeline-scroll-handle'>
+              <div className='scroll-handle'>
+                <div
+                  className='handle'
+                  ref={this._scrollbarHandle}
+                  style={{ width: 300 }}
+                >
+                  <div className='left-grabber'>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='text'>20</div>
+                  </div>
+                  <div
+                    className='scrollbar'
+                    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}
+                  ></div>
 
-    render() {
-        return (
-            <>
-                <div className="timeline">
-                    <Controls keyframes={this.props.keyframes}
-                        selected={this.props.selected}
-                        currentFrame={this.props.currentFrame}
-                        onCurrentFrameChange={this.props.onCurrentFrameChange}
-                        playPause={this.props.playPause}
-                        isPlaying={this.props.isPlaying}
-                        scrollable={this._scrollable} />
-                    <div className="timeline-wrapper">
-                        <div ref={this._scrollable} className="display-line" >
-                            <svg viewBox="0 0 2010 40" style={{ width: 2000, 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)}>
-
-                                <line x1={this.props.currentFrame * 10} y1="0" x2={this.props.currentFrame * 10} y2="40" style={{ stroke: '#12506b', strokeWidth: 6 }} />
-                                {
-                                    this.props.keyframes && this.props.keyframes.map((kf, i) => {
-
-                                        return <svg key={`kf_${i}`} style={{ cursor: 'pointer' }} tabIndex={i + 40} >
-                                            <line id={`kf_${i.toString()}`} x1={kf.frame * 10} y1="0" x2={kf.frame * 10} y2="40" style={{ stroke: 'red', strokeWidth: 6 }} />
-                                        </svg>
-                                    })
-                                }
-                                {
-                                    this._frames.map((frame, i) => {
-
-                                        return <svg key={`tl_${i}`}>
-                                            {i % 5 === 0 ?
-                                                <>
-                                                    <text x={(i * 5) - 3} y="18" style={{ fontSize: 10, fill: '#555555' }}>{i}</text>
-                                                    <line x1={i * 5} y1="22" x2={i * 5} y2="40" style={{ stroke: '#555555', strokeWidth: 0.5 }} />
-                                                </> : null}
-
-                                        </svg>
-                                    })
-                                }
-                            </svg>
-                        </div>
-
-                        <div className="timeline-scroll-handle">
-                            <div className="scroll-handle">
-                                <div className="handle" style={{ width: 300, marginLeft: 20 }}>
-                                    <div className="left-grabber">
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="text">20</div>
-                                    </div>
-
-
-                                    <div className="right-grabber">
-                                        <div className="text">100</div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                        <div className="grabber"></div>
-                                    </div>
-                                </div>
-                            </div>
-                            <div className="input-frame">
-                                <input type="number" value={this.props.currentFrame} onChange={(e) => this.handleInputChange(e)}></input>
-                            </div>
-                        </div>
-                    </div>
+                  <div className='right-grabber'>
+                    <div className='text'>100</div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                    <div className='grabber'></div>
+                  </div>
                 </div>
-            </>
-        )
-    }
-} 
+              </div>
+              <div className='input-frame'>
+                <input
+                  type='number'
+                  value={this.props.currentFrame}
+                  onChange={(e) => this.handleInputChange(e)}
+                ></input>
+              </div>
+            </div>
+          </div>
+        </div>
+      </>
+    );
+  }
+}