From 39c85293f6c3d385ea64ba0db8c9736dfaaec993 Mon Sep 17 00:00:00 2001 From: mehekj Date: Sun, 20 Mar 2022 15:22:50 -0400 Subject: cleaned up files and added some comments --- .../collections/CollectionStackedTimeline.tsx | 102 +++++++++++++++------ 1 file changed, 76 insertions(+), 26 deletions(-) (limited to 'src/client/views/collections/CollectionStackedTimeline.tsx') diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 7d9dc39ae..bd7d0083b 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -43,6 +43,19 @@ import { import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; + + +/** + * CollectionStackedTimeline + * Main component: CollectionStackedTimeline.tsx + * Supporting Components: AudioWaveform + * + * CollectionStackedTimeline is a collection view used for audio and video nodes to display a timeline of the temporal media documents with an audio waveform and markers for links and annotations + * The actual media is handled in the containing classes (AudioBox, VideoBox) but the timeline deals with rendering and updating timecodes, links, and trimming. + * When trimming there are two pairs of times that are tracked: trimStart and trimEnd are the bounds of the trim controls, clipStart and clipEnd are the actual trimmed playback bounds of the clip + */ + + type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); export type CollectionStackedTimelineProps = { @@ -60,38 +73,42 @@ export type CollectionStackedTimelineProps = { fieldKey: string; }; +// trimming state: shows full clip, current trim bounds, or not trimming export enum TrimScope { All = 2, Clip = 1, None = 0, } + @observer export class CollectionStackedTimeline extends CollectionSubView< PanZoomDocument, CollectionStackedTimelineProps >(PanZoomDocument) { - @observable static SelectingRegion: CollectionStackedTimeline | undefined; - @observable public static CurrentlyPlaying: Doc[]; + @observable static SelectingRegion: CollectionStackedTimeline | undefined; // timeline selection region + @observable public static CurrentlyPlaying: Doc[]; // tracks all currently playing audio and video docs static RangeScript: ScriptField; static LabelScript: ScriptField; static RangePlayScript: ScriptField; static LabelPlayScript: ScriptField; - private _timeline: HTMLDivElement | null = null; - private _timelineWrapper: HTMLDivElement | null = null; + private _timeline: HTMLDivElement | null = null; // ref to actual timeline div + private _timelineWrapper: HTMLDivElement | null = null; // ref to timeline wrapper div for zooming and scrolling private _markerStart: number = 0; @observable _markerEnd: number | undefined; @observable _trimming: number = TrimScope.None; - @observable _trimStart: number = 0; - @observable _trimEnd: number = 0; + @observable _trimStart: number = 0; // trim controls start pos + @observable _trimEnd: number = 0; // trim controls end pos @observable _zoomFactor: number = 1; @observable _scroll: number = 0; + // ensures that clip doesn't get trimmed so small that controls cannot be adjusted anymore get minTrimLength() { return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5) } + @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; } @computed get trimDuration() { return this.trimEnd - this.trimStart; } @computed get trimEnd() { return this.IsTrimming !== TrimScope.None ? this._trimEnd : this.clipEnd; } @@ -104,6 +121,7 @@ export class CollectionStackedTimeline extends CollectionSubView< @computed get zoomFactor() { return this._zoomFactor } + constructor(props: any) { super(props); // onClick play scripts @@ -135,6 +153,7 @@ export class CollectionStackedTimeline extends CollectionSubView< } } + public get IsTrimming() { return this._trimming; } @action @@ -155,24 +174,31 @@ export class CollectionStackedTimeline extends CollectionSubView< this._zoomFactor = zoom; } + anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this.props.endTag], val) ?? null); + + + // converts screen pixel offset to time toTimeline = (screen_delta: number, width: number) => { return Math.max( this.clipStart, Math.min(this.clipEnd, (screen_delta / width) * this.clipDuration + this.clipStart)); } + rangeClickScript = () => CollectionStackedTimeline.RangeScript; rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; - // for creating key anchors with key events + + // handles key events for for creating key anchors, scrubbing, exiting trim @action keyEvents = (e: KeyboardEvent) => { if ( !(e.target instanceof HTMLInputElement) && this.props.isSelected(true) ) { + // if shift pressed scrub 1 second otherwise 1/10th const jump = e.shiftKey ? 1 : 0.1; e.stopPropagation(); switch (e.key) { @@ -196,6 +222,7 @@ export class CollectionStackedTimeline extends CollectionSubView< } break; case "Escape": + // abandons current trim this._trimStart = this.clipStart; this._trimStart = this.clipEnd; this._trimming = TrimScope.None; @@ -210,6 +237,7 @@ export class CollectionStackedTimeline extends CollectionSubView< } } + getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; @@ -224,7 +252,8 @@ export class CollectionStackedTimeline extends CollectionSubView< return { la1, la2, linkTime }; } - // starting the drag event for anchor resizing + + // handles dragging selection to create markers @action onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); @@ -299,6 +328,8 @@ export class CollectionStackedTimeline extends CollectionSubView< } + + // for dragging trim start handle @action trimLeft = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); @@ -325,6 +356,7 @@ export class CollectionStackedTimeline extends CollectionSubView< ); } + // for dragging trim end handle @action trimRight = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); @@ -351,12 +383,15 @@ export class CollectionStackedTimeline extends CollectionSubView< ); } + + // for rendering scrolling when timeline zoomed @action setScroll = (e: React.UIEvent) => { e.stopPropagation(); this._scroll = this._timelineWrapper!.scrollLeft; } + // smooth scrolls to time like when following links overflowed due to zoom @action scrollToTime = (time: number) => { if (this._timelineWrapper) { @@ -371,6 +406,8 @@ export class CollectionStackedTimeline extends CollectionSubView< } } + + // handles dragging and dropping markers in timeline @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; @@ -396,6 +433,8 @@ export class CollectionStackedTimeline extends CollectionSubView< return false; } + + // creates marker on timeline @undoBatch @action static createAnchor( @@ -430,6 +469,7 @@ export class CollectionStackedTimeline extends CollectionSubView< return anchor; } + @action playOnClick = (anchorDoc: Doc, clientX: number) => { const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; @@ -521,14 +561,20 @@ export class CollectionStackedTimeline extends CollectionSubView< return level; } + dictationHeightPercent = 50; dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100; + @computed get timelineContentHeight() { return this.props.PanelHeight() * this.dictationHeightPercent / 100; } @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor - 4 }; // subtract size of container border + dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight); + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + currentTimecode = () => this.currentTime; + @computed get renderDictation() { const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null); return !dictation ? null : ( @@ -565,23 +611,8 @@ export class CollectionStackedTimeline extends CollectionSubView< ); } - @computed get renderAudioWaveform() { - return !this.props.mediaPath ? null : ( -
- -
- ); - } + + // renders selection region on timeline @computed get selectionContainer() { const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd; return markerEnd === undefined ? null : ( @@ -668,7 +699,6 @@ export class CollectionStackedTimeline extends CollectionSubView< ); })} {!this.IsTrimming && this.selectionContainer} - {/* {this.renderAudioWaveform} */} ScriptField; @@ -753,20 +789,26 @@ interface StackedTimelineAnchorProps { trimStart: number; trimEnd: number; } + + @observer class StackedTimelineAnchor extends React.Component { _lastTimecode: number; _disposer: IReactionDisposer | undefined; + constructor(props: any) { super(props); this._lastTimecode = this.props.currentTimecode(); } + // updates marker document title to reflect correct timecodes computeTitle = () => { const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart; const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart; return `#${formatTime(start)}-${formatTime(end)}`; } + + componentDidMount() { this._disposer = reaction( () => this.props.currentTimecode(), @@ -805,9 +847,12 @@ class StackedTimelineAnchor extends React.Component } ); } + componentWillUnmount() { this._disposer?.(); } + + // starting the drag event for anchor resizing onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { this.props._timeline?.setPointerCapture(e.pointerId); @@ -851,11 +896,15 @@ class StackedTimelineAnchor extends React.Component ); } + + // context menu contextMenuItems = () => { const resetTitle = { script: ScriptField.MakeFunction(`self.title = "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"])`)!, icon: "folder-plus", label: "Reset Title" }; return [resetTitle]; } + + // renders anchor LabelBox renderInner = computedFn(function ( this: StackedTimelineAnchor, mark: Doc, @@ -910,6 +959,7 @@ class StackedTimelineAnchor extends React.Component anchorScreenToLocalXf = () => this.props.ScreenToLocalTransform().translate(-this.props.left, -this.props.top); width = () => this.props.width; height = () => this.props.height; + render() { const inner = this.renderInner( this.props.mark, -- cgit v1.2.3-70-g09d2