diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/AudioWaveform.tsx | 26 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.scss | 167 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 46 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.scss | 178 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 14 |
5 files changed, 256 insertions, 175 deletions
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 270b3869c..7b9b1aa81 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -18,6 +18,7 @@ export interface AudioWaveformProps { layoutDoc: Doc; clipStart: number; clipEnd: number; + zoomFactor: number; PanelHeight: () => number; } @@ -28,28 +29,30 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { @computed get waveHeight() { return Math.max(50, this.props.PanelHeight()); } @computed get clipStart() { return this.props.clipStart; } @computed get clipEnd() { return this.props.clipEnd; } - @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd)], listSpec("number"), []); } + @computed get zoomFactor() { return this.props.zoomFactor; } + @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); } - audioBucketField = (start: number, end: number) => "audioBuckets/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_"); + audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10); componentWillUnmount() { this._disposer?.(); } componentDidMount() { - this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd) }), - ({ clipStart, clipEnd, fieldKey }) => { + console.log("new waveform"); + this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }), + ({ clipStart, clipEnd, fieldKey, zoomFactor }) => { if (!this.props.layoutDoc[fieldKey]) { // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time. - const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration)], listSpec("number")); + const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec("number")); this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length)); - setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd)); + setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor)); } }, { fireImmediately: true }); } // decodes the audio file into peaks for generating the waveform - createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number) => { + createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => { axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then( (response) => { const context = new window.AudioContext(); @@ -60,12 +63,15 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { const startInd = clipStart / this.props.rawDuration; const endInd = clipEnd / this.props.rawDuration; const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); + const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); + + console.log(numBuckets); const bucketDataSize = Math.floor( - decodedAudioData.length / AudioWaveform.NUMBER_OF_BUCKETS + decodedAudioData.length / numBuckets ); const brange = Array.from(Array(bucketDataSize)); - const bucketList = numberRange(AudioWaveform.NUMBER_OF_BUCKETS).map( + const bucketList = numberRange(numBuckets).map( (i: number) => brange.reduce( (p, x, j) => @@ -87,7 +93,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { <Waveform color={Colors.MEDIUM_BLUE} height={this.waveHeight} - barWidth={0.1} + barWidth={10.0 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 7a957ae5c..0ec5f9aef 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,94 +1,109 @@ @import "../global/globalCssVariables.scss"; .collectionStackedTimeline { - position: absolute; - width: 100%; - height: 100%; - z-index: 1000; - top: 0px; - - .collectionStackedTimeline-trim-shade { position: absolute; + width: 100%; height: 100%; - background-color: $dark-gray; - opacity: 0.3; - } + z-index: 1000; + top: 0px; + // overflow-x: scroll; - .collectionStackedTimeline-trim-controls { - height: 100%; - position: absolute; - box-sizing: border-box; - border: 2px solid $medium-blue; - display: flex; - justify-content: space-between; - max-width: 100%; + ::-webkit-scrollbar { + position: relative; + -webkit-appearance: none; + height: 5px; + background-color: white; + } - .collectionStackedTimeline-trim-handle { - background-color: $medium-blue; - height: 100%; - width: 5px; - cursor: ew-resize; + ::-webkit-scrollbar-thumb { + position: relative; + -webkit-appearance: none; + height: 5px; + background-color: $medium-gray; } - } - .collectionStackedTimeline-selector { - position: absolute; - width: 10px; - top: 2.5%; - height: 95%; - background: $light-blue; - border-radius: 3px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: $medium-blue; - border-width: 1px; - } + .collectionStackedTimeline-trim-shade { + position: absolute; + height: 100%; + background-color: $dark-gray; + opacity: 0.3; + } - .collectionStackedTimeline-current { - width: 1px; - height: 100%; - background-color: $pink; - position: absolute; - top: 0px; - pointer-events: none; - } + .collectionStackedTimeline-trim-controls { + height: 100%; + position: absolute; + box-sizing: border-box; + border: 2px solid $medium-blue; + display: flex; + justify-content: space-between; + max-width: 100%; - .collectionStackedTimeline-marker-timeline { - position: absolute; - top: 2.5%; - height: 95%; - border-radius: 4px; - background: $light-gray; - &:hover { - opacity: 1; + .collectionStackedTimeline-trim-handle { + background-color: $medium-blue; + height: 100%; + width: 5px; + cursor: ew-resize; + } } - .collectionStackedTimeline-left-resizer, - .collectionStackedTimeline-resizer { - background: $medium-gray; - position: absolute; - top: 0; - height: 100%; - width: 10px; - pointer-events: all; - cursor: ew-resize; - z-index: 100; + .collectionStackedTimeline-selector { + position: absolute; + width: 10px; + top: 2.5%; + height: 95%; + background: $light-blue; + border-radius: 3px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: $medium-blue; + border-width: 1px; } - .collectionStackedTimeline-resizer { - right: 0; + + .collectionStackedTimeline-current { + width: 1px; + height: 100%; + background-color: $pink; + position: absolute; + top: 0px; + pointer-events: none; } - .collectionStackedTimeline-left-resizer { - left: 0; + + .collectionStackedTimeline-marker-timeline { + position: absolute; + top: 2.5%; + height: 95%; + border-radius: 4px; + background: $light-gray; + &:hover { + opacity: 1; + } + + .collectionStackedTimeline-left-resizer, + .collectionStackedTimeline-resizer { + background: $medium-gray; + position: absolute; + top: 0; + height: 100%; + width: 10px; + pointer-events: all; + cursor: ew-resize; + z-index: 100; + } + .collectionStackedTimeline-resizer { + right: 0; + } + .collectionStackedTimeline-left-resizer { + left: 0; + } } - } - .collectionStackedTimeline-waveform { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - pointer-events: none; - } + .collectionStackedTimeline-waveform { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + } } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index b5c266526..5c02611bb 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -100,6 +100,8 @@ export class CollectionStackedTimeline extends CollectionSubView< @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } + @computed get zoomFactor() { return this._zoomFactor } + constructor(props: any) { super(props); // onClick play scripts @@ -120,6 +122,8 @@ export class CollectionStackedTimeline extends CollectionSubView< } componentDidMount() { + this.layoutDoc.clipStart = 0; + this.layoutDoc.clipEnd = this.props.rawDuration; document.addEventListener("keydown", this.keyEvents, true); } @@ -135,7 +139,6 @@ export class CollectionStackedTimeline extends CollectionSubView< @action public StartTrimming(scope: TrimScope) { - console.log(this.minTrimLength); this._trimStart = this.clipStart; this._trimEnd = this.clipEnd; this._trimming = scope; @@ -148,8 +151,9 @@ export class CollectionStackedTimeline extends CollectionSubView< } @action - public setZoom(change: number) { - this._zoomFactor = Math.max(1, this._zoomFactor + change); + public setZoom(zoom: number) { + this._zoomFactor = zoom; + // console.log(this._timeline?.scrollWidth); } anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); @@ -216,7 +220,9 @@ export class CollectionStackedTimeline extends CollectionSubView< @action onPointerDownTimeline = (e: React.PointerEvent): void => { const rect = this._timeline?.getBoundingClientRect(); + const scrollLeft = this._timeline?.scrollLeft; const clientX = e.clientX; + const diff = rect ? clientX - rect?.x : null; const shiftKey = e.shiftKey; if (rect && this.props.isContentActive()) { const wasPlaying = this.props.playing(); @@ -529,7 +535,7 @@ export class CollectionStackedTimeline extends CollectionSubView< } @computed get renderAudioWaveform() { return !this.props.mediaPath ? null : ( - <div className="collectionStackedTimeline-waveform"> + <div className="collectionStackedTimeline-waveform" style={{ width: `${this.zoomFactor * 100}%`, overflowX: "scroll" }}> <AudioWaveform rawDuration={this.props.rawDuration} duration={this.clipDuration} @@ -538,6 +544,7 @@ export class CollectionStackedTimeline extends CollectionSubView< clipStart={this.clipStart} clipEnd={this.clipEnd} PanelHeight={this.timelineContentHeight} + zoomFactor={this.zoomFactor} /> </div> ); @@ -556,7 +563,7 @@ export class CollectionStackedTimeline extends CollectionSubView< } render() { - const timelineContentWidth = this.props.PanelWidth(); + const timelineContentWidth = this.props.PanelWidth() * this.zoomFactor; const overlaps: { anchorStartTime: number; anchorEndTime: number; @@ -573,8 +580,9 @@ export class CollectionStackedTimeline extends CollectionSubView< ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} onClick={(e) => this.isContentActive() && StopEvent(e)} onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)} - style={{ width: `${this._zoomFactor * 100}%` }} + onPointerEnter={(e) => { console.log("scroll"); e.preventDefault(); e.stopPropagation(); }} > + {drawAnchors.map((d) => { const start = this.anchorStart(d.anchor); const end = this.anchorEnd( @@ -868,19 +876,19 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> {inner.view} {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? null : ( - <> - <div - key="left" - className="collectionStackedTimeline-left-resizer" - onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)} - /> - <div - key="right" - className="collectionStackedTimeline-resizer" - onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, false)} - /> - </> - )} + <> + <div + key="left" + className="collectionStackedTimeline-left-resizer" + onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)} + /> + <div + key="right" + className="collectionStackedTimeline-resizer" + onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, false)} + /> + </> + )} </> ); } diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss index b33c7f506..a2fdd38e5 100644 --- a/src/client/views/nodes/AudioBox.scss +++ b/src/client/views/nodes/AudioBox.scss @@ -1,51 +1,50 @@ @import "../global/globalCssVariables.scss"; - .audiobox-container, .audiobox-container-interactive { - width: 100%; - height: 100%; - position: inherit; - display: flex; - position: relative; - cursor: default; - - .audiobox-buttons { - display: flex; width: 100%; - align-items: center; + height: 100%; + position: inherit; + display: flex; + position: relative; + cursor: default; - .audiobox-dictation { - position: relative; - width: 30px; - height: 100%; - align-items: center; - display: inherit; - background: $medium-gray; - left: 0px; - color: $dark-gray; - &:hover { - color: $black; - cursor: pointer; - } + .audiobox-buttons { + display: flex; + width: 100%; + align-items: center; + + .audiobox-dictation { + position: relative; + width: 30px; + height: 100%; + align-items: center; + display: inherit; + background: $medium-gray; + left: 0px; + color: $dark-gray; + &:hover { + color: $black; + cursor: pointer; + } + } } - } - .audiobox-control, - .audiobox-control-interactive { - top: 0; - max-height: 32px; - width: 100%; - display: inline-block; - pointer-events: none; - } + .audiobox-control, + .audiobox-control-interactive { + top: 0; + max-height: 32px; + width: 100%; + display: inline-block; + pointer-events: none; + } - .audiobox-control-interactive { - pointer-events: all; - } + .audiobox-control-interactive { + pointer-events: all; + } - .audiobox-record-interactive, - .audiobox-record { + .audiobox-record-interactive, + .audiobox-record { pointer-events: all; cursor: pointer; width: 100%; @@ -59,45 +58,46 @@ color: white; font-weight: bold; background-color: $dark-gray; - } + } - .audiobox-record { + .audiobox-record { pointer-events: none; - } + } - .recording { - margin-top: auto; - margin-bottom: auto; - width: 100%; - height: 100%; - position: relative; - padding-right: 5px; - display: flex; - background-color: $medium-blue; + .recording { + margin-top: auto; + margin-bottom: auto; + width: 100%; + height: 100%; + position: relative; + padding-right: 5px; + display: flex; + background-color: $medium-blue; - .time { - position: relative; - width: 100%; - font-size: $large-header; - text-align: center; - } + .time { + position: relative; + width: 100%; + font-size: $large-header; + text-align: center; + } - .recording-buttons { - position: relative; - margin-top: auto; - margin-bottom: auto; - color: $dark-gray; - &:hover { - color: $black; - } - } + .recording-buttons { + position: relative; + margin-top: auto; + margin-bottom: auto; + color: $dark-gray; + &:hover { + color: $black; + } + } - .time, .recording-buttons { - display: flex; - align-items: center; - padding: 5px; + .time, + .recording-buttons { + display: flex; + align-items: center; + padding: 5px; + } } - } .audiobox-buttons { display: flex; width: 100%; @@ -267,6 +267,44 @@ right: 2px; } + .toolbar-slider { + position: absolute; + top: 75px; + left: 70px; + } + + input[type="range"] { + width: calc(100% - 100px); + height: 16px; + -webkit-appearance: none; + background: none; + } + + input[type="range"]:focus { + outline: none; + } + + input[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 5px; + cursor: pointer; + box-shadow: 0; + background: #dfdfdf; + border-radius: 3px; + } + + input[type="range"]::-webkit-slider-thumb { + box-shadow: 0; + border: 0; + height: 7px; + width: 7px; + border-radius: 10px; + background: #4476f7; + cursor: pointer; + -webkit-appearance: none; + margin: -1px; + } + .audiobox-zoom { bottom: 0; left: 30px; diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 2eb34d27a..f2001adcd 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -45,6 +45,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp public static Enabled = false; static playheadWidth = 40; // width of playhead static heightPercent = 75; // height of timeline in percent of height of audioBox. + static zoomInterval = 0.1; @observable static _scrubTime = 0; _dropDisposer?: DragManager.DragDropDisposer; @@ -374,6 +375,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp })); } + zoom = (zoom: number) => { + this.timeline?.setZoom(zoom); + } + setupTimelineDrop = (r: HTMLDivElement | null) => { if (r && this.timeline) { this._dropDisposer?.(); @@ -437,6 +442,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp icon={this.mediaState === media_state.Paused ? "play" : "pause"} size={"1x"} /> </div> + <div className="audiobox-buttons" title={this.timeline?.IsTrimming !== TrimScope.None ? "finish" : "trim"} onPointerDown={this.onClipPointerDown} @@ -445,6 +451,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} size={"1x"} /> </div> + <div className="audiobox-timeline" style={{ left: AudioBox.playheadWidth, @@ -457,6 +464,13 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="audioBox-current-time"> {this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))} </div> + + {/* <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor} + className="toolbar-slider" id="zoom-slider" + onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(e.target.value); }} + /> */} + <div className="audioBox-total-time"> {this.timeline && formatTime(Math.round(NumCast(this.timeline?.clipDuration)))} </div> |