diff options
Diffstat (limited to 'src/client')
22 files changed, 2085 insertions, 7 deletions
| diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 8f112de0c..d3286aa22 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -7,6 +7,7 @@      box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;      flex-direction: column;      background: whitesmoke; +    padding-top: 10px;       padding-bottom: 10px;      border-radius: 15px;      border: solid #BBBBBBBB 1px; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 244b217ed..a772f9523 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -38,6 +38,7 @@ import { OverlayView } from './OverlayView';  import PDFMenu from './pdf/PDFMenu';  import { PreviewCursor } from './PreviewCursor';  import { FilterBox } from './search/FilterBox'; +import { TimelineMenu } from './animationtimeline/TimelineMenu';  import PresModeMenu from './presentationview/PresentationModeMenu';  import { PresBox } from './nodes/PresBox';  import { LinkFollowBox } from './linking/LinkFollowBox'; @@ -205,6 +206,9 @@ export class MainView extends React.Component {          if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {              ContextMenu.Instance.closeMenu();          } +        if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !=="timeline-menu-input")){ +            TimelineMenu.Instance.closeMenu();  +        }       });      globalPointerUp = () => this.isPointerDown = false; @@ -581,6 +585,7 @@ export class MainView extends React.Component {                  {this.nodesMenu()}                  {this.miscButtons}                  <PDFMenu /> +                <TimelineMenu/>                  <MainOverlayTextBox firstinstance={true} />                  <OverlayView />              </div > diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss new file mode 100644 index 000000000..b1e8b0b65 --- /dev/null +++ b/src/client/views/animationtimeline/Keyframe.scss @@ -0,0 +1,94 @@ +@import "./../globalCssVariables.scss";  + +.bar { +    height: 100%; +    width: 5px; +    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;  +    } + +    .fadeIn-container, .fadeOut-container, .body-container{ +        position:absolute;  +        height:100%;  +        background-color: rgba(0, 0, 0, 0.5); +        opacity: 0;  +    } +  + +} +     + + diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx new file mode 100644 index 000000000..9728c2462 --- /dev/null +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -0,0 +1,683 @@ +import * as React from "react"; +import "./Keyframe.scss"; +import "./Timeline.scss"; +import "../globalCssVariables.scss"; +import { observer} from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction } from "mobx"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, 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"; +import { InkField, StrokeData } from "../../../new_fields/InkField"; +import { TimelineMenu } from "./TimelineMenu"; +import { Docs } from "../../documents/Documents"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; + +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; +        DocListCast(regions).forEach(region => { +            let neighbor = RegionData(region); +            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 calcMinLeft = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left +        let leftKf: (Doc | undefined) = undefined; +        let time: number = 0; +        let keyframes = await DocListCastAsync(region.keyframes!); +        keyframes!.forEach((kf) => { +            let compTime = currentBarX; +            if (ref) { +                compTime = NumCast(ref.time); +            } +            if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) { +                leftKf = kf; +                time = NumCast(kf.time); +            } +        }); +        return leftKf; +    }; + + +    export const calcMinRight = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right  +        let rightKf: (Doc | undefined) = undefined; +        let time: number = Infinity; +        let keyframes = await DocListCastAsync(region.keyframes!); +        keyframes!.forEach((kf) => { +            let compTime = 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; +    }; + +    export const defaultKeyframe = () => { +        let regiondata = new Doc(); //creating regiondata in MILI +        regiondata.duration = 4000; +        regiondata.position = 0; +        regiondata.fadeIn = 1000; +        regiondata.fadeOut = 1000; +        regiondata.functions = new List<Doc>();  +        return regiondata; +    }; + +    export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing:number, tickIncrement:number) => { +        let time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;  +        switch (unit) { +            case "mili": +                return time;  +            case "sec": +                return dir === "pixel" ? time / 1000 : time * 1000;  +            case "min": +                return dir === "pixel" ? time / 60000 : time * 60000;  +            case "hr": +                return dir === "pixel" ? time / 3600000 : time * 3600000;  +            default:  +                return time;  +        } +    }; +} + +export const RegionDataSchema = createSchema({ +    position: defaultSpec("number", 0), +    duration: defaultSpec("number", 0), +    keyframes: listSpec(Doc), +    fadeIn: defaultSpec("number", 0), +    fadeOut: defaultSpec("number", 0), +    functions: listSpec(Doc)  +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + +interface IProps { +    node: Doc; +    RegionData: Doc; +    collection: Doc; +    tickSpacing: number;  +    tickIncrement: number;  +    time: number;  +    changeCurrentBarX: (x: number) => void; +    transform: Transform; +} + +@observer +export class Keyframe extends React.Component<IProps> { + +    @observable private _bar = React.createRef<HTMLDivElement>(); +    @observable private _gain = 20; //default +    @observable private _mouseToggled = false;  +    @observable private _doubleClickEnabled = false;  + +    @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; +    } + +    @computed +    private get keyframes(){ +        return DocListCast(this.regiondata.keyframes);  +    } + +    @computed +    private get inks() { +        if (this.props.collection.data_ext) { +            let data_ext = Cast(this.props.collection.data_ext, Doc) as Doc; +            let ink = Cast(data_ext.ink, InkField) as InkField; +            if (ink) { +                return ink.inkData; +            } +        } +    } + +    @computed  +    private get pixelPosition(){ +        return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); +    } + +    @computed  +    private get pixelDuration(){ +        return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);  +    } + +    @computed +    private get pixelFadeIn() { +        return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);  +    } + +    @computed +    private get pixelFadeOut(){ +        return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);  +    } + +    async componentWillMount() { +        if (!this.regiondata.keyframes) { +            this.regiondata.keyframes = new List<Doc>(); +        } +        let fadeIn = await this.makeKeyData(this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade)!; +        let fadeOut = await this.makeKeyData(this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade)!; +        let start = await this.makeKeyData(this.regiondata.position, KeyframeFunc.KeyframeType.fade)!; +        let finish = await 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; +    } + +    @action +    makeKeyData = async (kfpos: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time  +        let doclist = (await DocListCastAsync(this.regiondata.keyframes))!; +        let existingkf: (Doc | undefined) = undefined; +        doclist.forEach(TK => { +            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); + +        let interpolationFunctions = new Doc();  +        interpolationFunctions.interpolationX = new List<number>([0, 1]);  +        interpolationFunctions.interpolationY = new List<number>([0,100]);  +        interpolationFunctions.pathX = new List<number>();  +        interpolationFunctions.pathY = new List<number>();  + +        this.regiondata.functions!.push(interpolationFunctions);  +        let found:boolean = false;  +        this.regiondata.keyframes!.forEach(compkf => { +            compkf = compkf as Doc;  +            if (kfpos < NumCast(compkf.time) && !found) { +                runInAction(() => { +                    this.regiondata.keyframes!.splice(doclist.indexOf(compkf as Doc), 0, TK); +                    this.regiondata.keyframes!.pop();  +                    found = true;  +                });  +                return;  +            } +        }); +        return TK; +    } + +     +    @action +    onBarPointerDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let clientX = e.clientX;   +        if (this._doubleClickEnabled){ +            this.createKeyframe(clientX);  +            this._doubleClickEnabled = false;  +        } else { +            setTimeout(() => {if(!this._mouseToggled && this._doubleClickEnabled)this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale);  +                this._mouseToggled = false;             +                this._doubleClickEnabled = false; }, 200);  +            this._doubleClickEnabled = true;  +            document.addEventListener("pointermove", this.onBarPointerMove); +            document.addEventListener("pointerup", (e: PointerEvent) => { +                document.removeEventListener("pointermove", this.onBarPointerMove); +            }); +        } +    } + + +    @action +    onBarPointerMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        if (e.movementX !== 0) { +            this._mouseToggled = true;  +        } +        let left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; +        let right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions!); +        let prevX = this.regiondata.position; +        let futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); +        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; +        }             +        let movement = this.regiondata.position - prevX;  +        this.keyframes.forEach(kf => { +            kf.time = NumCast(kf.time) + movement;  +        }); +    } + +    @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 = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); +        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.pixelPosition + this.pixelFadeIn); +            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; +        } +        this.keyframes[0].time = this.regiondata.position;  +        this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;  +    } + + +    @action +    onDragResizeRight = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let bar = this._bar.current!; +        let offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); +        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; +        } +        this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;  +        this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;  +        +    } + +    @action +    createKeyframe = async (clientX:number) => { +        this._mouseToggled = true;  +        let bar = this._bar.current!; +        let offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement); +        if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends +            let position = this.regiondata.position; +            await this.makeKeyData(Math.round(position + offset)); +            this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied +        } +    } + + +    @action +    moveKeyframe = async (e: React.MouseEvent, kf: Doc) => { +        e.preventDefault(); +        e.stopPropagation(); +        this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); +    } + + +    @action +    onKeyframeOver = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        this.props.node.backgroundColor = "#000000"; +    } + +    @action  +    makeKeyframeMenu = (kf :Doc, e:MouseEvent) => { +        TimelineMenu.Instance.addItem("button", "Show Data", () => { +        runInAction(() => {let kvp = Docs.Create.KVPDocument(Cast(kf.key, Doc) as Doc, { width: 300, height: 300 });  +            CollectionDockingView.AddRightSplit(kvp, (kf.key as Doc).data as Doc); }); +        }),  +        TimelineMenu.Instance.addItem("button", "Delete", () => { +            runInAction(() => { +                (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); +            });          +        }),  +        TimelineMenu.Instance.addItem("input", "Move", (val) => { +            runInAction(() => { +                if (this.checkInput(val)){ +                    let cannotMove:boolean = false;  +                    let kfIndex:number = this.keyframes.indexOf(kf);  +                    if (val < 0 ||( val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) ){ +                        cannotMove = true;  +                    } +                    if (!cannotMove){ +                        this.keyframes[kfIndex].time = parseInt(val, 10);  +                        this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;  +                    } +                } +              });   +            }); +        TimelineMenu.Instance.addMenu("Keyframe");  +        TimelineMenu.Instance.openMenu(e.clientX, e.clientY);  +    } + +    @action  +    makeRegionMenu = (kf: Doc, e: MouseEvent) => { +        TimelineMenu.Instance.addItem("button", "Add Ease", () => { +            // this.onContainerDown(kf, "interpolate"); +        }), +        TimelineMenu.Instance.addItem("button", "Add Path", () => { +            // this.onContainerDown(kf, "path"); +        }),          +        TimelineMenu.Instance.addItem("button", "Remove Region", ()=>{this.regions.splice(this.regions.indexOf(this.regiondata), 1);}), +        TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => { +            runInAction(() => { +                if (this.checkInput(val)){ +                    let cannotMove:boolean = false;  +                    if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position){ +                        cannotMove = true;  +                    } +                    if (!cannotMove){ +                        this.regiondata.fadeIn = parseInt(val, 10);  +                        this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;  +                    } +                } +        });}),  +        TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => { +            runInAction(() => { +                if (this.checkInput(val)){ +                    let cannotMove:boolean = false;  +                    if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)){ +                        cannotMove = true;  +                    }  +                    if (!cannotMove){ +                        this.regiondata.fadeOut = parseInt(val, 10);  +                        this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val;  +                    } +                } +        });}), +        TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => { +            runInAction(() => { +            if (this.checkInput(val)){            +                let prevPosition = this.regiondata.position;   +                let cannotMove:boolean = false;   +                DocListCast(this.regions).forEach(region => { +                    if (NumCast(region.position) !== this.regiondata.position){ +                        if ((val < 0) || (val > NumCast(region.position) && val < NumCast(region.position) + NumCast(region.duration) || (this.regiondata.duration + val > NumCast(region.position) && this.regiondata.duration + val < NumCast(region.position) + NumCast(region.duration)))){ +                            cannotMove = true; +                        } +                    } +                }); +                if (!cannotMove) { +                    this.regiondata.position = parseInt(val, 10);     +                    this.updateKeyframes(this.regiondata.position - prevPosition );  +                } +            } +        });}), +        TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => { +            runInAction(() => { +                if (this.checkInput(val)){            +                    let cannotMove:boolean = false;   +                    DocListCast(this.regions).forEach(region => { +                        if (NumCast(region.position) !== this.regiondata.position){ +                            val += this.regiondata.position;  +                            if ((val < 0 ) || (val > NumCast(region.position) && val < NumCast(region.position) + NumCast(region.duration))){ +                                cannotMove = true; +                            } +                        } +                    }); +                    if (!cannotMove) { +                        this.regiondata.duration = parseInt(val, 10); +                        this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;  +                        this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;  +                    } +                } +        });}), +        TimelineMenu.Instance.addMenu("Region");  +        TimelineMenu.Instance.openMenu(e.clientX, e.clientY);  +    } + +    checkInput = (val: any) => { +        return typeof(val === "number");  +    } + +    @action +    updateKeyframes = (incr:number, filter:number[] = []) => { +        this.keyframes.forEach(kf => { +            if (!filter.includes(this.keyframes.indexOf(kf))){ +                kf.time = NumCast(kf.time) + incr;  +            } +        }); +    } + +    onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { +        e.preventDefault(); +        e.stopPropagation(); +        let div = ref.current!; +        div.style.opacity = "1"; +        Doc.BrushDoc(this.props.node);  +    } + +    onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { +        e.preventDefault(); +        e.stopPropagation(); +        let div = ref.current!; +        div.style.opacity = "0"; +        Doc.UnBrushDoc(this.props.node);  +    } + + +    private _reac: (undefined | IReactionDisposer) = undefined; +    private _plotList: ([string, StrokeData] | undefined) = undefined; +    private _interpolationKeyframe: (Doc | undefined) = undefined;  +    private _type: string = "";  + +    @action +    onContainerDown = (kf: Doc, type: string) => { +        let listenerCreated = false;                  +        this._type = type;  +        this.props.collection.backgroundColor = "rgb(0,0,0)"; +        this._reac = reaction(() => { +            return this.inks; +        }, data => { +            if (!listenerCreated) { +                this._plotList = Array.from(data!)[data!.size - 1]!; +                this._interpolationKeyframe = kf;  +                document.addEventListener("pointerup", this.onReactionListen);  +                listenerCreated = true;  +            } +        }); +    } + +    @action +    onReactionListen = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let message = prompt("GRAPHING MODE: Enter gain");       +        if (message) { +            let messageContent = parseInt(message, 10);  +            if (messageContent === NaN) { +                this._gain = Infinity;  +            } else { +                this._gain = messageContent;  +            } +         +        }    +        if (this._reac && this._plotList && this._interpolationKeyframe) { +            this.props.collection.backgroundColor = "#FFF"; +            this._reac(); +            let xPlots = new List<number>(); +            let yPlots = new List<number>(); +            let maxY = 0; +            let minY = Infinity; +            let pathData = this._plotList![1].pathData; +            for (let i = 0; i < pathData.length - 1;) { +                let val = pathData[i]; +                if (val.y > maxY) { +                    maxY = val.y; +                } +                if (val.y < minY) { +                    minY = val.y; +                } +                xPlots.push(val.x); +                yPlots.push(val.y); +                let increment = Math.floor(pathData.length / this._gain); +                if (pathData.length > this._gain) { +                    if (i + increment < pathData.length) { +                        i = i + increment; +                    } else { +                        i = pathData.length - 1; +                    } +                } else { +                    i++; +                }  +            } +            let index = this.keyframes.indexOf(this._interpolationKeyframe!);  +            if (this._type === "interpolate"){ +                (Cast(this.regiondata.functions![index], Doc) as Doc).interpolationX = xPlots; +                (Cast(this.regiondata.functions![index], Doc) as Doc).interpolationY = yPlots; +            } else if (this._type === "path") { +                (Cast(this.regiondata.functions![index], Doc) as Doc).pathX = xPlots; +                (Cast(this.regiondata.functions![index], Doc) as Doc).pathY = yPlots; +            } +            this._reac = undefined;  +            this._interpolationKeyframe = undefined;  +            this._plotList = undefined;  +            this._type = "";  +            document.removeEventListener("pointerup", this.onReactionListen);  +        } +    } +    render() { +        return ( +            <div> +                <div className="bar" ref={this._bar} style={{ transform: `translate(${this.pixelPosition}px)`,  +                width: `${this.pixelDuration}px`,  +                background: `linear-gradient(90deg, rgba(77, 153, 0, 0) 0%, rgba(77, 153, 0, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(77, 153, 0, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(77, 153, 0, 0) 100% )` }} +                    onPointerDown={this.onBarPointerDown}> +                    <div className="leftResize" onPointerDown={this.onResizeLeft} ></div> +                    <div className="rightResize" onPointerDown={this.onResizeRight}></div> +                    {this.keyframes.map(kf => { +                        if (kf.type as KeyframeFunc.KeyframeType === KeyframeFunc.KeyframeType.default) { +                            return ( +                                <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> +                                    <div className="divider"></div> +                                    <div className="keyframeCircle" onPointerDown={(e) => { this.moveKeyframe(e, kf); }} onContextMenu={(e: React.MouseEvent) => { +                                        e.preventDefault(); +                                        e.stopPropagation(); +                                        this.makeKeyframeMenu(kf, e.nativeEvent);  +                                    }}></div> +                                </div> +                            ); +                        } +                        else { +                            return ( +                                <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> +                                    <div className="divider"></div> +                                </div> +                            ); +                        } +                       +                    })} +                    {this.keyframes.map( kf => { +                       if(this.keyframes.indexOf(kf ) !== this.keyframes.length - 1) { +                            let left = this.keyframes[this.keyframes.indexOf(kf) + 1];  +                            let bodyRef = React.createRef<HTMLDivElement>();  +                            let kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);  +                            let leftPos = KeyframeFunc.convertPixelTime(NumCast(left!.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);  +                            return ( +                                <div ref={bodyRef}className="body-container" style={{left: `${kfPos - this.pixelPosition}px`, width:`${leftPos - kfPos}px`}} +                                onPointerOver={(e) => { this.onContainerOver(e, bodyRef); }} +                                onPointerOut={(e) => { this.onContainerOut(e, bodyRef); }} +                                onContextMenu={(e) => { +                                    e.preventDefault();  +                                    e.stopPropagation();  +                                    this._mouseToggled = true;  +                                    this.makeRegionMenu(kf, e.nativeEvent);  +                                }}> +                                </div> +                            );  +                       }   +                    })} + +                </div> +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss new file mode 100644 index 000000000..1457d5a84 --- /dev/null +++ b/src/client/views/animationtimeline/Timeline.scss @@ -0,0 +1,122 @@ +@import "./../globalCssVariables.scss";  + +.minimize{ +    position:relative;  +    z-index: 1000;  +    height: 30px;  +    width: 100px;  +} + +.timeline-toolbox{ +    position:absolute;  +    display:flex; +    align-items: flex-start;  +    flex-direction: row;   +    top: 10px; +    div{ +        margin-left:10px;  +    } +} +.timeline-container{ +    width:100%; +    height:300px;  +    position:absolute; +    background-color: $light-color-secondary; +    box-shadow: 0px 10px 20px;  + +    .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/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx new file mode 100644 index 000000000..875a0b8f3 --- /dev/null +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -0,0 +1,437 @@ +import * as React from "react"; +import "./Timeline.scss"; +import { listSpec } from "../../../new_fields/Schema"; +import { observer } from "mobx-react"; +import { Track } from "./Track"; +import { observable, reaction, action, IReactionDisposer, computed, runInAction, observe, toJS } from "mobx"; +import { Cast, NumCast, StrCast, BoolCast } 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, faPauseCircle, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { ContextMenu } from "../ContextMenu"; +import { TimelineOverview } from "./TimelineOverview"; +import { FieldViewProps } from "../nodes/FieldView"; +import { KeyframeFunc } from "./Keyframe"; + + + +export interface FlyoutProps { +    x?: number; +    y?: number; +    display?: string; +    regiondata?: Doc; +    regions?: List<Doc>; +} + + +@observer +export class Timeline extends React.Component<FieldViewProps> { + +    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 = true; //timeline freeze +    @observable private _totalLength: number = 0; +    @observable private _visibleLength: number = 0;  +    @observable private _visibleStart: number = 0;  +    @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT; +    @observable private _time = 100000; //DEFAULT +    @observable private _ticks: number[] = []; +    @observable private _playButton = faPlayCircle;  +    @observable private _timelineVisible = false;  +    @observable private _mouseToggled = false;  +    @observable private _doubleClickEnabled = false;  +    @observable private _reactionDisposer:IReactionDisposer[] = [];  + + +    @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() { +        this.props.Document.isAnimating ? this.props.Document.isAnimating = true : this.props.Document.isAnimating = false;  +    } + +    componentDidMount() { +        if (StrCast(this.props.Document.type) === "video") { +            console.log("video"); +            console.log(this.props.Document.duration); +            if (this.props.Document.duration) { +                this._time = Math.round(NumCast(this.props.Document.duration)) * 1000; +                this._reactionDisposer.push(reaction(() => { +                    return NumCast(this.props.Document.curPage); +                }, curPage => { +                    if (!this._isPlaying) { +                        this.changeCurrentBarX(curPage * this._tickIncrement / this._tickSpacing);  +                        this.props.Document.curPage = this._currentBarX;  +                        this.play();  +                    } +                })); +            } +        } +        runInAction(() => { +            this._reactionDisposer.push(reaction(() => { +                return this._time;   +            }, () => { +                this._ticks = []; +                for (let i = 0; i < this._time;) { +                    this._ticks.push(i); +                    i += 1000; +                } +                this._totalLength = this._tickSpacing * (this._time/ this._tickIncrement);  +            }, {fireImmediately:true}));  +            this._totalLength = this._tickSpacing * (this._ticks.length/ this._tickIncrement);  +            this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width;  +            this._visibleStart = this._infoContainer.current!.scrollLeft;    +        }); +        + +    } + +    @action +    changeCurrentBarX = (pixel: number) => { +        pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel; +    } + +    //for playing +    @action +    onPlay = (e: React.MouseEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        this.play();  +    } + +    @action +    play = () => { +        if (this._isPlaying) { +            this._isPlaying = false; +            this._playButton = faPlayCircle;  +        } else { +            this._isPlaying = true; +            this._playButton = faPauseCircle;  +            const playTimeline = () => { +                if (this._isPlaying){ +                    if (this._currentBarX >= this._totalLength) { +                        this.changeCurrentBarX(0);  +                    } else { +                        this.changeCurrentBarX(this._currentBarX + this._windSpeed); +                    }                         +                    setTimeout(playTimeline, 15);   +                } +            };  +            playTimeline();  +        } +    } + + + +    @action +    windForward = (e: React.MouseEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        if (this._windSpeed < 64) { //max speed is 32 +            this._windSpeed = this._windSpeed * 2; +        } +    } + +    @action +    windBackward = (e: React.MouseEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        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.changeCurrentBarX(offsetX);  +    } + +    @action +    onScrubberClick = (e: React.MouseEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let scrubberbox = this._scrubberbox.current!; +        let offsetX = (e.clientX - scrubberbox.getBoundingClientRect().left) * this.props.ScreenToLocalTransform().Scale; +        this.changeCurrentBarX(offsetX);  +    } + + + +    @action +    onPanDown = (e: React.PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        let clientX = e.clientX;  +        if (this._doubleClickEnabled){ +            this._doubleClickEnabled = false;  +        } else { +            setTimeout(() => {if(!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX -  this._trackbox.current!.getBoundingClientRect().left);  +                this._mouseToggled = false; +                this._doubleClickEnabled = false;}, 200);  +            this._doubleClickEnabled = true;  +            document.addEventListener("pointermove", this.onPanMove); +            document.addEventListener("pointerup", () => { +                document.removeEventListener("pointermove", this.onPanMove); +                if (!this._doubleClickEnabled) { +                    this._mouseToggled = false;  +                } +            }); + +        } +    } + +    @action +    onPanMove = (e: PointerEvent) => { +        e.preventDefault(); +        e.stopPropagation(); +        if (e.movementX !== 0 || e.movementY !== 0) { +            this._mouseToggled = true;  +        } +        let trackbox = this._trackbox.current!; +        let titleContainer = this._titleContainer.current!; +        this.movePanX(this._visibleStart - e.movementX); +        trackbox.scrollTop = trackbox.scrollTop - e.movementY; +        titleContainer.scrollTop = titleContainer.scrollTop - e.movementY; +    } +    @action  +    movePanX = (pixel:number) => { +        let infoContainer = this._infoContainer.current!; +        infoContainer.scrollLeft = pixel;  +        this._visibleStart = infoContainer.scrollLeft; +    } + + +    @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.visibility = "visible"; +        } else { +            this._isMinimized = true; +            timelineContainer.style.visibility = "hidden"; +        } +    } + +    @action +    toReadTime = (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}`; +    } + +    timelineContextMenu = (e:MouseEvent): void => { +        let subitems: ContextMenuProps[] = []; +        let timelineContainer = this._timelineWrapper.current!; +        subitems.push({ +            description: "Pin to Top", event: action(() => { +                if (!this._isFrozen) { +                    timelineContainer.style.left = "0px"; +                    timelineContainer.style.top = "0px"; +                    timelineContainer.style.transition = "none"; +                } +            }), icon: faArrowUp +        }); +        subitems.push({ +            description: this._isFrozen ? "Unfreeze Timeline" : "Freeze Timeline", event: action(() => { +                this._isFrozen = !this._isFrozen;  +            }), icon: "thumbtack" +        }); +        subitems.push({ +            description: this._timelineVisible ? "Hide Timeline" : "Show Timeline", event: action(() => { +                this._timelineVisible = !this._timelineVisible;  +            }), icon: this._timelineVisible ? faEyeSlash : "eye" +        });  +        subitems.push({ description: BoolCast(this.props.Document.isAnimating) ? "Enter Play Mode" : "Enter Authoring Mode", event: () => { +            BoolCast(this.props.Document.isAnimating) ? this.props.Document.isAnimating = false : this.props.Document.isAnimating = true;} +            , icon:BoolCast(this.props.Document.isAnimating) ? "play" : "edit"});  +        ContextMenu.Instance.addItem({ description: "Timeline Funcs...", subitems: subitems, icon: faClock }); +    } + +    @action +    onWheelZoom = (e: React.WheelEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        let offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left;  +        let prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement); +        let prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX,"mili", "time", this._tickSpacing, this._tickIncrement);   +        e.deltaY < 0 ? this.zoom(true) : this.zoom(false);  +        let currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);  +        let currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);  +        this._infoContainer.current!.scrollLeft = currPixel - offset;  +        this._visibleStart = currPixel - offset;   +        this.changeCurrentBarX(currCurrent);   +    } + +    @action +    zoom = (dir: boolean) => { +        let spacingChange = this._tickSpacing;  +        let incrementChange = this._tickIncrement;  +        if (dir){ +            if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)){ +                if (this._tickSpacing >= 100) { +                    incrementChange /= 2;  +                    spacingChange = 50;  +                } else { +                    spacingChange += 5;  +                } +            }  +        } else { +            if (this._tickSpacing <= 50) { +                spacingChange = 100;  +                incrementChange *= 2;  +            } else { +                spacingChange -= 5;  +            } +        } +        let finalLength = spacingChange * (this._time / incrementChange);   +        if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width){ +            this._totalLength = finalLength;  +            this._tickSpacing = spacingChange;  +            this._tickIncrement = incrementChange;  +        } +    } + +    private timelineToolBox = (scale:number) => { +        let size = 50 * scale; //50 is default +        return ( +        <div key="timeline_toolbox" className="timeline-toolbox" style={{height:`${size}px`}}> +                <div key="timeline_windBack" onClick={this.windBackward}> <FontAwesomeIcon icon={faBackward} style={{height:`${size}px`, width: `${size}px`}} /> </div> +                <div key =" timeline_play" onClick={this.onPlay}> <FontAwesomeIcon icon={this._playButton} style={{height:`${size}px`, width: `${size}px`}}  /> </div> +                <div key="timeline_windForward" onClick={this.windForward}> <FontAwesomeIcon icon={faForward} style={{height:`${size}px`, width: `${size}px`}}  /> </div> +                <TimelineOverview scale={scale} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX}/>  +        </div> +        ); +    } +    render() { +        return ( +            <div style={{visibility: this._timelineVisible ? "visible" : "hidden"}}> +                <div key="timeline_wrapper" style={{visibility: BoolCast(this.props.Document.isAnimating && this._timelineVisible) ? "visible" :"hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)"}} ref={this._timelineWrapper}> +                    <button key="timeline_minimize" className="minimize" onClick={this.minimize}>Minimize</button> +                    <div key="timeline_container" className="timeline-container" style={{ height: `${this._containerHeight}px`, left: "0px", top: "30px" }} ref={this._timelineContainer} onPointerDown={this.onTimelineDown}> +                        {this.timelineToolBox(0.5)} +                        <div key ="timeline_info"className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}> +                            <div key="timeline_scrubberbox" className="scrubberbox" ref={this._scrubberbox} style={{width: `${this._totalLength}px`}} onClick={this.onScrubberClick}> +                                {this._ticks.map(element => { +                                    if(element % this._tickIncrement === 0) return <div className="tick" style={{ transform: `translate(${(element / this._tickIncrement)* this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toReadTime(element)}</p></div>; +                                })} +                            </div> +                            <div key="timeline_scrubber" className="scrubber" ref={this._scrubber} onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}> +                                <div key="timeline_scrubberhead" className="scrubberhead"></div> +                            </div> +                            <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{width: `${this._totalLength}px`}}> +                                {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing = {this._tickSpacing} tickIncrement ={this._tickIncrement} collection = {this.props.Document} timelineVisible = {this._timelineVisible}/>)} +                            </div> +                        </div> +                        <div key="timeline_title"className="title-container" ref={this._titleContainer}> +                            {DocListCast(this.children).map(doc => <div className="datapane" onPointerOver={() => {Doc.BrushDoc(doc);}} onPointerOut={() => {Doc.UnBrushDoc(doc);}}><p>{doc.title}</p></div>)} +                        </div> +                        <div key="timeline_resize" onPointerDown={this.onResizeDown}> +                            <FontAwesomeIcon className="resize" icon={faGripLines} /> +                        </div> +                    </div> +                </div> +                {BoolCast(this.props.Document.isAnimating) ? <div></div>: this.timelineToolBox(1) } +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss new file mode 100644 index 000000000..7ee0a43d5 --- /dev/null +++ b/src/client/views/animationtimeline/TimelineMenu.scss @@ -0,0 +1,94 @@ +@import "./../globalCssVariables.scss";  + + +.timeline-menu-container{ +    position: absolute; +    display: flex; +    box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; +    flex-direction: column; +    background: whitesmoke; +    z-index: 10000; +    width: 200px;  +    padding-bottom: 10px; +    border-radius: 15px; + +    border: solid #BBBBBBBB 1px; +  + + +    .timeline-menu-input{ +        font: $sans-serif;  +        font-size: 13px;  +        width:100%; +        text-transform: uppercase;  +        letter-spacing: 2px;  +        margin-left: 10px;  +        background-color: transparent;  +        border-width: 0px;  +        transition: border-width 500ms;  +    } + +    .timeline-menu-input:hover{ +        border-width: 2px;  +    } +     +     +     + +    .timeline-menu-header{ +        border-top-left-radius: 15px;  +        border-top-right-radius: 15px;  +        text-transform: uppercase;  +        background: $dark-color;  +        letter-spacing: 2px;  + +        .timeline-menu-header-desc{ +            font:$sans-serif;  +            font-size: 13px;  +            text-align: center;  +            color: whitesmoke;  +        } +    } + + +    .timeline-menu-item { +        // width: 11vw; //10vw +        height: 30px; //2vh +        background: whitesmoke; +        display: flex; //comment out to allow search icon to be inline with search text +        justify-content: left; +        align-items: center; +        -webkit-touch-callout: none; +        -webkit-user-select: none; +        -khtml-user-select: none; +        -moz-user-select: none; +        -ms-user-select: none; +        user-select: none; +        transition: all .1s; +        border-style: none; +        // padding: 10px 0px 10px 0px; +        white-space: nowrap; +        font-size: 13px; +        color: grey; +        letter-spacing: 2px; +        text-transform: uppercase; +        padding-right: 20px; +        padding-left: 10px;  +    } + +    .timeline-menu-item:hover { +        border-width: .11px; +        border-style: none; +        border-color: $intermediate-color; +        border-bottom-style: solid; +        border-top-style: solid; +        background: $darker-alt-accent;  +    } + +    .timeline-menu-desc { +        padding-left: 10px;  +        font:$sans-serif;  +        font-size: 13px;  +    } +     +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx new file mode 100644 index 000000000..f3b985297 --- /dev/null +++ b/src/client/views/animationtimeline/TimelineMenu.tsx @@ -0,0 +1,77 @@ +import * as React from "react"; +import {observable, action, runInAction} from "mobx";  +import {observer} from "mobx-react";  +import "./TimelineMenu.scss";  +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons"; + + +@observer +export class TimelineMenu extends React.Component { +    public static Instance:TimelineMenu;  + +    @observable private _opacity = 0; +    @observable private _x = 0;  +    @observable private _y = 0;  +    @observable private _currentMenu:JSX.Element[] = [];  + +    constructor (props:Readonly<{}>){ +        super(props);  +        TimelineMenu.Instance = this;  +    } +     +    @action +    openMenu = (x?:number, y?:number) => { +        this._opacity = 1;  +        x ? this._x = x : this._x = 0;  +        y ? this._y = y : this._y = 0;  +    } + +    @action +    closeMenu = () => { +        this._opacity = 0;  +        this._currentMenu = [];  +        this._x = -1000000;  +        this._y = -1000000;  +    } + +    @action +    addItem = (type: "input" | "button", title: string, event: (e:any, ...args:any[]) => void) => { +        if (type === "input"){ +            let inputRef = React.createRef<HTMLInputElement>();  +            let text = "";  +            this._currentMenu.push(<div className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => { +                e.stopPropagation(); +                text = e.target.value; +            }} onKeyDown={(e) => { +                if (e.keyCode === 13) { +                    event(text);  +                    this.closeMenu();                     +                     e.stopPropagation();  +                }  +            }}/></div>);  +        } else if (type === "button") { +            let buttonRef = React.createRef<HTMLDivElement>();  +            this._currentMenu.push( <div className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => { +                e.preventDefault();  +                e.stopPropagation();  +                event(e);  +                this.closeMenu();  +            }}>{title}</p></div>);  +        } +    } + +    @action  +    addMenu = (title:string) => { +        this._currentMenu.unshift(<div className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);      +    } + +    render() { +        return ( +            <div className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} > +                {this._currentMenu} +            </div> +        ); +    } + +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss new file mode 100644 index 000000000..9e69c2adf --- /dev/null +++ b/src/client/views/animationtimeline/TimelineOverview.scss @@ -0,0 +1,38 @@ +@import "./../globalCssVariables.scss";  + +.timeline-overview-container{ +    width: 300px;  +    height: 40px; +    margin-top: 10px;  +    margin-left: 20px;   +    background: white;  +    border: 2px solid black;  +    padding: 0px;  +    display:inline-block;  +    .timeline-overview-visible{ +        height: 100%;  +        background: green;  +        margin: 0px;  +    } +    .timeline-overview-scrubber-container{ +        height: 100%; +        margin-top: -40px;   +        margin-left: 0px;  +        width: 2px;  +        z-index: 1001; +        background-color:black;  +        display: inline-block;  +        .timeline-overview-scrubber-head{ +            position:absolute;  +            height: 30px;  +            width: 30px; +            background-color:transparent;  +            border-radius: 50%;  +            border: 5px solid black;  +            margin-left: -15px;  +            top: -15px;  + +        } +         +    } +}
\ No newline at end of file diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx new file mode 100644 index 000000000..38b823cbc --- /dev/null +++ b/src/client/views/animationtimeline/TimelineOverview.tsx @@ -0,0 +1,93 @@ +import * as React from "react";  +import {observable, action} from "mobx";  +import {observer} from "mobx-react"; +import "./TimelineOverview.scss";  + + + +interface TimelineOverviewProps{ +    scale: number;  +    totalLength: number;  +    visibleLength:number;  +    visibleStart:number; +    currentBarX:number;  +    changeCurrentBarX: (pixel:number) => void;  +    movePanX: (pixel:number) => any; +} + + +@observer +export class TimelineOverview extends React.Component<TimelineOverviewProps>{ +    @observable private _visibleRef = React.createRef<HTMLDivElement>();  +    @observable private _scrubberRef = React.createRef<HTMLDivElement>();  +    private readonly DEFAULT_HEIGHT = 50;  +    private readonly DEFAULT_WIDTH = 300;  + +    @action +    onPointerDown = (e:React.PointerEvent) => { +        e.stopPropagation();  +        e.preventDefault();  +        document.removeEventListener("pointermove", this.onPanX);  +        document.removeEventListener("pointerup", this.onPointerUp);  +        document.addEventListener("pointermove", this.onPanX);  +        document.addEventListener("pointerup", this.onPointerUp);  +    } + +    @action +    onPanX = (e: PointerEvent) => { +        e.stopPropagation();  +        e.preventDefault();  +        let movX = (this.props.visibleStart / this.props.totalLength)* (this.DEFAULT_WIDTH * this.props.scale) + e.movementX;  +        this.props.movePanX((movX / (this.DEFAULT_WIDTH * this.props.scale)) * this.props.totalLength);  +    } + +    @action +    onPointerUp = (e: PointerEvent) => { +        e.stopPropagation();  +        e.preventDefault();  +        document.removeEventListener("pointermove", this.onPanX);  +        document.removeEventListener("pointerup", this.onPointerUp);  +    } + +    @action +    onScrubberDown = ( e:React.PointerEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        document.removeEventListener("pointermove", this.onScrubberMove);  +        document.removeEventListener("pointerup", this.onScrubberUp);  +        document.addEventListener("pointermove", this.onScrubberMove);  +        document.addEventListener("pointerup", this.onScrubberUp); +    } + +    @action +    onScrubberMove = (e: PointerEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        let scrubberRef = this._scrubberRef.current!;  +        let left = scrubberRef.getBoundingClientRect().left;  +        let offsetX = Math.round(e.clientX - left);  +        this.props.changeCurrentBarX(((offsetX / (this.DEFAULT_WIDTH * this.props.scale)) * this.props.totalLength) + this.props.currentBarX);  +    } + +    @action +    onScrubberUp = (e:PointerEvent) => { +        e.preventDefault();  +        e.stopPropagation();  +        document.removeEventListener("pointermove", this.onScrubberMove);  +        document.removeEventListener("pointerup", this.onScrubberUp); +    } + +    render(){ +        return( +            <div key="timeline-overview-container" className="timeline-overview-container" style={{height: `${this.DEFAULT_HEIGHT * this.props.scale * 0.8}px` ,width:`${this.DEFAULT_WIDTH * this.props.scale}`}}> +                <div ref={this._visibleRef} key="timeline-overview-visible" className="timeline-overview-visible" style={{marginLeft:`${(this.props.visibleStart / this.props.totalLength)* this.DEFAULT_WIDTH * this.props.scale}px`, width:`${(this.props.visibleLength / this.props.totalLength) * this.DEFAULT_WIDTH * this.props.scale}px`}} onPointerDown={this.onPointerDown}></div> +                <div ref={this._scrubberRef} key="timeline-overview-scrubber-container" className="timeline-overview-scrubber-container" style={{marginLeft:`${(this.props.currentBarX / this.props.totalLength) * this.DEFAULT_WIDTH * this.props.scale}px`, marginTop: `${-this.DEFAULT_HEIGHT * this.props.scale * 0.8}px`}} onPointerDown={this.onScrubberDown}> +                    <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head" style={{}}></div> +                </div> +            </div> +        );  +    } + +} + + diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss new file mode 100644 index 000000000..c8d56edf6 --- /dev/null +++ b/src/client/views/animationtimeline/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/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx new file mode 100644 index 000000000..274b215d9 --- /dev/null +++ b/src/client/views/animationtimeline/Track.tsx @@ -0,0 +1,306 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, computed, runInAction, autorun } from "mobx"; +import "./Track.scss"; +import { Doc, DocListCastAsync, DocListCast, Field } 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 { Transform } from "../../util/Transform"; +import { Copy } from "../../../new_fields/FieldSymbols"; +import { ObjectField } from "../../../new_fields/ObjectField"; + +interface IProps { +    node: Doc; +    currentBarX: number; +    transform: Transform; +    collection: Doc; +    time: number; +    tickIncrement: number; +    tickSpacing: number; +    timelineVisible: boolean;  +    changeCurrentBarX: (x: number) => void; +} + +@observer +export class Track extends React.Component<IProps> { +    @observable private _inner = React.createRef<HTMLDivElement>(); +    @observable private _currentBarXReaction: any; +    @observable private _timelineVisibleReaction: any; +    @observable private _isOnKeyframe: boolean = false; +    @observable private _onKeyframe: (Doc | undefined) = undefined; +    @observable private _onRegionData: (Doc | undefined) = undefined; +    @observable private _storedState: (Doc | undefined) = undefined; +     +    @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>();             +        } +       +         +    } + +    componentDidMount() { +        runInAction(async () => { +            this._timelineVisibleReaction = this.timelineVisibleReaction();  +            this._currentBarXReaction = this.currentBarXReaction(); +            if (this.regions.length === 0) this.createRegion(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); +            this.props.node.hidden = false;                    +            this.props.node.opacity = 1;  +        }); + +    } + +    componentWillUnmount() { +        runInAction(() => { +            if (this._currentBarXReaction) this._currentBarXReaction(); +            if (this._timelineVisibleReaction) this._timelineVisibleReaction();  +        }); +    } + +    @action +    saveKeyframe = async (ref: Doc, regiondata: Doc) => { +        let keyframes: List<Doc> = (Cast(regiondata.keyframes, listSpec(Doc)) as List<Doc>); +        let kfIndex: number = keyframes.indexOf(ref); +        let kf = keyframes[kfIndex] as Doc; +        if (!kf) return;  +        if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades +            kf.key = Doc.MakeCopy(this.props.node, true); +            let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), kf); // lef keyframe, if it exists +            let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), kf); //right keyframe, if it exists  +            if (leftkf!.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades +                let edge: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), 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: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), 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; +            } +        } +        keyframes[kfIndex] = kf; +        this._onKeyframe = undefined; +        this._onRegionData = undefined; +        this._isOnKeyframe = false; +    } + +    @action  +    revertState = () => { +        let copyDoc = Doc.MakeCopy(this.props.node, true);  +        if (this._storedState) this.applyKeys(this._storedState); +        let newState = new Doc();  +        newState.key = copyDoc;  +        this._storedState = newState;  +    } + +    @action +    currentBarXReaction = () => { +        return reaction(() => this.props.currentBarX, async () => { +            let regiondata: (Doc | undefined) = await this.findRegion(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); +            if (regiondata) { +                this.props.node.hidden = false; +                await this.timeChange(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); +            } else { +                this.props.node.hidden = true; +                this.props.node.opacity = 0; +            } +        }); +    } +    @action  +    timelineVisibleReaction = () => { +        return reaction(() => { +            return this.props.timelineVisible;  +        }, isVisible => { +            this.revertState();  +        });  +    } + +    @action +    timeChange = async (time: number) => { +        if (this._isOnKeyframe && this._onKeyframe && this._onRegionData) { +            await this.saveKeyframe(this._onKeyframe, this._onRegionData); +        } +        let regiondata = await this.findRegion(Math.round(time)); //finds a region that the scrubber is on +        if (regiondata) { +            let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); // lef keyframe, if it exists +            let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); //right keyframe, if it exists             +            let currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe +            if (currentkf) { +                await this.applyKeys(currentkf); +                this._isOnKeyframe = true; +                this._onKeyframe = currentkf; +                this._onRegionData = regiondata; +            } else if (leftkf && rightkf) { +                await this.interpolate(leftkf, rightkf, regiondata); +            } +        } +    } + +    @action +    private applyKeys = async (kf: Doc) => { +        let kfNode = await Cast(kf.key, Doc) as Doc; +        let docFromApply = kfNode;         +        if (this.filterKeys(Doc.allKeys(this.props.node)).length > this.filterKeys(Doc.allKeys(kfNode)).length) docFromApply = this.props.node; +        this.filterKeys(Doc.allKeys(docFromApply)).forEach(key => { +            if (!kfNode[key]) { +                this.props.node[key] = undefined; +            } else { +                let stored = kfNode[key]; +                if(stored instanceof ObjectField){                     +                    this.props.node[key] = stored[Copy]();  +                } else { +                    this.props.node[key] = stored;  +                } +            } +        }); +    } + +    private filterList = [ +        "regions",  +        "cursors",  +        "hidden",  +        "nativeHeight",  +        "nativeWidth",  +        "schemaColumns",  +        "baseLayout",  +        "backgroundLayout",  +        "layout",  +    ];  + +    @action +    private filterKeys = (keys: string[]): string[] => { +        return keys.reduce((acc: string[], key: string) => { +            if (!this.filterList.includes(key)) acc.push(key);  +            return acc; +        }, []); +    } + +    @action +    calcCurrent = async (region: Doc) => { +        let currentkf: (Doc | undefined) = undefined; +        let keyframes = await DocListCastAsync(region.keyframes!); +        keyframes!.forEach((kf) => { +            if (NumCast(kf.time) === Math.round(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement))) currentkf = kf; +        }); +        return currentkf; +    } + + +    @action +    interpolate = async (left: Doc, right: Doc, regiondata: Doc) => { +        let leftNode = left.key as Doc; +        let rightNode = right.key as Doc; +        const dif_time = NumCast(right.time) - NumCast(left.time); +        const timeratio = (KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement) - NumCast(left.time)) / dif_time; //linear  +        let keyframes = (await DocListCastAsync(regiondata.keyframes!))!; +        let indexLeft = keyframes.indexOf(left); +        let interY: List<number> = (await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).interpolationY as List<number>)!; +        let realIndex = (interY.length - 1) * timeratio; +        let xIndex = Math.floor(realIndex); +        let yValue = interY[xIndex]; +        let secondYOffset: number = yValue; +        let minY = interY[0];  // for now +        let maxY = interY[interY.length - 1]; //for now  +        if (interY.length !== 1) { +            secondYOffset = interY[xIndex] + ((realIndex - xIndex) / 1) * (interY[xIndex + 1] - interY[xIndex]) - minY; +        } +        let finalRatio = secondYOffset / (maxY - minY); +        let pathX: List<number> = await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).pathX as List<number>; +        let pathY: List<number> = await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).pathY as List<number>; +        let proposedX = 0; +        let proposedY = 0; +        if (pathX.length !== 0) { +            let realPathCorrespondingIndex = finalRatio * (pathX.length - 1); +            let pathCorrespondingIndex = Math.floor(realPathCorrespondingIndex); +            if (pathCorrespondingIndex >= pathX.length - 1) { +                proposedX = pathX[pathX.length - 1]; +                proposedY = pathY[pathY.length - 1]; +            } else if (pathCorrespondingIndex < 0) { +                proposedX = pathX[0]; +                proposedY = pathY[0]; +            } else { +                proposedX = pathX[pathCorrespondingIndex] + ((realPathCorrespondingIndex - pathCorrespondingIndex) / 1) * (pathX[pathCorrespondingIndex + 1] - pathX[pathCorrespondingIndex]); +                proposedY = pathY[pathCorrespondingIndex] + ((realPathCorrespondingIndex - pathCorrespondingIndex) / 1) * (pathY[pathCorrespondingIndex + 1] - pathY[pathCorrespondingIndex]); +            } + +        } +        this.filterKeys(Doc.allKeys(leftNode)).forEach(key => { +            if (leftNode[key] && rightNode[key] && typeof (leftNode[key]) === "number" && typeof (rightNode[key]) === "number") { //if it is number, interpolate +                if ((key === "x" || key === "y") && pathX.length !== 0) { +                    if (key === "x") this.props.node[key] = proposedX; +                    if (key === "y") this.props.node[key] = proposedY; +                } else { +                    const diff = NumCast(rightNode[key]) - NumCast(leftNode[key]); +                    const adjusted = diff * finalRatio; +                    this.props.node[key] = NumCast(leftNode[key]) + adjusted; +                } +            } else { +                let stored = leftNode[key]; +                if(stored instanceof ObjectField){                     +                    this.props.node[key] = stored[Copy]();  +                } else { +                    this.props.node[key] = stored;  +                } +            } +        }); +    } + +    @action +    findRegion = async (time: number) => { +        let foundRegion: (Doc | undefined) = undefined; +        let regions = await DocListCastAsync(this.regions); +        regions!.forEach(region => { +            region = region as RegionData; +            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(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); +    } + +    createRegion = (position: number) => { +        let regiondata = KeyframeFunc.defaultKeyframe(); +        regiondata.position = position; +        let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); + +        if (rightRegion && rightRegion.position - regiondata.position <= 4000) { +            regiondata.duration = rightRegion.position - regiondata.position; +        } +        if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) { +            this.regions.push(regiondata); +            return regiondata; +        } + +    } +    render() { +        return ( +            <div className="track-container"> +                <div className="track"> +                    <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick} onPointerOver = {() => {Doc.BrushDoc(this.props.node);}}onPointerOut={() => {Doc.UnBrushDoc(this.props.node);}}> +                        {DocListCast(this.regions).map((region) => { +                            return <Keyframe {...this.props} RegionData={region} />; +                        })} +                    </div> +                </div> +            </div> +        ); +    } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 954a27cbd..069269b06 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -15,13 +15,13 @@ import { DocumentType } from "../../documents/DocumentTypes";  import { Docs, DocumentOptions } 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, GoogleRef } from "../nodes/FormattedTextBox";  import { CollectionPDFView } from "./CollectionPDFView";  import { CollectionVideoView } from "./CollectionVideoView";  import { CollectionView } from "./CollectionView";  import React = require("react"); +import { DocComponent } from "../DocComponent";  export interface CollectionViewProps extends FieldViewProps {      addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -38,7 +38,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {      ruleProvider: Doc | undefined;  } -export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { +export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {       class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {          private dropDisposer?: DragManager.DragDropDisposer;          private _childLayoutDisposer?: IReactionDisposer; @@ -129,7 +129,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {                  }                  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.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;                      // note that it's possible the drag function might create a drop document that's not the same as the @@ -137,7 +140,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {                      added = movedDocs.reduce((added: boolean, d, i) =>                          de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || 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/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss index 64411b5fe..7217b6f30 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionViewChromes.scss @@ -34,6 +34,18 @@                  outline-color: black;              } +            .collectionViewBaseChrome-button{ +                font-size: 75%; +                text-transform: uppercase; +                letter-spacing: 2px; +                background: rgb(238, 238, 238); +                color: purple; +                outline-color: black; +                border: none; +                padding: 12px 10px 11px 10px; +                margin-left: 10px; +            } +              .collectionViewBaseChrome-collapse {                  transition: all .5s, opacity 0.3s;                  position: absolute; diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 47b300efc..cefa9eebc 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -192,9 +192,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro      @action.bound      applyFilter = (e: React.MouseEvent) => { -          this.openViewSpecs(e); -          let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";          let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;          let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 721732774..225f67b3e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -38,6 +38,9 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso  import "./CollectionFreeFormView.scss";  import { MarqueeView } from "./MarqueeView";  import React = require("react"); +import v5 = require("uuid/v5"); +import { Timeline } from "../../animationtimeline/Timeline"; +import { number } from "prop-types";  library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard); @@ -63,6 +66,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {      private _clusterDistance: number = 75;      private _hitCluster = false;      @observable _clusterSets: (Doc[])[] = []; +    @observable _timelineRef = React.createRef<Timeline>();       @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }      @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } @@ -635,6 +639,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {          if (this.childDocs.some(d => BoolCast(d.isTemplate))) {              layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });          } +        this._timelineRef.current!.timelineContextMenu(e.nativeEvent);          layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });          layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });          layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); @@ -669,6 +674,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                  input.click();              }          }); +        //@ts-ignore +        let subitems: ContextMenuProps[] = +            DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({ +                description: (i + 1) + ": " + StrCast(note.title), +                event: () => console.log("Hi"), +                icon: "eye" +            }));          layoutItems.push({              description: "Add Note ...", @@ -713,6 +725,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {                          <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />                      </CollectionFreeFormViewPannableContents>                  </MarqueeView> +                <Timeline ref={this._timelineRef} {...this.props} />                  {this.overlayViews}                  <CollectionFreeFormOverlayView  {...this.props} {...this.getDocumentViewProps(this.props.Document)} />              </div> diff --git a/src/client/views/graph/Graph.tsx b/src/client/views/graph/Graph.tsx new file mode 100644 index 000000000..d925cc32c --- /dev/null +++ b/src/client/views/graph/Graph.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import {observable} from "mobx";  +import { observer } from "mobx-react"; +import { Document, listSpec } from "../../../new_fields/Schema"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionSubView, CollectionViewProps, SubCollectionViewProps } from "../collections/CollectionSubView"; + + + + +export class Graph extends CollectionSubView(Document) { +    static Instance:Graph;  + +    private constructor(props:SubCollectionViewProps) { +        super(props);  +        Graph.Instance = this;  +    } + + + + +    render() { +        let collection = <CollectionFreeFormView {...this.props}/>;  +  +        return ( +            <div> +            </div> +             +        );  +    } + +}
\ No newline at end of file diff --git a/src/client/views/graph/GraphManager.ts b/src/client/views/graph/GraphManager.ts new file mode 100644 index 000000000..b62f2337b --- /dev/null +++ b/src/client/views/graph/GraphManager.ts @@ -0,0 +1,45 @@ + + +import {Graph} from "./Graph";  +import {observable, computed} from 'mobx';  +import { Dictionary } from "typescript-collections"; +import { string } from "prop-types"; +import { Doc } from "../../../new_fields/Doc"; + + +export class GraphManager { +    @observable public Graphs: Graph[] = [];  + +    @observable public GraphData: Doc =  new Doc();   + +    private static _instance: GraphManager;  + +    @computed +    public static get Instance():GraphManager { +        return this._instance || (this._instance = new this());  +    } +     +    private constructor(){ + +    } + + + + +    public set addGraph(graph:Graph){ +        this.Graphs.push(graph);  +    } + +     +    defaultGraphs = ()  => { +         +    } + + + + +   + +     +     +}
\ No newline at end of file diff --git a/src/client/views/graph/GraphMenu.tsx b/src/client/views/graph/GraphMenu.tsx new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/graph/GraphMenu.tsx diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index c3d2c9e51..af2651dc8 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -115,6 +115,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF                                          StrCast(this.layoutDoc.boxShadow, ""),                      borderRadius: this.borderRounding(),                      transform: this.transform, +                    //CHANGE                      transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),                      width: this.width,                      height: this.height, diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 6ee88f834..ced1fa4df 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -96,6 +96,10 @@ export interface DocumentViewProps {  }  export const documentSchema = createSchema({ +    layout: "string", // should also allow Doc but that can't be expressed in the schema +    hidden: "boolean",  +    excludeFromLibrary: "boolean", +    fitToBox: "boolean",      // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out      title: "string",            // document title (can be on either data document or layout)      nativeWidth: "number",      // native width of document which determines how much document contents are scaled when the document's width is set @@ -646,7 +650,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu              <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}                  ref={this._mainCont}                  style={{ -                    transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), +                    //transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),                      pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",                      color: StrCast(this.Document.color),                      outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree], diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b7d9a1eab..9469a2a0f 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -154,6 +154,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD                  this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect;              }          } +         +        this.player && (this.player.style.transform = "");       }      componentWillUnmount() { | 
