Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: clamp max canvas width #3697

Merged
merged 1 commit into from
May 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 68 additions & 77 deletions src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,32 +454,26 @@ class Renderer extends EventEmitter<RendererEvents> {
}

private renderSingleCanvas(
channelData: Array<Float32Array | number[]>,
data: Array<Float32Array | number[]>,
options: WaveSurferOptions,
width: number,
height: number,
start: number,
end: number,
offset: number,
canvasContainer: HTMLElement,
progressContainer: HTMLElement,
) {
const pixelRatio = window.devicePixelRatio || 1
const canvas = document.createElement('canvas')
const length = channelData[0].length
canvas.width = Math.round((width * (end - start)) / length)
canvas.height = height * pixelRatio
canvas.style.width = `${Math.floor(canvas.width / pixelRatio)}px`
canvas.width = Math.round(width * pixelRatio)
canvas.height = Math.round(height * pixelRatio)
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.style.left = `${Math.floor((start * width) / pixelRatio / length)}px`
canvas.style.left = `${Math.round(offset)}px`
canvasContainer.appendChild(canvas)

const ctx = canvas.getContext('2d') as CanvasRenderingContext2D

this.renderWaveform(
channelData.map((channel) => channel.slice(start, end)),
options,
ctx,
)
this.renderWaveform(data, options, ctx)

// Draw a progress canvas
if (canvas.width > 0 && canvas.height > 0) {
Expand All @@ -495,100 +489,97 @@ class Renderer extends EventEmitter<RendererEvents> {
}
}

private async renderChannel(
private async renderMultiCanvas(
channelData: Array<Float32Array | number[]>,
{ overlay, ...options }: WaveSurferOptions & { overlay?: boolean },
options: WaveSurferOptions,
width: number,
channelIndex: number,
): Promise<void> {
// A container for canvases
const canvasContainer = document.createElement('div')
const height = this.getHeight(options.height)
canvasContainer.style.height = `${height}px`
if (overlay && channelIndex > 0) {
canvasContainer.style.marginTop = `-${height}px`
}
this.canvasWrapper.style.minHeight = `${height}px`
this.canvasWrapper.appendChild(canvasContainer)

// A container for progress canvases
const progressContainer = canvasContainer.cloneNode() as HTMLElement
this.progressWrapper.appendChild(progressContainer)

const dataLength = channelData[0].length

// Draw a portion of the waveform from start peak to end peak
const draw = (start: number, end: number) => {
this.renderSingleCanvas(
channelData,
options,
width,
height,
Math.max(0, start),
Math.min(end, dataLength),
canvasContainer,
progressContainer,
)
}

// Draw the entire waveform
// Note, when the waveform container's width is set to a very large value, then the waveform will not be scrollable.
// However, we still want to draw the waveform in chunks for a) performance improvements, and b) so that the canvas never exceeds the browser's maximum canvas width.
if (!this.isScrollable && width < Renderer.MAX_CANVAS_WIDTH) {
draw(0, dataLength)
return
}

// Determine the currently visible part of the waveform
const { scrollLeft, scrollWidth, clientWidth } = this.scrollContainer
const scale = dataLength / scrollWidth

let viewportWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, clientWidth)
height: number,
canvasContainer: HTMLElement,
progressContainer: HTMLElement,
) {
const pixelRatio = window.devicePixelRatio || 1
const totalWidth = width / pixelRatio
let singleCanvasWidth = Math.min(Renderer.MAX_CANVAS_WIDTH, this.scrollContainer.clientWidth)

// Adjust width to avoid gaps between canvases when using bars
if (options.barWidth || options.barGap) {
const barWidth = options.barWidth || 0.5
const barGap = options.barGap || barWidth / 2
const totalBarWidth = barWidth + barGap
if (viewportWidth % totalBarWidth !== 0) {
viewportWidth = Math.floor(viewportWidth / totalBarWidth) * totalBarWidth
if (singleCanvasWidth % totalBarWidth !== 0) {
singleCanvasWidth = Math.floor(singleCanvasWidth / totalBarWidth) * totalBarWidth
}
}

const start = Math.floor(Math.abs(scrollLeft) * scale)
const end = Math.floor(start + viewportWidth * scale)
const viewportLen = end - start
const draw = (index: number) => {
const offset = index * singleCanvasWidth
const clampedWidth = Math.min(totalWidth - offset, singleCanvasWidth)
const data = channelData.map((channel) => {
const start = Math.floor((offset / totalWidth) * channel.length)
const end = Math.floor(((offset + clampedWidth) / totalWidth) * channel.length)
return channel.slice(start, end)
})
this.renderSingleCanvas(data, options, clampedWidth, height, offset, canvasContainer, progressContainer)
}

const numCanvases = Math.ceil(totalWidth / singleCanvasWidth)

if (viewportLen <= 0) {
if (numCanvases === 1) {
draw(0)
return
}

// Draw the visible part of the waveform
draw(start, end)
// Draw the canvases in the viewport first
const viewPosition = this.scrollContainer.scrollLeft / totalWidth
const startCanvas = Math.floor(viewPosition * numCanvases)

draw(startCanvas)
draw(startCanvas + 1)

// Draw the waveform in chunks equal to the size of the viewport, starting from the position of the viewport
await Promise.all([
// Draw the chunks to the left of the viewport
// Render canvases to the left of the viewport
(async () => {
if (start === 0) return
const delay = this.createDelay()
for (let i = start; i >= 0; i -= viewportLen) {
for (let i = startCanvas - 1; i >= 0; i--) {
await delay()
draw(Math.max(0, i - viewportLen), i)
draw(i)
}
})(),
// Draw the chunks to the right of the viewport
// Render canvases to the right of the viewport
(async () => {
if (end === dataLength) return
const delay = this.createDelay()
for (let i = end; i < dataLength; i += viewportLen) {
for (let i = startCanvas + 2; i < numCanvases; i++) {
await delay()
draw(i, Math.min(dataLength, i + viewportLen))
draw(i)
}
})(),
])
}

private async renderChannel(
channelData: Array<Float32Array | number[]>,
{ overlay, ...options }: WaveSurferOptions & { overlay?: boolean },
width: number,
channelIndex: number,
) {
// A container for canvases
const canvasContainer = document.createElement('div')
const height = this.getHeight(options.height)
canvasContainer.style.height = `${height}px`
if (overlay && channelIndex > 0) {
canvasContainer.style.marginTop = `-${height}px`
}
this.canvasWrapper.style.minHeight = `${height}px`
this.canvasWrapper.appendChild(canvasContainer)

// A container for progress canvases
const progressContainer = canvasContainer.cloneNode() as HTMLElement
this.progressWrapper.appendChild(progressContainer)

// Render the waveform
await this.renderMultiCanvas(channelData, options, width, height, canvasContainer, progressContainer)
}

async render(audioData: AudioBuffer) {
// Clear previous timeouts
this.timeouts.forEach((clear) => clear())
Expand Down
Loading