Bläddra i källkod

Merge pull request #8114 from toledoal/timeline

Timeline
David Catuhe 5 år sedan
förälder
incheckning
a68c2259fe

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

@@ -6,6 +6,7 @@
 - Added the `ShadowDepthWrapper` class to support accurate shadow generation for custom as well as node material shaders. [Doc](https://doc.babylonjs.com/babylon101/shadows#custom-shadow-map-shaders) ([Popov72](https://github.com/Popov72))
 - Added Babylon.js Texture [tools](https://www.babylonjs.com/tools/ibl) to prefilter HDR files ([Sebavan](https://github.com/sebavan/))
 - Added editing of PBR materials and Post processes in the node material editor ([Popov72](https://github.com/Popov72))
+- Added Curve editor to view selected entity's animations in the Inspector ([pixelspace](https://github.com/devpixelspace))
 
 ## Updates
 
@@ -42,9 +43,7 @@
 - Added right click options to create PBR and Standard Materials ([Deltakosh](https://github.com/deltakosh))
 - Added support for recording GIF ([Deltakosh](https://github.com/deltakosh))
 - Popup Window available (To be used in Curve Editor) ([pixelspace](https://github.com/devpixelspace))
-- Curve Editor to view current entity's animations (Read only) ([pixelspace](https://github.com/devpixelspace))
 - Add support to update inspector when switching to new scene ([belfortk](https://github.com/belfortk))
-- Drag keyframes in Curve Editor ([pixelspace](https://github.com/devpixelspace))
 
 ### Cameras
 

+ 109 - 93
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -7,6 +7,8 @@ import { EasingFunction, BezierCurveEase } from 'babylonjs/Animations/easing';
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
 import { IKeyframeSvgPoint } from './keyframeSvgPoint';
 import { SvgDraggableArea } from './svgDraggableArea';
+import { Timeline } from './timeline';
+import { Playhead } from './playhead';
 import { Scene } from "babylonjs/scene";
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 
@@ -22,7 +24,7 @@ interface IAnimationCurveEditorComponentProps {
     entity: IAnimatable;
 }
 
-export class AnimationCurveEditorComponent extends React.Component<IAnimationCurveEditorComponentProps, { animations: Animation[], animationName: string, animationTargetProperty: string, isOpen: boolean, selected: Animation, currentPathData: string | undefined, svgKeyframes: IKeyframeSvgPoint[] | undefined }> {
+export class AnimationCurveEditorComponent extends React.Component<IAnimationCurveEditorComponentProps, { animations: Animation[], animationName: string, animationTargetProperty: string, isOpen: boolean, selected: Animation, currentPathData: string | undefined, svgKeyframes: IKeyframeSvgPoint[] | undefined, currentFrame: number }> {
 
     readonly _heightScale: number = 100;
     private _newAnimations: Animation[] = [];
@@ -31,7 +33,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     private _isPlaying: boolean = false;
     constructor(props: IAnimationCurveEditorComponentProps) {
         super(props);
-        this.state = { animations: this._newAnimations, selected: this.props.animations[0], isOpen: true, currentPathData: this.getPathData(this.props.animations[0]), svgKeyframes: this._svgKeyframes, animationTargetProperty: 'position.x', animationName: "" }
+        this.state = { animations: this._newAnimations, selected: this.props.animations[0], isOpen: true, currentPathData: this.getPathData(this.props.animations[0]), svgKeyframes: this._svgKeyframes, animationTargetProperty: 'position.x', animationName: "", currentFrame: 0 }
 
     }
 
@@ -340,12 +342,12 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     }
 
     isAnimationPlaying() {
-          this._isPlaying = this.props.scene.getAllAnimatablesByTarget(this.props.entity).length > 0;
-           if (this._isPlaying){
+        this._isPlaying = this.props.scene.getAllAnimatablesByTarget(this.props.entity).length > 0;
+        if (this._isPlaying) {
             this.props.playOrPause();
-           } else {
-               this._isPlaying = false;
-           }
+        } else {
+            this._isPlaying = false;
+        }
     }
 
     selectAnimation(animation: Animation) {
@@ -404,6 +406,10 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
     }
 
+    changeCurrentFrame(frame: number){
+        this.setState({ currentFrame: frame });
+    }
+
     render() {
         return (
             <div id="animation-curve-editor">
@@ -415,104 +421,114 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                 </div>
                 <div className="content">
 
-                    <div className="animation-list">
+                    <div className="row">
+                        <div className="animation-list">
 
-                        <div>
                             <div>
-                                <label>Animation Name</label>
-                                <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
-                            </div>
-                            <div>
-                                <label>Target Property</label>
-                                <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
-                            </div>
-                            <div className="add" onClick={(e) => this.addAnimation(e)}>
-                                <FontAwesomeIcon icon={faPlusCircle} />
+                                <div>
+                                    <label>Animation Name</label>
+                                    <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
+                                </div>
+                                <div>
+                                    <label>Target Property</label>
+                                    <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
+                                </div>
+                                <div className="add" onClick={(e) => this.addAnimation(e)}>
+                                    <FontAwesomeIcon icon={faPlusCircle} />
+                                </div>
                             </div>
-                        </div>
 
-                        <h2>{this.props.entityName}</h2>
-                        <ul>
-                            {this.props.animations && this.props.animations.map((animation, i) => {
-                                return <li className={this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>{animation.name} <strong>{animation.targetProperty}</strong></li>
-                            })}
+                            <h2>{this.props.entityName}</h2>
+                            <ul>
+                                {this.props.animations && this.props.animations.map((animation, i) => {
+                                    return <li className={this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>{animation.name} <strong>{animation.targetProperty}</strong></li>
+                                })}
 
-                        </ul>
+                            </ul>
 
-                        <h2>New Animations</h2>
-                        <ul>
-                            {this.state.animations && this.state.animations.map((animation, i) => {
-                                return <li className={this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>{animation.name} <strong>{animation.targetProperty}</strong></li>
-                            })}
+                            <h2>New Animations</h2>
+                            <ul>
+                                {this.state.animations && this.state.animations.map((animation, i) => {
+                                    return <li className={this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>{animation.name} <strong>{animation.targetProperty}</strong></li>
+                                })}
 
-                        </ul>
-                    </div>
-                    <div className="graph-chart">
-
-                        {this.state.svgKeyframes && <SvgDraggableArea keyframeSvgPoints={this.state.svgKeyframes} updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) => this.renderPoints(updatedSvgKeyFrame, index)}>
-
-                            {/* Frame Labels  */}
-                            <text x="10" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>10</text>
-                            <text x="20" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>20</text>
-                            <text x="30" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>30</text>
-                            <text x="40" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>40</text>
-                            <text x="50" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>50</text>
-                            <text x="60" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>60</text>
-                            <text x="70" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>70</text>
-                            <text x="80" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>80</text>
-                            <text x="90" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>90</text>
-
-                            { /* Vertical Grid  */}
-                            <line x1="10" y1="0" x2="10" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="20" y1="0" x2="20" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="30" y1="0" x2="30" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="40" y1="0" x2="40" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="50" y1="0" x2="50" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="60" y1="0" x2="60" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="70" y1="0" x2="70" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="80" y1="0" x2="80" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="90" y1="0" x2="90" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-
-                            { /* Value Labels  */}
-                            <text x="0" y="10" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.8</text>
-                            <text x="0" y="20" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.6</text>
-                            <text x="0" y="30" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.4</text>
-                            <text x="0" y="40" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.2</text>
-                            <text x="0" y="50" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1</text>
-                            <text x="0" y="60" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.8</text>
-                            <text x="0" y="70" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.6</text>
-                            <text x="0" y="80" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.4</text>
-                            <text x="0" y="90" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.2</text>
-
-                            { /* Horizontal Grid  */}
-                            <line x1="0" y1="10" x2="100" y2="10" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="20" x2="100" y2="20" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="30" x2="100" y2="30" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="40" x2="100" y2="40" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="50" x2="100" y2="50" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="60" x2="100" y2="60" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="70" x2="100" y2="70" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="80" x2="100" y2="80" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-                            <line x1="0" y1="90" x2="100" y2="90" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
-
-                            { /* Single Curve -Modify this for multiple selection and view  */}
-                            <path id="curve" d={this.state.currentPathData} style={{ stroke: 'red', fill: 'none', strokeWidth: '0.5' }}></path>
-
-                            {this._frames && this._frames.map(frame =>
-                                <svg x={frame.x} y={frame.y} style={{ overflow: 'visible' }}>
-                                    <circle cx="0" cy="0" r="2" stroke="black" strokeWidth="1" fill="white" />
-                                </svg>
-
-                            )}
-
-                        </SvgDraggableArea>
-
-                        }
+                            </ul>
+                        </div>
+                        <div className="graph-chart">
+
+                            <Playhead frame={this.state.currentFrame}/>
+
+                            {this.state.svgKeyframes && <SvgDraggableArea keyframeSvgPoints={this.state.svgKeyframes} updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) => this.renderPoints(updatedSvgKeyFrame, index)}>
+
+                                {/* Frame Labels  */}
+                                <text x="10" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>10</text>
+                                <text x="20" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>20</text>
+                                <text x="30" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>30</text>
+                                <text x="40" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>40</text>
+                                <text x="50" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>50</text>
+                                <text x="60" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>60</text>
+                                <text x="70" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>70</text>
+                                <text x="80" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>80</text>
+                                <text x="90" y="0" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>90</text>
+
+                                { /* Vertical Grid  */}
+                                <line x1="10" y1="0" x2="10" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="20" y1="0" x2="20" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="30" y1="0" x2="30" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="40" y1="0" x2="40" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="50" y1="0" x2="50" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="60" y1="0" x2="60" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="70" y1="0" x2="70" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="80" y1="0" x2="80" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="90" y1="0" x2="90" y2="100" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+
+                                { /* Value Labels  */}
+                                <text x="0" y="10" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.8</text>
+                                <text x="0" y="20" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.6</text>
+                                <text x="0" y="30" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.4</text>
+                                <text x="0" y="40" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1.2</text>
+                                <text x="0" y="50" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>1</text>
+                                <text x="0" y="60" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.8</text>
+                                <text x="0" y="70" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.6</text>
+                                <text x="0" y="80" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.4</text>
+                                <text x="0" y="90" dx="-1em" style={{ font: 'italic 0.2em sans-serif' }}>0.2</text>
+
+                                { /* Horizontal Grid  */}
+                                <line x1="0" y1="10" x2="100" y2="10" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="20" x2="100" y2="20" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="30" x2="100" y2="30" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="40" x2="100" y2="40" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="50" x2="100" y2="50" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="60" x2="100" y2="60" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="70" x2="100" y2="70" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="80" x2="100" y2="80" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+                                <line x1="0" y1="90" x2="100" y2="90" style={{ stroke: 'black', strokeWidth: '0.2px' }}></line>
+
+                                { /* Single Curve -Modify this for multiple selection and view  */}
+                                <path id="curve" d={this.state.currentPathData} style={{ stroke: 'red', fill: 'none', strokeWidth: '0.5' }}></path>
+
+                                {this._frames && this._frames.map(frame =>
+                                    <svg x={frame.x} y={frame.y} style={{ overflow: 'visible' }}>
+                                        <circle cx="0" cy="0" r="2" stroke="black" strokeWidth="1" fill="white" />
+                                    </svg>
+
+                                )}
+
+                            </SvgDraggableArea>
+
+                            }
 
                         Animation name: {this.state.selected.name}
 
+                        </div>
+                       
+                    </div>
+                    <div className="row">
+                            <Timeline currentFrame={this.state.currentFrame} onCurrentFrameChange={(frame: number) => this.changeCurrentFrame(frame)} keyframes={this.state.selected.getKeys()} selected={this.state.selected.getKeys()[0]}></Timeline>
                     </div>
                 </div>
+
+
             </div>
         );
     }

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

@@ -26,11 +26,70 @@
         display: flex;
         align-items: flex-start;
         justify-content: flex-start;
+        flex-direction: column;
+
+        .row {
+            display: flex;
+            align-items: stretch;
+            justify-content: flex-start;
+            flex-direction: row;
+            width: 100vw;
+
+            .timeline{
+                width: 100vw;
+                background: gray;
+                display: flex;
+                align-items: center;
+                justify-content: stretch;
+                height: 2.5rem;
+
+                .display-line {
+                    width: 80vw;
+                    height: 2em;
+                    overflow: hidden;
+                    overflow-x: scroll;
+                    scrollbar-color: cornflowerblue slategrey;
+                    scrollbar-width: thin;
+                    margin-right: 1.3em;
+                    padding-left: 1em;
+                    padding-right: 1em;
+
+                    &::-webkit-scrollbar{
+                        height: 0.4em;
+                    }
+
+                    &::-webkit-scrollbar-track {
+                        box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
+                      }
+                       
+                    &::-webkit-scrollbar-thumb {
+                        background-color: cornflowerblue;
+                        outline: 1px solid slategrey;
+                      }
+                }
+
+                .controls {
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    width: 8em;
+
+                    .input-frame input {
+                        width: 3em;
+                    }
+
+                    .button {
+                        margin-left: 0.5em;
+                        margin-right: 0.5em;
+                    }
+                }
+            }
+        }
 
         .animation-list{
             padding: 1.5rem;
             background: lightgrey;
-            height: 100vh;
+            // height: 100vh;
             ul {
                 list-style:none;
                 padding-left: 0px;
@@ -77,6 +136,40 @@
                 overflow: visible;
                 background-color: 'aliceblue';
             }
+
+            .playhead-wrapper {
+                position: relative;
+                left: -13px;
+            }
+
+            .playhead {
+                width: fit-content;
+                background-color: #ffc60e;
+                color: black;
+                text-align: center;
+                min-width: 2em;
+                justify-content: center;
+                display: flex;
+                padding: 0.1em;
+                font-size: 0.75em;
+            }
+
+            .playhead-triangle {
+                background-color: transparent;
+                width: 0px;
+                height: 0px;
+                border-left: 13.5px solid transparent;
+                border-right: 13.5px solid transparent;
+                border-top: 12px solid #ffc60e;
+            }
+
+            .playhead-line {
+                width: 2px;
+                height: calc(45vh - 100px);
+                background-color: rgb(255, 198, 14);
+                position: absolute;
+                margin-left: 12.5px;
+            }
         }
     }
 

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

@@ -0,0 +1,24 @@
+
+import * as React from "react";
+
+interface IPlayheadProps {
+   frame: number;
+}
+
+export class Playhead extends React.Component<IPlayheadProps>{ 
+    constructor(props: IPlayheadProps) {
+        super(props);
+    }
+    
+    render() { 
+       return (
+           <div className="playhead-wrapper" id="playhead" style={{left: `calc(${this.props.frame * 3.02}px - 13px)`}}>
+            <div className="playhead">{this.props.frame}</div>
+            <div className="playhead-triangle"></div>
+            <div className="playhead-line"></div>
+           </div>
+        )
+    }
+} 
+
+

+ 116 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -0,0 +1,116 @@
+
+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";
+
+interface ITimelineProps {
+    keyframes: IAnimationKey[];
+    selected: IAnimationKey;
+    currentFrame: number;
+    onCurrentFrameChange: (frame: number) => void;
+}
+
+
+export class Timeline extends React.Component<ITimelineProps, { selected: IAnimationKey }>{
+    readonly _frames: object[] = Array(300).fill({});
+    private _scrollable: React.RefObject<HTMLDivElement>;
+    constructor(props: ITimelineProps) {
+        super(props);
+        this.state = { selected: this.props.selected };
+        this._scrollable = React.createRef();
+    }
+
+    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);
+        }
+    }
+
+    nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
+        event.preventDefault();
+        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();
+        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);
+        }
+    }
+
+    render() {
+        return (
+            <>
+                <div className="timeline">
+                    <div ref={this._scrollable} className="display-line">
+                        <svg viewBox="0 0 2010 100" style={{ width: 2000 }}>
+
+                            <line x1={this.props.currentFrame * 10} y1="10" x2={this.props.currentFrame * 10} y2="20" style={{ stroke: '#12506b', strokeWidth: 6 }} />
+
+                            {
+                                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 }} />
+                                    </svg>
+
+                                })
+                            }
+
+                            {
+                                this._frames.map((frame, i) => {
+
+                                    return <svg key={`tl_${i}`}>
+                                        {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">
+                        <div className="input-frame">
+                            <input type="number" value={this.props.currentFrame} onChange={(e) => this.handleInputChange(e)}></input>
+                        </div>
+                        <div className="previous-frame button" onClick={(e) => this.previousFrame(e)}>
+                            <FontAwesomeIcon icon={faCaretLeft} />
+                        </div>
+                        <div className="previous-key-frame button" onClick={(e) => this.previousKeyframe(e)}>
+                            <FontAwesomeIcon icon={faStepBackward} />
+                        </div>
+                        <div className="next-key-frame button" onClick={(e) => this.nextKeyframe(e)}>
+                            <FontAwesomeIcon icon={faStepForward} />
+                        </div>
+                        <div className="next-frame button" onClick={(e) => this.nextFrame(e)}>
+                            <FontAwesomeIcon icon={faCaretRight} />
+                        </div>
+                    </div>
+                </div>
+            </>
+        )
+    }
+}