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