diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 19 | ||||
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/Keyframe.scss | 88 | ||||
| -rw-r--r-- | src/client/views/nodes/Keyframe.tsx | 361 | ||||
| -rw-r--r-- | src/client/views/nodes/Timeline.scss | 170 | ||||
| -rw-r--r-- | src/client/views/nodes/Timeline.tsx | 514 | ||||
| -rw-r--r-- | src/client/views/nodes/Track.scss | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/Track.tsx | 268 | 
8 files changed, 1434 insertions, 5 deletions
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 2ddefb3c0..55ba71722 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -5,14 +5,13 @@ import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";  import { Id } from "../../../new_fields/FieldSymbols";  import { List } from "../../../new_fields/List";  import { listSpec } from "../../../new_fields/Schema"; -import { BoolCast, Cast } from "../../../new_fields/Types"; +import { BoolCast, Cast, PromiseValue } from "../../../new_fields/Types";  import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";  import { RouteStore } from "../../../server/RouteStore";  import { DocServer } from "../../DocServer";  import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents";  import { DragManager } from "../../util/DragManager";  import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DocComponent } from "../DocComponent";  import { FieldViewProps } from "../nodes/FieldView";  import { FormattedTextBox } from "../nodes/FormattedTextBox";  import { CollectionPDFView } from "./CollectionPDFView"; @@ -21,6 +20,7 @@ import { CollectionView } from "./CollectionView";  import React = require("react");  import { MainView } from "../MainView";  import { Utils } from "../../../Utils"; +import { DocComponent } from "../DocComponent";  export interface CollectionViewProps extends FieldViewProps {      addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -98,15 +98,26 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {          @action          protected drop(e: Event, de: DragManager.DropEvent): boolean {              if (de.data instanceof DragManager.DocumentDragData) { +                if (de.data.dropAction || de.data.userDropAction) { +                    ["width", "height", "curPage"].map(key => +                        de.data.draggedDocuments.map((draggedDocument: Doc, i: number) => +                            PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f)))); +                }                  let added = false;                  if (de.data.dropAction || de.data.userDropAction) { -                    added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); +                    added = de.data.droppedDocuments.reduce((added: boolean, d) => { +                        let moved = this.props.addDocument(d); +                        return moved || added; +                    }, false);                  } else if (de.data.moveDocument) {                      let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;                      added = movedDocs.reduce((added: boolean, d) =>                          de.data.moveDocument(d, /*this.props.DataDoc ? this.props.DataDoc :*/ this.props.Document, this.props.addDocument) || added, false);                  } else { -                    added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false); +                    added = de.data.droppedDocuments.reduce((added: boolean, d) => { +                        let moved = this.props.addDocument(d); +                        return moved || added; +                    }, false);                  }                  e.stopPropagation();                  return added; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 703873681..4a085bb70 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -27,6 +27,7 @@ import "./CollectionFreeFormView.scss";  import { MarqueeView } from "./MarqueeView";  import React = require("react");  import v5 = require("uuid/v5"); +import { Timeline } from "../../nodes/Timeline";  import { ScriptField } from "../../../../new_fields/ScriptField";  import { OverlayView, OverlayElementOptions } from "../../OverlayView";  import { ScriptBox } from "../../ScriptBox"; @@ -534,7 +535,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                          <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />                      </CollectionFreeFormViewPannableContents>                  </MarqueeView> -                <CollectionFreeFormOverlayView  {...this.props} {...this.getDocumentViewProps(this.props.Document)} /> +                <CollectionFreeFormOverlayView  {...this.props} {...this.getDocumentViewProps(this.props.Document)} />                 +                <Timeline {...this.props} />              </div>          );      } diff --git a/src/client/views/nodes/Keyframe.scss b/src/client/views/nodes/Keyframe.scss new file mode 100644 index 000000000..19a61bde1 --- /dev/null +++ b/src/client/views/nodes/Keyframe.scss @@ -0,0 +1,88 @@ +@import "./../globalCssVariables.scss";  + +.bar { +    height: 100%; +    width: 5px; +    background-color: #4d9900; +    position: absolute; +     +    // pointer-events: none;  +    .menubox { +        width: 200px;  +        height:200px; +        top: 50%; +        position: relative;  +        background-color: $light-color;  +        .menutable{ +            tr:nth-child(odd){ +                background-color:$light-color-secondary; +            } +        } +    } + +    .leftResize{ +        left:-12.5px;  +        height:25px;  +        width:25px;  +        border-radius: 50%;  +        background-color: white; +        border:3px solid black; +        top: calc(50% - 12.5px);  +        z-index: 1000;  +        position:absolute;  +    } +    .rightResize{ +        right:-12.5px;  +        height:25px;  +        width:25px;  +        border-radius: 50%;  +        top:calc(50% - 12.5px);  +        background-color:white;  +        border:3px solid black; +        z-index: 1000;  +        position:absolute;  +    } +    .fadeLeft{ +        left:0px;  +        height:100%;  +        position:absolute;  +        pointer-events: none;  +        background: linear-gradient(to left, #4d9900 10%,  $light-color); +    } + +    .fadeRight{ +        right:0px;  +        height:100%;  +        position:absolute;  +        pointer-events: none;  +        background: linear-gradient(to right, #4d9900 10%,  $light-color); +    } +    .divider{ +        height:100%;  +        width: 1px;  +        position: absolute;  +        background-color:black;  +        cursor: col-resize;  +        pointer-events:none;  +    } +    .keyframe{ +        height:100%;  +        position:absolute; +    } +    .keyframeCircle{         +        left:-15px;          +        height:30px;  +        width:30px;  +        border-radius: 50%;  +        top:calc(50% - 15px);  +        background-color:white;  +        border:3px solid green; +        z-index: 1000;  +        position:absolute;  +    } + + +} +     + + diff --git a/src/client/views/nodes/Keyframe.tsx b/src/client/views/nodes/Keyframe.tsx new file mode 100644 index 000000000..e90c6e436 --- /dev/null +++ b/src/client/views/nodes/Keyframe.tsx @@ -0,0 +1,361 @@ +import * as React from "react"; +import "./Keyframe.scss"; +import "./Timeline.scss"; +import "./../globalCssVariables.scss"; +import { observer, Observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, IObservableArray, computed, toJS, isComputedProp } from "mobx"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { FlyoutProps } from "./Timeline"; +import { Transform } from "../../util/Transform"; + +export namespace KeyframeFunc { +    export enum KeyframeType { +        fade = "fade", +        default = "default", +    } +    export enum Direction { +        left = "left", +        right = "right" +    } +    export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: List<Doc>): (RegionData | undefined) => { +        let leftMost: (RegionData | undefined) = undefined; +        let rightMost: (RegionData | undefined) = undefined; +        regions.forEach(region => { +            let neighbor = RegionData(region as Doc); +            if (currentRegion.position! > neighbor.position) { +                if (!leftMost || neighbor.position > leftMost.position) { +                    leftMost = neighbor; +                } +            } else if (currentRegion.position! < neighbor.position) { +                if (!rightMost || neighbor.position < rightMost.position) { +                    rightMost = neighbor; +                } +            } +        }); +        if (dir === Direction.left) { +            return leftMost; +        } else if (dir === Direction.right) { +            return rightMost; +        } +    }; + +    export const defaultKeyframe = () => { +        let regiondata = new Doc(); //creating regiondata +        regiondata.duration = 200; +        regiondata.position = 0; +        regiondata.fadeIn = 20; +        regiondata.fadeOut = 20; +        return regiondata; +    }; +} + +export const RegionDataSchema = createSchema({ +    position: defaultSpec("number", 0), +    duration: defaultSpec("number", 0), +    keyframes: listSpec(Doc), +    fadeIn: defaultSpec("number", 0), +    fadeOut: defaultSpec("number", 0) +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + +interface IProps { +    node: Doc; +    RegionData: Doc; +    changeCurrentBarX: (x: number) => void; +    setFlyout: (props: FlyoutProps) => any; +    transform: Transform;  +} + +@observer +export class Keyframe extends React.Component<IProps> { + +    @observable private _bar = React.createRef<HTMLDivElement>(); + +    @computed +    private get regiondata() { +        let index = this.regions.indexOf(this.props.RegionData); +        return RegionData(this.regions[index] as Doc); +    } + +    @computed +    private get regions() { +        return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; +    } + +    @computed +    private get firstKeyframe() { +        let first: (Doc | undefined) = undefined; +        DocListCast(this.regiondata.keyframes!).forEach(kf => { +            if (kf.type !== KeyframeFunc.KeyframeType.fade) { +                if (!first || first && NumCast(kf.time) < NumCast(first.time)) { +                    first = kf; +                } +            } +        }); +        return first; +    } + +    @computed +    private get lastKeyframe() { +        let last: (Doc | undefined) = undefined; +        DocListCast(this.regiondata.keyframes!).forEach(kf => { +            if (kf.type !== KeyframeFunc.KeyframeType.fade) { +                if (!last || last && NumCast(kf.time) > NumCast(last.time)) { +                    last = kf; +                } +            } +        }); +        return last; +    } + + +    componentWillMount() { +        if (!this.regiondata.keyframes) { +            this.regiondata.keyframes = new List<Doc>(); +        } +    } + + +    @action +    componentDidMount() { +       +        console.log(toJS(this.props.node)); +        console.log("hi");       +        let fadeIn =  this.makeKeyData(this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade)!; +        let fadeOut =  this.makeKeyData(this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade)!; +        let start =  this.makeKeyData(this.regiondata.position, KeyframeFunc.KeyframeType.fade)!; +        let finish =  this.makeKeyData(this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.fade)!; +        (fadeIn.key! as Doc).opacity = 1; +        (fadeOut.key! as Doc).opacity = 1; +        (start.key! as Doc).opacity = 0.1; +        (finish.key! as Doc).opacity = 0.1; + +        observe(this.regiondata, change => { +            if (change.type === "update") { +                fadeIn.time = this.regiondata.position + this.regiondata.fadeIn; +                fadeOut.time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; +                start.time = this.regiondata.position; +                finish.time = this.regiondata.position + this.regiondata.duration; + +                let fadeInIndex = this.regiondata.keyframes!.indexOf(fadeIn); +                let fadeOutIndex = this.regiondata.keyframes!.indexOf(fadeOut); +                let startIndex = this.regiondata.keyframes!.indexOf(start); +                let finishIndex = this.regiondata.keyframes!.indexOf(finish); + +                this.regiondata.keyframes![fadeInIndex] = fadeIn; +                this.regiondata.keyframes![fadeOutIndex] = fadeOut; +                this.regiondata.keyframes![startIndex] = start; +                this.regiondata.keyframes![finishIndex] = finish; +                this.forceUpdate(); +            } +        }); +    } + + +    @action +    makeKeyData = (kfpos: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time  +        let doclist =  DocListCast(this.regiondata.keyframes!); +        let existingkf: (Doc | undefined) = undefined; +        if (doclist) { +            doclist.forEach(TK => { //TK is TimeAndKey +                if (TK.time === kfpos) { +                    existingkf = TK; +                } +            }); +        } +        if (existingkf) { +            return existingkf; +        } +        let TK: Doc = new Doc(); +        TK.time = kfpos; +        TK.key = Doc.MakeCopy(this.props.node, true); +        TK.type = type; +        this.regiondata.keyframes!.push(TK); +        return TK; +    } + +    @action +    onBarPointerDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onBarPointerMove); +        document.addEventListener("pointerup", (e: PointerEvent) => { +            document.removeEventListener("pointermove", this.onBarPointerMove); +        }); +    } + + +    @action +    onBarPointerMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; +        let right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions!); +        // let bar = this._bar.current!;  +        // let barX = bar.getBoundingClientRect().left; +        // let offset = e.clientX - barX; +        let prevX = this.regiondata.position; +        let futureX = this.regiondata.position + e.movementX; +        if (futureX <= 0) { +            this.regiondata.position = 0; +        } else if ((left && left.position + left.duration >= futureX)) { +            this.regiondata.position = left.position + left.duration; +        } else if ((right && right.position <= futureX + this.regiondata.duration)) { +            this.regiondata.position = right.position - this.regiondata.duration; +        } else { +            this.regiondata.position = futureX; +        } +        for (let i = 0; i < this.regiondata.keyframes!.length; i++) { +            if ((this.regiondata.keyframes![i] as Doc).type !== KeyframeFunc.KeyframeType.fade) { +                let movement = this.regiondata.position - prevX; +                (this.regiondata.keyframes![i] as Doc).time = NumCast((this.regiondata.keyframes![i] as Doc).time) + movement; +            } +        } +        this.forceUpdate(); +    } + +    @action +    onResizeLeft = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onDragResizeLeft); +        document.addEventListener("pointerup", () => { +            document.removeEventListener("pointermove", this.onDragResizeLeft); +        }); +    } + +    @action +    onResizeRight = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onDragResizeRight); +        document.addEventListener("pointerup", () => { +            document.removeEventListener("pointermove", this.onDragResizeRight); +        }); +    } + +    @action +    onDragResizeLeft = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let bar = this._bar.current!; +        let offset = Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale); +        let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); +        let firstkf: (Doc | undefined) = this.firstKeyframe; +        if (firstkf && this.regiondata.position + this.regiondata.fadeIn + offset >= NumCast(firstkf!.time)) { +            let dif = NumCast(firstkf!.time) - (this.regiondata.position + this.regiondata.fadeIn); +            this.regiondata.position = NumCast(firstkf!.time) - this.regiondata.fadeIn; +            this.regiondata.duration -= dif; +        } else if (this.regiondata.duration - offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // no keyframes, just fades +            this.regiondata.position -= (this.regiondata.fadeIn + this.regiondata.fadeOut - this.regiondata.duration); +            this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut; +        } else if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { +            let dif = this.regiondata.position - (leftRegion.position + leftRegion.duration); +            this.regiondata.position = leftRegion.position + leftRegion.duration; +            this.regiondata.duration += dif; + +        } else { +            this.regiondata.duration -= offset; +            this.regiondata.position += offset; +        } +    } + + +    @action +    onDragResizeRight = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let bar = this._bar.current!; +        let offset = Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale); +        let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); +        if (this.lastKeyframe! && this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= NumCast((this.lastKeyframe! as Doc).time)) { +            let dif = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut - NumCast((this.lastKeyframe! as Doc).time); +            this.regiondata.duration -= dif; +        } else if (this.regiondata.duration + offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // nokeyframes, just fades +            this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut; +        } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) { +            let dif = rightRegion.position - (this.regiondata.position + this.regiondata.duration); +            this.regiondata.duration += dif; +        } else { +            this.regiondata.duration += offset; +        } +    } + +    createDivider = (type?: KeyframeFunc.Direction): JSX.Element => { +        if (type === "left") { +            return <div className="divider" style={{ right: "0px" }}></div>; +        } else if (type === "right") { +            return <div className="divider" style={{ left: "0px" }}> </div>; +        } +        return <div className="divider"></div>; +    } + +    @action +    createKeyframe = (e: React.MouseEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let bar = this._bar.current!; +        let offset = Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale); +        if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends +            let position = NumCast(this.regiondata.position); +            this.makeKeyData(Math.round(position + offset)); +            console.log(this.regiondata.keyframes!.length);  +            this.props.changeCurrentBarX(NumCast(Math.round(position + offset))); //first move the keyframe to the correct location and make a copy so the correct file gets coppied +        } +    } +     + +    @action +    moveKeyframe = (e: React.MouseEvent, kf: Doc) => { +        e.preventDefault(); +        e.stopPropagation(); +        this.props.changeCurrentBarX(NumCast(kf.time!)); +    } + + +    @action +    private createKeyframeJSX = (kf: Doc, type = KeyframeFunc.KeyframeType.default) => { +        if (type === KeyframeFunc.KeyframeType.default) { +            return ( +                <div className="keyframe" style={{ left: `${NumCast(kf.time) - this.regiondata.position}px` }}> +                    {this.createDivider()} +                    <div className="keyframeCircle" onPointerDown={(e) => { this.moveKeyframe(e, kf as Doc); }} onContextMenu={(e: React.MouseEvent) => { +                        e.preventDefault(); +                        e.stopPropagation(); +                    }}></div> +                </div>); +        } +        return ( +            <div className="keyframe" style={{ left: `${NumCast(kf.time) - this.regiondata.position}px` }}> +                {this.createDivider()} +            </div> +        ); +    } + +    render() { +        return ( +            <div> +                <div className="bar" ref={this._bar} style={{ transform: `translate(${this.regiondata.position}px)`, width: `${this.regiondata.duration}px` }} +                    onPointerDown={this.onBarPointerDown} +                    onDoubleClick={this.createKeyframe} +                    onContextMenu={action((e: React.MouseEvent) => { +                        e.preventDefault(); +                        e.stopPropagation(); +                        let offsetLeft = this._bar.current!.getBoundingClientRect().left - this._bar.current!.parentElement!.getBoundingClientRect().left; +                        let offsetTop = this._bar.current!.getBoundingClientRect().top; //+ this._bar.current!.parentElement!.getBoundingClientRect().top;  +                        this.props.setFlyout({ x: offsetLeft, y: offsetTop, display: "block", regiondata: this.regiondata, regions: this.regions }); +                    })}> +                    <div className="leftResize" onPointerDown={this.onResizeLeft} ></div> +                    <div className="rightResize" onPointerDown={this.onResizeRight}></div> +                    {this.regiondata.keyframes!.map(kf => { +                        return this.createKeyframeJSX(kf as Doc, (kf! as Doc).type as KeyframeFunc.KeyframeType); +                    })} +                </div> +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Timeline.scss b/src/client/views/nodes/Timeline.scss new file mode 100644 index 000000000..47f448adb --- /dev/null +++ b/src/client/views/nodes/Timeline.scss @@ -0,0 +1,170 @@ +@import "./../globalCssVariables.scss";  + +.minimize{ +    position:relative;  +    z-index: 1000;  +    height: 30px;  +    width: 100px;  +} +.flyout-container{ +    background-color: transparent;  +    position:absolute;  +             +    z-index:9999;  +    height: 150px;  +    width: 150px;  + +    .flyout{ +        background-color: transparent;  +        transform: rotate(180deg);  +        left:0px;  +        top:0px;  +        width: 100%;  +        height: 100%;  +    } +    .input-container{ +        position: absolute;  +        right:0px;  +        top: 30px;  +        width: 70px;  +        input{ +            width: 100%;  +        } +    } +    .text-container{ +        position:absolute;  +        top:30px;  +        left:0px;  +        color:white +    } +} + +.placement-highlight{ +    background-color:blue;  +    transform: translate(0px, 0px);  +    transition: width 1000ms ease-in-out;  +    transition: height 1000ms ease-in-out; +    position: absolute;  +} + +.timeline-container{ +    width:100%; +    height:300px;  +    position:absolute; +    background-color: $light-color-secondary; +    box-shadow: 0px 10px 20px;  +    //transition: transform 1000ms ease-in-out;  +     +    .toolbox{ +        position:absolute;  +        width: 100%;    +        top: 10px;  +        left: 20px;  +        div{ +            float:left;  +            margin-left: 10px;  +            position:relative;  +            .overview{ +                width: 200px;  +                height: 100%;  +                background-color: black;  +                position:absolute;  +            } +        } +    } +    .info-container{  +        margin-top: 50px;  +        right:20px;  +        position:absolute;  +        height: calc(100% - 100px);  +        width: calc(100% - 140px);  +        overflow: hidden; + +        .scrubberbox{ +            position:absolute;  +            background-color: transparent;  +            height: 30px; +            width:100%;  +             +            .tick{ +                height:100%;  +                width: 1px;  +                background-color:black;  + +            }    +        } +        .scrubber{ +            top:30px;  +            height: 100%;  +            width: 2px;  +            position:absolute; +            z-index: 1001; +            background-color:black;  +            .scrubberhead{ +                top: -30px;  +                height: 30px;  +                width: 30px; +                background-color:transparent;  +                border-radius: 50%;  +                border: 5px solid black;  +                left: -15px; +                position:absolute;                  +            } +        } + +        .trackbox{ +            top: 30px;  +            height:calc(100% - 30px); +            width:100%; +            border:1px; +            overflow:hidden; +            background-color:white; +            position:absolute; +            box-shadow: -10px 0px 10px 10px grey;  +        } +         +    } +    .title-container{ +        margin-top: 80px;  +        margin-left: 20px;  +        height: calc(100% - 100px - 30px);  +        width: 100px;  +        background-color:white; +        overflow: hidden;   +        .datapane{ +            top:0px;  +            width: 100px;  +            height: 75px;  +            border: 1px solid $dark-color; +            background-color: $intermediate-color;  +            color: white;  +            position:relative;  +            float:left;  +            border-style:solid;  +        } +    } +    .resize{ +        bottom: 5px;  +        position:absolute;  +        height: 30px;  +        width: 50px;  +        left: calc(50% - 25px); +    } +} + + + +.overview{ +    position: absolute;  +    height: 50px;  +    width: 200px;  +    background-color: black;  +    .container{         +        position: absolute;  +        float: left 0px;  +        top: 25%;  +        height: 75%;  +        width: 100%;  +        background-color: grey;  +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Timeline.tsx b/src/client/views/nodes/Timeline.tsx new file mode 100644 index 000000000..1f0fe8b77 --- /dev/null +++ b/src/client/views/nodes/Timeline.tsx @@ -0,0 +1,514 @@ +import * as React from "react"; +import "./Timeline.scss"; +import { CollectionSubView } from "../collections/CollectionSubView"; +import { Document, listSpec } from "../../../new_fields/Schema"; +import { observer } from "mobx-react"; +import { Track } from "./Track"; +import { observable, reaction, action, IReactionDisposer, observe, IObservableArray, computed, toJS, Reaction, IObservableObject, trace, autorun, runInAction } from "mobx"; +import { Cast, NumCast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPlayCircle, faBackward, faForward, faGripLines, faArrowUp, faArrowDown, faClock } from "@fortawesome/free-solid-svg-icons"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { ContextMenu } from "../ContextMenu"; +import { DocumentManager } from "../../util/DocumentManager"; +import { VideoBox } from "./VideoBox"; +import { VideoField } from "../../../new_fields/URLField"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { Transform } from "../../util/Transform"; + + +export interface FlyoutProps { +    x?: number; +    y?: number; +    display?: string; +    regiondata?: Doc; +    regions?: List<Doc>; +} + + +@observer +export class Timeline extends CollectionSubView(Document) { +    private readonly DEFAULT_CONTAINER_HEIGHT: number = 300; +    private readonly DEFAULT_TICK_SPACING: number = 50; +    private readonly MIN_CONTAINER_HEIGHT: number = 205; +    private readonly MAX_CONTAINER_HEIGHT: number = 800; +    private readonly DEFAULT_TICK_INCREMENT: number = 1000; + +    @observable private _isMinimized = false; +    @observable private _tickSpacing = this.DEFAULT_TICK_SPACING; +    @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT; + +    @observable private _scrubberbox = React.createRef<HTMLDivElement>(); +    @observable private _scrubber = React.createRef<HTMLDivElement>(); +    @observable private _trackbox = React.createRef<HTMLDivElement>(); +    @observable private _titleContainer = React.createRef<HTMLDivElement>(); +    @observable private _timelineContainer = React.createRef<HTMLDivElement>(); + +    @observable private _timelineWrapper = React.createRef<HTMLDivElement>(); +    @observable private _infoContainer = React.createRef<HTMLDivElement>(); + + +    @observable private _currentBarX: number = 0; +    @observable private _windSpeed: number = 1; +    @observable private _isPlaying: boolean = false; //scrubber playing +    @observable private _isFrozen: boolean = false; //timeline freeze +    @observable private _boxLength: number = 0; +    @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT; +    @observable private _time = 100000; //DEFAULT +    @observable private _ticks: number[] = []; +    @observable private flyoutInfo: FlyoutProps = { x: 0, y: 0, display: "none", regiondata: new Doc(), regions: new List<Doc>() }; + +    @computed +    private get children(): List<Doc> { +        let extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type)); +        if (extendedDocument) { +            if (this.props.Document.data_ext) { +                return Cast((Cast(this.props.Document.data_ext, Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>; +            } else { +                return new List<Doc>(); +            } +        } +        return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>; +    } + +    componentWillMount(){ +    } +    componentDidMount() { +        if (StrCast(this.props.Document.type) === "video") { +            console.log("ran"); +            console.log(this.props.Document.duration); +            if (this.props.Document.duration) { +                this._time = Math.round(NumCast(this.props.Document.duration)) * 1000; + +                reaction(() => { +                    return NumCast(this.props.Document.curPage); +                }, curPage => { +                    this.changeCurrentBarX(curPage * this._tickIncrement / this._tickSpacing) +                }); + +            } + +        } +        runInAction(() => { +            reaction(() => { +                return this._time; +            }, () => { +                this._ticks = []; +                for (let i = 0; i < this._time;) { +                    this._ticks.push(i); +                    i += this._tickIncrement; +                } +                let trackbox = this._trackbox.current!; +                this._boxLength = this._tickIncrement / 1000 * this._tickSpacing * this._ticks.length; +                trackbox.style.width = `${this._boxLength}`; +                this._scrubberbox.current!.style.width = `${this._boxLength}`; +            }, { fireImmediately: true }); +        }); +    } + +    componentDidUpdate() { +    } + +    @action +    changeCurrentBarX = (x: number) => { +        this._currentBarX = x; +    } + +    //for playing +    @action +    onPlay = async (e: React.MouseEvent) => { +        if (this._isPlaying) { +            this._isPlaying = false; +        } else { +            this._isPlaying = true; +            this.changeCurrentX(); +        } +    } + +    @action +    changeCurrentX = () => { +        if (this._currentBarX === this._boxLength && this._isPlaying) { +            this._currentBarX = 0; +        } +        if (this._currentBarX <= this._boxLength && this._isPlaying) { +            this._currentBarX = this._currentBarX + this._windSpeed; +            setTimeout(this.changeCurrentX, 15); +        } +    } + +    @action +    windForward = (e: React.MouseEvent) => { +        if (this._windSpeed < 64) { //max speed is 32 +            this._windSpeed = this._windSpeed * 2; +        } +    } + +    @action +    windBackward = (e: React.MouseEvent) => { +        if (this._windSpeed > 1 / 16) { // min speed is 1/8 +            this._windSpeed = this._windSpeed / 2; +        } +    } + +    //for scrubber action  +    @action +    onScrubberDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onScrubberMove); +        document.addEventListener("pointerup", () => { +            document.removeEventListener("pointermove", this.onScrubberMove); +        }); +    } + +    @action +    onScrubberMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let scrubberbox = this._scrubberbox.current!; +        let left = scrubberbox.getBoundingClientRect().left; +        let offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale; +        this._currentBarX = offsetX; +    } + +    @action +    onScrubberClick = (e: React.MouseEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let scrubberbox = this._scrubberbox.current!; +        let offset = (e.clientX - scrubberbox.getBoundingClientRect().left) * this.props.ScreenToLocalTransform().Scale; +        this._currentBarX = offset; +    } + + + +    @action +    onPanDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onPanMove); +        document.addEventListener("pointerup", () => { +            document.removeEventListener("pointermove", this.onPanMove); +        }); +    } + +    @action +    onPanMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let infoContainer = this._infoContainer.current!; +        let trackbox = this._trackbox.current!; +        let titleContainer = this._titleContainer.current!; +        infoContainer.scrollLeft = infoContainer.scrollLeft - e.movementX; +        trackbox.scrollTop = trackbox.scrollTop - e.movementY; +        titleContainer.scrollTop = titleContainer.scrollTop - e.movementY; +    } + + +    @action +    onResizeDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        document.addEventListener("pointermove", this.onResizeMove); +        document.addEventListener("pointerup", () => { +            document.removeEventListener("pointermove", this.onResizeMove); +        }); +    } + +    @action +    onResizeMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom; +        if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) { +            this._containerHeight = this.MIN_CONTAINER_HEIGHT; +        } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) { +            this._containerHeight = this.MAX_CONTAINER_HEIGHT; +        } else { +            this._containerHeight += offset; +        } +    } + +    @action +    onTimelineDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        if (e.nativeEvent.which === 1 && !this._isFrozen) { +            document.addEventListener("pointermove", this.onTimelineMove); +            document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onTimelineMove); }); +        } +    } + +    @action +    onTimelineMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let timelineContainer = this._timelineWrapper.current!; +        let left = parseFloat(timelineContainer.style.left!); +        let top = parseFloat(timelineContainer.style.top!); +        timelineContainer.style.left = `${left + e.movementX}px`; +        timelineContainer.style.top = `${top + e.movementY}px`; +    } + +    @action +    minimize = (e: React.MouseEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let timelineContainer = this._timelineContainer.current!; +        if (this._isMinimized) { +            this._isMinimized = false; +            timelineContainer.style.transform = `translate(0px, 0px)`; +        } else { +            this._isMinimized = true; +            timelineContainer.style.transform = `translate(0px, ${- this._containerHeight - 30}px)`; +        } +    } + +    @action +    toTime = (time: number): string => { +        const inSeconds = time / 1000; +        let min: (string | number) = Math.floor(inSeconds / 60); +        let sec: (string | number) = inSeconds % 60; + +        if (Math.floor(sec / 10) === 0) { +            sec = "0" + sec; +        } +        return `${min}:${sec}`; +    } + + +    private _freezeText = "Freeze Timeline"; + +    timelineContextMenu = (e: React.MouseEvent): void => { +        let subitems: ContextMenuProps[] = []; +        let timelineContainer = this._timelineWrapper.current!; +        subitems.push({ +            description: "Pin to Top", event: action(() => { +                if (!this._isFrozen) { +                    timelineContainer.style.transition = "top 1000ms ease-in, left 1000ms ease-in";  //????? +                    timelineContainer.style.left = "0px"; +                    timelineContainer.style.top = "0px"; +                    timelineContainer.style.transition = "none"; +                } +            }), icon: faArrowUp +        }); +        subitems.push({ +            description: "Pin to Bottom", event: action(() => { +                console.log(this.props.Document.y); + +                if (!this._isFrozen) { +                    timelineContainer.style.transform = `translate(0px, ${e.pageY - this._containerHeight}px)`; +                } +            }), icon: faArrowDown +        }); +        subitems.push({ +            description: this._freezeText, event: action(() => { +                if (this._isFrozen) { +                    this._isFrozen = false; +                    this._freezeText = "Freeze Timeline"; +                } else { +                    this._isFrozen = true; +                    this._freezeText = "Unfreeze Timeline"; +                } +            }), icon: "thumbtack" +        }); +        ContextMenu.Instance.addItem({ description: "Timeline Funcs...", subitems: subitems, icon: faClock }); +    } + + + +    @action +    getFlyout = (props: FlyoutProps) => { +        for (const [k, v] of Object.entries(props)) { +            (this.flyoutInfo as any)[k] = v; +        } +    } + +    render() { +        return ( +            <div style={{ left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)" }} ref={this._timelineWrapper}> +                <button className="minimize" onClick={this.minimize}>Minimize</button> +                <div className="timeline-container" style={{ height: `${this._containerHeight}px`, left: "0px", top: "30px" }} ref={this._timelineContainer} onPointerDown={this.onTimelineDown} onContextMenu={this.timelineContextMenu}> +                    {/* <TimelineFlyout flyoutInfo={this.flyoutInfo} tickSpacing={this._tickSpacing}/> */} +                    <div className="toolbox"> +                        <div onClick={this.windBackward}> <FontAwesomeIcon icon={faBackward} size="2x" /> </div> +                        <div onClick={this.onPlay}> <FontAwesomeIcon icon={faPlayCircle} size="2x" /> </div> +                        <div onClick={this.windForward}> <FontAwesomeIcon icon={faForward} size="2x" /> </div> +                        {/* <TimelineOverview currentBarX = {this._currentBarX}/> */} +                    </div> +                    <div className="info-container" ref={this._infoContainer}> +                        <div className="scrubberbox" ref={this._scrubberbox} onClick={this.onScrubberClick}> +                            {this._ticks.map(element => { +                                return <div className="tick" style={{ transform: `translate(${element / 1000 * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toTime(element)}</p></div>; +                            })} +                        </div> +                        <div className="scrubber" ref={this._scrubber} onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}> +                            <div className="scrubberhead"></div> +                        </div> +                        <div className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown}> +                            {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} setFlyout={this.getFlyout} transform={this.props.ScreenToLocalTransform()}/>)} +                        </div> +                    </div> +                    <div className="title-container" ref={this._titleContainer}> +                        {DocListCast(this.children).map(doc => <div className="datapane"><p>{doc.title}</p></div>)} +                    </div> +                    <div onPointerDown={this.onResizeDown}> +                        <FontAwesomeIcon className="resize" icon={faGripLines} /> +                    </div> +                </div> +            </div> +        ); +    } + +} + + +interface TimelineFlyoutProps { +    flyoutInfo: FlyoutProps; +    tickSpacing: number; + +} + +interface TimelineOverviewProps { +    currentBarX: number; +} + +class TimelineOverview extends React.Component<TimelineOverviewProps>{ + +    componentWillMount() { + +    } + +    render() { +        return ( +            <div className="overview"> +                <div className="container"> +                    <div className="scrubber"> +                        <div className="scrubberhead"></div> +                    </div> +                </div> +            </div> +        ); +    } +} + +class TimelineFlyout extends React.Component<TimelineFlyoutProps>{ + +    @observable private _timeInput = React.createRef<HTMLInputElement>(); +    @observable private _durationInput = React.createRef<HTMLInputElement>(); +    @observable private _fadeInInput = React.createRef<HTMLInputElement>(); +    @observable private _fadeOutInput = React.createRef<HTMLInputElement>(); + +    private block = false; + +    componentDidMount() { +        document.addEventListener("pointerdown", this.closeFlyout); +    } +    componentWillUnmount() { +        document.removeEventListener("pointerdown", this.closeFlyout); +    } + +    componentDidUpdate() { +        console.log(this.props.flyoutInfo); +    } + + +    @action +    changeTime = (e: React.KeyboardEvent) => { +        let time = this._timeInput.current!; +        if (e.keyCode === 13) { +            if (!Number.isNaN(Number(time.value))) { +                this.props.flyoutInfo.regiondata!.position = Number(time.value) / 1000 * this.props.tickSpacing; +                time.placeholder = time.value + "ms"; +                time.value = ""; +            } +        } +    } +    @action +    onFlyoutDown = (e: React.PointerEvent) => { +        this.props.flyoutInfo.display = "block"; +        this.block = true; +    } + +    @action +    closeFlyout = (e: PointerEvent) => { +        if (this.block) { +            this.block = false; +            return; +        } +        this.props.flyoutInfo.display = "none"; +    } + +    @action +    changeDuration = (e: React.KeyboardEvent) => { +        let duration = this._durationInput.current!; +        if (e.keyCode === 13) { +            if (!Number.isNaN(Number(duration.value))) { +                this.props.flyoutInfo.regiondata!.duration = Number(duration.value) / 1000 * this.props.tickSpacing; +                duration.placeholder = duration.value + "ms"; +                duration.value = ""; +            } +        } +    } + +    @action +    changeFadeIn = (e: React.KeyboardEvent) => { +        let fadeIn = this._fadeInInput.current!; +        if (e.keyCode === 13) { +            if (!Number.isNaN(Number(fadeIn.value))) { +                this.props.flyoutInfo.regiondata!.fadeIn = Number(fadeIn.value); +                fadeIn.placeholder = fadeIn.value + "ms"; +                fadeIn.value = ""; +            } +        } +    } + +    @action +    changeFadeOut = (e: React.KeyboardEvent) => { +        let fadeOut = this._fadeOutInput.current!; +        if (e.keyCode === 13) { +            if (!Number.isNaN(Number(fadeOut.value))) { +                this.props.flyoutInfo.regiondata!.fadeOut = Number(fadeOut.value); +                fadeOut.placeholder = fadeOut.value + "ms"; +                fadeOut.value = ""; +            } +        } +    } + +    render() { +        return ( +            <div> +                <div className="flyout-container" style={{ left: `${this.props.flyoutInfo.x}px`, top: `${this.props.flyoutInfo.y}px`, display: `${this.props.flyoutInfo.display!}` }} onPointerDown={this.onFlyoutDown}> +                    <FontAwesomeIcon className="flyout" icon="comment-alt" color="grey" /> +                    <div className="text-container"> +                        <p>Time:</p> +                        <p>Duration:</p> +                        <p>Fade-in</p> +                        <p>Fade-out</p> +                    </div> +                    <div className="input-container"> +                        <input ref={this._timeInput} type="text" placeholder={`${Math.round(NumCast(this.props.flyoutInfo.regiondata!.position) / this.props.tickSpacing * 1000)}ms`} onKeyDown={this.changeTime} /> +                        <input ref={this._durationInput} type="text" placeholder={`${Math.round(NumCast(this.props.flyoutInfo.regiondata!.duration) / this.props.tickSpacing * 1000)}ms`} onKeyDown={this.changeDuration} /> +                        <input ref={this._fadeInInput} type="text" placeholder={`${Math.round(NumCast(this.props.flyoutInfo.regiondata!.fadeIn))}ms`} onKeyDown={this.changeFadeIn} /> +                        <input ref={this._fadeOutInput} type="text" placeholder={`${Math.round(NumCast(this.props.flyoutInfo.regiondata!.fadeOut))}ms`} onKeyDown={this.changeFadeOut} /> +                    </div> +                    <button onClick={action((e: React.MouseEvent) => { this.props.flyoutInfo.regions!.splice(this.props.flyoutInfo.regions!.indexOf(this.props.flyoutInfo.regiondata!), 1); this.props.flyoutInfo.display = "none"; })}>delete</button> +                </div> +            </div> +        ); +    } +} + +class TimelineZoom extends React.Component { +    componentDidMount() { + +    } +    render() { +        return ( +            <div> + +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Track.scss b/src/client/views/nodes/Track.scss new file mode 100644 index 000000000..c8d56edf6 --- /dev/null +++ b/src/client/views/nodes/Track.scss @@ -0,0 +1,15 @@ +@import "./../globalCssVariables.scss";  + +.track-container{ + +    .track { +        .inner {   +            top:0px;  +            height: 75px; +            width: calc(100%);  +            background-color: $light-color;      +            border: 1px solid $dark-color;         +            position:relative;  +        } +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Track.tsx b/src/client/views/nodes/Track.tsx new file mode 100644 index 000000000..ee8bee56f --- /dev/null +++ b/src/client/views/nodes/Track.tsx @@ -0,0 +1,268 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, IObservableArray, computed, toJS, IObservableObject, runInAction } from "mobx"; +import "./Track.scss"; +import { Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; +import {listSpec} from "../../../new_fields/Schema"; +import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe"; +import { FlyoutProps } from "./Timeline"; +import { Transform } from "../../util/Transform"; +import { AddComparisonParameters } from "../../northstar/model/idea/idea"; + +interface IProps { +    node: Doc; +    currentBarX: number; +    transform: Transform;  +    changeCurrentBarX: (x:number) => void; +    setFlyout: (props:FlyoutProps) => any;  +} + +@observer +export class Track extends React.Component<IProps> { +    @observable private _inner = React.createRef<HTMLDivElement>();    +    @observable private _reactionDisposers: IReactionDisposer[] = []; +    @observable private _keyReaction:any; //reaction that is used to dispose when necessary  +    @observable private _currentBarXReaction:any;  + +    @computed +    private get regions() { +        return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; +    } + +    componentWillMount() { +        if (!this.props.node.regions){ +            this.props.node.regions = new List<Doc>(); +        } +        this.props.node.opacity = 1;          +    } + +    componentDidMount() { +        runInAction(() => { +            this.createRegion(this.props.currentBarX);  +            this.props.node.hidden = false;   +            this._currentBarXReaction = this.currentBarXReaction();  +        });  +    } + +    componentWillUnmount() { +       runInAction(() => { +           if (this._keyReaction) this._keyReaction();  +           if (this._currentBarXReaction) this._currentBarXReaction();  +       });  +    } + +    @action  +    keyReaction = () => { +        return reaction(() => { +            let keys = Doc.allKeys(this.props.node);  +            return keys.map(key => FieldValue(this.props.node[key]));      +        }, data => { +            console.log("full reaction");  +            let regiondata = this.findRegion(this.props.currentBarX); +            if (regiondata){ +                DocListCast(regiondata.keyframes!).forEach((kf) => { +                    if (kf.type === KeyframeFunc.KeyframeType.default){ +                        kf.key = Doc.MakeCopy(this.props.node, true);  +                        let leftkf: (Doc | undefined) = this.calcMinLeft(regiondata!, kf); // lef keyframe, if it exists +                        let rightkf: (Doc | undefined) = this.calcMinRight(regiondata!, kf); //right keyframe, if it exists +                        if (leftkf!.type === KeyframeFunc.KeyframeType.fade){ //replicating this keyframe to fades +                            let edge = this.calcMinLeft(regiondata!, leftkf!);  +                            edge!.key = Doc.MakeCopy(kf.key as Doc, true);  +                            leftkf!.key = Doc.MakeCopy(kf.key as Doc, true) ;  +                            (Cast(edge!.key, Doc)! as Doc).opacity = 0.1;  +                            (Cast(leftkf!.key, Doc)! as Doc).opacity = 1;  +                        }  +                        if (rightkf!.type === KeyframeFunc.KeyframeType.fade){ +                            let edge = this.calcMinRight(regiondata!, rightkf!);   +                            edge!.key = Doc.MakeCopy(kf.key as Doc, true);  +                            rightkf!.key = Doc.MakeCopy(kf.key as Doc, true);  +                            (Cast(edge!.key, Doc)! as Doc).opacity = 0.1;  +                            (Cast(rightkf!.key, Doc)! as Doc).opacity = 1;  +                        } +                    }  +                });  +            } +        });  +    } + +    @action  +    currentBarXReaction = () => { +        return reaction(() => this.props.currentBarX, () => {             +            console.log("currentbar changed");  +            if (this._keyReaction) this._keyReaction(); //dispose previous reaction first +            let regiondata: (Doc | undefined) = this.findRegion(this.props.currentBarX); +            if (regiondata) {                   +                this.props.node.hidden = false;                  +                this.timeChange(this.props.currentBarX);                         +                DocListCast(regiondata.keyframes).forEach((kf) => { +                    if (kf.time === this.props.currentBarX && kf.type === KeyframeFunc.KeyframeType.default){ +                        console.log("HI!");  +                        this._keyReaction = this.keyReaction(); //reactivates reaction.  +                    } +                });                  +            } else { +                this.props.node.hidden = true; +            } +        }); +    } + + +    @action +    timeChange = (time: number) => { +        let regiondata = this.findRegion(Math.round(time)); //finds a region that the scrubber is on +        if (regiondata) { +            let leftkf: (Doc | undefined) = this.calcMinLeft(regiondata!); // lef keyframe, if it exists +            let rightkf: (Doc | undefined) = this.calcMinRight(regiondata!); //right keyframe, if it exists +            if(leftkf && rightkf) { +                this.interpolate(leftkf, rightkf); +            }             +            let currentkf: (Doc | undefined) = this.calcCurrent(regiondata!); //if the scrubber is on top of the keyframe + +            if (currentkf){ +                console.log(toJS(currentkf));  +                this.applyKeys(Cast(currentkf.key, Doc) as Doc);   +            }  +        } +        +    } + +    @action  +    private applyKeys = (kf: Doc) => { +        this.filterKeys(Doc.allKeys(kf)).forEach(key => { +            if (key === "title" || key === "documentText") Doc.SetOnPrototype(this.props.node, key, StrCast(kf[key])); +            this.props.node[key] = kf[key];  +        });  +    } + + +    @action  +    private filterKeys = (keys:string[]):string[] => { +        return keys.reduce((acc:string[], key:string) => { +            if ( key !== "regions" && key !== "data" && key !== "creationDate" && key !== "cursors" && key !== "hidden") acc.push(key);  +            return acc;  +        }, []) as string[]; +    } + +    @action  +    calcCurrent = (region:Doc):(Doc|undefined) => { +        let currentkf:(Doc|undefined) = undefined;  +        DocListCast(region.keyframes!).forEach((kf) => { +            if (NumCast(kf.time) === Math.round(this.props.currentBarX)) currentkf = kf;  +        });  +        return currentkf;  +    } + + +    @action +    calcMinLeft = (region: Doc, ref?:Doc): (Doc | undefined) => { //returns the time of the closet keyframe to the left +        let leftKf:(Doc| undefined) = undefined; +        let time:number = 0;  +        DocListCast(region.keyframes!).forEach((kf) => { +            let compTime = this.props.currentBarX;  +            if (ref){ +                compTime = NumCast(ref.time);  +            }  +            if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) { +                leftKf = kf; +                time = NumCast(kf.time);  +            } +        }); +        return leftKf; +    } + + +    @action +    calcMinRight = (region: Doc, ref?:Doc): (Doc | undefined) => { //returns the time of the closest keyframe to the right  +        let rightKf: (Doc|undefined) = undefined; +        let time:number = Infinity;  +        DocListCast(region.keyframes!).forEach((kf) => { +            let compTime = this.props.currentBarX;  +            if (ref){ +                compTime = NumCast(ref.time);  +            } +            if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) { +                rightKf = kf; +                time = NumCast(kf.time);  +            } +        }); +        return rightKf; +    } + +    @action +    interpolate = (left: Doc, right: Doc) => { +        console.log("interpolating");  +        let leftNode = left.key as Doc; +        let rightNode = right.key as Doc; + +        console.log(toJS(leftNode));  +        console.log(toJS(rightNode));  + +        const dif_time = NumCast(right.time) - NumCast(left.time); +        const ratio = (this.props.currentBarX - NumCast(left.time)) / dif_time; //linear  + + +        this.filterKeys(Doc.allKeys(leftNode)).forEach(key => { +            console.log(key);  +            if (leftNode[key] && rightNode[key] && typeof(leftNode[key]) === "number" && typeof(rightNode[key]) === "number"){ //if it is number, interpolate +                const diff = NumCast(rightNode[key]) - NumCast(leftNode[key]); +                const adjusted = diff * ratio; +                this.props.node[key] = NumCast(leftNode[key]) + adjusted; +            } else if (key === "title" || key === "documentText") { +                Doc.SetOnPrototype(this.props.node, key, StrCast(leftNode[key])); +                this.props.node[key] = leftNode[key];  +            } +            console.log(this.props.node[key]);  +        }); +        console.log("done");  +    } + +    @action +    findRegion(time: number): (Doc | undefined) { +        let foundRegion = undefined; +        this.regions.map(region => { +            region = region as Doc;  +            if (time >= NumCast(region.position) && time <= (NumCast(region.position) + NumCast(region.duration))) { +                foundRegion = region; +            } +        }); +        return foundRegion; +    } + +    @action +    onInnerDoubleClick = (e: React.MouseEvent) => { +        let inner = this._inner.current!; +        let offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale); +        this.createRegion(offsetX);  +    } + +    createRegion = (position: number) => { +        let regiondata = KeyframeFunc.defaultKeyframe(); +        regiondata.position = position;  +        let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, regiondata, this.regions);  +        let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);  +        if ((rightRegion && leftRegion && rightRegion.position - (leftRegion.position + leftRegion.duration) < NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut)) || (rightRegion && rightRegion.position - regiondata.position < NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))){ +            return;  +        } else if (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut)){ +            regiondata.duration = rightRegion.position - regiondata.position;  +        } +        this.regions.push(regiondata); +        return regiondata;  +    } + + +    render() { +        return ( +            <div className="track-container"> +                <div className="track"> +                    <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick}> +                        {DocListCast(this.regions).map((region) => { +                            return <Keyframe node={this.props.node} RegionData={region} changeCurrentBarX={this.props.changeCurrentBarX} setFlyout={this.props.setFlyout} transform={this.props.transform}/>; +                        })} +                    </div> +                </div> +            </div> +        ); +    } +}
\ No newline at end of file  | 
