diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 22 | ||||
-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 | 344 | ||||
-rw-r--r-- | src/client/views/nodes/Timeline.scss | 170 | ||||
-rw-r--r-- | src/client/views/nodes/Timeline.tsx | 515 | ||||
-rw-r--r-- | src/client/views/nodes/Track.scss | 15 | ||||
-rw-r--r-- | src/client/views/nodes/Track.tsx | 262 |
8 files changed, 1413 insertions, 7 deletions
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a15ed8f94..fceb40c42 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,8 +20,8 @@ import { CollectionView } from "./CollectionView"; import React = require("react"); import { MainView } from "../MainView"; import { Utils } from "../../../Utils"; +import { DocComponent } from "../DocComponent"; import { ScriptField } from "../../../new_fields/ScriptField"; -import { CompileScript } from "../../util/Scripting"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -58,7 +57,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]); - let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); + let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); if (viewSpecScript) { let script = viewSpecScript.script; docs = docs.filter(d => { @@ -112,15 +111,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.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 d70022280..a63994e6e 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"; @@ -592,7 +593,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..82f5b18cc --- /dev/null +++ b/src/client/views/nodes/Keyframe.tsx @@ -0,0 +1,344 @@ +import * as React from "react"; +import "./Keyframe.scss"; +import "./Timeline.scss"; +import "./../globalCssVariables.scss"; +import { observer, Observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, IObservableArray, computed, toJS, isComputedProp } from "mobx"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types"; +import { List } from "../../../new_fields/List"; +import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { FlyoutProps } from "./Timeline"; +import { Transform } from "../../util/Transform"; + +export namespace KeyframeFunc { + export enum KeyframeType { + fade = "fade", + default = "default", + } + export enum Direction { + left = "left", + right = "right" + } + export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: List<Doc>): (RegionData | undefined) => { + let leftMost: (RegionData | undefined) = undefined; + let rightMost: (RegionData | undefined) = undefined; + DocListCast(regions).forEach(region => { + let neighbor = RegionData(region as Doc); + if (currentRegion.position! > neighbor.position) { + if (!leftMost || neighbor.position > leftMost.position) { + leftMost = neighbor; + } + } else if (currentRegion.position! < neighbor.position) { + if (!rightMost || neighbor.position < rightMost.position) { + rightMost = neighbor; + } + } + }); + if (dir === Direction.left) { + return leftMost; + } else if (dir === Direction.right) { + return rightMost; + } + }; + + export const defaultKeyframe = () => { + let regiondata = new Doc(); //creating regiondata + regiondata.duration = 200; + regiondata.position = 0; + regiondata.fadeIn = 20; + regiondata.fadeOut = 20; + return regiondata; + }; +} + +export const RegionDataSchema = createSchema({ + position: defaultSpec("number", 0), + duration: defaultSpec("number", 0), + keyframes: listSpec(Doc), + fadeIn: defaultSpec("number", 0), + fadeOut: defaultSpec("number", 0) +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + +interface IProps { + node: Doc; + RegionData: Doc; + changeCurrentBarX: (x: number) => void; + setFlyout: (props: FlyoutProps) => any; + transform: Transform; +} + +@observer +export class Keyframe extends React.Component<IProps> { + + @observable private _bar = React.createRef<HTMLDivElement>(); + + @computed + private get regiondata() { + let index = this.regions.indexOf(this.props.RegionData); + return RegionData(this.regions[index] as Doc); + } + + @computed + private get regions() { + return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; + } + + @computed + private get firstKeyframe() { + let first: (Doc | undefined) = undefined; + DocListCast(this.regiondata.keyframes!).forEach(kf => { + if (kf.type !== KeyframeFunc.KeyframeType.fade) { + if (!first || first && NumCast(kf.time) < NumCast(first.time)) { + first = kf; + } + } + }); + return first; + } + + @computed + private get lastKeyframe() { + let last: (Doc | undefined) = undefined; + DocListCast(this.regiondata.keyframes!).forEach(kf => { + if (kf.type !== KeyframeFunc.KeyframeType.fade) { + if (!last || last && NumCast(kf.time) > NumCast(last.time)) { + last = kf; + } + } + }); + return last; + } + + + 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; + + 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 = TK as Doc; + 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 prevX = this.regiondata.position; + let futureX = this.regiondata.position + e.movementX; + if (futureX <= 0) { + this.regiondata.position = 0; + } else if ((left && left.position + left.duration >= futureX)) { + this.regiondata.position = left.position + left.duration; + } else if ((right && right.position <= futureX + this.regiondata.duration)) { + this.regiondata.position = right.position - this.regiondata.duration; + } else { + this.regiondata.position = futureX; + } + for (let i = 0; i < this.regiondata.keyframes!.length; i++) { + if ((this.regiondata.keyframes![i] as Doc).type !== KeyframeFunc.KeyframeType.fade) { + let movement = this.regiondata.position - prevX; + (this.regiondata.keyframes![i] as Doc).time = NumCast((this.regiondata.keyframes![i] as Doc).time) + movement; + } + } + this.forceUpdate(); + } + + @action + onResizeLeft = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onDragResizeLeft); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onDragResizeLeft); + }); + } + + @action + onResizeRight = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onDragResizeRight); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onDragResizeRight); + }); + } + + @action + onDragResizeLeft = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let bar = this._bar.current!; + let offset = Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale); + let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); + let firstkf: (Doc | undefined) = this.firstKeyframe; + if (firstkf && this.regiondata.position + this.regiondata.fadeIn + offset >= NumCast(firstkf!.time)) { + let dif = NumCast(firstkf!.time) - (this.regiondata.position + this.regiondata.fadeIn); + this.regiondata.position = NumCast(firstkf!.time) - this.regiondata.fadeIn; + this.regiondata.duration -= dif; + } else if (this.regiondata.duration - offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // no keyframes, just fades + this.regiondata.position -= (this.regiondata.fadeIn + this.regiondata.fadeOut - this.regiondata.duration); + this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut; + } else if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { + let dif = this.regiondata.position - (leftRegion.position + leftRegion.duration); + this.regiondata.position = leftRegion.position + leftRegion.duration; + this.regiondata.duration += dif; + + } else { + this.regiondata.duration -= offset; + this.regiondata.position += offset; + } + } + + + @action + onDragResizeRight = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let bar = this._bar.current!; + let offset = Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale); + let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); + if (this.lastKeyframe! && this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= NumCast((this.lastKeyframe! as Doc).time)) { + let dif = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut - NumCast((this.lastKeyframe! as Doc).time); + this.regiondata.duration -= dif; + } else if (this.regiondata.duration + offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // nokeyframes, just fades + this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut; + } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) { + let dif = rightRegion.position - (this.regiondata.position + this.regiondata.duration); + this.regiondata.duration += dif; + } else { + this.regiondata.duration += offset; + } + } + + createDivider = (type?: KeyframeFunc.Direction): JSX.Element => { + if (type === "left") { + return <div className="divider" style={{ right: "0px" }}></div>; + } else if (type === "right") { + return <div className="divider" style={{ left: "0px" }}> </div>; + } + return <div className="divider"></div>; + } + + @action + createKeyframe = async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + let bar = this._bar.current!; + let offset = Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale); + if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends + let position = NumCast(this.regiondata.position); + await this.makeKeyData(Math.round(position + offset)); + console.log(this.regiondata.keyframes!.length); + this.props.changeCurrentBarX(NumCast(Math.round(position + offset))); //first move the keyframe to the correct location and make a copy so the correct file gets coppied + } + } + + + @action + moveKeyframe = async (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 * this.props.transform.Scale, y: offsetTop * this.props.transform.Scale, display: "block", regiondata: this.regiondata, regions: this.regions }); + })}> + <div className="leftResize" onPointerDown={this.onResizeLeft} ></div> + <div className="rightResize" onPointerDown={this.onResizeRight}></div> + {this.regiondata.keyframes!.map(kf => { + return this.createKeyframeJSX(kf as Doc, (kf! as Doc).type as KeyframeFunc.KeyframeType); + })} + </div> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Timeline.scss b/src/client/views/nodes/Timeline.scss new file mode 100644 index 000000000..47f448adb --- /dev/null +++ b/src/client/views/nodes/Timeline.scss @@ -0,0 +1,170 @@ +@import "./../globalCssVariables.scss"; + +.minimize{ + position:relative; + z-index: 1000; + height: 30px; + width: 100px; +} +.flyout-container{ + background-color: transparent; + position:absolute; + + z-index:9999; + height: 150px; + width: 150px; + + .flyout{ + background-color: transparent; + transform: rotate(180deg); + left:0px; + top:0px; + width: 100%; + height: 100%; + } + .input-container{ + position: absolute; + right:0px; + top: 30px; + width: 70px; + input{ + width: 100%; + } + } + .text-container{ + position:absolute; + top:30px; + left:0px; + color:white + } +} + +.placement-highlight{ + background-color:blue; + transform: translate(0px, 0px); + transition: width 1000ms ease-in-out; + transition: height 1000ms ease-in-out; + position: absolute; +} + +.timeline-container{ + width:100%; + height:300px; + position:absolute; + background-color: $light-color-secondary; + box-shadow: 0px 10px 20px; + //transition: transform 1000ms ease-in-out; + + .toolbox{ + position:absolute; + width: 100%; + top: 10px; + left: 20px; + div{ + float:left; + margin-left: 10px; + position:relative; + .overview{ + width: 200px; + height: 100%; + background-color: black; + position:absolute; + } + } + } + .info-container{ + margin-top: 50px; + right:20px; + position:absolute; + height: calc(100% - 100px); + width: calc(100% - 140px); + overflow: hidden; + + .scrubberbox{ + position:absolute; + background-color: transparent; + height: 30px; + width:100%; + + .tick{ + height:100%; + width: 1px; + background-color:black; + + } + } + .scrubber{ + top:30px; + height: 100%; + width: 2px; + position:absolute; + z-index: 1001; + background-color:black; + .scrubberhead{ + top: -30px; + height: 30px; + width: 30px; + background-color:transparent; + border-radius: 50%; + border: 5px solid black; + left: -15px; + position:absolute; + } + } + + .trackbox{ + top: 30px; + height:calc(100% - 30px); + width:100%; + border:1px; + overflow:hidden; + background-color:white; + position:absolute; + box-shadow: -10px 0px 10px 10px grey; + } + + } + .title-container{ + margin-top: 80px; + margin-left: 20px; + height: calc(100% - 100px - 30px); + width: 100px; + background-color:white; + overflow: hidden; + .datapane{ + top:0px; + width: 100px; + height: 75px; + border: 1px solid $dark-color; + background-color: $intermediate-color; + color: white; + position:relative; + float:left; + border-style:solid; + } + } + .resize{ + bottom: 5px; + position:absolute; + height: 30px; + width: 50px; + left: calc(50% - 25px); + } +} + + + +.overview{ + position: absolute; + height: 50px; + width: 200px; + background-color: black; + .container{ + position: absolute; + float: left 0px; + top: 25%; + height: 75%; + width: 100%; + background-color: grey; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/Timeline.tsx b/src/client/views/nodes/Timeline.tsx new file mode 100644 index 000000000..99db2a643 --- /dev/null +++ b/src/client/views/nodes/Timeline.tsx @@ -0,0 +1,515 @@ +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, faPauseCircle } from "@fortawesome/free-solid-svg-icons"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { ContextMenu } from "../ContextMenu"; +import { DocumentManager } from "../../util/DocumentManager"; +import { VideoBox } from "./VideoBox"; +import { VideoField } from "../../../new_fields/URLField"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { Transform } from "../../util/Transform"; +import { faGrinTongueSquint } from "@fortawesome/free-regular-svg-icons"; + + +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 _playButton = faPlayCircle; + @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") { + console.log("ran"); + console.log(this.props.Document.duration); + if (this.props.Document.duration) { + this._time = Math.round(NumCast(this.props.Document.duration)) * 1000; + + reaction(() => { + return NumCast(this.props.Document.curPage); + }, curPage => { + this.changeCurrentBarX(curPage * this._tickIncrement / this._tickSpacing); + }); + + } + + } + runInAction(() => { + + reaction(() => { + return this._time; + }, () => { + this._ticks = []; + for (let i = 0; i < this._time;) { + this._ticks.push(i); + i += this._tickIncrement; + } + let trackbox = this._trackbox.current!; + this._boxLength = this._tickIncrement / 1000 * this._tickSpacing * this._ticks.length; + trackbox.style.width = `${this._boxLength}`; + this._scrubberbox.current!.style.width = `${this._boxLength}`; + }, { fireImmediately: true }); + }); + } + + @action + changeCurrentBarX = (x: number) => { + this._currentBarX = x; + } + + //for playing + @action + onPlay = async (e: React.MouseEvent) => { + if (this._isPlaying) { + this._isPlaying = false; + this._playButton = faPlayCircle; + } else { + this._isPlaying = true; + this._playButton = faPauseCircle; + this.changeCurrentX(); + } + } + + @action + changeCurrentX = () => { + if (this._currentBarX === this._boxLength && this._isPlaying) { + this._currentBarX = 0; + } + if (this._currentBarX <= this._boxLength && this._isPlaying) { + this._currentBarX = this._currentBarX + this._windSpeed; + setTimeout(this.changeCurrentX, 15); + } + } + + @action + windForward = (e: React.MouseEvent) => { + if (this._windSpeed < 64) { //max speed is 32 + this._windSpeed = this._windSpeed * 2; + } + } + + @action + windBackward = (e: React.MouseEvent) => { + if (this._windSpeed > 1 / 16) { // min speed is 1/8 + this._windSpeed = this._windSpeed / 2; + } + } + + //for scrubber action + @action + onScrubberDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onScrubberMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onScrubberMove); + }); + } + + @action + onScrubberMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let scrubberbox = this._scrubberbox.current!; + let left = scrubberbox.getBoundingClientRect().left; + let offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale; + this._currentBarX = offsetX; + } + + @action + onScrubberClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + let scrubberbox = this._scrubberbox.current!; + let offset = (e.clientX - scrubberbox.getBoundingClientRect().left) * this.props.ScreenToLocalTransform().Scale; + this._currentBarX = offset; + } + + + + @action + onPanDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onPanMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onPanMove); + }); + } + + @action + onPanMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let infoContainer = this._infoContainer.current!; + let trackbox = this._trackbox.current!; + let titleContainer = this._titleContainer.current!; + infoContainer.scrollLeft = infoContainer.scrollLeft - e.movementX; + trackbox.scrollTop = trackbox.scrollTop - e.movementY; + titleContainer.scrollTop = titleContainer.scrollTop - e.movementY; + } + + + @action + onResizeDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + document.addEventListener("pointermove", this.onResizeMove); + document.addEventListener("pointerup", () => { + document.removeEventListener("pointermove", this.onResizeMove); + }); + } + + @action + onResizeMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom; + if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) { + this._containerHeight = this.MIN_CONTAINER_HEIGHT; + } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) { + this._containerHeight = this.MAX_CONTAINER_HEIGHT; + } else { + this._containerHeight += offset; + } + } + + @action + onTimelineDown = (e: React.PointerEvent) => { + e.preventDefault(); + if (e.nativeEvent.which === 1 && !this._isFrozen) { + document.addEventListener("pointermove", this.onTimelineMove); + document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onTimelineMove); }); + } + } + + @action + onTimelineMove = (e: PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + let timelineContainer = this._timelineWrapper.current!; + let left = parseFloat(timelineContainer.style.left!); + let top = parseFloat(timelineContainer.style.top!); + timelineContainer.style.left = `${left + e.movementX}px`; + timelineContainer.style.top = `${top + e.movementY}px`; + } + + @action + minimize = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + let timelineContainer = this._timelineContainer.current!; + if (this._isMinimized) { + this._isMinimized = false; + timelineContainer.style.visibility = "visible"; + } else { + this._isMinimized = true; + timelineContainer.style.visibility = "hidden"; + } + } + + @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; + } + console.log(this.flyoutInfo); + } + + 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={this._playButton} size="2x" /> </div> + <div onClick={this.windForward}> <FontAwesomeIcon icon={faForward} size="2x" /> </div> + </div> + <div className="info-container" ref={this._infoContainer}> + <div className="scrubberbox" ref={this._scrubberbox} onClick={this.onScrubberClick}> + {this._ticks.map(element => { + return <div className="tick" style={{ transform: `translate(${element / 1000 * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toTime(element)}</p></div>; + })} + </div> + <div className="scrubber" ref={this._scrubber} onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}> + <div className="scrubberhead"></div> + </div> + <div className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown}> + {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} setFlyout={this.getFlyout} transform={this.props.ScreenToLocalTransform()}/>)} + </div> + </div> + <div className="title-container" ref={this._titleContainer}> + {DocListCast(this.children).map(doc => <div className="datapane"><p>{doc.title}</p></div>)} + </div> + <div onPointerDown={this.onResizeDown}> + <FontAwesomeIcon className="resize" icon={faGripLines} /> + </div> + </div> + </div> + ); + } + +} + + +interface TimelineFlyoutProps { + flyoutInfo: FlyoutProps; + tickSpacing: number; + +} + +interface TimelineOverviewProps { + currentBarX: number; +} + +class TimelineOverview extends React.Component<TimelineOverviewProps>{ + + componentWillMount() { + + } + + render() { + return ( + <div className="overview"> + <div className="container"> + <div className="scrubber"> + <div className="scrubberhead"></div> + </div> + </div> + </div> + ); + } +} + +class TimelineFlyout extends React.Component<TimelineFlyoutProps>{ + + @observable private _timeInput = React.createRef<HTMLInputElement>(); + @observable private _durationInput = React.createRef<HTMLInputElement>(); + @observable private _fadeInInput = React.createRef<HTMLInputElement>(); + @observable private _fadeOutInput = React.createRef<HTMLInputElement>(); + @observable private _data: FlyoutProps = { x: 0, y: 0, display: "none", regiondata: new Doc(), regions: new List<Doc>() }; + + private block = false; + + componentDidMount() { + + document.addEventListener("pointerdown", this.closeFlyout); + } + + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.closeFlyout); + } + + + @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._data.display = "block"; + this.block = true; + } + + @action + closeFlyout = (e: PointerEvent) => { + if (this.block) { + this.block = false; + return; + } + this._data.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._data.x}px`, top: `${this._data.y}px`, display: `${this._data.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..e4302e721 --- /dev/null +++ b/src/client/views/nodes/Track.tsx @@ -0,0 +1,262 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, reaction, action, IReactionDisposer, observe, IObservableArray, computed, toJS, IObservableObject, 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 { FlyoutProps } from "./Timeline"; +import { Transform } from "../../util/Transform"; +import { AddComparisonParameters } from "../../northstar/model/idea/idea"; + +interface IProps { + node: Doc; + currentBarX: number; + transform: Transform; + changeCurrentBarX: (x: number) => void; + setFlyout: (props: FlyoutProps) => any; +} + +@observer +export class Track extends React.Component<IProps> { + @observable private _inner = React.createRef<HTMLDivElement>(); + @observable private _reactionDisposers: IReactionDisposer[] = []; + @observable private _keyReaction: any; //reaction that is used to dispose when necessary + @observable private _currentBarXReaction: any; + + @computed + private get regions() { + return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>; + } + + componentWillMount() { + if (!this.props.node.regions) { + this.props.node.regions = new List<Doc>(); + } + this.props.node.opacity = 1; + } + + componentDidMount() { + runInAction(() => { + this._currentBarXReaction = this.currentBarXReaction(); + if (this.regions.length === 0) this.createRegion(this.props.currentBarX); + this.props.node.hidden = false; + }); + } + + componentWillUnmount() { + runInAction(() => { + if (this._keyReaction) this._keyReaction(); + if (this._currentBarXReaction) this._currentBarXReaction(); + }); + } + + @action + keyReaction = () => { + return reaction( () => { + return Doc.allKeys(this.props.node).map(key => FieldValue(this.props.node[key])); + }, async () => { + let regiondata = this.findRegion(this.props.currentBarX); + if (regiondata) { + let keyframes = await DocListCastAsync(regiondata.keyframes!); + keyframes!.forEach( async (kf) => { + if (kf.type === KeyframeFunc.KeyframeType.default && kf.time === this.props.currentBarX) { + console.log("full keychange triggered"); + //for this specific keyframe + kf.key = Doc.MakeCopy(this.props.node, true); + + //for fades + let leftkf: (Doc | undefined) = await this.calcMinLeft(regiondata!, kf); // lef keyframe, if it exists + let rightkf: (Doc | undefined) = await this.calcMinRight(regiondata!, kf); //right keyframe, if it exists + if (leftkf!.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades + let edge:(Doc | undefined) = await 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:(Doc | undefined) = await 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; + } + } + }); + } + }, {fireImmediately: true}); + } + + @action + currentBarXReaction = () => { + return reaction(() => this.props.currentBarX, async () => { + if (this._keyReaction) this._keyReaction(); //dispose previous reaction first + let regiondata: (Doc | undefined) = this.findRegion(this.props.currentBarX); + if (regiondata) { + this.props.node.hidden = false; + await this.timeChange(this.props.currentBarX); + } else { + this.props.node.hidden = true; + } + }, { fireImmediately: true }); + } + + + @action + timeChange = async (time: number) => { + let regiondata = this.findRegion(Math.round(time)); //finds a region that the scrubber is on + if (regiondata) { + let leftkf: (Doc | undefined) = await this.calcMinLeft(regiondata!); // lef keyframe, if it exists + let rightkf: (Doc | undefined) = await this.calcMinRight(regiondata!); //right keyframe, if it exists + let currentkf: (Doc | undefined) = await this.calcCurrent(regiondata!); //if the scrubber is on top of the keyframe + if (currentkf) { + this.applyKeys(currentkf); + this._keyReaction = this.keyReaction(); //reactivates reaction. + } else if (leftkf && rightkf) { + this.interpolate(leftkf, rightkf); + } + } + } + + @action + private applyKeys = (kf: Doc) => { + let kfNode = 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 (key === "title" || key === "documentText") Doc.SetOnPrototype(this.props.node, key, StrCast(kf[key])); + if (!kfNode[key]) { + this.props.node[key] = undefined; + } else { + this.props.node[key] = kfNode[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" && key !== "nativeHeight" && key !== "nativeWidth") acc.push(key); + return acc; + }, []) as string[]; + } + + @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(this.props.currentBarX)) currentkf = kf; + }); + return currentkf; + } + + + @action + calcMinLeft = async (region: Doc, 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 = this.props.currentBarX; + if (ref) { + compTime = NumCast(ref.time); + } + if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) { + leftKf = kf; + time = NumCast(kf.time); + } + }); + return leftKf; + } + + + @action + calcMinRight = async (region: Doc, 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 = this.props.currentBarX; + if (ref) { + compTime = NumCast(ref.time); + } + if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) { + rightKf = kf; + time = NumCast(kf.time); + } + }); + return rightKf; + } + + @action + interpolate = (left: Doc, right: Doc) => { + console.log("interpolating"); + let leftNode = left.key as Doc; + let rightNode = right.key as Doc; + const dif_time = NumCast(right.time) - NumCast(left.time); + const ratio = (this.props.currentBarX - NumCast(left.time)) / dif_time; //linear + this.filterKeys(Doc.allKeys(leftNode)).forEach(key => { + if (leftNode[key] && rightNode[key] && typeof (leftNode[key]) === "number" && typeof (rightNode[key]) === "number") { //if it is number, interpolate + const diff = NumCast(rightNode[key]) - NumCast(leftNode[key]); + const adjusted = diff * ratio; + this.props.node[key] = NumCast(leftNode[key]) + adjusted; + } else if (key === "title" || key === "documentText") { + Doc.SetOnPrototype(this.props.node, key, StrCast(leftNode[key])); + this.props.node[key] = leftNode[key]; + } + }); + } + + @action + findRegion(time: number): (RegionData | undefined) { + let foundRegion = undefined; + DocListCast(this.regions).map(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(offsetX); + } + + createRegion = (position: number) => { + let regiondata = KeyframeFunc.defaultKeyframe(); + regiondata.position = position; + let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, regiondata, this.regions); + let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); + if ((rightRegion && leftRegion && rightRegion.position - (leftRegion.position + leftRegion.duration) < NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut)) || (rightRegion && rightRegion.position - regiondata.position < NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) { + return; + } else if (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut)) { + regiondata.duration = rightRegion.position - regiondata.position; + } + this.regions.push(regiondata); + return regiondata; + } + + + render() { + return ( + <div className="track-container"> + <div className="track"> + <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick}> + {DocListCast(this.regions).map((region) => { + return <Keyframe node={this.props.node} RegionData={region} changeCurrentBarX={this.props.changeCurrentBarX} setFlyout={this.props.setFlyout} transform={this.props.transform} />; + })} + </div> + </div> + </div> + ); + } +}
\ No newline at end of file |