diff options
-rw-r--r-- | src/client/views/animationtimeline/Region.scss (renamed from src/client/views/animationtimeline/Keyframe.scss) | 13 | ||||
-rw-r--r-- | src/client/views/animationtimeline/Region.tsx (renamed from src/client/views/animationtimeline/Keyframe.tsx) | 142 | ||||
-rw-r--r-- | src/client/views/animationtimeline/Timeline.tsx | 49 | ||||
-rw-r--r-- | src/client/views/animationtimeline/TimelineMenu.tsx | 100 | ||||
-rw-r--r-- | src/client/views/animationtimeline/TimelineOverview.tsx | 112 | ||||
-rw-r--r-- | src/client/views/animationtimeline/Track.tsx | 77 | ||||
-rw-r--r-- | src/fields/List.ts | 5 |
7 files changed, 261 insertions, 237 deletions
diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Region.scss index 38eb103c6..f7476ab55 100644 --- a/src/client/views/animationtimeline/Keyframe.scss +++ b/src/client/views/animationtimeline/Region.scss @@ -1,5 +1,4 @@ -@import "./../global/globalCssVariables.scss"; - +@import './../global/globalCssVariables.scss'; $timelineColor: #9acedf; $timelineDark: #77a1aa; @@ -9,7 +8,7 @@ $timelineDark: #77a1aa; width: 5px; position: absolute; - // pointer-events: none; + // pointer-events: none; .menubox { width: 200px; height: 200px; @@ -27,11 +26,15 @@ $timelineDark: #77a1aa; .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 { @@ -100,6 +103,4 @@ $timelineDark: #77a1aa; 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/Region.tsx index addc00c85..df00924c6 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Region.tsx @@ -5,19 +5,16 @@ 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 './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 KeyframeFunc { +export namespace RegionHelpers { export enum KeyframeType { end = 'end', fade = 'fade', @@ -29,7 +26,7 @@ export namespace KeyframeFunc { right = 'right', } - export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => { + 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 => { @@ -128,10 +125,11 @@ interface IProps { collection: Doc; tickSpacing: number; tickIncrement: number; + saveStateKf: Doc | undefined; time: number; changeCurrentBarX: (x: number) => void; transform: Transform; - makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc; + makeKeyData: (region: RegionData, pos: number, kftype: RegionHelpers.KeyframeType) => Doc; } /** @@ -158,7 +156,7 @@ interface IProps { * @author Andrew Kim */ @observer -export class Keyframe extends React.Component<IProps> { +export class Region extends React.Component<IProps> { @observable private _bar = React.createRef<HTMLDivElement>(); @observable private _mouseToggled = false; @observable private _doubleClickEnabled = false; @@ -173,16 +171,16 @@ export class Keyframe extends React.Component<IProps> { return DocListCast(this.regiondata.keyframes); } @computed private get pixelPosition() { - return KeyframeFunc.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); + return RegionHelpers.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); + return RegionHelpers.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); + return RegionHelpers.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); + return RegionHelpers.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement); } constructor(props: any) { @@ -192,10 +190,10 @@ export class Keyframe extends React.Component<IProps> { setTimeout(() => { //giving it a temporary 1sec delay... if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>(); - 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); + 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; @@ -233,10 +231,10 @@ export class Keyframe extends React.Component<IProps> { 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 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 + KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement); + 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) { @@ -275,8 +273,8 @@ export class Keyframe extends React.Component<IProps> { 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); + 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); @@ -299,8 +297,8 @@ export class Keyframe extends React.Component<IProps> { 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 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 @@ -318,13 +316,13 @@ export class Keyframe extends React.Component<IProps> { 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); + 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), KeyframeFunc.KeyframeType.default); + this.props.makeKeyData(this.regiondata, Math.round(position + offset), RegionHelpers.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 + 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 } }; @@ -332,7 +330,7 @@ export class Keyframe extends React.Component<IProps> { 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)); + this.props.changeCurrentBarX(RegionHelpers.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); }; /** @@ -340,17 +338,14 @@ export class Keyframe extends React.Component<IProps> { */ @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<Doc>).splice(this.keyframes.indexOf(kf), 1); - this.forceUpdate(); - }) - ), + TimelineMenu.Instance.addItem( + 'button', + 'Delete', + action(() => { + (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); + this.forceUpdate(); + }) + ), TimelineMenu.Instance.addItem( 'input', 'Move', @@ -362,7 +357,10 @@ export class Keyframe extends React.Component<IProps> { } if (!cannotMove) { this.keyframes[kfIndex].time = parseInt(val, 10); - this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; + if (kfIndex === 1) { + this.regiondata.fadeIn = parseInt(val, 10) - this.regiondata.position; + } + // this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn; } }) ); @@ -485,38 +483,34 @@ export class Keyframe extends React.Component<IProps> { drawKeyframes = () => { const keyframeDivs: JSX.Element[] = []; return DocListCast(this.regiondata.keyframes).map(kf => { - if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) { - 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 keyframe-indicator" - onPointerDown={e => { - 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(); - }}></div> - </div> - <div className="keyframe-information" /> - </> - ); - } else { - return ( - <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> - <div className="divider" /> + return ( + <> + <div className="keyframe" style={{ left: `${RegionHelpers.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}> + <div className="divider"></div> + <div + className="keyframeCircle keyframe-indicator" + style={{ + borderColor: this.props.saveStateKf === kf ? 'red' : undefined, + }} + onPointerDown={e => { + 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(); + }} + /> </div> - ); - } + <div className="keyframe-information" /> + </> + ); }); }; @@ -531,8 +525,8 @@ export class Keyframe extends React.Component<IProps> { if (index !== this.keyframes.length - 1) { const right = this.keyframes[index + 1]; const bodyRef = React.createRef<HTMLDivElement>(); - 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); + 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( <div ref={bodyRef} @@ -579,12 +573,12 @@ export class Keyframe extends React.Component<IProps> { }%, rgba(154, 206, 223, 0) 100% )`, }} onPointerDown={this.onBarPointerDown}> + {this.drawKeyframes()} + {this.drawKeyframeDividers()} <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft}></div> {/* <div className="keyframe-information"></div> */} <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div> {/* <div className="keyframe-information"></div> */} - {this.drawKeyframes()} - {this.drawKeyframeDividers()} </div> ); } 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<FieldViewProps> { //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<FieldViewProps> { @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<FieldViewProps> { @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<FieldViewProps> { 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<FieldViewProps> { 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<FieldViewProps> { // @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<FieldViewProps> { @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<FieldViewProps> { {this.children.map(doc => ( <Track ref={ref => 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<HTMLInputElement>(); - let text = ""; - this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard as IconLookup} size="lg" /><input className="timeline-menu-input" ref={inputRef} placeholder={title} onChange={(e) => { - e.stopPropagation(); - text = e.target.value; - }} onKeyDown={(e) => { - if (e.keyCode === 13) { - event(text); - this.closeMenu(); - e.stopPropagation(); - } - }} /></div>); - } else if (type === "button") { - this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine as IconLookup} size="lg" /><p className="timeline-menu-desc" onClick={(e) => { - e.preventDefault(); - e.stopPropagation(); - event(e); - this.closeMenu(); - }}>{title}</p></div>); + let text = ''; + this._currentMenu.push( + <div key={Utils.GenerateGuid()} className="timeline-menu-item"> + <FontAwesomeIcon icon={faClipboard as IconLookup} size="lg" /> + <input + className="timeline-menu-input" + ref={inputRef} + placeholder={title} + onChange={e => { + e.stopPropagation(); + text = e.target.value; + }} + onKeyDown={e => { + if (e.keyCode === 13) { + event(Number(text)); + this.closeMenu(); + e.stopPropagation(); + } + }} + /> + </div> + ); + } else if (type === 'button') { + this._currentMenu.push( + <div key={Utils.GenerateGuid()} className="timeline-menu-item"> + <FontAwesomeIcon icon={faChartLine as IconLookup} size="lg" /> + <p + className="timeline-menu-desc" + onClick={e => { + e.preventDefault(); + e.stopPropagation(); + event(e); + this.closeMenu(); + }}> + {title} + </p> + </div> + ); } - } + }; @action addMenu = (title: string) => { - this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>); - } + this._currentMenu.unshift( + <div key={Utils.GenerateGuid()} className="timeline-menu-header"> + <p className="timeline-menu-header-desc">{title}</p> + </div> + ); + }; render() { return ( - <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{ opacity: this._opacity, left: this._x, top: this._y }} > + <div key={Utils.GenerateGuid()} 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.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<TimelineOverviewProps>{ +export class TimelineOverview extends React.Component<TimelineOverviewProps> { @observable private _visibleRef = React.createRef<HTMLDivElement>(); @observable private _scrubberRef = React.createRef<HTMLDivElement>(); @observable private authoringContainer = React.createRef<HTMLDivElement>(); @@ -49,13 +46,13 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ this.setOverviewWidth(); }); } - }, + } ); - } + }; componentWillUnmount = () => { this._authoringReaction && this._authoringReaction(); - } + }; @action setOverviewWidth() { @@ -66,8 +63,7 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ if (this.props.isAuthoring) { this.activeOverviewWidth = this.overviewBarWidth; - } - else { + } else { this.activeOverviewWidth = this.playbarWidth; } } @@ -76,37 +72,37 @@ export class TimelineOverview extends React.Component<TimelineOverviewProps>{ 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<TimelineOverviewProps>{ 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<TimelineOverviewProps>{ 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<TimelineOverviewProps>{ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth; if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth; - const timeline = this.props.isAuthoring ? [ - - <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}> - <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>, - <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}> - <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div> - </div> - </div> - ] : [ - <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}> - <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div> - </div>, - <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div> - ]; + const timeline = this.props.isAuthoring + ? [ + <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}> + <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>, + <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}> + <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div> + </div> + </div>, + ] + : [ + <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}> + <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div> + </div>, + <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div>, + ]; return ( <div className="timeline-flex"> - <div className="timelineOverview-bounding"> - {timeline} - </div> + <div className="timelineOverview-bounding">{timeline}</div> </div> ); } - } - - 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<IProps> { @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<IProps> { */ @action saveKeyframe = async () => { - const keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List<Doc>; - 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<Doc>; + 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<IProps> { 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<IProps> { */ @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<IProps> { @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<IProps> { 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<IProps> { 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<IProps> { }; @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<IProps> { @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 ( <div className="track-container"> <div className="track"> @@ -380,7 +387,7 @@ export class Track extends React.Component<IProps> { onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)} onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}> {this.regions?.map((region, i) => { - return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />; + return <Region key={`${i}`} {...this.props} saveStateKf={saveStateKf} RegionData={region} makeKeyData={this.makeKeyData} />; })} </div> </div> 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<T extends Field> extends ObjectField { const list = new Proxy<this>(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 { |