瀏覽代碼

Merge pull request #8278 from toledoal/animation-list-component

Animation list component
David Catuhe 5 年之前
父節點
當前提交
78c2f25450

+ 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 in animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
+- Added Curve editor to manage entity's animations and edit animation within 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))
 

+ 184 - 54
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -17,6 +17,7 @@ import { Scene } from "babylonjs/scene";
 import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+import { Nullable } from 'babylonjs/types';
 
 require("./curveEditor.scss");
 
@@ -34,7 +35,6 @@ interface ICanvasAxis {
 }
 
 export class AnimationCurveEditorComponent extends React.Component<IAnimationCurveEditorComponentProps, {
-    animations: Animation[],
     animationName: string,
     animationType: string,
     animationTargetProperty: string,
@@ -49,12 +49,14 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     isFlatTangentMode: boolean,
     isTangentMode: boolean,
     isBrokenMode: boolean,
+    lerpMode: boolean,
     scale: number,
     playheadOffset: number,
     notification: string,
     currentPoint: SVGPoint | undefined,
     lastFrame: number,
-    playheadPos: number
+    playheadPos: number,
+    isPlaying: boolean
 }> {
 
     // Height scale *Review this functionaliy
@@ -62,7 +64,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     // Canvas Length *Review this functionality
     readonly _entityName: string;
     readonly _canvasLength: number = 20;
-    private _newAnimations: Animation[] = [];
     private _svgKeyframes: IKeyframeSvgPoint[] = [];
     private _frames: Vector2[] = [];
     private _isPlaying: boolean = false;
@@ -82,12 +83,15 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
         let initialSelection;
         let initialPathData;
-        if (this.props.entity instanceof TargetedAnimation){
+        let initialLerpMode;
+        if (this.props.entity instanceof TargetedAnimation) {
             this._isTargetedAnimation = true;
             initialSelection = this.props.entity.animation;
+            initialLerpMode = this.analizeAnimation(this.props.entity.animation);
             initialPathData = this.getPathData(this.props.entity.animation);
         } else {
             this._isTargetedAnimation = false;
+            initialLerpMode = this.analizeAnimation(this.props.entity.animations && this.props.entity.animations[0]);
             initialSelection = this.props.entity.animations !== null ? this.props.entity.animations[0] : null;
             initialPathData = this.props.entity.animations !== null ? this.getPathData(this.props.entity.animations[0]) : "";
         }
@@ -95,7 +99,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         // will update this until we have a top scroll/zoom feature
         let valueInd = [2, 1.8, 1.6, 1.4, 1.2, 1, 0.8, 0.6, 0.4, 0.2, 0];
         this.state = {
-            animations: this._newAnimations,
             selected: initialSelection,
             isOpen: true,
             currentPathData: initialPathData,
@@ -108,6 +111,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             isFlatTangentMode: false,
             isTangentMode: false,
             isBrokenMode: false,
+            lerpMode: initialLerpMode,
             playheadOffset: this._graphCanvas.current ? (this._graphCanvas.current.children[1].clientWidth) / (this._canvasLength * 10) : 0,
             frameAxisLength: (new Array(this._canvasLength)).fill(0).map((s, i) => { return { value: i * 10, label: i * 10 } }),
             valueAxisLength: (new Array(10)).fill(0).map((s, i) => { return { value: i * 10, label: valueInd[i] } }),
@@ -116,6 +120,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             currentPoint: undefined,
             scale: 1,
             playheadPos: 0,
+            isPlaying: this.isAnimationPlaying()
         }
     }
 
@@ -151,9 +156,8 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     }
 
     setAxesLength() {
-
-        let length = Math.round(this._canvasLength * this.state.scale);
-
+        
+        let length = Math.round(this._canvasLength * this.state.scale);// Check Undefined, or NaN
         let highestFrame = 100;
         if (this.state.selected !== null) {
             highestFrame = this.state.selected.getHighestFrame();
@@ -220,6 +224,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                 element = <li className={this.state.selected && this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>
                     <p>{animation.name}&nbsp;
                     <span>{animation.targetProperty}</span></p>
+                    {!(this.props.entity instanceof TargetedAnimation) ? this.state.selected && this.state.selected.name === animation.name ? <ButtonLineComponent label={"Remove"} onClick={() => this.deleteAnimation()} /> : null : null}
                 </li>
                 break;
             case Animation.ANIMATIONTYPE_VECTOR2:
@@ -314,6 +319,19 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
     }
 
+    deleteAnimation() {
+        let currentSelected = this.state.selected;
+        if (this.props.entity instanceof TargetedAnimation) {
+            console.log("no animation remove allowed");
+        } else {
+            let animations = (this.props.entity as IAnimatable).animations;
+            if (animations) {
+                let updatedAnimations = animations.filter(anim => anim !== currentSelected);
+                (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
+            }
+        }
+    }
+
     addAnimation() {
         if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
 
@@ -411,24 +429,39 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                         break;
                 }
 
-                let animation = new Animation(this.state.animationName, this.state.animationTargetProperty, 30, animationDataType);
+                let alreadyAnimatedProperty = (this.props.entity as IAnimatable).animations?.find(anim =>
+                    anim.targetProperty === this.state.animationTargetProperty
+                    , this);
 
-                // Start with two keyframes
-                var keys = [];
-                keys.push({
-                    frame: 0,
-                    value: startValue,
-                    outTangent: outTangent
-                });
+                let alreadyAnimationName = (this.props.entity as IAnimatable).animations?.find(anim =>
+                    anim.name === this.state.animationName
+                    , this);
 
-                keys.push({
-                    inTangent: inTangent,
-                    frame: 100,
-                    value: endValue
-                });
+                if (alreadyAnimatedProperty) {
+                    this.setState({ notification: `The property "${this.state.animationTargetProperty}" already has an animation` });
+                } else if (alreadyAnimationName) {
+                    this.setState({ notification: `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
+                    });
 
-                animation.setKeys(keys);
-                (this.props.entity as IAnimatable).animations?.push(animation);
+                    keys.push({
+                        inTangent: inTangent,
+                        frame: 100,
+                        value: endValue
+                    });
+
+                    animation.setKeys(keys);
+                    (this.props.entity as IAnimatable).animations?.push(animation);
+                }
 
             } else {
                 this.setState({ notification: `The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type` });
@@ -492,17 +525,17 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         keys[index].value = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2; // this value comes inverted svg from 0 = 100 to 100 = 0
 
 
-        if (!this.state.isBrokenMode) {
-            if (updatedSvgKeyFrame.isLeftActive) {
+        if (updatedSvgKeyFrame.isLeftActive) {
 
-                if (updatedSvgKeyFrame.leftControlPoint !== null) {
-                    // Rotate 
-                    let updatedValue = ((this._heightScale - updatedSvgKeyFrame.leftControlPoint.y) / this._heightScale) * 2;
+            if (updatedSvgKeyFrame.leftControlPoint !== null) {
+                // Rotate 
+                let updatedValue = ((this._heightScale - updatedSvgKeyFrame.leftControlPoint.y) / this._heightScale) * 2;
 
-                    let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
+                let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
 
-                    keys[index].inTangent = keyframeValue - updatedValue;
+                keys[index].inTangent = keyframeValue - updatedValue;
 
+                if (!this.state.isBrokenMode) {
                     // Right control point if exists
                     if (updatedSvgKeyFrame.rightControlPoint !== null) {
                         // Sets opposite value
@@ -510,17 +543,19 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                     }
                 }
             }
+        }
 
-            if (updatedSvgKeyFrame.isRightActive) {
+        if (updatedSvgKeyFrame.isRightActive) {
 
-                if (updatedSvgKeyFrame.rightControlPoint !== null) {
-                    // Rotate 
-                    let updatedValue = ((this._heightScale - updatedSvgKeyFrame.rightControlPoint.y) / this._heightScale) * 2;
+            if (updatedSvgKeyFrame.rightControlPoint !== null) {
+                // Rotate 
+                let updatedValue = ((this._heightScale - updatedSvgKeyFrame.rightControlPoint.y) / this._heightScale) * 2;
 
-                    let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
+                let keyframeValue = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2;
 
-                    keys[index].outTangent = keyframeValue - updatedValue;
+                keys[index].outTangent = keyframeValue - updatedValue;
 
+                if (!this.state.isBrokenMode) {
                     if (updatedSvgKeyFrame.leftControlPoint !== null) {   // Sets opposite value
                         keys[index].inTangent = keys[index].outTangent * -1
                     }
@@ -528,6 +563,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             }
         }
 
+
         animation.setKeys(keys);
 
         this.selectAnimation(animation);
@@ -587,6 +623,13 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
+    setLerpMode() {
+        if (this.state.selected !== null) {
+            let animation = this.state.selected;
+            this.setState({ lerpMode: !this.state.lerpMode }, () => this.selectAnimation(animation));
+        }
+    }
+
     addKeyframeClick() {
 
         if (this.state.selected !== null) {
@@ -771,16 +814,35 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         // START OF LINE/CURVE
         let data: string | undefined = `M${startKey.frame}, ${this._heightScale - (startKey.value * middle)}`;
 
-        if (this.getAnimationData(animation).usesTangents) {
-            data = this.curvePathWithTangents(keyframes, data, middle, animation.dataType);
+        if (this.state && this.state.lerpMode) {
+            data = this.linearInterpolation(keyframes, data, middle);
         } else {
-            console.log("no tangents in this animation");
-            const { easingMode, easingType } = this.getAnimationProperties(animation);
-            if (easingType === undefined && easingMode === undefined) {
-                data = this.linearInterpolation(keyframes, data, middle);
+            if (this.getAnimationData(animation).usesTangents) {
+                data = this.curvePathWithTangents(keyframes, data, middle, animation.dataType);
             } else {
-                let easingFunction = animation.getEasingFunction();
-                data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction)
+                const { easingMode, easingType } = this.getAnimationProperties(animation);
+                if (easingType !== undefined && easingMode !== undefined) {
+                    let easingFunction = animation.getEasingFunction();
+                    data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction)
+                } else {
+                    if (this.state !== undefined) {
+                        let emptyTangents = keyframes.map((kf, i) => {
+                            if (i === 0) {
+                                kf.outTangent = 0;
+                            } else if (i === keyframes.length - 1) {
+                                kf.inTangent = 0;
+                            } else {
+                                kf.inTangent = 0;
+                                kf.outTangent = 0;
+                            }
+                            return kf;
+                        });
+                        data = this.curvePathWithTangents(emptyTangents, data, middle, animation.dataType);
+                    } else {
+                        data = this.linearInterpolation(keyframes, data, middle);
+                    }
+
+                }
             }
         }
 
@@ -1067,7 +1129,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     */
     selectAnimation(animation: Animation) {
 
-        this.isAnimationPlaying();
+        this.playStopAnimation();
 
         this._svgKeyframes = [];
 
@@ -1084,17 +1146,69 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     }
 
     isAnimationPlaying() {
-
         let target = this.props.entity;
-        if (this.props.entity instanceof TargetedAnimation){
+        if (this.props.entity instanceof TargetedAnimation) {
             target = this.props.entity.target;
         }
 
+        return this.props.scene.getAllAnimatablesByTarget(target).length > 0;
+    }
+
+    playPause(direction: number) {
+        if (this.state.selected) {
+            let target = this.props.entity;
+            if (this.props.entity instanceof TargetedAnimation) {
+                target = this.props.entity.target;
+            }
+            if (this.state.isPlaying) {
+                this.props.scene.stopAnimation(target);
+                this.setState({ isPlaying: false })
+                this._isPlaying = false;
+                this.forceUpdate();
+            } else {
+                let keys = this.state.selected.getKeys();
+                let firstFrame = keys[0].frame;
+                let LastFrame = keys[keys.length - 1].frame;
+                if (direction === 1){
+                    this.props.scene.beginAnimation(target, firstFrame, LastFrame, true);
+                } 
+                if (direction === -1){
+                    this.props.scene.beginAnimation(target, LastFrame, firstFrame, true);
+                } 
+                this._isPlaying = true;
+                this.setState({ isPlaying: true });
+                this.forceUpdate();
+            }
+        }
+    }
+
+    playStopAnimation() {
+        let target = this.props.entity;
+        if (this.props.entity instanceof TargetedAnimation) {
+            target = this.props.entity.target;
+        }
         this._isPlaying = this.props.scene.getAllAnimatablesByTarget(target).length > 0;
         if (this._isPlaying) {
             this.props.playOrPause && this.props.playOrPause();
+            return true;
         } else {
             this._isPlaying = false;
+            return false;
+        }
+    }
+
+    analizeAnimation(animation: Animation | null) {
+        if (animation !== null) {
+            const { easingMode, easingType } = this.getAnimationProperties(animation);
+            let hasDefinedTangents = this.getAnimationData(animation).usesTangents;
+
+            if (easingType === undefined && easingMode === undefined && !hasDefinedTangents) {
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            return false;
         }
     }
 
@@ -1124,6 +1238,20 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
+    updateFrameInKeyFrame(frame: number, index: number) {
+
+        if (this.state && this.state.selected) {
+            let animation = this.state.selected;
+            let keys = [...animation.getKeys()];
+
+            keys[index].frame = frame;
+
+            animation.setKeys(keys);
+
+            this.selectAnimation(animation);
+        }
+    }
+
     render() {
         return (
             <div id="animation-curve-editor">
@@ -1145,12 +1273,14 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                     removeKeyframe={() => this.removeKeyframeClick()}
                     brokenMode={this.state.isBrokenMode}
                     brokeTangents={() => this.setBrokenMode()}
+                    lerpMode={this.state.lerpMode}
+                    setLerpMode={() => this.setLerpMode()}
                     flatTangent={() => this.setFlatTangent()} />
 
                 <div className="content">
                     <div className="row">
                         <div className="animation-list">
-                            <div style={{display: this._isTargetedAnimation ? "none" : "block"}}>
+                            <div style={{ display: this._isTargetedAnimation ? "none" : "block" }}>
                                 <div className="label-input">
                                     <label>Animation Name</label>
                                     <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
@@ -1178,13 +1308,13 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                                 <h2>{this._entityName}</h2>
                                 <ul>
                                     {
-                                    
-                                    this.props.entity instanceof TargetedAnimation ? this.setListItem(this.props.entity.animation, 0) :
-                                    this.props.entity.animations && this.props.entity.animations.map((animation, i) => {
 
-                                      return this.setListItem(animation, i);
+                                        this.props.entity instanceof TargetedAnimation ? this.setListItem(this.props.entity.animation, 0) :
+                                            this.props.entity.animations && this.props.entity.animations.map((animation, i) => {
+
+                                                return this.setListItem(animation, i);
 
-                                    })}
+                                            })}
 
                                 </ul>
                             </div>
@@ -1234,7 +1364,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                         </div>
                     </div>
                     <div className="row">
-                        <Timeline currentFrame={this.state.currentFrame} onCurrentFrameChange={(frame: number) => this.changeCurrentFrame(frame)} keyframes={this.state.selected && this.state.selected.getKeys()} selected={this.state.selected && this.state.selected.getKeys()[0]}></Timeline>
+                        <Timeline currentFrame={this.state.currentFrame} playPause={(direction: number) => this.playPause(direction)} isPlaying={this.state.isPlaying} dragKeyframe={(frame: number, index: number) => this.updateFrameInKeyFrame(frame, index)} onCurrentFrameChange={(frame: number) => this.changeCurrentFrame(frame)} keyframes={this.state.selected && this.state.selected.getKeys()} selected={this.state.selected && this.state.selected.getKeys()[0]}></Timeline>
                     </div>
                 </div>
             </div>

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

@@ -121,7 +121,7 @@
                     display: flex;
                     justify-content: center;
                     align-items: center;
-                    width: 8em;
+                    width: 15em;
 
                     .input-frame input {
                         width: 3em;

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

@@ -9,7 +9,9 @@ interface IGraphActionsBarProps {
    handleFrameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
    flatTangent: () => void;
    brokeTangents: () => void;
+   setLerpMode: () => void;
    brokenMode: boolean;
+   lerpMode: boolean;
    currentValue: number;
    currentFrame: number;
 }
@@ -34,6 +36,7 @@ export class GraphActionsBar extends React.Component<IGraphActionsBarProps>{
               <ButtonLineComponent label={"Remove Keyframe"} onClick={this.props.removeKeyframe} />
               <ButtonLineComponent label={"Flat Tangents"} onClick={this.props.flatTangent} />
               <ButtonLineComponent label={this.props.brokenMode ? "Broken Mode On" : "Broken Mode Off" } onClick={this.props.brokeTangents} />
+              <ButtonLineComponent label={this.props.lerpMode ? "Lerp On" : "lerp Off" } onClick={this.props.setLerpMode} />
            </div>
         )
     }

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

@@ -71,7 +71,7 @@ export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps>{
             if (coord !== undefined) {
 
                 var newPoints = [...this.props.keyframeSvgPoints];
-
+                // Check for NaN values here. 
                 if (this._isCurrentPointControl === "left") {
                     newPoints[this._currentPointIndex].leftControlPoint = coord;
                 } else if (this._isCurrentPointControl === "right") {

+ 110 - 26
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -2,25 +2,44 @@
 import * as React from "react";
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faCaretRight, faCaretLeft, faStepBackward, faStepForward } from "@fortawesome/free-solid-svg-icons";
+import { faCaretRight, faCaretLeft, faStepBackward, faStepForward, faPlay, faPause } from "@fortawesome/free-solid-svg-icons";
 
 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;
 }
 
 
-export class Timeline extends React.Component<ITimelineProps, { selected: IAnimationKey }>{
+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 };
+        if (this.props.selected !== null) {
+            this.state = { selected: this.props.selected, activeKeyframe: null };
         }
         this._scrollable = React.createRef();
+        this._direction = 0;
+    }
+
+    playBackwards(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);
+        }
     }
 
     handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
@@ -44,47 +63,105 @@ export class Timeline extends React.Component<ITimelineProps, { selected: IAnima
 
     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;
+        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;
+            }
         }
     }
-    }
 
     previousKeyframe(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);
+        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);
+            }
+        }
+    }
+
+    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);
+
+            }
         }
     }
+
+    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;
+        }
+    }
+
+    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">
-                    <div ref={this._scrollable} className="display-line">
-                        <svg viewBox="0 0 2010 100" style={{ width: 2000 }}>
+                    <div ref={this._scrollable} className="display-line" >
+                        <svg viewBox="0 0 2010 100" style={{ width: 2000 }} 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="10" x2={this.props.currentFrame * 10} y2="20" style={{ stroke: '#12506b', strokeWidth: 6 }} />
-
                             {
                                 this.props.keyframes && this.props.keyframes.map((kf, i) => {
 
-                                    return <svg key={`kf_${i}`}>
-                                        <line x1={kf.frame * 10} y1="10" x2={kf.frame * 10} y2="20" style={{ stroke: 'red', strokeWidth: 6 }} />
+                                    return <svg key={`kf_${i}`} style={{ cursor: 'pointer' }} tabIndex={i + 40} >
+                                        <line id={`kf_${i.toString()}`} x1={kf.frame * 10} y1="10" x2={kf.frame * 10} y2="20" style={{ stroke: 'red', strokeWidth: 6 }} />
                                     </svg>
-
                                 })
                             }
-
                             {
                                 this._frames.map((frame, i) => {
 
@@ -92,10 +169,8 @@ export class Timeline extends React.Component<ITimelineProps, { selected: IAnima
                                         {i % 10 === 0 ? <text x={(i * 10) - 3} y="8" style={{ fontSize: 10 }}>{i}</text> : null}
                                         <line x1={i * 10} y1="10" x2={i * 10} y2="20" style={{ stroke: 'black', strokeWidth: 0.5 }} />
                                     </svg>
-
                                 })
                             }
-
                         </svg>
                     </div>
                     <div className="controls">
@@ -108,6 +183,15 @@ export class Timeline extends React.Component<ITimelineProps, { selected: IAnima
                         <div className="previous-key-frame button" onClick={(e) => this.previousKeyframe(e)}>
                             <FontAwesomeIcon icon={faStepBackward} />
                         </div>
+                        <div className="previous-key-frame button" onClick={(e) => this.playBackwards(e)}>
+                            <FontAwesomeIcon icon={faPlay} style={{ transform: 'rotate(180deg)' }} />
+                        </div>
+                        <div className="previous-key-frame button" onClick={(e) => this.pause(e)}>
+                            <FontAwesomeIcon icon={faPause} />
+                        </div>
+                        <div className="previous-key-frame button" onClick={(e) => this.play(e)}>
+                            <FontAwesomeIcon icon={faPlay} />
+                        </div>
                         <div className="next-key-frame button" onClick={(e) => this.nextKeyframe(e)}>
                             <FontAwesomeIcon icon={faStepForward} />
                         </div>