aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/views/ContextMenu.scss1
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/animationtimeline/Keyframe.scss94
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx595
-rw-r--r--src/client/views/animationtimeline/Timeline.scss122
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx437
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.scss94
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx73
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.scss38
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.tsx93
-rw-r--r--src/client/views/animationtimeline/Track.scss15
-rw-r--r--src/client/views/animationtimeline/Track.tsx305
-rw-r--r--src/client/views/collections/CollectionSubView.tsx14
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss12
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx15
-rw-r--r--src/client/views/graph/Graph.tsx32
-rw-r--r--src/client/views/graph/GraphManager.ts45
-rw-r--r--src/client/views/graph/GraphMenu.tsx0
-rw-r--r--src/client/views/nodes/DocumentView.tsx4
-rw-r--r--src/client/views/nodes/VideoBox.tsx2
21 files changed, 1991 insertions, 7 deletions
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 8f112de0c..d3286aa22 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -7,6 +7,7 @@
box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
flex-direction: column;
background: whitesmoke;
+ padding-top: 10px;
padding-bottom: 10px;
border-radius: 15px;
border: solid #BBBBBBBB 1px;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 003919866..1526dad34 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -38,6 +38,7 @@ import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
+import { TimelineMenu } from './animationtimeline/TimelineMenu';
import PresModeMenu from './presentationview/PresentationModeMenu';
import { PresBox } from './nodes/PresBox';
import { LinkFollowBox } from './linking/LinkFollowBox';
@@ -205,6 +206,9 @@ export class MainView extends React.Component {
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
}
+ if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !=="timeline-menu-input")){
+ TimelineMenu.Instance.closeMenu();
+ }
});
globalPointerUp = () => this.isPointerDown = false;
@@ -580,6 +584,7 @@ export class MainView extends React.Component {
{this.nodesMenu()}
{this.miscButtons}
<PDFMenu />
+ <TimelineMenu/>
<MainOverlayTextBox firstinstance={true} />
<OverlayView />
</div >
diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss
new file mode 100644
index 000000000..b1e8b0b65
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.scss
@@ -0,0 +1,94 @@
+@import "./../globalCssVariables.scss";
+
+.bar {
+ height: 100%;
+ width: 5px;
+ position: absolute;
+
+ // pointer-events: none;
+ .menubox {
+ width: 200px;
+ height:200px;
+ top: 50%;
+ position: relative;
+ background-color: $light-color;
+ .menutable{
+ tr:nth-child(odd){
+ background-color:$light-color-secondary;
+ }
+ }
+ }
+
+ .leftResize{
+ left:-12.5px;
+ height:25px;
+ width:25px;
+ border-radius: 50%;
+ background-color: white;
+ border:3px solid black;
+ top: calc(50% - 12.5px);
+ z-index: 1000;
+ position:absolute;
+ }
+ .rightResize{
+ right:-12.5px;
+ height:25px;
+ width:25px;
+ border-radius: 50%;
+ top:calc(50% - 12.5px);
+ background-color:white;
+ border:3px solid black;
+ z-index: 1000;
+ position:absolute;
+ }
+ .fadeLeft{
+ left:0px;
+ height:100%;
+ position:absolute;
+ pointer-events: none;
+ background: linear-gradient(to left, #4d9900 10%, $light-color);
+ }
+
+ .fadeRight{
+ right:0px;
+ height:100%;
+ position:absolute;
+ pointer-events: none;
+ background: linear-gradient(to right, #4d9900 10%, $light-color);
+ }
+ .divider{
+ height:100%;
+ width: 1px;
+ position: absolute;
+ background-color:black;
+ cursor: col-resize;
+ pointer-events:none;
+ }
+ .keyframe{
+ height:100%;
+ position:absolute;
+ }
+ .keyframeCircle{
+ left:-15px;
+ height:30px;
+ width:30px;
+ border-radius: 50%;
+ top:calc(50% - 15px);
+ background-color:white;
+ border:3px solid green;
+ z-index: 1000;
+ position:absolute;
+ }
+
+ .fadeIn-container, .fadeOut-container, .body-container{
+ position:absolute;
+ height:100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ }
+
+
+}
+
+
+
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
new file mode 100644
index 000000000..253515dfd
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -0,0 +1,595 @@
+import * as React from "react";
+import "./Keyframe.scss";
+import "./Timeline.scss";
+import "../globalCssVariables.scss";
+import { observer} from "mobx-react";
+import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction } from "mobx";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { FlyoutProps } from "./Timeline";
+import { Transform } from "../../util/Transform";
+import { InkField, StrokeData } from "../../../new_fields/InkField";
+import { TimelineMenu } from "./TimelineMenu";
+import { Docs } from "../../documents/Documents";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+
+export namespace KeyframeFunc {
+ export enum KeyframeType {
+ fade = "fade",
+ default = "default",
+ }
+ export enum Direction {
+ left = "left",
+ right = "right"
+ }
+ export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: List<Doc>): (RegionData | undefined) => {
+ let leftMost: (RegionData | undefined) = undefined;
+ let rightMost: (RegionData | undefined) = undefined;
+ DocListCast(regions).forEach(region => {
+ let neighbor = RegionData(region);
+ if (currentRegion.position! > neighbor.position) {
+ if (!leftMost || neighbor.position > leftMost.position) {
+ leftMost = neighbor;
+ }
+ } else if (currentRegion.position! < neighbor.position) {
+ if (!rightMost || neighbor.position < rightMost.position) {
+ rightMost = neighbor;
+ }
+ }
+ });
+ if (dir === Direction.left) {
+ return leftMost;
+ } else if (dir === Direction.right) {
+ return rightMost;
+ }
+ };
+
+ export const calcMinLeft = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+ let leftKf: (Doc | undefined) = undefined;
+ let time: number = 0;
+ let keyframes = await DocListCastAsync(region.keyframes!);
+ keyframes!.forEach((kf) => {
+ let compTime = currentBarX;
+ if (ref) {
+ compTime = NumCast(ref.time);
+ }
+ if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
+ leftKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return leftKf;
+ };
+
+
+ export const calcMinRight = async (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right
+ let rightKf: (Doc | undefined) = undefined;
+ let time: number = Infinity;
+ let keyframes = await DocListCastAsync(region.keyframes!);
+ keyframes!.forEach((kf) => {
+ let compTime = currentBarX;
+ if (ref) {
+ compTime = NumCast(ref.time);
+ }
+ if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
+ rightKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return rightKf;
+ };
+
+ export const defaultKeyframe = () => {
+ let regiondata = new Doc(); //creating regiondata in MILI
+ regiondata.duration = 4000;
+ regiondata.position = 0;
+ regiondata.fadeIn = 1000;
+ regiondata.fadeOut = 1000;
+ regiondata.functions = new List<Doc>();
+ return regiondata;
+ };
+
+ export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing:number, tickIncrement:number) => {
+ let time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+ switch (unit) {
+ case "mili":
+ return time;
+ case "sec":
+ return dir === "pixel" ? time / 1000 : time * 1000;
+ case "min":
+ return dir === "pixel" ? time / 60000 : time * 60000;
+ case "hr":
+ return dir === "pixel" ? time / 3600000 : time * 3600000;
+ default:
+ return time;
+ }
+ };
+}
+
+export const RegionDataSchema = createSchema({
+ position: defaultSpec("number", 0),
+ duration: defaultSpec("number", 0),
+ keyframes: listSpec(Doc),
+ fadeIn: defaultSpec("number", 0),
+ fadeOut: defaultSpec("number", 0),
+ functions: listSpec(Doc)
+});
+export type RegionData = makeInterface<[typeof RegionDataSchema]>;
+export const RegionData = makeInterface(RegionDataSchema);
+
+interface IProps {
+ node: Doc;
+ RegionData: Doc;
+ collection: Doc;
+ tickSpacing: number;
+ tickIncrement: number;
+ time: number;
+ changeCurrentBarX: (x: number) => void;
+ transform: Transform;
+}
+
+@observer
+export class Keyframe extends React.Component<IProps> {
+
+ @observable private _bar = React.createRef<HTMLDivElement>();
+ @observable private _gain = 20; //default
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+
+ @computed
+ private get regiondata() {
+ let index = this.regions.indexOf(this.props.RegionData);
+ return RegionData(this.regions[index] as Doc);
+ }
+
+ @computed
+ private get regions() {
+ return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;
+ }
+
+ @computed
+ private get firstKeyframe() {
+ let first: (Doc | undefined) = undefined;
+ DocListCast(this.regiondata.keyframes!).forEach(kf => {
+ if (kf.type !== KeyframeFunc.KeyframeType.fade) {
+ if (!first || first && NumCast(kf.time) < NumCast(first.time)) {
+ first = kf;
+ }
+ }
+ });
+ return first;
+ }
+
+ @computed
+ private get lastKeyframe() {
+ let last: (Doc | undefined) = undefined;
+ DocListCast(this.regiondata.keyframes!).forEach(kf => {
+ if (kf.type !== KeyframeFunc.KeyframeType.fade) {
+ if (!last || last && NumCast(kf.time) > NumCast(last.time)) {
+ last = kf;
+ }
+ }
+ });
+ return last;
+ }
+
+ @computed
+ private get keyframes(){
+ return DocListCast(this.regiondata.keyframes);
+ }
+
+ @computed
+ private get inks() {
+ if (this.props.collection.data_ext) {
+ let data_ext = Cast(this.props.collection.data_ext, Doc) as Doc;
+ let ink = Cast(data_ext.ink, InkField) as InkField;
+ if (ink) {
+ return ink.inkData;
+ }
+ }
+ }
+
+ @computed
+ private get pixelPosition(){
+ return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ }
+
+ @computed
+ private get pixelDuration(){
+ return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ }
+
+ @computed
+ private get pixelFadeIn() {
+ return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ }
+
+ @computed
+ private get pixelFadeOut(){
+ return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ }
+
+ async componentWillMount() {
+ if (!this.regiondata.keyframes) {
+ this.regiondata.keyframes = new List<Doc>();
+ }
+ let fadeIn = await this.makeKeyData(this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade)!;
+ let fadeOut = await this.makeKeyData(this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade)!;
+ let start = await this.makeKeyData(this.regiondata.position, KeyframeFunc.KeyframeType.fade)!;
+ let finish = await this.makeKeyData(this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.fade)!;
+ (fadeIn.key! as Doc).opacity = 1;
+ (fadeOut.key! as Doc).opacity = 1;
+ (start.key! as Doc).opacity = 0.1;
+ (finish.key! as Doc).opacity = 0.1;
+ }
+
+ @action
+ makeKeyData = async (kfpos: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time
+ let doclist = (await DocListCastAsync(this.regiondata.keyframes))!;
+ let existingkf: (Doc | undefined) = undefined;
+ doclist.forEach(TK => {
+ if (TK.time === kfpos) existingkf = TK;
+ });
+ if (existingkf) return existingkf;
+ let TK: Doc = new Doc();
+ TK.time = kfpos;
+ TK.key = Doc.MakeCopy(this.props.node, true);
+ TK.type = type;
+ this.regiondata.keyframes!.push(TK);
+
+ let interpolationFunctions = new Doc();
+ interpolationFunctions.interpolationX = new List<number>([0, 1]);
+ interpolationFunctions.interpolationY = new List<number>([0,100]);
+ interpolationFunctions.pathX = new List<number>();
+ interpolationFunctions.pathY = new List<number>();
+
+ this.regiondata.functions!.push(interpolationFunctions);
+ let found:boolean = false;
+ this.regiondata.keyframes!.forEach(compkf => {
+ compkf = compkf as Doc;
+ if (kfpos < NumCast(compkf.time) && !found) {
+ runInAction(() => {
+ this.regiondata.keyframes!.splice(doclist.indexOf(compkf as Doc), 0, TK);
+ this.regiondata.keyframes!.pop();
+ found = true;
+ });
+ return;
+ }
+ });
+ return TK;
+ }
+
+
+ @action
+ onBarPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let clientX = e.clientX;
+ if (this._doubleClickEnabled){
+ this.createKeyframe(clientX);
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {if(!this._mouseToggled && this._doubleClickEnabled)this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false; }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onBarPointerMove);
+ document.addEventListener("pointerup", (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onBarPointerMove);
+ });
+ }
+ }
+
+
+ @action
+ onBarPointerMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0) {
+ this._mouseToggled = true;
+ }
+ let left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
+ let right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions!);
+ let prevX = this.regiondata.position;
+ let futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (futureX <= 0) {
+ this.regiondata.position = 0;
+ } else if ((left && left.position + left.duration >= futureX)) {
+ this.regiondata.position = left.position + left.duration;
+ } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+ this.regiondata.position = right.position - this.regiondata.duration;
+ } else {
+ this.regiondata.position = futureX;
+ }
+ let movement = this.regiondata.position - prevX;
+ this.keyframes.forEach(kf => {
+ kf.time = NumCast(kf.time) + movement;
+ });
+ }
+
+ @action
+ onResizeLeft = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeLeft);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeLeft);
+ });
+ }
+
+ @action
+ onResizeRight = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeRight);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeRight);
+ });
+ }
+
+ @action
+ onDragResizeLeft = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let bar = this._bar.current!;
+ let offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
+ let firstkf: (Doc | undefined) = this.firstKeyframe;
+ if (firstkf && this.regiondata.position + this.regiondata.fadeIn + offset >= NumCast(firstkf!.time)) {
+ let dif = NumCast(firstkf!.time) - (this.pixelPosition + this.pixelFadeIn);
+ this.regiondata.position = NumCast(firstkf!.time) - this.regiondata.fadeIn;
+ this.regiondata.duration -= dif;
+ } else if (this.regiondata.duration - offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // no keyframes, just fades
+ this.regiondata.position -= (this.regiondata.fadeIn + this.regiondata.fadeOut - this.regiondata.duration);
+ this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut;
+ } else if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
+ let dif = this.regiondata.position - (leftRegion.position + leftRegion.duration);
+ this.regiondata.position = leftRegion.position + leftRegion.duration;
+ this.regiondata.duration += dif;
+ } else {
+ this.regiondata.duration -= offset;
+ this.regiondata.position += offset;
+ }
+ this.keyframes[0].time = this.regiondata.position;
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+
+
+ @action
+ onDragResizeRight = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let bar = this._bar.current!;
+ let offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
+ if (this.lastKeyframe! && this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= NumCast((this.lastKeyframe! as Doc).time)) {
+ let dif = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut - NumCast((this.lastKeyframe! as Doc).time);
+ this.regiondata.duration -= dif;
+ } else if (this.regiondata.duration + offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // nokeyframes, just fades
+ this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut;
+ } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) {
+ let dif = rightRegion.position - (this.regiondata.position + this.regiondata.duration);
+ this.regiondata.duration += dif;
+ } else {
+ this.regiondata.duration += offset;
+ }
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+
+ }
+
+ @action
+ createKeyframe = async (clientX:number) => {
+ this._mouseToggled = true;
+ let bar = this._bar.current!;
+ let offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+ let position = this.regiondata.position;
+ await this.makeKeyData(Math.round(position + offset));
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
+ }
+ }
+
+
+ @action
+ moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+
+ @action
+ onKeyframeOver = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.node.backgroundColor = "#000000";
+ }
+
+ @action
+ makeKeyframeMenu = (kf :Doc, e:MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Show Data", () => {
+ runInAction(() => {let kvp = Docs.Create.KVPDocument(Cast(kf.key, Doc) as Doc, { width: 300, height: 300 });
+ CollectionDockingView.Instance.AddRightSplit(kvp, (kf.key as Doc).data as Doc); });
+ }),
+ TimelineMenu.Instance.addItem("button", "Delete", () => {
+ runInAction(() => {
+ console.log(this.keyframes.indexOf(kf));
+ this.keyframes.splice(this.keyframes.indexOf(kf), 1);
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", "Move", (val) => {kf.time = parseInt(val, 10);});
+ TimelineMenu.Instance.addMenu("Keyframe");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ @action
+ makeRegionMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Add Ease", () => {this.onContainerDown(kf, "interpolate");}),
+ TimelineMenu.Instance.addItem("button", "Add Path", () => {this.onContainerDown(kf, "path");}),
+ TimelineMenu.Instance.addItem("button", "Remove Region", ()=>{this.regions.splice(this.regions.indexOf(this.regiondata), 1);}),
+ TimelineMenu.Instance.addItem("input", "fadeIn", (val) => {this.regiondata.fadeIn = parseInt(val, 10);}),
+ TimelineMenu.Instance.addItem("input", "fadeOut", (val) => {this.regiondata.fadeOut = parseInt(val, 10);}),
+ TimelineMenu.Instance.addItem("input", "position", (val) => {this.regiondata.position = parseInt(val, 10);}),
+ TimelineMenu.Instance.addItem("input", "duration", (val) => {this.regiondata.duration = parseInt(val, 10);}),
+ TimelineMenu.Instance.addMenu("Region");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+ @action
+ private createKeyframeJSX = (kf: Doc, type = KeyframeFunc.KeyframeType.default) => {
+
+ }
+
+ onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let div = ref.current!;
+ div.style.opacity = "1";
+ }
+
+ onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let div = ref.current!;
+ div.style.opacity = "0";
+ }
+
+
+ private _reac: (undefined | IReactionDisposer) = undefined;
+ private _plotList: ([string, StrokeData] | undefined) = undefined;
+ private _interpolationKeyframe: (Doc | undefined) = undefined;
+ private _type: string = "";
+
+ @action
+ onContainerDown = (kf: Doc, type: string) => {
+ let listenerCreated = false;
+ this._type = type;
+ this.props.collection.backgroundColor = "rgb(0,0,0)";
+ this._reac = reaction(() => {
+ return this.inks;
+ }, data => {
+ if (!listenerCreated) {
+ this._plotList = Array.from(data!)[data!.size - 1]!;
+ this._interpolationKeyframe = kf;
+ document.addEventListener("pointerup", this.onReactionListen);
+ listenerCreated = true;
+ }
+ });
+
+
+ }
+
+ @action
+ onReactionListen = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let message = prompt("GRAPHING MODE: Enter gain");
+ if (message) {
+ let messageContent = parseInt(message, 10);
+ if (messageContent === NaN) {
+ this._gain = Infinity;
+ } else {
+ this._gain = messageContent;
+ }
+
+ }
+ if (this._reac && this._plotList && this._interpolationKeyframe) {
+ this.props.collection.backgroundColor = "#FFF";
+ this._reac();
+ let xPlots = new List<number>();
+ let yPlots = new List<number>();
+ let maxY = 0;
+ let minY = Infinity;
+ let pathData = this._plotList![1].pathData;
+ for (let i = 0; i < pathData.length - 1;) {
+ let val = pathData[i];
+ if (val.y > maxY) {
+ maxY = val.y;
+ }
+ if (val.y < minY) {
+ minY = val.y;
+ }
+ xPlots.push(val.x);
+ yPlots.push(val.y);
+ let increment = Math.floor(pathData.length / this._gain);
+ if (pathData.length > this._gain) {
+ if (i + increment < pathData.length) {
+ i = i + increment;
+ } else {
+ i = pathData.length - 1;
+ }
+ } else {
+ i++;
+ }
+ }
+ let index = this.keyframes.indexOf(this._interpolationKeyframe!);
+ if (this._type === "interpolate"){
+ (Cast(this.regiondata.functions![index], Doc) as Doc).interpolationX = xPlots;
+ (Cast(this.regiondata.functions![index], Doc) as Doc).interpolationY = yPlots;
+ } else if (this._type === "path") {
+ (Cast(this.regiondata.functions![index], Doc) as Doc).pathX = xPlots;
+ (Cast(this.regiondata.functions![index], Doc) as Doc).pathY = yPlots;
+ }
+ this._reac = undefined;
+ this._interpolationKeyframe = undefined;
+ this._plotList = undefined;
+ this._type = "";
+ document.removeEventListener("pointerup", this.onReactionListen);
+ }
+ }
+ render() {
+ return (
+ <div>
+ <div className="bar" ref={this._bar} style={{ transform: `translate(${this.pixelPosition}px)`,
+ width: `${this.pixelDuration}px`,
+ background: `linear-gradient(90deg, rgba(77, 153, 0, 0) 0%, rgba(77, 153, 0, 1) ${this.pixelFadeIn / this.pixelDuration}%, rgba(77, 153, 0, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(77, 153, 0, 0) 100% )` }}
+ onPointerDown={this.onBarPointerDown}>
+ <div className="leftResize" onPointerDown={this.onResizeLeft} ></div>
+ <div className="rightResize" onPointerDown={this.onResizeRight}></div>
+ {this.keyframes.map(kf => {
+ if (kf.type as KeyframeFunc.KeyframeType === KeyframeFunc.KeyframeType.default) {
+ return (
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ <div className="keyframeCircle" onPointerDown={(e) => { this.moveKeyframe(e, kf); }} onContextMenu={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.makeKeyframeMenu(kf, e.nativeEvent);
+ }}></div>
+ </div>
+ );
+ }
+ return (
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ </div>
+ );
+ })}
+ {this.keyframes.map( kf => {
+ if(this.keyframes.indexOf(kf ) !== this.keyframes.length - 1) {
+ let left = this.keyframes[this.keyframes.indexOf(kf) + 1];
+ let bodyRef = React.createRef<HTMLDivElement>();
+ let kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ let leftPos = KeyframeFunc.convertPixelTime(NumCast(left!.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ return (
+ <div ref={bodyRef}className="body-container" style={{left: `${kfPos - this.pixelPosition}px`, width:`${leftPos - kfPos}px`}}
+ onPointerOver={(e) => { this.onContainerOver(e, bodyRef); }}
+ onPointerOut={(e) => { this.onContainerOut(e, bodyRef); }}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this._mouseToggled = true;
+ this.makeRegionMenu(kf, e.nativeEvent);
+ }}>
+ </div>
+ );
+ }
+ })}
+
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
new file mode 100644
index 000000000..1457d5a84
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -0,0 +1,122 @@
+@import "./../globalCssVariables.scss";
+
+.minimize{
+ position:relative;
+ z-index: 1000;
+ height: 30px;
+ width: 100px;
+}
+
+.timeline-toolbox{
+ position:absolute;
+ display:flex;
+ align-items: flex-start;
+ flex-direction: row;
+ top: 10px;
+ div{
+ margin-left:10px;
+ }
+}
+.timeline-container{
+ width:100%;
+ height:300px;
+ position:absolute;
+ background-color: $light-color-secondary;
+ box-shadow: 0px 10px 20px;
+
+ .info-container{
+ margin-top: 50px;
+ right:20px;
+ position:absolute;
+ height: calc(100% - 100px);
+ width: calc(100% - 140px);
+ overflow: hidden;
+
+ .scrubberbox{
+ position:absolute;
+ background-color: transparent;
+ height: 30px;
+ width:100%;
+
+ .tick{
+ height:100%;
+ width: 1px;
+ background-color:black;
+
+ }
+ }
+ .scrubber{
+ top:30px;
+ height: 100%;
+ width: 2px;
+ position:absolute;
+ z-index: 1001;
+ background-color:black;
+ .scrubberhead{
+ top: -30px;
+ height: 30px;
+ width: 30px;
+ background-color:transparent;
+ border-radius: 50%;
+ border: 5px solid black;
+ left: -15px;
+ position:absolute;
+ }
+ }
+
+ .trackbox{
+ top: 30px;
+ height:calc(100% - 30px);
+ width:100%;
+ border:1px;
+ overflow:hidden;
+ background-color:white;
+ position:absolute;
+ box-shadow: -10px 0px 10px 10px grey;
+ }
+
+ }
+ .title-container{
+ margin-top: 80px;
+ margin-left: 20px;
+ height: calc(100% - 100px - 30px);
+ width: 100px;
+ background-color:white;
+ overflow: hidden;
+ .datapane{
+ top:0px;
+ width: 100px;
+ height: 75px;
+ border: 1px solid $dark-color;
+ background-color: $intermediate-color;
+ color: white;
+ position:relative;
+ float:left;
+ border-style:solid;
+ }
+ }
+ .resize{
+ bottom: 5px;
+ position:absolute;
+ height: 30px;
+ width: 50px;
+ left: calc(50% - 25px);
+ }
+}
+
+
+
+.overview{
+ position: absolute;
+ height: 50px;
+ width: 200px;
+ background-color: black;
+ .container{
+ position: absolute;
+ float: left 0px;
+ top: 25%;
+ height: 75%;
+ width: 100%;
+ background-color: grey;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
new file mode 100644
index 000000000..c50ffa51b
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -0,0 +1,437 @@
+import * as React from "react";
+import "./Timeline.scss";
+import { listSpec } from "../../../new_fields/Schema";
+import { observer } from "mobx-react";
+import { Track } from "./Track";
+import { observable, reaction, action, IReactionDisposer, computed, runInAction, observe, toJS } from "mobx";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPlayCircle, faBackward, faForward, faGripLines, faArrowUp, faArrowDown, faClock, faPauseCircle, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
+import { TimelineOverview } from "./TimelineOverview";
+import { FieldViewProps } from "../nodes/FieldView";
+import { KeyframeFunc } from "./Keyframe";
+
+
+
+export interface FlyoutProps {
+ x?: number;
+ y?: number;
+ display?: string;
+ regiondata?: Doc;
+ regions?: List<Doc>;
+}
+
+
+@observer
+export class Timeline extends React.Component<FieldViewProps> {
+
+ private readonly DEFAULT_CONTAINER_HEIGHT: number = 300;
+ private readonly DEFAULT_TICK_SPACING: number = 50;
+ private readonly MIN_CONTAINER_HEIGHT: number = 205;
+ private readonly MAX_CONTAINER_HEIGHT: number = 800;
+ private readonly DEFAULT_TICK_INCREMENT: number = 1000;
+
+ @observable private _isMinimized = false;
+ @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
+ @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
+
+ @observable private _scrubberbox = React.createRef<HTMLDivElement>();
+ @observable private _scrubber = React.createRef<HTMLDivElement>();
+ @observable private _trackbox = React.createRef<HTMLDivElement>();
+ @observable private _titleContainer = React.createRef<HTMLDivElement>();
+ @observable private _timelineContainer = React.createRef<HTMLDivElement>();
+ @observable private _timelineWrapper = React.createRef<HTMLDivElement>();
+ @observable private _infoContainer = React.createRef<HTMLDivElement>();
+
+ @observable private _currentBarX: number = 0;
+ @observable private _windSpeed: number = 1;
+ @observable private _isPlaying: boolean = false; //scrubber playing
+ @observable private _isFrozen: boolean = true; //timeline freeze
+ @observable private _totalLength: number = 0;
+ @observable private _visibleLength: number = 0;
+ @observable private _visibleStart: number = 0;
+ @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _time = 100000; //DEFAULT
+ @observable private _ticks: number[] = [];
+ @observable private _playButton = faPlayCircle;
+ @observable private _timelineVisible = false;
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+ @observable private _reactionDisposer:IReactionDisposer[] = [];
+
+
+ @computed
+ private get children(): List<Doc> {
+ let extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type));
+ if (extendedDocument) {
+ if (this.props.Document.data_ext) {
+ return Cast((Cast(this.props.Document.data_ext, Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>;
+ } else {
+ return new List<Doc>();
+ }
+ }
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>;
+ }
+
+
+ componentWillMount() {
+ this.props.Document.isAnimating ? this.props.Document.isAnimating = true : this.props.Document.isAnimating = false;
+ }
+
+ componentDidMount() {
+ if (StrCast(this.props.Document.type) === "video") {
+ console.log("video");
+ console.log(this.props.Document.duration);
+ if (this.props.Document.duration) {
+ this._time = Math.round(NumCast(this.props.Document.duration)) * 1000;
+ this._reactionDisposer.push(reaction(() => {
+ return NumCast(this.props.Document.curPage);
+ }, curPage => {
+ if (!this._isPlaying) {
+ this.changeCurrentBarX(curPage * this._tickIncrement / this._tickSpacing);
+ this.props.Document.curPage = this._currentBarX;
+ this.play();
+ }
+ }));
+ }
+ }
+ runInAction(() => {
+ this._reactionDisposer.push(reaction(() => {
+ return this._time;
+ }, () => {
+ this._ticks = [];
+ for (let i = 0; i < this._time;) {
+ this._ticks.push(i);
+ i += 1000;
+ }
+ this._totalLength = this._tickSpacing * (this._time/ this._tickIncrement);
+ }, {fireImmediately:true}));
+ this._totalLength = this._tickSpacing * (this._ticks.length/ this._tickIncrement);
+ this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width;
+ this._visibleStart = this._infoContainer.current!.scrollLeft;
+ });
+
+
+ }
+
+ @action
+ changeCurrentBarX = (pixel: number) => {
+ pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel;
+ }
+
+ //for playing
+ @action
+ onPlay = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.play();
+ }
+
+ @action
+ play = () => {
+ if (this._isPlaying) {
+ this._isPlaying = false;
+ this._playButton = faPlayCircle;
+ } else {
+ this._isPlaying = true;
+ this._playButton = faPauseCircle;
+ const playTimeline = () => {
+ if (this._isPlaying){
+ if (this._currentBarX >= this._totalLength) {
+ this.changeCurrentBarX(0);
+ } else {
+ this.changeCurrentBarX(this._currentBarX + this._windSpeed);
+ }
+ setTimeout(playTimeline, 15);
+ }
+ };
+ playTimeline();
+ }
+ }
+
+
+
+ @action
+ windForward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed < 64) { //max speed is 32
+ this._windSpeed = this._windSpeed * 2;
+ }
+ }
+
+ @action
+ windBackward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed > 1 / 16) { // min speed is 1/8
+ this._windSpeed = this._windSpeed / 2;
+ }
+ }
+
+ //for scrubber action
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ });
+ }
+
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let scrubberbox = this._scrubberbox.current!;
+ let left = scrubberbox.getBoundingClientRect().left;
+ let offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale;
+ this.changeCurrentBarX(offsetX);
+ }
+
+ @action
+ onScrubberClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let scrubberbox = this._scrubberbox.current!;
+ let offsetX = (e.clientX - scrubberbox.getBoundingClientRect().left) * this.props.ScreenToLocalTransform().Scale;
+ this.changeCurrentBarX(offsetX);
+ }
+
+
+
+ @action
+ onPanDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let clientX = e.clientX;
+ if (this._doubleClickEnabled){
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {if(!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX - this._trackbox.current!.getBoundingClientRect().left);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;}, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onPanMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onPanMove);
+ if (!this._doubleClickEnabled) {
+ this._mouseToggled = false;
+ }
+ });
+
+ }
+ }
+
+ @action
+ onPanMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0 || e.movementY !== 0) {
+ this._mouseToggled = true;
+ }
+ let trackbox = this._trackbox.current!;
+ let titleContainer = this._titleContainer.current!;
+ this.movePanX(this._visibleStart - e.movementX);
+ trackbox.scrollTop = trackbox.scrollTop - e.movementY;
+ titleContainer.scrollTop = titleContainer.scrollTop - e.movementY;
+ }
+ @action
+ movePanX = (pixel:number) => {
+ let infoContainer = this._infoContainer.current!;
+ infoContainer.scrollLeft = pixel;
+ this._visibleStart = infoContainer.scrollLeft;
+ }
+
+
+ @action
+ onResizeDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onResizeMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onResizeMove);
+ });
+ }
+
+ @action
+ onResizeMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom;
+ if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MIN_CONTAINER_HEIGHT;
+ } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MAX_CONTAINER_HEIGHT;
+ } else {
+ this._containerHeight += offset;
+ }
+ }
+
+ @action
+ onTimelineDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ if (e.nativeEvent.which === 1 && !this._isFrozen) {
+ document.addEventListener("pointermove", this.onTimelineMove);
+ document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onTimelineMove); });
+ }
+ }
+
+ @action
+ onTimelineMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let timelineContainer = this._timelineWrapper.current!;
+ let left = parseFloat(timelineContainer.style.left!);
+ let top = parseFloat(timelineContainer.style.top!);
+ timelineContainer.style.left = `${left + e.movementX}px`;
+ timelineContainer.style.top = `${top + e.movementY}px`;
+ }
+
+ @action
+ minimize = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let timelineContainer = this._timelineContainer.current!;
+ if (this._isMinimized) {
+ this._isMinimized = false;
+ timelineContainer.style.visibility = "visible";
+ } else {
+ this._isMinimized = true;
+ timelineContainer.style.visibility = "hidden";
+ }
+ }
+
+ @action
+ toReadTime = (time: number): string => {
+ const inSeconds = time / 1000;
+ let min: (string | number) = Math.floor(inSeconds / 60);
+ let sec: (string | number) = inSeconds % 60;
+
+ if (Math.floor(sec / 10) === 0) {
+ sec = "0" + sec;
+ }
+ return `${min}:${sec}`;
+ }
+
+ timelineContextMenu = (e:MouseEvent): void => {
+ let subitems: ContextMenuProps[] = [];
+ let timelineContainer = this._timelineWrapper.current!;
+ subitems.push({
+ description: "Pin to Top", event: action(() => {
+ if (!this._isFrozen) {
+ timelineContainer.style.left = "0px";
+ timelineContainer.style.top = "0px";
+ timelineContainer.style.transition = "none";
+ }
+ }), icon: faArrowUp
+ });
+ subitems.push({
+ description: this._isFrozen ? "Unfreeze Timeline" : "Freeze Timeline", event: action(() => {
+ this._isFrozen = !this._isFrozen;
+ }), icon: "thumbtack"
+ });
+ subitems.push({
+ description: this._timelineVisible ? "Hide Timeline" : "Show Timeline", event: action(() => {
+ this._timelineVisible = !this._timelineVisible;
+ }), icon: this._timelineVisible ? faEyeSlash : "eye"
+ });
+ subitems.push({ description: BoolCast(this.props.Document.isAnimating) ? "Enter Play Mode" : "Enter Authoring Mode", event: () => {
+ BoolCast(this.props.Document.isAnimating) ? this.props.Document.isAnimating = false : this.props.Document.isAnimating = true;}
+ , icon:BoolCast(this.props.Document.isAnimating) ? "play" : "edit"});
+ ContextMenu.Instance.addItem({ description: "Timeline Funcs...", subitems: subitems, icon: faClock });
+ }
+
+ @action
+ onWheelZoom = (e: React.WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left;
+ let prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement);
+ let prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX,"mili", "time", this._tickSpacing, this._tickIncrement);
+ e.deltaY < 0 ? this.zoom(true) : this.zoom(false);
+ let currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ let currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._infoContainer.current!.scrollLeft = currPixel - offset;
+ this._visibleStart = currPixel - offset;
+ this.changeCurrentBarX(currCurrent);
+ }
+
+ @action
+ zoom = (dir: boolean) => {
+ let spacingChange = this._tickSpacing;
+ let incrementChange = this._tickIncrement;
+ if (dir){
+ if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)){
+ if (this._tickSpacing >= 100) {
+ incrementChange /= 2;
+ spacingChange = 50;
+ } else {
+ spacingChange += 5;
+ }
+ }
+ } else {
+ if (this._tickSpacing <= 50) {
+ spacingChange = 100;
+ incrementChange *= 2;
+ } else {
+ spacingChange -= 5;
+ }
+ }
+ let finalLength = spacingChange * (this._time / incrementChange);
+ if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width){
+ this._totalLength = finalLength;
+ this._tickSpacing = spacingChange;
+ this._tickIncrement = incrementChange;
+ }
+ }
+
+ private timelineToolBox = (scale:number) => {
+ let size = 50 * scale; //50 is default
+ return (
+ <div key="timeline_toolbox" className="timeline-toolbox" style={{height:`${size}px`}}>
+ <div key="timeline_windBack" onClick={this.windBackward}> <FontAwesomeIcon icon={faBackward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <div key =" timeline_play" onClick={this.onPlay}> <FontAwesomeIcon icon={this._playButton} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <div key="timeline_windForward" onClick={this.windForward}> <FontAwesomeIcon icon={faForward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <TimelineOverview scale={scale} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX}/>
+ </div>
+ );
+ }
+ render() {
+ return (
+ <div style={{visibility: this._timelineVisible ? "visible" : "hidden"}}>
+ <div key="timeline_wrapper" style={{visibility: BoolCast(this.props.Document.isAnimating && this._timelineVisible) ? "visible" :"hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)"}} ref={this._timelineWrapper}>
+ <button key="timeline_minimize" className="minimize" onClick={this.minimize}>Minimize</button>
+ <div key="timeline_container" className="timeline-container" style={{ height: `${this._containerHeight}px`, left: "0px", top: "30px" }} ref={this._timelineContainer} onPointerDown={this.onTimelineDown}>
+ {this.timelineToolBox(0.5)}
+ <div key ="timeline_info"className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
+ <div key="timeline_scrubberbox" className="scrubberbox" ref={this._scrubberbox} style={{width: `${this._totalLength}px`}} onClick={this.onScrubberClick}>
+ {this._ticks.map(element => {
+ if(element % this._tickIncrement === 0) return <div className="tick" style={{ transform: `translate(${(element / this._tickIncrement)* this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toReadTime(element)}</p></div>;
+ })}
+ </div>
+ <div key="timeline_scrubber" className="scrubber" ref={this._scrubber} onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}>
+ <div key="timeline_scrubberhead" className="scrubberhead"></div>
+ </div>
+ <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{width: `${this._totalLength}px`}}>
+ {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing = {this._tickSpacing} tickIncrement ={this._tickIncrement} collection = {this.props.Document} timelineVisible = {this._timelineVisible}/>)}
+ </div>
+ </div>
+ <div key="timeline_title"className="title-container" ref={this._titleContainer}>
+ {DocListCast(this.children).map(doc => <div className="datapane"><p>{doc.title}</p></div>)}
+ </div>
+ <div key="timeline_resize" onPointerDown={this.onResizeDown}>
+ <FontAwesomeIcon className="resize" icon={faGripLines} />
+ </div>
+ </div>
+ </div>
+ {BoolCast(this.props.Document.isAnimating) ? <div></div>: this.timelineToolBox(1) }
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss
new file mode 100644
index 000000000..90cc53b4c
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.scss
@@ -0,0 +1,94 @@
+@import "./../globalCssVariables.scss";
+
+
+.timeline-menu-container{
+ position: absolute;
+ display: flex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+ background: whitesmoke;
+ z-index: 10000;
+ width: 150px;
+ padding-bottom: 10px;
+ border-radius: 15px;
+
+ border: solid #BBBBBBBB 1px;
+
+
+
+ .timeline-menu-input{
+ font: $sans-serif;
+ font-size: 13px;
+ width:100%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-left: 10px;
+ background-color: transparent;
+ border-width: 0px;
+ transition: border-width 500ms;
+ }
+
+ .timeline-menu-input:hover{
+ border-width: 2px;
+ }
+
+
+
+
+ .timeline-menu-header{
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ text-transform: uppercase;
+ background: $dark-color;
+ letter-spacing: 2px;
+
+ .timeline-menu-header-desc{
+ font:$sans-serif;
+ font-size: 13px;
+ text-align: center;
+ color: whitesmoke;
+ }
+ }
+
+
+ .timeline-menu-item {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: whitesmoke;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-style: none;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ color: grey;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-right: 20px;
+ padding-left: 10px;
+ }
+
+ .timeline-menu-item:hover {
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ border-top-style: solid;
+ background: $darker-alt-accent;
+ }
+
+ .timeline-menu-desc {
+ padding-left: 10px;
+ font:$sans-serif;
+ font-size: 13px;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
new file mode 100644
index 000000000..1fd97c6c1
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -0,0 +1,73 @@
+import * as React from "react";
+import {observable, action, runInAction} from "mobx";
+import {observer} from "mobx-react";
+import "./TimelineMenu.scss";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons";
+
+
+@observer
+export class TimelineMenu extends React.Component {
+ public static Instance:TimelineMenu;
+
+ @observable private _opacity = 0;
+ @observable private _x = 0;
+ @observable private _y = 0;
+ @observable private _currentMenu:JSX.Element[] = [];
+
+ constructor (props:Readonly<{}>){
+ super(props);
+ TimelineMenu.Instance = this;
+ }
+
+ @action
+ openMenu = (x?:number, y?:number) => {
+ this._opacity = 1;
+ x ? this._x = x : this._x = 0;
+ y ? this._y = y : this._y = 0;
+ }
+
+ @action
+ closeMenu = () => {
+ this._opacity = 0;
+ this._currentMenu = [];
+ this._x = -1000000;
+ this._y = -1000000;
+ }
+
+ @action
+ addItem = (type: "input" | "button", title: string, event: (e:any) => void) => {
+ if (type === "input"){
+ let inputRef = React.createRef<HTMLInputElement>();
+ this._currentMenu.push( <div className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => {
+ let text = e.target.value;
+ document.addEventListener("keypress", (e:KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ event(text);
+ this.closeMenu();
+ }
+ });
+ }}/></div>);
+ } else if (type === "button") {
+ let buttonRef = React.createRef<HTMLDivElement>();
+ this._currentMenu.push( <div className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => {
+ event(e);
+ this.closeMenu();
+ }}>{title}</p></div>);
+ }
+ }
+
+ @action
+ addMenu = (title:string) => {
+ this._currentMenu.unshift(<div className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
+ }
+
+ render() {
+ return (
+ <div className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} >
+ {this._currentMenu}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss
new file mode 100644
index 000000000..9e69c2adf
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.scss
@@ -0,0 +1,38 @@
+@import "./../globalCssVariables.scss";
+
+.timeline-overview-container{
+ width: 300px;
+ height: 40px;
+ margin-top: 10px;
+ margin-left: 20px;
+ background: white;
+ border: 2px solid black;
+ padding: 0px;
+ display:inline-block;
+ .timeline-overview-visible{
+ height: 100%;
+ background: green;
+ margin: 0px;
+ }
+ .timeline-overview-scrubber-container{
+ height: 100%;
+ margin-top: -40px;
+ margin-left: 0px;
+ width: 2px;
+ z-index: 1001;
+ background-color:black;
+ display: inline-block;
+ .timeline-overview-scrubber-head{
+ position:absolute;
+ height: 30px;
+ width: 30px;
+ background-color:transparent;
+ border-radius: 50%;
+ border: 5px solid black;
+ margin-left: -15px;
+ top: -15px;
+
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
new file mode 100644
index 000000000..38b823cbc
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -0,0 +1,93 @@
+import * as React from "react";
+import {observable, action} from "mobx";
+import {observer} from "mobx-react";
+import "./TimelineOverview.scss";
+
+
+
+interface TimelineOverviewProps{
+ scale: number;
+ totalLength: number;
+ visibleLength:number;
+ visibleStart:number;
+ currentBarX:number;
+ changeCurrentBarX: (pixel:number) => void;
+ movePanX: (pixel:number) => any;
+}
+
+
+@observer
+export class TimelineOverview extends React.Component<TimelineOverviewProps>{
+ @observable private _visibleRef = React.createRef<HTMLDivElement>();
+ @observable private _scrubberRef = React.createRef<HTMLDivElement>();
+ private readonly DEFAULT_HEIGHT = 50;
+ private readonly DEFAULT_WIDTH = 300;
+
+ @action
+ onPointerDown = (e:React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPanX);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPanX = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ let movX = (this.props.visibleStart / this.props.totalLength)* (this.DEFAULT_WIDTH * this.props.scale) + e.movementX;
+ this.props.movePanX((movX / (this.DEFAULT_WIDTH * this.props.scale)) * this.props.totalLength);
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onScrubberDown = ( e:React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let scrubberRef = this._scrubberRef.current!;
+ let left = scrubberRef.getBoundingClientRect().left;
+ let offsetX = Math.round(e.clientX - left);
+ this.props.changeCurrentBarX(((offsetX / (this.DEFAULT_WIDTH * this.props.scale)) * this.props.totalLength) + this.props.currentBarX);
+ }
+
+ @action
+ onScrubberUp = (e:PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ }
+
+ render(){
+ return(
+ <div key="timeline-overview-container" className="timeline-overview-container" style={{height: `${this.DEFAULT_HEIGHT * this.props.scale * 0.8}px` ,width:`${this.DEFAULT_WIDTH * this.props.scale}`}}>
+ <div ref={this._visibleRef} key="timeline-overview-visible" className="timeline-overview-visible" style={{marginLeft:`${(this.props.visibleStart / this.props.totalLength)* this.DEFAULT_WIDTH * this.props.scale}px`, width:`${(this.props.visibleLength / this.props.totalLength) * this.DEFAULT_WIDTH * this.props.scale}px`}} onPointerDown={this.onPointerDown}></div>
+ <div ref={this._scrubberRef} key="timeline-overview-scrubber-container" className="timeline-overview-scrubber-container" style={{marginLeft:`${(this.props.currentBarX / this.props.totalLength) * this.DEFAULT_WIDTH * this.props.scale}px`, marginTop: `${-this.DEFAULT_HEIGHT * this.props.scale * 0.8}px`}} onPointerDown={this.onScrubberDown}>
+ <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head" style={{}}></div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+
diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss
new file mode 100644
index 000000000..c8d56edf6
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.scss
@@ -0,0 +1,15 @@
+@import "./../globalCssVariables.scss";
+
+.track-container{
+
+ .track {
+ .inner {
+ top:0px;
+ height: 75px;
+ width: calc(100%);
+ background-color: $light-color;
+ border: 1px solid $dark-color;
+ position:relative;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
new file mode 100644
index 000000000..89533c4df
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -0,0 +1,305 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, reaction, action, IReactionDisposer, computed, runInAction, autorun } from "mobx";
+import "./Track.scss";
+import { Doc, DocListCastAsync, DocListCast, Field } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe";
+import { Transform } from "../../util/Transform";
+import { Copy } from "../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../new_fields/ObjectField";
+
+interface IProps {
+ node: Doc;
+ currentBarX: number;
+ transform: Transform;
+ collection: Doc;
+ time: number;
+ tickIncrement: number;
+ tickSpacing: number;
+ timelineVisible: boolean;
+ changeCurrentBarX: (x: number) => void;
+}
+
+@observer
+export class Track extends React.Component<IProps> {
+ @observable private _inner = React.createRef<HTMLDivElement>();
+ @observable private _currentBarXReaction: any;
+ @observable private _timelineVisibleReaction: any;
+ @observable private _isOnKeyframe: boolean = false;
+ @observable private _onKeyframe: (Doc | undefined) = undefined;
+ @observable private _onRegionData: (Doc | undefined) = undefined;
+ @observable private _storedState: (Doc | undefined) = undefined;
+
+ @computed
+ private get regions() {
+ return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;
+ }
+
+ componentWillMount() {
+ if (!this.props.node.regions) {
+ this.props.node.regions = new List<Doc>();
+ }
+
+
+ }
+
+ componentDidMount() {
+ runInAction(async () => {
+ this._timelineVisibleReaction = this.timelineVisibleReaction();
+ this._currentBarXReaction = this.currentBarXReaction();
+ if (this.regions.length === 0) this.createRegion(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ this.props.node.hidden = false;
+ this.props.node.opacity = 1;
+ });
+
+ }
+
+ componentWillUnmount() {
+ runInAction(() => {
+ if (this._currentBarXReaction) this._currentBarXReaction();
+ if (this._timelineVisibleReaction) this._timelineVisibleReaction();
+ });
+ }
+
+ @action
+ saveKeyframe = async (ref: Doc, regiondata: Doc) => {
+ let keyframes: List<Doc> = (Cast(regiondata.keyframes, listSpec(Doc)) as List<Doc>);
+ let kfIndex: number = keyframes.indexOf(ref);
+ let kf = keyframes[kfIndex] as Doc;
+ if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades
+ kf.key = Doc.MakeCopy(this.props.node, true);
+ let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), kf); // lef keyframe, if it exists
+ let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), kf); //right keyframe, if it exists
+ if (leftkf!.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades
+ let edge: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), leftkf!);
+ edge!.key = Doc.MakeCopy(kf.key as Doc, true);
+ leftkf!.key = Doc.MakeCopy(kf.key as Doc, true);
+ (Cast(edge!.key, Doc)! as Doc).opacity = 0.1;
+ (Cast(leftkf!.key, Doc)! as Doc).opacity = 1;
+ }
+ if (rightkf!.type === KeyframeFunc.KeyframeType.fade) {
+ let edge: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata!, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement), rightkf!);
+ edge!.key = Doc.MakeCopy(kf.key as Doc, true);
+ rightkf!.key = Doc.MakeCopy(kf.key as Doc, true);
+ (Cast(edge!.key, Doc)! as Doc).opacity = 0.1;
+ (Cast(rightkf!.key, Doc)! as Doc).opacity = 1;
+ }
+ }
+ keyframes[kfIndex] = kf;
+ this._onKeyframe = undefined;
+ this._onRegionData = undefined;
+ this._isOnKeyframe = false;
+ }
+
+ @action
+ revertState = () => {
+ let copyDoc = Doc.MakeCopy(this.props.node, true);
+ if (this._storedState) this.applyKeys(this._storedState);
+ let newState = new Doc();
+ newState.key = copyDoc;
+ this._storedState = newState;
+ }
+
+ @action
+ currentBarXReaction = () => {
+ return reaction(() => this.props.currentBarX, async () => {
+ let regiondata: (Doc | undefined) = await this.findRegion(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ if (regiondata) {
+ this.props.node.hidden = false;
+ await this.timeChange(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ } else {
+ this.props.node.hidden = true;
+ this.props.node.opacity = 0;
+ }
+ });
+ }
+ @action
+ timelineVisibleReaction = () => {
+ return reaction(() => {
+ return this.props.timelineVisible;
+ }, isVisible => {
+ this.revertState();
+ });
+ }
+
+ @action
+ timeChange = async (time: number) => {
+ if (this._isOnKeyframe && this._onKeyframe && this._onRegionData) {
+ await this.saveKeyframe(this._onKeyframe, this._onRegionData);
+ }
+ let regiondata = await this.findRegion(Math.round(time)); //finds a region that the scrubber is on
+ if (regiondata) {
+ let leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); // lef keyframe, if it exists
+ let rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); //right keyframe, if it exists
+ let currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
+ if (currentkf) {
+ await this.applyKeys(currentkf);
+ this._isOnKeyframe = true;
+ this._onKeyframe = currentkf;
+ this._onRegionData = regiondata;
+ } else if (leftkf && rightkf) {
+ await this.interpolate(leftkf, rightkf, regiondata);
+ }
+ }
+ }
+
+ @action
+ private applyKeys = async (kf: Doc) => {
+ let kfNode = await Cast(kf.key, Doc) as Doc;
+ let docFromApply = kfNode;
+ if (this.filterKeys(Doc.allKeys(this.props.node)).length > this.filterKeys(Doc.allKeys(kfNode)).length) docFromApply = this.props.node;
+ this.filterKeys(Doc.allKeys(docFromApply)).forEach(key => {
+ if (!kfNode[key]) {
+ this.props.node[key] = undefined;
+ } else {
+ let stored = kfNode[key];
+ if(stored instanceof ObjectField){
+ this.props.node[key] = stored[Copy]();
+ } else {
+ this.props.node[key] = stored;
+ }
+ }
+ });
+ }
+
+ private filterList = [
+ "regions",
+ "cursors",
+ "hidden",
+ "nativeHeight",
+ "nativeWidth",
+ "schemaColumns",
+ "baseLayout",
+ "backgroundLayout",
+ "layout",
+ ];
+
+ @action
+ private filterKeys = (keys: string[]): string[] => {
+ return keys.reduce((acc: string[], key: string) => {
+ if (!this.filterList.includes(key)) acc.push(key);
+ return acc;
+ }, []);
+ }
+
+ @action
+ calcCurrent = async (region: Doc) => {
+ let currentkf: (Doc | undefined) = undefined;
+ let keyframes = await DocListCastAsync(region.keyframes!);
+ keyframes!.forEach((kf) => {
+ if (NumCast(kf.time) === Math.round(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement))) currentkf = kf;
+ });
+ return currentkf;
+ }
+
+
+ @action
+ interpolate = async (left: Doc, right: Doc, regiondata: Doc) => {
+ let leftNode = left.key as Doc;
+ let rightNode = right.key as Doc;
+ const dif_time = NumCast(right.time) - NumCast(left.time);
+ const timeratio = (KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement) - NumCast(left.time)) / dif_time; //linear
+ let keyframes = (await DocListCastAsync(regiondata.keyframes!))!;
+ let indexLeft = keyframes.indexOf(left);
+ let interY: List<number> = (await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).interpolationY as List<number>)!;
+ let realIndex = (interY.length - 1) * timeratio;
+ let xIndex = Math.floor(realIndex);
+ let yValue = interY[xIndex];
+ let secondYOffset: number = yValue;
+ let minY = interY[0]; // for now
+ let maxY = interY[interY.length - 1]; //for now
+ if (interY.length !== 1) {
+ secondYOffset = interY[xIndex] + ((realIndex - xIndex) / 1) * (interY[xIndex + 1] - interY[xIndex]) - minY;
+ }
+ let finalRatio = secondYOffset / (maxY - minY);
+ let pathX: List<number> = await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).pathX as List<number>;
+ let pathY: List<number> = await ((regiondata.functions as List<Doc>)[indexLeft] as Doc).pathY as List<number>;
+ let proposedX = 0;
+ let proposedY = 0;
+ if (pathX.length !== 0) {
+ let realPathCorrespondingIndex = finalRatio * (pathX.length - 1);
+ let pathCorrespondingIndex = Math.floor(realPathCorrespondingIndex);
+ if (pathCorrespondingIndex >= pathX.length - 1) {
+ proposedX = pathX[pathX.length - 1];
+ proposedY = pathY[pathY.length - 1];
+ } else if (pathCorrespondingIndex < 0) {
+ proposedX = pathX[0];
+ proposedY = pathY[0];
+ } else {
+ proposedX = pathX[pathCorrespondingIndex] + ((realPathCorrespondingIndex - pathCorrespondingIndex) / 1) * (pathX[pathCorrespondingIndex + 1] - pathX[pathCorrespondingIndex]);
+ proposedY = pathY[pathCorrespondingIndex] + ((realPathCorrespondingIndex - pathCorrespondingIndex) / 1) * (pathY[pathCorrespondingIndex + 1] - pathY[pathCorrespondingIndex]);
+ }
+
+ }
+ this.filterKeys(Doc.allKeys(leftNode)).forEach(key => {
+ if (leftNode[key] && rightNode[key] && typeof (leftNode[key]) === "number" && typeof (rightNode[key]) === "number") { //if it is number, interpolate
+ if ((key === "x" || key === "y") && pathX.length !== 0) {
+ if (key === "x") this.props.node[key] = proposedX;
+ if (key === "y") this.props.node[key] = proposedY;
+ } else {
+ const diff = NumCast(rightNode[key]) - NumCast(leftNode[key]);
+ const adjusted = diff * finalRatio;
+ this.props.node[key] = NumCast(leftNode[key]) + adjusted;
+ }
+ } else {
+ let stored = leftNode[key];
+ if(stored instanceof ObjectField){
+ this.props.node[key] = stored[Copy]();
+ } else {
+ this.props.node[key] = stored;
+ }
+ }
+ });
+ }
+
+ @action
+ findRegion = async (time: number) => {
+ let foundRegion: (Doc | undefined) = undefined;
+ let regions = await DocListCastAsync(this.regions);
+ regions!.forEach(region => {
+ region = region as RegionData;
+ if (time >= NumCast(region.position) && time <= (NumCast(region.position) + NumCast(region.duration))) {
+ foundRegion = region;
+ }
+ });
+ return foundRegion;
+ }
+
+ @action
+ onInnerDoubleClick = (e: React.MouseEvent) => {
+ let inner = this._inner.current!;
+ let offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale);
+ this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+ createRegion = (position: number) => {
+ let regiondata = KeyframeFunc.defaultKeyframe();
+ regiondata.position = position;
+ let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);
+
+ if (rightRegion && rightRegion.position - regiondata.position <= 4000) {
+ regiondata.duration = rightRegion.position - regiondata.position;
+ }
+ if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
+ this.regions.push(regiondata);
+ return regiondata;
+ }
+
+ }
+ render() {
+ return (
+ <div className="track-container">
+ <div className="track">
+ <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick}>
+ {DocListCast(this.regions).map((region) => {
+ return <Keyframe {...this.props} RegionData={region} />;
+ })}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 774e6b1b9..c11dd6150 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -15,13 +15,13 @@ import { DocumentType } from "../../documents/DocumentTypes";
import { Docs, DocumentOptions } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
+import { DocComponent } from "../DocComponent";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -37,7 +37,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
ruleProvider: Doc | undefined;
}
-export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
+export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
private _childLayoutDisposer?: IReactionDisposer;
@@ -128,7 +128,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
let added = false;
if (de.data.dropAction || de.data.userDropAction) {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
} else if (de.data.moveDocument) {
let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
// note that it's possible the drag function might create a drop document that's not the same as the
@@ -136,7 +139,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
added = movedDocs.reduce((added: boolean, d, i) =>
de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false);
} else {
- added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
}
e.stopPropagation();
return added;
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 64411b5fe..7217b6f30 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -34,6 +34,18 @@
outline-color: black;
}
+ .collectionViewBaseChrome-button{
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: purple;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 10px;
+ }
+
.collectionViewBaseChrome-collapse {
transition: all .5s, opacity 0.3s;
position: absolute;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 7510b86a0..20786f690 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -192,9 +192,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action.bound
applyFilter = (e: React.MouseEvent) => {
-
this.openViewSpecs(e);
-
let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";
let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 36e62842c..5157d0c75 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -37,6 +37,9 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
+import v5 = require("uuid/v5");
+import { Timeline } from "../../animationtimeline/Timeline";
+import { number } from "prop-types";
import { DocServer } from "../../../DocServer";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
@@ -186,6 +189,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _lastY: number = 0;
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
+ private _timelineRef = React.createRef<Timeline>();
private get parentScaling() {
return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
}
@@ -773,6 +777,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: pos.width || 0, height: pos.height || 0 }
});
}
+ // }
return prev;
}, elements);
@@ -845,6 +850,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
}
+ this._timelineRef.current!.timelineContextMenu(e.nativeEvent);
layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
@@ -879,6 +885,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
input.click();
}
});
+ //@ts-ignore
+ let subitems: ContextMenuProps[] =
+ DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
+ description: (i + 1) + ": " + StrCast(note.title),
+ event: () => console.log("Hi"),
+ icon: "eye"
+ }));
layoutItems.push({
description: "Add Note ...",
@@ -925,7 +938,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
};
}
-
render() {
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x;
@@ -952,6 +964,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
+ <Timeline ref={this._timelineRef} {...this.props} />
{this.overlayViews}
<CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
</div>
diff --git a/src/client/views/graph/Graph.tsx b/src/client/views/graph/Graph.tsx
new file mode 100644
index 000000000..d925cc32c
--- /dev/null
+++ b/src/client/views/graph/Graph.tsx
@@ -0,0 +1,32 @@
+import * as React from "react";
+import {observable} from "mobx";
+import { observer } from "mobx-react";
+import { Document, listSpec } from "../../../new_fields/Schema";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionSubView, CollectionViewProps, SubCollectionViewProps } from "../collections/CollectionSubView";
+
+
+
+
+export class Graph extends CollectionSubView(Document) {
+ static Instance:Graph;
+
+ private constructor(props:SubCollectionViewProps) {
+ super(props);
+ Graph.Instance = this;
+ }
+
+
+
+
+ render() {
+ let collection = <CollectionFreeFormView {...this.props}/>;
+
+ return (
+ <div>
+ </div>
+
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/graph/GraphManager.ts b/src/client/views/graph/GraphManager.ts
new file mode 100644
index 000000000..b62f2337b
--- /dev/null
+++ b/src/client/views/graph/GraphManager.ts
@@ -0,0 +1,45 @@
+
+
+import {Graph} from "./Graph";
+import {observable, computed} from 'mobx';
+import { Dictionary } from "typescript-collections";
+import { string } from "prop-types";
+import { Doc } from "../../../new_fields/Doc";
+
+
+export class GraphManager {
+ @observable public Graphs: Graph[] = [];
+
+ @observable public GraphData: Doc = new Doc();
+
+ private static _instance: GraphManager;
+
+ @computed
+ public static get Instance():GraphManager {
+ return this._instance || (this._instance = new this());
+ }
+
+ private constructor(){
+
+ }
+
+
+
+
+ public set addGraph(graph:Graph){
+ this.Graphs.push(graph);
+ }
+
+
+ defaultGraphs = () => {
+
+ }
+
+
+
+
+
+
+
+
+} \ No newline at end of file
diff --git a/src/client/views/graph/GraphMenu.tsx b/src/client/views/graph/GraphMenu.tsx
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/graph/GraphMenu.tsx
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d8cfff973..014067d06 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -96,6 +96,10 @@ export interface DocumentViewProps {
}
export const documentSchema = createSchema({
+ layout: "string", // should also allow Doc but that can't be expressed in the schema
+ hidden: "boolean",
+ excludeFromLibrary: "boolean",
+ fitToBox: "boolean",
// layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
title: "string", // document title (can be on either data document or layout)
nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b7d9a1eab..9469a2a0f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -154,6 +154,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect;
}
}
+
+ this.player && (this.player.style.transform = "");
}
componentWillUnmount() {