diff options
author | mehekj <mehek.jethani@gmail.com> | 2021-11-04 16:09:13 -0400 |
---|---|---|
committer | mehekj <mehek.jethani@gmail.com> | 2021-11-04 16:09:13 -0400 |
commit | bcae1802bc5277811476ce968a337813a7841fb6 (patch) | |
tree | 783aca01ca1fb09dfc8f0804f8d663f1d1fdb5ab | |
parent | 3ba076f93e2aa182698a229b381a77765f11213e (diff) |
smooth scroll in timeline while playing audio
-rw-r--r-- | src/Utils.ts | 20 | ||||
-rw-r--r-- | src/client/views/AudioWaveform.tsx | 118 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.scss | 17 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackedTimeline.tsx | 59 | ||||
-rw-r--r-- | src/client/views/nodes/AudioBox.tsx | 4 |
5 files changed, 178 insertions, 40 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index bfb29fe8d..9faea8d60 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -514,6 +514,26 @@ export function smoothScroll(duration: number, element: HTMLElement | HTMLElemen }; animateScroll(); } + +export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) { + const elements = (element instanceof HTMLElement ? [element] : element); + const starts = elements.map(element => element.scrollLeft); + const startDate = new Date().getTime(); + + const animateScroll = () => { + const currentDate = new Date().getTime(); + const currentTime = currentDate - startDate; + elements.map((element, i) => element.scrollLeft = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)); + + if (currentTime < duration) { + requestAnimationFrame(animateScroll); + } else { + elements.forEach(element => element.scrollLeft = to); + } + }; + animateScroll(); +} + export function addStyleSheet(styleType: string = "text/css") { const style = document.createElement("style"); style.type = styleType; diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 4d9c039c4..8a3c3c319 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -10,6 +10,7 @@ import { Cast, NumCast } from "../../fields/Types"; import { numberRange } from "../../Utils"; import "./AudioWaveform.scss"; import { Colors } from "./global/globalEnums"; +import Color = require("color"); export interface AudioWaveformProps { duration: number; // length of media clip @@ -19,15 +20,15 @@ export interface AudioWaveformProps { clipStart: number; clipEnd: number; zoomFactor: number; - PanelHeight: () => number; - PanelWidth: () => number; + PanelHeight: number; + PanelWidth: number; } @observer export class AudioWaveform extends React.Component<AudioWaveformProps> { public static NUMBER_OF_BUCKETS = 100; _disposer: IReactionDisposer | undefined; - @computed get waveHeight() { return Math.max(50, this.props.PanelHeight()); } + @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 zoomFactor() { return this.props.zoomFactor; } @@ -88,10 +89,17 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { render() { return ( <div className="audioWaveform"> + {/* <Waveform + barWidth={2} + width={() => this.props.PanelWidth} + height={this.props.PanelHeight} + peaks={this.audioBuckets} + color={Colors.MEDIUM_BLUE} + /> */} <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} - barWidth={2} + barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} @@ -103,14 +111,102 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { } -// export interface WaveformProps { -// barWidth: number; -// width: () => number; -// height: () => number; -// peaks: number[]; -// color: string; -// } +export interface WaveformProps { + barWidth: number; + width: () => number; + height: () => number; + peaks: number[]; + color: string; +} +// @observer // export class Waveform extends React.Component<WaveformProps> { +// private _canvas: HTMLCanvasElement | null = null; + +// get width() { return this.props.width(); } +// get height() { return this.props.height(); } +// get peaks() { return this.props.peaks; } + +// componentDidMount() { +// this.drawBars(); +// } + +// drawBars() { +// const waveCanvasCtx = this._canvas?.getContext("2d"); + +// if (waveCanvasCtx) { +// const pixelRatio = window.devicePixelRatio; +// console.log(pixelRatio); + +// const displayWidth = Math.round(this.width); +// const displayHeight = Math.round(this.height); +// waveCanvasCtx.canvas.width = this.width; +// waveCanvasCtx.canvas.height = this.height; +// waveCanvasCtx.canvas.style.width = `${displayWidth}px`; +// waveCanvasCtx.canvas.style.height = `${displayHeight}px`; + +// waveCanvasCtx.clearRect(0, 0, this.width, this.height); + +// const hasMinVals = [].some.call(this.peaks, (val) => val < 0); +// let filteredPeaks = this.peaks; +// if (hasMinVals) { +// // If the first value is negative, add 1 to the filtered indices +// let indexOffset = 0; +// if (this.peaks[0] < 0) { +// indexOffset = 1; +// } +// filteredPeaks = [].filter.call( +// this.peaks, +// (_, index) => (index + indexOffset) % 2 == 0 +// ); +// } + +// const $ = 0.5; +// const height = this.height; +// const offsetY = 0; +// const halfH = this.height / 2; +// const length = filteredPeaks.length; +// const bar = this.props.barWidth; +// const gap = 2; +// const step = bar + gap; + +// let absmax = 1; +// absmax = this.absMax(filteredPeaks); + +// const scale = length / this.width; + +// waveCanvasCtx.fillStyle = this.props.color; + +// for (let i = 0; i < this.width; i += step) { +// let h = Math.round(filteredPeaks[Math.floor(i * scale)] / absmax * halfH) +// if (h === 0) { +// h = 1 +// } +// waveCanvasCtx.fillRect(i + $, halfH - h + offsetY, bar + $, h * 2) +// } +// } +// } + +// absMax = (values: number[]) => { +// let max = -Infinity; +// for (const i in values) { +// const num = Math.abs(values[i]); +// if (num > max) { +// max = num; +// } +// } + +// return max; +// } + +// render() { +// return this.props.peaks ? ( +// <canvas +// ref={(instance) => { +// this._canvas = instance; +// }} +// /> +// ) : null +// } // }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 19913350b..fce105a44 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -3,25 +3,13 @@ .timeline-container { height: calc(100% - 50px); overflow-x: auto; + overflow-y: hidden; border: none; background-color: $white; border: 2px solid $dark-gray; border-width: 0 2px 0 2px; } -::-webkit-scrollbar { - position: relative; - -webkit-appearance: none; - height: 5px; -} - -::-webkit-scrollbar-thumb { - position: relative; - -webkit-appearance: none; - height: 5px; - background-color: $medium-gray; -} - .collectionStackedTimeline { position: absolute; background: $off-white; @@ -33,6 +21,7 @@ height: 100%; background-color: $dark-gray; opacity: 0.3; + top: 0; } .collectionStackedTimeline-trim-controls { @@ -43,6 +32,8 @@ display: flex; justify-content: space-between; max-width: 100%; + top: 0; + left: 0; .collectionStackedTimeline-trim-handle { background-color: $medium-blue; diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index b63835b00..ced8a68e8 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -23,6 +23,8 @@ import { setupMoveUpEvents, StopEvent, returnTrue, + smoothScroll, + smoothScrollHorizontal, } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { LinkManager } from "../../util/LinkManager"; @@ -81,6 +83,7 @@ export class CollectionStackedTimeline extends CollectionSubView< static LabelPlayScript: ScriptField; private _timeline: HTMLDivElement | null = null; + private _timelineWrapper: HTMLDivElement | null = null; private _markerStart: number = 0; @observable _markerEnd: number | undefined; @observable _trimming: number = TrimScope.None; @@ -345,8 +348,23 @@ export class CollectionStackedTimeline extends CollectionSubView< } @action - setScroll = (e: React.MouseEvent) => { - this._scroll = e.currentTarget.scrollLeft; + setScroll = (e: React.UIEvent) => { + e.stopPropagation(); + this._scroll = this._timelineWrapper!.scrollLeft; + } + + @action + scrollToTime = (time: number) => { + console.log("rightmost visible time: " + this.toTimeline(this._scroll + this.props.PanelWidth(), this.timelineContentWidth)); + if (this._timelineWrapper && time > this.toTimeline(this._scroll + this.props.PanelWidth(), this.timelineContentWidth)) { + console.log("scrolled"); + this._scroll = Math.min(this._scroll + this.props.PanelWidth(), this.timelineContentWidth - this.props.PanelWidth()); + smoothScrollHorizontal(100, this._timelineWrapper, this._scroll); + } + else if (this._timelineWrapper && time < this.toTimeline(this._scroll, this.timelineContentWidth)) { + this._scroll = time / this.timelineContentWidth * this.clipDuration; + smoothScrollHorizontal(100, this._timelineWrapper, this._scroll); + } } @action @@ -357,7 +375,7 @@ export class CollectionStackedTimeline extends CollectionSubView< // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); const x = localPt[0] - docDragData.offset[0]; - const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth()); + const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth); docDragData.droppedDocuments.forEach(drop => { const anchorEnd = this.anchorEnd(drop); if (anchorEnd !== undefined) { @@ -466,7 +484,7 @@ export class CollectionStackedTimeline extends CollectionSubView< m: Doc, placed: { anchorStartTime: number; anchorEndTime: number; level: number }[] ) => { - const timelineContentWidth = this.timelineContentWidth(); + const timelineContentWidth = this.timelineContentWidth; const x1 = this.anchorStart(m); const x2 = this.anchorEnd( m, @@ -497,9 +515,9 @@ export class CollectionStackedTimeline extends CollectionSubView< dictationHeightPercent = 50; dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100; - timelineContentHeight = () => (this.props.PanelHeight() * this.dictationHeightPercent) / 100; - timelineContentWidth = () => (this.props.PanelWidth() * this.zoomFactor - 4); // subtract size of container border - dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight()); + @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; @@ -510,7 +528,7 @@ export class CollectionStackedTimeline extends CollectionSubView< style={{ position: "absolute", height: "100%", - top: this.timelineContentHeight(), + top: this.timelineContentHeight, background: Colors.LIGHT_BLUE, }} > @@ -570,7 +588,6 @@ export class CollectionStackedTimeline extends CollectionSubView< } render() { - const timelineContentWidth = this.timelineContentWidth(); const overlaps: { anchorStartTime: number; anchorEndTime: number; @@ -584,25 +601,26 @@ export class CollectionStackedTimeline extends CollectionSubView< return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}> <div className="timeline-container" style={{ width: this.props.PanelWidth() }} - onScroll={this.setScroll}> + onScroll={this.setScroll} + ref={(wrapper: HTMLDivElement | null) => (this._timelineWrapper = wrapper)}> <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} onClick={(e) => this.isContentActive() && StopEvent(e)} onPointerDown={(e) => this.isContentActive() && this.onPointerDownTimeline(e)} - style={{ width: timelineContentWidth }}> + style={{ width: this.timelineContentWidth }}> {drawAnchors.map((d) => { const start = this.anchorStart(d.anchor); const end = this.anchorEnd( d.anchor, - start + (10 / timelineContentWidth) * this.clipDuration + start + (10 / this.timelineContentWidth) * this.clipDuration ); if (end < this.clipStart || start > this.clipEnd) return (null); - const left = Math.max((start - this.clipStart) / this.clipDuration * timelineContentWidth, 0); + const left = Math.max((start - this.clipStart) / this.clipDuration * this.timelineContentWidth, 0); const top = (d.level / maxLevel) * this.props.PanelHeight(); const timespan = Math.max(0, end - this.clipStart) - Math.max(0, start - this.clipStart); - const width = (timespan / this.clipDuration) * timelineContentWidth; + const width = (timespan / this.clipDuration) * this.timelineContentWidth; const height = this.props.PanelHeight() / maxLevel; return this.props.Document.hideAnchors ? null : ( <div @@ -641,7 +659,18 @@ export class CollectionStackedTimeline extends CollectionSubView< ); })} {!this.IsTrimming && this.selectionContainer} - {this.renderAudioWaveform} + {/* {this.renderAudioWaveform} */} + <AudioWaveform + rawDuration={this.props.rawDuration} + duration={this.clipDuration} + mediaPath={this.props.mediaPath} + layoutDoc={this.layoutDoc} + clipStart={this.clipStart} + clipEnd={this.clipEnd} + zoomFactor={this.zoomFactor} + PanelHeight={this.timelineContentHeight} + PanelWidth={this.timelineContentWidth} + /> {this.renderDictation} <div diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index a896c7d90..b51908e20 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -21,7 +21,7 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent"; import "./AudioBox.scss"; import { FieldView, FieldViewProps } from "./FieldView"; -import { timeStamp } from "console"; +import { time, timeStamp } from "console"; declare class MediaRecorder { constructor(e: any); // whatever MediaRecorder has @@ -139,6 +139,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } }); this.layoutDoc._currentTimecode = this._ele.currentTime; + console.log("current time: " + this.layoutDoc._currentTimecode); + this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode)); } } |