From 661c1367d27fa23c3aeb62369e92cd36eb5edabd Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 21 Oct 2023 00:41:23 -0400 Subject: change to doc decorations to be more "lightweight". made linkBox render links in a freeform view as a DocView. added an auto-reset view option for freeforms. fixed highlighting ink strokes. Made groups behave better for selecting things 'inside' the group bounding box that aren't in the group. Added vertically centered text option. --- src/fields/Doc.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'src/fields') diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 9e3eb28f9..feacdc9c5 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -687,16 +687,12 @@ export namespace Doc { */ export function ComputeContentBounds(docList: Doc[]) { const bounds = docList.reduce( - (bounds, doc) => { - const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - const [bptX, bptY] = [sptX + doc[Width](), sptY + doc[Height]()]; - return { - x: Math.min(sptX, bounds.x), - y: Math.min(sptY, bounds.y), - r: Math.max(bptX, bounds.r), - b: Math.max(bptY, bounds.b), - }; - }, + (bounds, doc) => ({ + x: Math.min(NumCast(doc.x), bounds.x), + y: Math.min(NumCast(doc.y), bounds.y), + r: Math.max(NumCast(doc.x) + doc[Width](), bounds.r), + b: Math.max(NumCast(doc.y) + doc[Height](), bounds.b), + }), { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE } ); return bounds; -- cgit v1.2.3-70-g09d2 From 545508987903be8c2f361bbee8b3beae683c73b5 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 26 Oct 2023 23:23:48 -0400 Subject: a variety of fixes to the animation timeline to make it make some sense. lots still broken. --- src/client/views/animationtimeline/Keyframe.scss | 105 ---- src/client/views/animationtimeline/Keyframe.tsx | 591 --------------------- src/client/views/animationtimeline/Region.scss | 106 ++++ src/client/views/animationtimeline/Region.tsx | 585 ++++++++++++++++++++ src/client/views/animationtimeline/Timeline.tsx | 49 +- .../views/animationtimeline/TimelineMenu.tsx | 100 ++-- .../views/animationtimeline/TimelineOverview.tsx | 112 ++-- src/client/views/animationtimeline/Track.tsx | 77 +-- src/fields/List.ts | 5 +- 9 files changed, 877 insertions(+), 853 deletions(-) delete mode 100644 src/client/views/animationtimeline/Keyframe.scss delete mode 100644 src/client/views/animationtimeline/Keyframe.tsx create mode 100644 src/client/views/animationtimeline/Region.scss create mode 100644 src/client/views/animationtimeline/Region.tsx (limited to 'src/fields') diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss deleted file mode 100644 index 38eb103c6..000000000 --- a/src/client/views/animationtimeline/Keyframe.scss +++ /dev/null @@ -1,105 +0,0 @@ -@import "./../global/globalCssVariables.scss"; - - -$timelineColor: #9acedf; -$timelineDark: #77a1aa; - -.bar { - height: 100%; - width: 5px; - position: absolute; - - // pointer-events: none; - .menubox { - width: 200px; - height: 200px; - top: 50%; - position: relative; - background-color: $white; - - .menutable { - tr:nth-child(odd) { - background-color: $light-gray; - } - } - } - - .leftResize { - left: -10px; - border: 3px solid black; - } - - .rightResize { - right: -10px; - border: 3px solid black; - } - - .keyframe-indicator { - height: 20px; - width: 20px; - top: calc(50% - 10px); - background-color: white; - -ms-transform: rotate(45deg); - -webkit-transform: rotate(45deg); - transform: rotate(45deg); - z-index: 1000; - position: absolute; - } - - .keyframe-information { - display: none; - position: relative; - // z-index: 100000; - // background: $timelineDark; - width: 100px; - // left: -50px; - height: 100px; - // top: 40px; - } - - .keyframeCircle { - left: -10px; - border: 3px solid $timelineDark; - } - - .fadeLeft { - left: 0px; - height: 100%; - position: absolute; - pointer-events: none; - background: linear-gradient(to left, $timelineColor 10%, $white); - } - - .fadeRight { - right: 0px; - height: 100%; - position: absolute; - pointer-events: none; - background: linear-gradient(to right, $timelineColor 10%, $white); - } - - .divider { - height: 100%; - width: 1px; - position: absolute; - background-color: black; - cursor: col-resize; - pointer-events: none; - } - - .keyframe { - height: 100%; - position: absolute; - } - - .fadeIn-container, - .fadeOut-container, - .body-container { - position: absolute; - height: 100%; - background-color: rgba(0, 0, 0, 0.5); - opacity: 0; - } - - -} \ No newline at end of file diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx deleted file mode 100644 index addc00c85..000000000 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ /dev/null @@ -1,591 +0,0 @@ -import { action, computed, observable, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; -import { List } from '../../../fields/List'; -import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema'; -import { Cast, NumCast } from '../../../fields/Types'; -import { Docs } from '../../documents/Documents'; -import { Transform } from '../../util/Transform'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; -import '../global/globalCssVariables.scss'; -import { OpenWhereMod } from '../nodes/DocumentView'; -import './Keyframe.scss'; -import './Timeline.scss'; -import { TimelineMenu } from './TimelineMenu'; - -/** - * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also - */ -export namespace KeyframeFunc { - export enum KeyframeType { - end = 'end', - fade = 'fade', - default = 'default', - } - - export enum Direction { - left = 'left', - right = 'right', - } - - export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { - let leftMost: RegionData | undefined = undefined; - let rightMost: RegionData | undefined = undefined; - regions.forEach(region => { - const 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 = (region: Doc, currentBarX: number, ref?: Doc) => { - //returns the time of the closet keyframe to the left - let leftKf: Opt; - let time: number = 0; - const keyframes = DocListCast(region.keyframes!); - keyframes.map(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 = (region: Doc, currentBarX: number, ref?: Doc) => { - //returns the time of the closest keyframe to the right - let rightKf: Opt; - let time: number = Infinity; - DocListCast(region.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 = () => { - const regiondata = new Doc(); //creating regiondata in MILI - regiondata.duration = 4000; - regiondata.position = 0; - regiondata.fadeIn = 1000; - regiondata.fadeOut = 1000; - regiondata.functions = new List(); - regiondata.hasData = false; - return regiondata; - }; - - export const convertPixelTime = (pos: number, unit: 'mili' | 'sec' | 'min' | 'hr', dir: 'pixel' | 'time', tickSpacing: number, tickIncrement: number) => { - const 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), - hasData: defaultSpec('boolean', false), -}); -export type RegionData = makeInterface<[typeof RegionDataSchema]>; -export const RegionData = makeInterface(RegionDataSchema); - -interface IProps { - animatedDoc: Doc; - RegionData: Doc; - collection: Doc; - tickSpacing: number; - tickIncrement: number; - time: number; - changeCurrentBarX: (x: number) => void; - transform: Transform; - makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc; -} - -/** - * - * This class handles the green region stuff - * Key facts: - * - * Structure looks like this - * - * region as a whole - * <------------------------------REGION-------------------------------> - * - * region broken down - * - * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void......... - * (start) (Fade 2) - * (fade 1) (finish) - * - * - * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. - * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop. - * If you want to know the exact location of the computer, message me. - * - * @author Andrew Kim - */ -@observer -export class Keyframe extends React.Component { - @observable private _bar = React.createRef(); - @observable private _mouseToggled = false; - @observable private _doubleClickEnabled = false; - - @computed private get regiondata() { - return RegionData(this.props.RegionData); - } - @computed private get regions() { - return DocListCast(this.props.animatedDoc.regions); - } - @computed private get keyframes() { - return DocListCast(this.regiondata.keyframes); - } - @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); - } - - constructor(props: any) { - super(props); - } - componentDidMount() { - setTimeout(() => { - //giving it a temporary 1sec delay... - if (!this.regiondata.keyframes) this.regiondata.keyframes = new List(); - const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end); - const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade); - const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade); - const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end); - fadeIn.opacity = 1; - fadeOut.opacity = 1; - start.opacity = 0.1; - finish.opacity = 0.1; - this.forceUpdate(); //not needed, if setTimeout is gone... - }, 1000); - } - - @action - onBarPointerDown = (e: React.PointerEvent) => { - e.preventDefault(); - e.stopPropagation(); - const 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; - } - const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!; - const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!; - const prevX = this.regiondata.position; - const 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; - } - const 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(); - const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); - const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions); - if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { - this.regiondata.position = leftRegion.position + leftRegion.duration; - this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration); - } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) { - this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn; - this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn; - } else if (NumCast(this.keyframes[0].time) + offset <= 0) { - this.regiondata.position = 0; - this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time); - } 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(); - const bar = this._bar.current!; - const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); - const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions); - const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time); - if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { - //case 1: when third to last keyframe is in the way - this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut; - } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) { - this.regiondata.duration = rightRegion.position - this.regiondata.position; - } 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; - const bar = this._bar.current!; - const 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 - const position = this.regiondata.position; - this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default); - this.regiondata.hasData = true; - 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)); - }; - - /** - * custom keyframe context menu items (when clicking on the keyframe circle) - */ - @action - makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem('button', 'Toggle Fade Only', () => { - kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade; - }), - TimelineMenu.Instance.addItem( - 'button', - 'Delete', - action(() => { - (this.regiondata.keyframes as List).splice(this.keyframes.indexOf(kf), 1); - this.forceUpdate(); - }) - ), - TimelineMenu.Instance.addItem( - 'input', - 'Move', - action(val => { - let cannotMove: boolean = false; - const kfIndex: number = this.keyframes.indexOf(kf); - if (val < 0 || val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) { - cannotMove = true; - } - if (!cannotMove) { - this.keyframes[kfIndex].time = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; - } - }) - ); - TimelineMenu.Instance.addMenu('Keyframe'); - TimelineMenu.Instance.openMenu(e.clientX, e.clientY); - }; - - /** - * context menu for region (anywhere on the green region). - */ - @action - makeRegionMenu = (kf: Doc, e: MouseEvent) => { - TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.animatedDoc.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), - TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => { - runInAction(() => { - let cannotMove: boolean = false; - if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) { - cannotMove = true; - } - if (!cannotMove) { - this.regiondata.fadeIn = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; - } - }); - }), - TimelineMenu.Instance.addItem('input', `fadeOut: ${this.regiondata.fadeOut}ms`, val => { - runInAction(() => { - let cannotMove: boolean = false; - if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) { - cannotMove = true; - } - if (!cannotMove) { - this.regiondata.fadeOut = parseInt(val, 10); - this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val; - } - }); - }), - TimelineMenu.Instance.addItem('input', `position: ${this.regiondata.position}ms`, val => { - runInAction(() => { - const prevPosition = this.regiondata.position; - let cannotMove: boolean = false; - this.regions - .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) - .forEach(({ pos, dur }) => { - if (pos !== this.regiondata.position) { - if (val < 0 || (val > pos && val < pos + dur) || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur)) { - cannotMove = true; - } - } - }); - if (!cannotMove) { - this.regiondata.position = parseInt(val, 10); - this.updateKeyframes(this.regiondata.position - prevPosition); - } - }); - }), - TimelineMenu.Instance.addItem('input', `duration: ${this.regiondata.duration}ms`, val => { - runInAction(() => { - let cannotMove: boolean = false; - this.regions - .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) - .forEach(({ pos, dur }) => { - if (pos !== this.regiondata.position) { - val += this.regiondata.position; - if (val < 0 || (val > pos && val < pos + dur)) { - cannotMove = true; - } - } - }); - if (!cannotMove) { - this.regiondata.duration = parseInt(val, 10); - this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; - this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; - } - }); - }), - TimelineMenu.Instance.addMenu('Region'); - TimelineMenu.Instance.openMenu(e.clientX, e.clientY); - }; - - @action - updateKeyframes = (incr: number, filter: number[] = []) => { - this.keyframes.forEach(kf => { - if (!filter.includes(this.keyframes.indexOf(kf))) { - kf.time = NumCast(kf.time) + incr; - } - }); - }; - - /** - * hovering effect when hovered (hidden div darkens) - */ - @action - onContainerOver = (e: React.PointerEvent, ref: React.RefObject) => { - e.preventDefault(); - e.stopPropagation(); - const div = ref.current!; - div.style.opacity = '1'; - Doc.BrushDoc(this.props.animatedDoc); - }; - - /** - * hovering effect when hovered out (hidden div becomes invisible) - */ - @action - onContainerOut = (e: React.PointerEvent, ref: React.RefObject) => { - e.preventDefault(); - e.stopPropagation(); - const div = ref.current!; - div.style.opacity = '0'; - Doc.UnBrushDoc(this.props.animatedDoc); - }; - - ///////////////////////UI STUFF ///////////////////////// - - /** - * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades) - * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable) - */ - drawKeyframes = () => { - const keyframeDivs: JSX.Element[] = []; - return DocListCast(this.regiondata.keyframes).map(kf => { - if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) { - return ( - <> -
-
-
{ - e.preventDefault(); - e.stopPropagation(); - this.moveKeyframe(e, kf); - }} - onContextMenu={(e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.makeKeyframeMenu(kf, e.nativeEvent); - }} - onDoubleClick={e => { - e.preventDefault(); - e.stopPropagation(); - }}>
-
-
- - ); - } else { - return ( -
-
-
- ); - } - }); - }; - - /** - * drawing the hidden divs that partition different intervals within a region. - */ - @action - drawKeyframeDividers = () => { - const keyframeDividers: JSX.Element[] = []; - DocListCast(this.regiondata.keyframes).forEach(kf => { - const index = this.keyframes.indexOf(kf); - if (index !== this.keyframes.length - 1) { - const right = this.keyframes[index + 1]; - const bodyRef = React.createRef(); - const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); - const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); - keyframeDividers.push( -
{ - e.preventDefault(); - e.stopPropagation(); - this.onContainerOver(e, bodyRef); - }} - onPointerOut={e => { - e.preventDefault(); - e.stopPropagation(); - this.onContainerOut(e, bodyRef); - }} - onContextMenu={e => { - e.preventDefault(); - e.stopPropagation(); - if (index !== 0 || index !== this.keyframes.length - 2) { - this._mouseToggled = true; - } - this.makeRegionMenu(kf, e.nativeEvent); - }}>
- ); - } - }); - return keyframeDividers; - }; - - /** - * rendering that green region - */ - //154, 206, 223 - render() { - return ( -
-
- {/*
*/} -
- {/*
*/} - {this.drawKeyframes()} - {this.drawKeyframeDividers()} -
- ); - } -} diff --git a/src/client/views/animationtimeline/Region.scss b/src/client/views/animationtimeline/Region.scss new file mode 100644 index 000000000..f7476ab55 --- /dev/null +++ b/src/client/views/animationtimeline/Region.scss @@ -0,0 +1,106 @@ +@import './../global/globalCssVariables.scss'; + +$timelineColor: #9acedf; +$timelineDark: #77a1aa; + +.bar { + height: 100%; + width: 5px; + position: absolute; + + // pointer-events: none; + .menubox { + width: 200px; + height: 200px; + top: 50%; + position: relative; + background-color: $white; + + .menutable { + tr:nth-child(odd) { + background-color: $light-gray; + } + } + } + + .leftResize { + left: -10px; + border: 3px solid black; + transform: rotate(45deg) scale(0.25) !important; + background-color: black !important; + } + + .rightResize { + right: -10px; + border: 3px solid black; + transform: rotate(45deg) scale(0.25) !important; + background-color: black !important; + } + + .keyframe-indicator { + height: 20px; + width: 20px; + top: calc(50% - 10px); + background-color: white; + -ms-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + z-index: 1000; + position: absolute; + } + + .keyframe-information { + display: none; + position: relative; + // z-index: 100000; + // background: $timelineDark; + width: 100px; + // left: -50px; + height: 100px; + // top: 40px; + } + + .keyframeCircle { + left: -10px; + border: 3px solid $timelineDark; + } + + .fadeLeft { + left: 0px; + height: 100%; + position: absolute; + pointer-events: none; + background: linear-gradient(to left, $timelineColor 10%, $white); + } + + .fadeRight { + right: 0px; + height: 100%; + position: absolute; + pointer-events: none; + background: linear-gradient(to right, $timelineColor 10%, $white); + } + + .divider { + height: 100%; + width: 1px; + position: absolute; + background-color: black; + cursor: col-resize; + pointer-events: none; + } + + .keyframe { + height: 100%; + 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/Region.tsx b/src/client/views/animationtimeline/Region.tsx new file mode 100644 index 000000000..df00924c6 --- /dev/null +++ b/src/client/views/animationtimeline/Region.tsx @@ -0,0 +1,585 @@ +import { action, computed, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { List } from '../../../fields/List'; +import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema'; +import { Cast, NumCast } from '../../../fields/Types'; +import { Transform } from '../../util/Transform'; +import '../global/globalCssVariables.scss'; +import './Region.scss'; +import './Timeline.scss'; +import { TimelineMenu } from './TimelineMenu'; + +/** + * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also + */ +export namespace RegionHelpers { + export enum KeyframeType { + end = 'end', + fade = 'fade', + default = 'default', + } + + export enum Direction { + left = 'left', + right = 'right', + } + + export const findAdjacentRegion = (dir: RegionHelpers.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { + let leftMost: RegionData | undefined = undefined; + let rightMost: RegionData | undefined = undefined; + regions.forEach(region => { + const 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 = (region: Doc, currentBarX: number, ref?: Doc) => { + //returns the time of the closet keyframe to the left + let leftKf: Opt; + let time: number = 0; + const keyframes = DocListCast(region.keyframes!); + keyframes.map(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 = (region: Doc, currentBarX: number, ref?: Doc) => { + //returns the time of the closest keyframe to the right + let rightKf: Opt; + let time: number = Infinity; + DocListCast(region.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 = () => { + const regiondata = new Doc(); //creating regiondata in MILI + regiondata.duration = 4000; + regiondata.position = 0; + regiondata.fadeIn = 1000; + regiondata.fadeOut = 1000; + regiondata.functions = new List(); + regiondata.hasData = false; + return regiondata; + }; + + export const convertPixelTime = (pos: number, unit: 'mili' | 'sec' | 'min' | 'hr', dir: 'pixel' | 'time', tickSpacing: number, tickIncrement: number) => { + const 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), + hasData: defaultSpec('boolean', false), +}); +export type RegionData = makeInterface<[typeof RegionDataSchema]>; +export const RegionData = makeInterface(RegionDataSchema); + +interface IProps { + animatedDoc: Doc; + RegionData: Doc; + collection: Doc; + tickSpacing: number; + tickIncrement: number; + saveStateKf: Doc | undefined; + time: number; + changeCurrentBarX: (x: number) => void; + transform: Transform; + makeKeyData: (region: RegionData, pos: number, kftype: RegionHelpers.KeyframeType) => Doc; +} + +/** + * + * This class handles the green region stuff + * Key facts: + * + * Structure looks like this + * + * region as a whole + * <------------------------------REGION-------------------------------> + * + * region broken down + * + * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void......... + * (start) (Fade 2) + * (fade 1) (finish) + * + * + * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. + * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop. + * If you want to know the exact location of the computer, message me. + * + * @author Andrew Kim + */ +@observer +export class Region extends React.Component { + @observable private _bar = React.createRef(); + @observable private _mouseToggled = false; + @observable private _doubleClickEnabled = false; + + @computed private get regiondata() { + return RegionData(this.props.RegionData); + } + @computed private get regions() { + return DocListCast(this.props.animatedDoc.regions); + } + @computed private get keyframes() { + return DocListCast(this.regiondata.keyframes); + } + @computed private get pixelPosition() { + return RegionHelpers.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelDuration() { + return RegionHelpers.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelFadeIn() { + return RegionHelpers.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + @computed private get pixelFadeOut() { + return RegionHelpers.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + } + + constructor(props: any) { + super(props); + } + componentDidMount() { + setTimeout(() => { + //giving it a temporary 1sec delay... + if (!this.regiondata.keyframes) this.regiondata.keyframes = new List(); + const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, RegionHelpers.KeyframeType.end); + const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, RegionHelpers.KeyframeType.fade); + const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, RegionHelpers.KeyframeType.fade); + const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, RegionHelpers.KeyframeType.end); + fadeIn.opacity = 1; + fadeOut.opacity = 1; + start.opacity = 0.1; + finish.opacity = 0.1; + this.forceUpdate(); //not needed, if setTimeout is gone... + }, 1000); + } + + @action + onBarPointerDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + const 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; + } + const left = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.left, this.regiondata, this.regions)!; + const right = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, this.regiondata, this.regions)!; + const prevX = this.regiondata.position; + const futureX = this.regiondata.position + RegionHelpers.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; + } + const 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(); + const bar = this._bar.current!; + const offset = RegionHelpers.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const leftRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.left, this.regiondata, this.regions); + if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) { + this.regiondata.position = leftRegion.position + leftRegion.duration; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration); + } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) { + this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn; + } else if (NumCast(this.keyframes[0].time) + offset <= 0) { + this.regiondata.position = 0; + this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time); + } 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(); + const bar = this._bar.current!; + const offset = RegionHelpers.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const rightRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, this.regiondata, this.regions); + const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time); + if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { + //case 1: when third to last keyframe is in the way + this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut; + } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) { + this.regiondata.duration = rightRegion.position - this.regiondata.position; + } 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; + const bar = this._bar.current!; + const offset = RegionHelpers.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 + const position = this.regiondata.position; + this.props.makeKeyData(this.regiondata, Math.round(position + offset), RegionHelpers.KeyframeType.default); + this.regiondata.hasData = true; + this.props.changeCurrentBarX(RegionHelpers.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(RegionHelpers.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); + }; + + /** + * custom keyframe context menu items (when clicking on the keyframe circle) + */ + @action + makeKeyframeMenu = (kf: Doc, e: MouseEvent) => { + TimelineMenu.Instance.addItem( + 'button', + 'Delete', + action(() => { + (this.regiondata.keyframes as List).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + }) + ), + TimelineMenu.Instance.addItem( + 'input', + 'Move', + action(val => { + let cannotMove: boolean = false; + const kfIndex: number = this.keyframes.indexOf(kf); + if (val < 0 || val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) { + cannotMove = true; + } + if (!cannotMove) { + this.keyframes[kfIndex].time = parseInt(val, 10); + if (kfIndex === 1) { + this.regiondata.fadeIn = parseInt(val, 10) - this.regiondata.position; + } + // this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + }) + ); + TimelineMenu.Instance.addMenu('Keyframe'); + TimelineMenu.Instance.openMenu(e.clientX, e.clientY); + }; + + /** + * context menu for region (anywhere on the green region). + */ + @action + makeRegionMenu = (kf: Doc, e: MouseEvent) => { + TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.animatedDoc.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)), + TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => { + runInAction(() => { + let cannotMove: boolean = false; + if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) { + cannotMove = true; + } + if (!cannotMove) { + this.regiondata.fadeIn = parseInt(val, 10); + this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + } + }); + }), + TimelineMenu.Instance.addItem('input', `fadeOut: ${this.regiondata.fadeOut}ms`, val => { + runInAction(() => { + let cannotMove: boolean = false; + if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) { + cannotMove = true; + } + if (!cannotMove) { + this.regiondata.fadeOut = parseInt(val, 10); + this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val; + } + }); + }), + TimelineMenu.Instance.addItem('input', `position: ${this.regiondata.position}ms`, val => { + runInAction(() => { + const prevPosition = this.regiondata.position; + let cannotMove: boolean = false; + this.regions + .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) + .forEach(({ pos, dur }) => { + if (pos !== this.regiondata.position) { + if (val < 0 || (val > pos && val < pos + dur) || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur)) { + cannotMove = true; + } + } + }); + if (!cannotMove) { + this.regiondata.position = parseInt(val, 10); + this.updateKeyframes(this.regiondata.position - prevPosition); + } + }); + }), + TimelineMenu.Instance.addItem('input', `duration: ${this.regiondata.duration}ms`, val => { + runInAction(() => { + let cannotMove: boolean = false; + this.regions + .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })) + .forEach(({ pos, dur }) => { + if (pos !== this.regiondata.position) { + val += this.regiondata.position; + if (val < 0 || (val > pos && val < pos + dur)) { + cannotMove = true; + } + } + }); + if (!cannotMove) { + this.regiondata.duration = parseInt(val, 10); + this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration; + this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut; + } + }); + }), + TimelineMenu.Instance.addMenu('Region'); + TimelineMenu.Instance.openMenu(e.clientX, e.clientY); + }; + + @action + updateKeyframes = (incr: number, filter: number[] = []) => { + this.keyframes.forEach(kf => { + if (!filter.includes(this.keyframes.indexOf(kf))) { + kf.time = NumCast(kf.time) + incr; + } + }); + }; + + /** + * hovering effect when hovered (hidden div darkens) + */ + @action + onContainerOver = (e: React.PointerEvent, ref: React.RefObject) => { + e.preventDefault(); + e.stopPropagation(); + const div = ref.current!; + div.style.opacity = '1'; + Doc.BrushDoc(this.props.animatedDoc); + }; + + /** + * hovering effect when hovered out (hidden div becomes invisible) + */ + @action + onContainerOut = (e: React.PointerEvent, ref: React.RefObject) => { + e.preventDefault(); + e.stopPropagation(); + const div = ref.current!; + div.style.opacity = '0'; + Doc.UnBrushDoc(this.props.animatedDoc); + }; + + ///////////////////////UI STUFF ///////////////////////// + + /** + * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades) + * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable) + */ + drawKeyframes = () => { + const keyframeDivs: JSX.Element[] = []; + return DocListCast(this.regiondata.keyframes).map(kf => { + return ( + <> +
+
+
{ + e.preventDefault(); + e.stopPropagation(); + this.moveKeyframe(e, kf); + }} + onContextMenu={(e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.makeKeyframeMenu(kf, e.nativeEvent); + }} + onDoubleClick={e => { + e.preventDefault(); + e.stopPropagation(); + }} + /> +
+
+ + ); + }); + }; + + /** + * drawing the hidden divs that partition different intervals within a region. + */ + @action + drawKeyframeDividers = () => { + const keyframeDividers: JSX.Element[] = []; + DocListCast(this.regiondata.keyframes).forEach(kf => { + const index = this.keyframes.indexOf(kf); + if (index !== this.keyframes.length - 1) { + const right = this.keyframes[index + 1]; + const bodyRef = React.createRef(); + const kfPos = RegionHelpers.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + const rightPos = RegionHelpers.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + keyframeDividers.push( +
{ + e.preventDefault(); + e.stopPropagation(); + this.onContainerOver(e, bodyRef); + }} + onPointerOut={e => { + e.preventDefault(); + e.stopPropagation(); + this.onContainerOut(e, bodyRef); + }} + onContextMenu={e => { + e.preventDefault(); + e.stopPropagation(); + if (index !== 0 || index !== this.keyframes.length - 2) { + this._mouseToggled = true; + } + this.makeRegionMenu(kf, e.nativeEvent); + }}>
+ ); + } + }); + return keyframeDividers; + }; + + /** + * rendering that green region + */ + //154, 206, 223 + render() { + return ( +
+ {this.drawKeyframes()} + {this.drawKeyframeDividers()} +
+ {/*
*/} +
+ {/*
*/} +
+ ); + } +} diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 7ca13756a..3675238fd 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -1,28 +1,28 @@ +import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faBackward, faForward, faGripLines, faPauseCircle, faPlayCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction, trace } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../Utils'; +import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import clamp from '../../util/clamp'; import { FieldViewProps } from '../nodes/FieldView'; -import { KeyframeFunc } from './Keyframe'; +import { RegionHelpers } from './Region'; import './Timeline.scss'; import { TimelineOverview } from './TimelineOverview'; import { Track } from './Track'; -import clamp from '../../util/clamp'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { IconLookup } from '@fortawesome/fontawesome-svg-core'; /** - * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are + * Timeline class controls most of timeline functions besides individual region and track mechanism. Main functions are * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make * any logical changes here. Most work is needed on UI. * * The hierarchy works this way: * - * Timeline.tsx --> Track.tsx --> Keyframe.tsx + * Timeline.tsx --> Track.tsx --> Region.tsx | | | TimelineMenu.tsx (timeline's custom contextmenu) | @@ -58,7 +58,6 @@ export class Timeline extends React.Component { //boolean vars and instance vars @observable private _currentBarX: number = 0; @observable private _windSpeed: number = 1; - @observable private _isPlaying: boolean = false; //scrubber playing @observable private _totalLength: number = 0; @observable private _visibleLength: number = 0; @observable private _visibleStart: number = 0; @@ -69,6 +68,8 @@ export class Timeline extends React.Component { @observable private _playButton = faPlayCircle; @observable private _titleHeight = 0; + @observable public IsPlaying: boolean = false; //scrubber playing + /** * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit. */ @@ -144,14 +145,17 @@ export class Timeline extends React.Component { @action play = () => { const playTimeline = () => { - if (this._isPlaying) { + if (this.IsPlaying) { this.changeCurrentBarX(this._currentBarX >= this._totalLength ? 0 : this._currentBarX + this._windSpeed); setTimeout(playTimeline, 15); } }; - this._isPlaying = !this._isPlaying; - this._playButton = this._isPlaying ? faPauseCircle : faPlayCircle; - this._isPlaying && playTimeline(); + Array.from(this.mapOfTracks.values()) + .filter(key => key) + .forEach(key => key!.saveKeyframe()); + this.IsPlaying = !this.IsPlaying; + this._playButton = this.IsPlaying ? faPauseCircle : faPlayCircle; + this.IsPlaying && playTimeline(); }; /** @@ -221,7 +225,7 @@ export class Timeline extends React.Component { if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) { this._visibleStart -= e.movementX; this._totalLength -= e.movementX; - this._time -= KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + this._time -= RegionHelpers.convertPixelTime(e.movementX, 'mili', 'time', this._tickSpacing, this._tickIncrement); this.props.Document.AnimationLength = this._time; } return false; @@ -278,11 +282,11 @@ export class Timeline extends React.Component { e.preventDefault(); e.stopPropagation(); const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left; - const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, 'mili', 'time', this._tickSpacing, this._tickIncrement); - const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const prevTime = RegionHelpers.convertPixelTime(this._visibleStart + offset, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const prevCurrent = RegionHelpers.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); this.zoom(e.deltaY < 0); - const currPixel = KeyframeFunc.convertPixelTime(prevTime, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); - const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + const currPixel = RegionHelpers.convertPixelTime(prevTime, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + const currCurrent = RegionHelpers.convertPixelTime(prevCurrent, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); this._infoContainer.current!.scrollLeft = currPixel - offset; this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0; this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0; @@ -478,7 +482,7 @@ export class Timeline extends React.Component { // @computed getCurrentTime = () => { - const current = KeyframeFunc.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); + const current = RegionHelpers.convertPixelTime(this._currentBarX, 'mili', 'time', this._tickSpacing, this._tickIncrement); return this.toReadTime(current > this._time ? this._time : current); }; @@ -505,13 +509,13 @@ export class Timeline extends React.Component { @action toAuthoring = () => { this._time = Math.ceil((this.findLongestTime() ?? 1) / 100000) * 100000; - this._totalLength = KeyframeFunc.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); }; @action toPlay = () => { this._time = this.findLongestTime(); - this._totalLength = KeyframeFunc.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); + this._totalLength = RegionHelpers.convertPixelTime(this._time, 'mili', 'pixel', this._tickSpacing, this._tickIncrement); }; /** @@ -535,6 +539,7 @@ export class Timeline extends React.Component { {this.children.map(doc => ( this.mapOfTracks.push(ref)} + timeline={this} animatedDoc={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx index aa422c092..1769c41bd 100644 --- a/src/client/views/animationtimeline/TimelineMenu.tsx +++ b/src/client/views/animationtimeline/TimelineMenu.tsx @@ -1,12 +1,11 @@ -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"; -import { Utils } from "../../../Utils"; -import { IconLookup } from "@fortawesome/fontawesome-svg-core"; - +import { IconLookup } from '@fortawesome/fontawesome-svg-core'; +import { faChartLine, faClipboard } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Utils } from '../../../Utils'; +import './TimelineMenu.scss'; @observer export class TimelineMenu extends React.Component { @@ -25,9 +24,9 @@ export class TimelineMenu extends React.Component { @action openMenu = (x?: number, y?: number) => { this._opacity = 1; - x ? this._x = x : this._x = 0; - y ? this._y = y : this._y = 0; - } + x ? (this._x = x) : (this._x = 0); + y ? (this._y = y) : (this._y = 0); + }; @action closeMenu = () => { @@ -35,44 +34,67 @@ export class TimelineMenu extends React.Component { this._currentMenu = []; this._x = -1000000; this._y = -1000000; - } + }; @action - addItem = (type: "input" | "button", title: string, event: (e: any, ...args: any[]) => void) => { - if (type === "input") { + addItem = (type: 'input' | 'button', title: string, event: (e: any, ...args: any[]) => void) => { + if (type === 'input') { const inputRef = React.createRef(); - let text = ""; - this._currentMenu.push(
{ - e.stopPropagation(); - text = e.target.value; - }} onKeyDown={(e) => { - if (e.keyCode === 13) { - event(text); - this.closeMenu(); - e.stopPropagation(); - } - }} />
); - } else if (type === "button") { - this._currentMenu.push(

{ - e.preventDefault(); - e.stopPropagation(); - event(e); - this.closeMenu(); - }}>{title}

); + let text = ''; + this._currentMenu.push( +
+ + { + e.stopPropagation(); + text = e.target.value; + }} + onKeyDown={e => { + if (e.keyCode === 13) { + event(Number(text)); + this.closeMenu(); + e.stopPropagation(); + } + }} + /> +
+ ); + } else if (type === 'button') { + this._currentMenu.push( +
+ +

{ + e.preventDefault(); + e.stopPropagation(); + event(e); + this.closeMenu(); + }}> + {title} +

+
+ ); } - } + }; @action addMenu = (title: string) => { - this._currentMenu.unshift(

{title}

); - } + this._currentMenu.unshift( +
+

{title}

+
+ ); + }; render() { return ( -
+
{this._currentMenu}
); } - -} \ No newline at end of file +} diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx index 81a5587e4..82ac69a3b 100644 --- a/src/client/views/animationtimeline/TimelineOverview.tsx +++ b/src/client/views/animationtimeline/TimelineOverview.tsx @@ -1,11 +1,9 @@ -import * as React from "react"; -import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx"; -import { observer } from "mobx-react"; -import "./TimelineOverview.scss"; -import * as $ from 'jquery'; -import { Timeline } from "./Timeline"; -import { Keyframe, KeyframeFunc } from "./Keyframe"; - +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { RegionHelpers } from './Region'; +import { Timeline } from './Timeline'; +import './TimelineOverview.scss'; interface TimelineOverviewProps { totalLength: number; @@ -21,9 +19,8 @@ interface TimelineOverviewProps { tickIncrement: number; } - @observer -export class TimelineOverview extends React.Component{ +export class TimelineOverview extends React.Component { @observable private _visibleRef = React.createRef(); @observable private _scrubberRef = React.createRef(); @observable private authoringContainer = React.createRef(); @@ -49,13 +46,13 @@ export class TimelineOverview extends React.Component{ this.setOverviewWidth(); }); } - }, + } ); - } + }; componentWillUnmount = () => { this._authoringReaction && this._authoringReaction(); - } + }; @action setOverviewWidth() { @@ -66,8 +63,7 @@ export class TimelineOverview extends React.Component{ if (this.props.isAuthoring) { this.activeOverviewWidth = this.overviewBarWidth; - } - else { + } else { this.activeOverviewWidth = this.playbarWidth; } } @@ -76,37 +72,37 @@ export class TimelineOverview extends React.Component{ 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); - } + 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(); - const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX; - this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength); - } + const movX = (this.props.visibleStart / this.props.totalLength) * this.DEFAULT_WIDTH + e.movementX; + this.props.movePanX((movX / this.DEFAULT_WIDTH) * this.props.totalLength); + }; @action onPointerUp = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - document.removeEventListener("pointermove", this.onPanX); - document.removeEventListener("pointerup", this.onPointerUp); - } + 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); - } + document.removeEventListener('pointermove', this.onScrubberMove); + document.removeEventListener('pointerup', this.onScrubberUp); + document.addEventListener('pointermove', this.onScrubberMove); + document.addEventListener('pointerup', this.onScrubberUp); + }; @action onScrubberMove = (e: PointerEvent) => { @@ -115,22 +111,22 @@ export class TimelineOverview extends React.Component{ const scrubberRef = this._scrubberRef.current!; const left = scrubberRef.getBoundingClientRect().left; const offsetX = Math.round(e.clientX - left); - this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX); - } + this.props.changeCurrentBarX((offsetX / this.activeOverviewWidth) * this.props.totalLength + this.props.currentBarX); + }; @action onScrubberUp = (e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); - document.removeEventListener("pointermove", this.onScrubberMove); - document.removeEventListener("pointerup", this.onScrubberUp); - } + document.removeEventListener('pointermove', this.onScrubberMove); + document.removeEventListener('pointerup', this.onScrubberUp); + }; @action getTimes() { - const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); - const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); - const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement); + const vis = RegionHelpers.convertPixelTime(this.props.visibleLength, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const x = RegionHelpers.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + const start = RegionHelpers.convertPixelTime(this.props.visibleStart, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); this.visibleTime = vis; this.currentX = x; this.visibleStart = start; @@ -144,7 +140,7 @@ export class TimelineOverview extends React.Component{ const visibleBarWidth = percentVisible * this.activeOverviewWidth; const percentScrubberStart = this.currentX / this.props.time; - let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth; + let scrubberStart = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth; const percentBarStart = this.visibleStart / this.props.time; @@ -153,29 +149,25 @@ export class TimelineOverview extends React.Component{ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth; - const timeline = this.props.isAuthoring ? [ - -
-
, -
-
-
-
- ] : [ -
-
-
, -
- ]; + const timeline = this.props.isAuthoring + ? [ +
+
, +
+
+
+
, + ] + : [ +
+
+
, +
, + ]; return (
-
- {timeline} -
+
{timeline}
); } - } - - diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 1010332f5..2fd062a88 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -1,17 +1,19 @@ import { action, computed, intercept, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt, DocListCastAsync } from '../../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc'; import { Copy } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast } from '../../../fields/Types'; import { Transform } from '../../util/Transform'; -import { Keyframe, KeyframeFunc, RegionData } from './Keyframe'; +import { Region, RegionData, RegionHelpers } from './Region'; +import { Timeline } from './Timeline'; import './Track.scss'; interface IProps { + timeline: Timeline; animatedDoc: Doc; currentBarX: number; transform: Transform; @@ -32,14 +34,14 @@ export class Track extends React.Component { @observable private _newKeyframe: boolean = false; private readonly MAX_TITLE_HEIGHT = 75; @observable private _trackHeight = 0; - private primitiveWhitelist = ['x', 'y', '_width', '_height', 'opacity', '_layout_scrollTop']; + private primitiveWhitelist = ['x', 'y', '_width', '_height', '_rotation', 'opacity', '_layout_scrollTop']; private objectWhitelist = ['data']; @computed private get regions() { return DocListCast(this.props.animatedDoc.regions); } @computed private get time() { - return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); + return NumCast(RegionHelpers.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); } async componentDidMount() { @@ -85,36 +87,36 @@ export class Track extends React.Component { */ @action saveKeyframe = async () => { - const keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List; - const kfIndex = keyframes.indexOf(this.saveStateKf!); + if (this.props.timeline.IsPlaying || !this.saveStateRegion || !this.saveStateKf) { + this.saveStateKf = undefined; + this.saveStateRegion = undefined; + return; + } + const keyframes = Cast(this.saveStateRegion.keyframes, listSpec(Doc)) as List; + const kfIndex = keyframes.indexOf(this.saveStateKf); const kf = keyframes[kfIndex] as Doc; //index in the keyframe if (this._newKeyframe) { - DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => { + DocListCast(this.saveStateRegion.keyframes).forEach((kf, index) => { this.copyDocDataToKeyFrame(kf); kf.opacity = index === 0 || index === 3 ? 0.1 : 1; }); this._newKeyframe = false; } if (!kf) return; - if (kf.type === KeyframeFunc.KeyframeType.default) { - // only save for non-fades - this.copyDocDataToKeyFrame(kf); - const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists - const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists - if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { + // only save for non-fades + if (this.copyDocDataToKeyFrame(kf)) { + const leftkf = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, kf); // lef keyframe, if it exists + const rightkf = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, kf); //right keyframe, if it exists + if (leftkf?.type === RegionHelpers.KeyframeType.end) { //replicating this keyframe to fades - const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf); + const edge = RegionHelpers.calcMinLeft(this.saveStateRegion, this.time, leftkf); edge && this.copyDocDataToKeyFrame(edge); leftkf && this.copyDocDataToKeyFrame(leftkf); - edge && (edge.opacity = 0.1); - leftkf && (leftkf.opacity = 1); } - if (rightkf?.type === KeyframeFunc.KeyframeType.fade) { - const edge = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf); + if (rightkf?.type === RegionHelpers.KeyframeType.end) { + const edge = RegionHelpers.calcMinRight(this.saveStateRegion, this.time, rightkf); edge && this.copyDocDataToKeyFrame(edge); rightkf && this.copyDocDataToKeyFrame(rightkf); - edge && (edge.opacity = 0.1); - rightkf && (rightkf.opacity = 1); } } keyframes[kfIndex] = kf; @@ -143,7 +145,7 @@ export class Track extends React.Component { const r = region as RegionData; //for some region is returning undefined... which is not the case if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot - this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default); + this.makeKeyData(r, this.time, RegionHelpers.KeyframeType.default); } } }, @@ -222,20 +224,20 @@ export class Track extends React.Component { */ @action timeChange = async () => { - if (this.saveStateKf !== undefined) { - await this.saveKeyframe(); - } else if (this._newKeyframe) { + if (this.saveStateKf !== undefined || this._newKeyframe) { await this.saveKeyframe(); } const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on if (regiondata) { - const leftkf: Doc | undefined = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists - const rightkf: Doc | undefined = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists + const leftkf: Doc | undefined = await RegionHelpers.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists + const rightkf: Doc | undefined = await RegionHelpers.calcMinRight(regiondata, this.time); //right keyframe, if it exists const currentkf: Doc | undefined = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe if (currentkf) { await this.applyKeys(currentkf); - this.saveStateKf = currentkf; - this.saveStateRegion = regiondata; + runInAction(() => { + this.saveStateKf = currentkf; + this.saveStateRegion = regiondata; + }); } else if (leftkf && rightkf) { await this.interpolate(leftkf, rightkf); } @@ -277,7 +279,7 @@ export class Track extends React.Component { @action interpolate = async (left: Doc, right: Doc) => { this.primitiveWhitelist.forEach(key => { - if (left[key] && right[key] && typeof left[key] === 'number' && typeof right[key] === 'number') { + if (typeof left[key] === 'number' && typeof right[key] === 'number') { //if it is number, interpolate const dif = NumCast(right[key]) - NumCast(left[key]); const deltaLeft = this.time - NumCast(left.time); @@ -306,7 +308,7 @@ export class Track extends React.Component { onInnerDoubleClick = (e: React.MouseEvent) => { const inner = this._inner.current!; const 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)); + this.createRegion(RegionHelpers.convertPixelTime(offsetX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); }; /** @@ -316,10 +318,10 @@ export class Track extends React.Component { createRegion = (time: number) => { if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions) - const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data + const regiondata = RegionHelpers.defaultKeyframe(); //create keyframe data regiondata.position = time; //set position - const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); + const rightRegion = RegionHelpers.findAdjacentRegion(RegionHelpers.Direction.right, regiondata, this.regions); if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region @@ -335,7 +337,7 @@ export class Track extends React.Component { }; @action - makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { + makeKeyData = (regiondata: RegionData, time: number, type: RegionHelpers.KeyframeType = RegionHelpers.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time const trackKeyFrames = DocListCast(regiondata.keyframes); const existingkf = trackKeyFrames.find(TK => TK.time === time); @@ -359,16 +361,21 @@ export class Track extends React.Component { @action copyDocDataToKeyFrame = (doc: Doc) => { + var somethingChanged = false; this.primitiveWhitelist.map(key => { const originalVal = this.props.animatedDoc[key]; - doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; + somethingChanged = somethingChanged || originalVal !== doc[key]; + if (doc.type === RegionHelpers.KeyframeType.end && key === 'opacity') doc.opacity = 0; + else doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; }); + return somethingChanged; }; /** * UI sstuff here. Not really much to change */ render() { + const saveStateKf = this.saveStateKf; return (
@@ -380,7 +387,7 @@ export class Track extends React.Component { onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)} onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}> {this.regions?.map((region, i) => { - return ; + return ; })}
diff --git a/src/fields/List.ts b/src/fields/List.ts index f3fcc87f7..da007e972 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -236,7 +236,10 @@ class ListImpl extends ObjectField { const list = new Proxy(this, { set: setter, get: ListImpl.listGetter, - ownKeys: target => Object.keys(target.__fieldTuples), + ownKeys: target => { + const keys = Object.keys(target.__fieldTuples); + return [...keys, '__realFields']; + }, getOwnPropertyDescriptor: (target, prop) => { if (prop in target[FieldTuples]) { return { -- cgit v1.2.3-70-g09d2