瀏覽代碼

Style timeline (#8353)

* timeline 1

* Styling 3

* whats new

* active key

* formatting

Co-authored-by: Alejandro Toledo <alex@pixelspace.com>
Alejandro Toledo Martinez 5 年之前
父節點
當前提交
f5956bbd5f

+ 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))
 

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

@@ -3,34 +3,34 @@ import * as React from "react";
 import { Vector2 } from 'babylonjs/Maths/math.vector';
 
 interface IAnchorSvgPointProps {
-   control: Vector2;
-   anchor: Vector2;
-   active: boolean;
-   type: string;
-   index: string;
-   selected: boolean;
-   selectControlPoint: (id: string) => void;
+    control: Vector2;
+    anchor: Vector2;
+    active: boolean;
+    type: string;
+    index: string;
+    selected: boolean;
+    selectControlPoint: (id: string) => void;
 }
 
 
-export class AnchorSvgPoint extends React.Component<IAnchorSvgPointProps>{ 
+export class AnchorSvgPoint extends React.Component<IAnchorSvgPointProps>{
     constructor(props: IAnchorSvgPointProps) {
         super(props);
     }
 
-    select(){
+    select() {
         this.props.selectControlPoint(this.props.type);
     }
-    
+
     render() {
         return (
-        <>
-            <svg x={this.props.control.x} y={this.props.control.y} style={{overflow:'visible'}} onClick={() => this.select()}>
-                <circle type={this.props.type} data-id={this.props.index} className={`draggable control-point ${this.props.active ? 'active' : ''}`} cx="0" cy="0"  r="2" stroke="white" strokeWidth={this.props.selected ? 1 : 0}  fill={this.props.active ? "blue" : "black"}   />
-            </svg>
-            <line className={`control-point ${this.props.active ? 'active' : ''}`} x1={this.props.anchor.x} y1={this.props.anchor.y} x2={this.props.control.x} y2={this.props.control.y} strokeWidth="1" />
-        </>
+            <>
+                <svg x={this.props.control.x} y={this.props.control.y} style={{ overflow: 'visible' }} onClick={() => this.select()}>
+                    <circle type={this.props.type} data-id={this.props.index} className={`draggable control-point ${this.props.active ? 'active' : ''}`} cx="0" cy="0" r="1" stroke="white" strokeWidth={this.props.selected ? 0 : 0} fill={this.props.active ? "#e9db1e" : "white"} />
+                </svg>
+                <line className={`control-point ${this.props.active ? 'active' : ''}`} x1={this.props.anchor.x} y1={this.props.anchor.y} x2={this.props.control.x} y2={this.props.control.y} strokeWidth="1" />
+            </>
         )
     }
-} 
+}
 

+ 35 - 21
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -144,7 +144,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     */
     zoom(e: React.WheelEvent<HTMLDivElement>) {
         e.nativeEvent.stopImmediatePropagation();
-        console.log(e.deltaY);
+        //console.log(e.deltaY);
         let scaleX = 1;
         if (Math.sign(e.deltaY) === -1) {
             scaleX = (this.state.scale - 0.01);
@@ -158,10 +158,15 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
     setAxesLength() {
 
-        let length = Math.round(this._canvasLength * this.state.scale);// Check Undefined, or NaN
+        let length = 20;
+        let newlength = Math.round(this._canvasLength * this.state.scale);// Check Undefined, or NaN
+        if (!isNaN(newlength) || newlength !== undefined) {
+            length = newlength;
+        }
         let highestFrame = 100;
-        if (this.state.selected !== null) {
+        if (this.state.selected !== null && this.state.selected !== undefined) {
             highestFrame = this.state.selected.getHighestFrame();
+
         }
 
         if (length < (highestFrame * 2) / 10) {
@@ -169,6 +174,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
 
         let valueLines = Math.round((this.state.scale * this._heightScale) / 10);
+        console.log(highestFrame);
         let newFrameLength = (new Array(length)).fill(0).map((s, i) => { return { value: i * 10, label: i * 10 } });
         let newValueLength = (new Array(valueLines)).fill(0).map((s, i) => { return { value: i * 10, label: this.getValueLabel(i * 10) } });
         this.setState({ frameAxisLength: newFrameLength, valueAxisLength: newValueLength });
@@ -1121,7 +1127,7 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
 
                         <div ref={this._graphCanvas} className="graph-chart" onWheel={(e) => this.zoom(e)} >
 
-                            <Playhead frame={this.state.currentFrame} offset={this.state.playheadOffset} />
+
 
                             {this.state.svgKeyframes && <SvgDraggableArea ref={this._svgCanvas}
                                 selectKeyframe={(id: string) => this.selectKeyframe(id)}
@@ -1130,38 +1136,46 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                                 selectedControlPoint={(type: string, id: string) => this.selectedControlPoint(type, id)}
                                 updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, id: string) => this.renderPoints(updatedSvgKeyFrame, id)}>
 
-                                {/* Frame Labels  */}
-                                { /* Vertical Grid  */}
-                                {this.state.frameAxisLength.map((f, i) =>
-                                    <svg key={i}>
-                                        <text x={f.value} y="-2" dx="-1em" style={{ font: 'italic 0.2em sans-serif', fontSize: `${0.2 * this.state.scale}em` }}>{f.value}</text>
-                                        <line x1={f.value} y1="0" x2={f.value} y2="100%"></line>
-                                    </svg>
-                                )}
+                                { /* Multiple Curves  */}
+                                {
+                                    this.state.selectedPathData?.map((curve, i) =>
+                                        <path key={i} ref={curve.domCurve} pathLength={curve.pathLength} id="curve" d={curve.pathData} style={{ stroke: curve.color, fill: 'none', strokeWidth: '0.5' }}></path>
+                                    )
+                                }
+
+                                <svg>
+                                    <rect x="-4%" y="0%" width="5%" height="101%" fill="#222"></rect>
+                                </svg>
 
                                 {this.state.valueAxisLength.map((f, i) => {
                                     return <svg key={i}>
-                                        <text x="-3" y={f.value} dx="-1em" style={{ font: 'italic 0.2em sans-serif', fontSize: `${0.2 * this.state.scale}em` }}>{f.label.toFixed(1)}</text>
-                                        <line x1="0" y1={f.value} x2="100%" y2={f.value}></line>
+                                        <text x="-4" y={f.value} dx="0" dy="1" style={{ fontSize: `${0.2 * this.state.scale}em` }}>{f.label.toFixed(1)}</text>
+                                        <line x1="0" y1={f.value} x2="105%" y2={f.value}></line>
                                     </svg>
 
                                 })}
 
-                                { /* Multiple Curves  */}
-                                {
-                                    this.state.selectedPathData?.map((curve, i) =>
-                                        <path key={i} ref={curve.domCurve} pathLength={curve.pathLength} id="curve" d={curve.pathData} style={{ stroke: curve.color, fill: 'none', strokeWidth: '0.5' }}></path>
-                                    )
-                                }
+                                <svg>
+                                    <rect x="0%" y="91%" width="105%" height="10%" fill="#222"></rect>
+                                </svg>
+
+                                {this.state.frameAxisLength.map((f, i) =>
+                                    <svg key={i} x="0" y="96%">
+                                        <text x={f.value} y="0" dx="2px" style={{ fontSize: `${0.2 * this.state.scale}em` }}>{f.value}</text>
+                                        <line x1={f.value} y1="0" x2={f.value} y2="5%"></line>
+                                    </svg>
+                                )}
 
 
                             </SvgDraggableArea>
 
                             }
 
+                            <Playhead frame={this.state.currentFrame} offset={this.state.playheadOffset} />
+
                         </div>
                     </div>
-                    <div className="row">
+                    <div className="row-bottom">
                         <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>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/assets/animationPlayFwdHoverIcon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 45"><defs><style>.cls-1{fill:none;}.cls-2{fill:#ccc;}</style></defs><g id="UI"><rect class="cls-1" width="23" height="45"/><polygon class="cls-2" points="18.19 23.39 4.81 31.12 4.81 15.66 18.19 23.39"/></g></svg>

+ 1 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/assets/animationPlayRevHoverIcon.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 45"><defs><style>.cls-1{fill:none;}.cls-2{fill:#ccc;}</style></defs><g id="UI"><rect class="cls-1" width="23" height="45"/><polygon class="cls-2" points="4.81 23.39 18.19 31.12 18.19 15.66 4.81 23.39"/></g></svg>

+ 94 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/controls.tsx

@@ -0,0 +1,94 @@
+
+import * as React from "react";
+import { IAnimationKey } from 'babylonjs/Animations/animationKey';
+import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
+
+interface IControlsProps {
+    keyframes: IAnimationKey[] | null;
+    selected: IAnimationKey | null;
+    currentFrame: number;
+    onCurrentFrameChange: (frame: number) => void;
+    playPause: (direction: number) => void;
+    isPlaying: boolean;
+    scrollable: React.RefObject<HTMLDivElement>
+}
+
+export class Controls extends React.Component<IControlsProps, { selected: IAnimationKey }>{
+    constructor(props: IControlsProps) {
+        super(props);
+        if (this.props.selected !== null) {
+            this.state = { selected: this.props.selected };
+        }
+    }
+
+    playBackwards() {
+        this.props.playPause(-1);
+    }
+
+    play() {
+        this.props.playPause(1);
+    }
+
+    pause() {
+        if (this.props.isPlaying) {
+            this.props.playPause(1);
+        }
+    }
+
+    nextFrame() {
+        this.props.onCurrentFrameChange(this.props.currentFrame + 1);
+        (this.props.scrollable.current as HTMLDivElement).scrollLeft = this.props.currentFrame * 5;
+    }
+
+    previousFrame() {
+        if (this.props.currentFrame !== 0) {
+            this.props.onCurrentFrameChange(this.props.currentFrame - 1);
+            (this.props.scrollable.current as HTMLDivElement).scrollLeft = -(this.props.currentFrame * 5);
+        }
+    }
+
+    nextKeyframe() {
+        if (this.props.keyframes !== null) {
+            let first = this.props.keyframes.find(kf => kf.frame > this.props.currentFrame);
+            if (first) {
+                this.props.onCurrentFrameChange(first.frame);
+                this.setState({ selected: first });
+                (this.props.scrollable.current as HTMLDivElement).scrollLeft = first.frame * 5;
+            }
+        }
+    }
+
+    previousKeyframe() {
+        if (this.props.keyframes !== null) {
+            let keyframes = [...this.props.keyframes]
+            let first = keyframes.reverse().find(kf => kf.frame < this.props.currentFrame);
+            if (first) {
+                this.props.onCurrentFrameChange(first.frame);
+                this.setState({ selected: first });
+                (this.props.scrollable.current as HTMLDivElement).scrollLeft = -(first.frame * 5);
+            }
+        }
+    }
+
+    render() {
+        return (
+            <div className="controls">
+                <IconButtonLineComponent tooltip="Animation Start" icon="animation-start" onClick={() => this.previousFrame()}></IconButtonLineComponent>
+                <IconButtonLineComponent tooltip="Previous Keyframe" icon="animation-lastkey" onClick={() => this.previousKeyframe()}></IconButtonLineComponent>
+                {this.props.isPlaying ?
+                    <div className="stop-container">
+                        <IconButtonLineComponent tooltip="Play Reverse" icon="animation-stop" onClick={() => this.pause()}></IconButtonLineComponent>
+                    </div>
+                    :
+                    <div className="stop-container">
+                        <IconButtonLineComponent tooltip="Play Reverse" icon="animation-playrev" onClick={() => this.playBackwards()}></IconButtonLineComponent>
+                        <IconButtonLineComponent tooltip="Play Forward" icon="animation-playfwd" onClick={() => this.play()}></IconButtonLineComponent>
+                    </div>
+                }
+                <IconButtonLineComponent tooltip="Next Keyframe" icon="animation-nextkey" onClick={() => this.nextKeyframe()}></IconButtonLineComponent>
+                <IconButtonLineComponent tooltip="Animation End" icon="animation-end" onClick={() => this.nextFrame()}></IconButtonLineComponent>
+
+            </div>
+        )
+    }
+} 

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

@@ -128,6 +128,9 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            width: 20px;
+            cursor: pointer;
+            background-position: center;
         }
 
         &.animation-lastkey {
@@ -135,7 +138,9 @@
             background-repeat: no-repeat;
             background-color: transparent;
             background-size: contain;
-
+            width: 20px;
+            cursor: pointer;
+            background-position: center;
             &:hover{
                 background-image: url('./assets/animationLastKeyHoverIcon.svg');
             }
@@ -146,7 +151,9 @@
             background-repeat: no-repeat;
             background-color: transparent;
             background-size: contain;
-
+            width: 20px;
+            cursor: pointer;
+            background-position: center;
             &:hover{
                 background-image: url('./assets/animationNextKeyHoverIcon.svg');
             }
@@ -158,6 +165,8 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            cursor: pointer;
+            background-position: center;
             &:hover{
                 background-color: #888888 !important;
             }
@@ -169,6 +178,13 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            background-position: center;
+            width: 20px;
+            cursor: pointer;
+            background-position: center;
+            &:hover {
+                background-image: url('./assets/animationPlayFwdHoverIcon.svg');
+            }
         }
 
         &.animation-playrev {
@@ -177,6 +193,13 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            background-position: center;
+            width: 20px;
+            cursor: pointer;
+            background-position: center;
+            &:hover {
+                background-image: url('./assets/animationPlayRevHoverIcon.svg');
+            }
         }
 
         &.animation-start {
@@ -185,6 +208,10 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            background-position: center;
+            cursor: pointer;
+            background-position: center;
+            width: 20px;
         }
 
         &.animation-stop {
@@ -193,6 +220,10 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            background-position: center;
+            cursor: pointer;
+            background-position: center;
+            width: 20px;
         }
 
         &.animation-triangle {
@@ -373,6 +404,7 @@
         justify-content: flex-start;
         align-items: center;
         background: #333333;
+        height: 40px;
 
         .close {
             position: absolute;
@@ -419,14 +451,23 @@
         align-items: flex-start;
         justify-content: flex-start;
         flex-direction: column;
+        height: 462px;
 
         .row {
+            width: 1024px;
+            height: 427px;
+            display: flex;
+            flex-flow: row;
+            background-color: #333333;
+        }
+
+        .row-bottom {
             display: flex;
             align-items: stretch;
             justify-content: flex-start;
             flex-direction: row;
-            width: 100vw;
-            height: 84vh;
+            width: 1024px;
+            height: 45px;
             background-color: #333333;
 
             .timeline{
@@ -438,15 +479,12 @@
                 height: 2.5rem;
 
                 .display-line {
-                    width: 80vw;
-                    height: 2em;
+                    width: 75vw;
+                    height: 40px;
                     overflow: hidden;
-                    overflow-x: scroll;
-                    scrollbar-color: cornflowerblue slategrey;
-                    scrollbar-width: thin;
-                    margin-right: 1.3em;
-                    padding-left: 1em;
-                    padding-right: 1em;
+                    margin-right: 0px;
+                    padding-left: 10px;
+                    padding-right: 10px;
 
                     &::-webkit-scrollbar{
                         height: 0.4em;
@@ -462,19 +500,100 @@
                       }
                 }
 
+                .input-frame{
+                    width: 60px;
+                    margin-left: 10px;
+                    margin-right: 10px;
+
+                    input {
+                        text-align: center;
+                        width: 60px;
+                        border: none;
+                        background: #222222;
+                        color: white;
+                        height: 25px;
+                        font-size: 15px;
+                        font-family: acumin-pro-condensed;
+                        &::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
+                            appearance: none;
+                            -webkit-appearance: none; 
+                            margin: 0;
+                        }
+                    }
+                }
+
+                .timeline-scroll-handle{
+                    display: flex;
+                    flex-direction: row;
+                    height: 25px;
+                    margin: 10px;
+                    
+                    .scroll-handle {
+                        width: 703px;
+                        background-color: #222222;
+                        height: 25px;
+                        display: flex;
+                        align-items: center;
+                        .handle {
+                            display: flex;
+                            flex-direction: row;
+                            height: 20px;
+                            background-color: #666666;
+                            justify-content: space-between;
+
+                            .left-grabber, .right-grabber {
+                                display: flex;
+                                align-items: center;
+                                cursor: pointer;
+                            }
+
+                            .left-grabber {
+                                padding-left: 3px;
+                            }
+                            .right-grabber {
+                                padding-right: 3px;
+                            }
+                            .grabber{
+                                background-color: #333333;
+                                width:2px;
+                                height: 16px;
+                                margin-right:2px
+                            }
+
+                            .text{
+                                margin-left:10px;
+                                margin-right: 10px;
+                                font-size: 12px;
+                                font-family: acumin-pro-condensed;
+                                color: #222222;
+                            }
+                        }
+                    }
+                }
+
+                .timeline-wrapper {
+                    margin-top: -40px;
+                    margin-left: -2px;
+                }
+
                 .controls {
                     display: flex;
                     justify-content: center;
                     align-items: center;
-                    width: 15em;
+                    padding-left: 46px;
+                    padding-right: 46px;
+                    margin-left: 10px;
 
-                    .input-frame input {
-                        width: 3em;
+                    .stop-container{
+                        width: 40px;
+                        display: flex;
+                        flex-direction: row;
+                        justify-content: space-between;
+                        align-items: center;
                     }
 
-                    .button {
-                        margin-left: 0.5em;
-                        margin-right: 0.5em;
+                    .input-frame input {
+                        width: 3em;
                     }
                 }
             }
@@ -719,18 +838,18 @@
         }
 
         .graph-chart{
-            flex: 1 1 0%;
-            overflow-x: scroll;
-            padding-left: 32px;
-            overflow-y: scroll;
+            overflow: hidden;
             scroll-behavior: smooth;
-            background-color: #111111;
-            height: 100%;
+            background-color: rgb(17, 17, 17);
+            height: 364px;
+            width: 782px;
 
             .linear{
                 overflow: visible;
-                border: 1px solid lightgrey;
-                height: 100%;
+                border: 0px solid white;
+                height: 362px;
+                width: 780px;
+                outline: none;
 
                 svg {
                     overflow: visible;
@@ -741,11 +860,12 @@
                 }
 
                 line {
-                    stroke: #cecece;
+                    stroke: #555555;
                     stroke-width: 0.2;
                 }
                 text {
-                    fill: #cecece;
+                    fill: #555555;
+                    font-family: 'acumin-pro-condensed';
                 }
 
                 .control-point {
@@ -754,41 +874,46 @@
 
                 .control-point.active {
                     display: inline;
+                    stroke: #e9db1e;
+                    stroke-width: 0.2;
                 }
             }
 
             .playhead-wrapper {
                 position: relative;
                 left: -13px;
+                bottom: 366px;
             }
 
-            .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-handle {
+                position: relative;
+                top: 340px;
+                .playhead {
+                    width: 22px;
+                    background-color: transparent;
+                    color: #555555;
+                    text-align: center;
+                    font-size: 12px;
+                    position: absolute;
+                    top: 1px;
+                }
+    
+                .playhead-circle {
+                    background-color: #ffffff;
+                    width: 22px;
+                    height: 22px;
+                    border-radius: 50%;
+                    position: absolute;
+                    top: 0;
+                }
             }
 
             .playhead-line {
-                width: 2px;
-                height: calc(90vh - 100px);
-                background-color: rgb(255, 198, 14);
+                width: 1px;
+                height: 341px;
+                background-color: #ffffff;
                 position: absolute;
-                margin-left: 12.5px;
+                margin-left: 9.5px;
             }
         }
     }

+ 11 - 7
inspector/src/components/actionTabs/tabs/propertyGrids/animations/keyframeSvgPoint.tsx

@@ -2,6 +2,10 @@ import * as React from "react";
 import { Vector2 } from 'babylonjs/Maths/math.vector';
 import { AnchorSvgPoint } from './anchorSvgPoint';
 
+const keyInactive = require("./assets/keyInactiveIcon.svg") as string;
+//const keyActive = require("./assets/keyActiveIcon.svg") as string; uncomment when setting active multiselect
+const keySelected = require("./assets/keySelectedIcon.svg") as string;
+
 export interface IKeyframeSvgPoint {
     keyframePoint: Vector2;
     rightControlPoint: Vector2 | null;
@@ -31,24 +35,24 @@ interface IKeyframeSvgPointProps {
     isRightActive: boolean;
 }
 
-export class KeyframeSvgPoint extends React.Component<IKeyframeSvgPointProps>{ 
- 
+export class KeyframeSvgPoint extends React.Component<IKeyframeSvgPointProps>{
+
     constructor(props: IKeyframeSvgPointProps) {
         super(props);
     }
 
-    select(){
+    select() {
         this.props.selectKeyframe(this.props.id);
     }
 
     render() {
         return (
             <>
-                <svg className="draggable" x={this.props.keyframePoint.x} y={this.props.keyframePoint.y} style={{overflow:'visible', cursor: 'pointer'}} >
-                    <circle data-id={this.props.id} className="draggable" cx="0" cy="0"  r="2" stroke="none" strokeWidth="0" fill={this.props.selected ? "red" : "black"} onClick={() => this.select()}/>
+                <svg className="draggable" x={this.props.keyframePoint.x} y={this.props.keyframePoint.y} style={{ overflow: 'visible', cursor: 'pointer' }} >
+                    <image data-id={this.props.id} className="draggable" x="-1" y="-1.5" width="3" height="3" href={this.props.selected ? keySelected : keyInactive} onClick={() => this.select()} />
                 </svg>
-               { this.props.leftControlPoint && <AnchorSvgPoint type="left" index={this.props.id} control={this.props.leftControlPoint} anchor={this.props.keyframePoint} active={this.props.selected} selected={this.props.isLeftActive} selectControlPoint={(type: string) => this.props.selectedControlPoint(type, this.props.id)}/>} 
-               { this.props.rightControlPoint &&  <AnchorSvgPoint type="right" index={this.props.id} control={this.props.rightControlPoint} anchor={this.props.keyframePoint} active={this.props.selected} selected={this.props.isRightActive} selectControlPoint={(type: string) => this.props.selectedControlPoint(type, this.props.id)}/>}
+                {this.props.leftControlPoint && <AnchorSvgPoint type="left" index={this.props.id} control={this.props.leftControlPoint} anchor={this.props.keyframePoint} active={this.props.selected} selected={this.props.isLeftActive} selectControlPoint={(type: string) => this.props.selectedControlPoint(type, this.props.id)} />}
+                {this.props.rightControlPoint && <AnchorSvgPoint type="right" index={this.props.id} control={this.props.rightControlPoint} anchor={this.props.keyframePoint} active={this.props.selected} selected={this.props.isRightActive} selectControlPoint={(type: string) => this.props.selectedControlPoint(type, this.props.id)} />}
             </>
         )
     }

+ 14 - 12
inspector/src/components/actionTabs/tabs/propertyGrids/animations/playhead.tsx

@@ -2,24 +2,26 @@
 import * as React from "react";
 
 interface IPlayheadProps {
-   frame: number;
-   offset: number;
+    frame: number;
+    offset: number;
 }
 
-export class Playhead extends React.Component<IPlayheadProps>{ 
+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 * (this.props.offset) }px - 13px)`}}>
-            <div className="playhead">{this.props.frame}</div>
-            <div className="playhead-triangle"></div>
-            <div className="playhead-line"></div>
-           </div>
+
+    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>
         )
     }
-} 
+}
 
 

+ 64 - 55
inspector/src/components/actionTabs/tabs/propertyGrids/animations/timeline.tsx

@@ -1,8 +1,7 @@
 
 import * as React from "react";
 import { IAnimationKey } from 'babylonjs/Animations/animationKey';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faCaretRight, faCaretLeft, faStepBackward, faStepForward, faPlay, faPause } from "@fortawesome/free-solid-svg-icons";
+import { Controls } from './controls';
 
 interface ITimelineProps {
     keyframes: IAnimationKey[] | null;
@@ -14,7 +13,6 @@ interface ITimelineProps {
     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>;
@@ -144,59 +142,70 @@ export class Timeline extends React.Component<ITimelineProps, { selected: IAnima
         return (
             <>
                 <div className="timeline">
-                    <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}`} 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) => {
-
-                                    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="previous-key-frame button" onClick={(e) => this.playBackwards(e)}>
-                            <FontAwesomeIcon icon={faPlay} style={{ transform: 'rotate(180deg)' }} />
+                    <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="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>
-                        <div className="next-frame button" onClick={(e) => this.nextFrame(e)}>
-                            <FontAwesomeIcon icon={faCaretRight} />
+
+                        <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>