From bcae1802bc5277811476ce968a337813a7841fb6 Mon Sep 17 00:00:00 2001 From: mehekj Date: Thu, 4 Nov 2021 16:09:13 -0400 Subject: smooth scroll in timeline while playing audio --- src/Utils.ts | 20 ++++ src/client/views/AudioWaveform.tsx | 118 +++++++++++++++++++-- .../collections/CollectionStackedTimeline.scss | 17 +-- .../collections/CollectionStackedTimeline.tsx | 59 ++++++++--- src/client/views/nodes/AudioBox.tsx | 4 +- 5 files changed, 178 insertions(+), 40 deletions(-) (limited to 'src') 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 { 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 { render() { return (
+ {/* this.props.PanelWidth} + height={this.props.PanelHeight} + peaks={this.audioBuckets} + color={Colors.MEDIUM_BLUE} + /> */} { } -// 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 { +// 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 ? ( +// { +// 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 (
+ onScroll={this.setScroll} + ref={(wrapper: HTMLDivElement | null) => (this._timelineWrapper = wrapper)}>
(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 : (
{this.renderDictation}