150 lines
5.0 KiB
TypeScript
150 lines
5.0 KiB
TypeScript
|
|
import type { WaveformPoint } from '../types'
|
||
|
|
import type { ResolvedWaveformRenderingOptions } from './renderingOptions'
|
||
|
|
|
||
|
|
export interface VisiblePointRange {
|
||
|
|
start: number
|
||
|
|
end: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RenderablePointSelectionContext {
|
||
|
|
points: WaveformPoint[]
|
||
|
|
range: VisiblePointRange
|
||
|
|
domain: [number, number]
|
||
|
|
width: number
|
||
|
|
options: ResolvedWaveformRenderingOptions
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RenderablePointSelectionStrategy {
|
||
|
|
readonly name: 'complete' | 'peak-preserving'
|
||
|
|
select(context: RenderablePointSelectionContext): WaveformPoint[]
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectionBounds(range: VisiblePointRange, pointCount: number) {
|
||
|
|
return {
|
||
|
|
start: Math.max(0, range.start - 1),
|
||
|
|
end: Math.min(pointCount, range.end + 1),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushUniquePoint(target: WaveformPoint[], point: WaveformPoint | undefined) {
|
||
|
|
if (point && target[target.length - 1] !== point) target.push(point)
|
||
|
|
}
|
||
|
|
|
||
|
|
export class CompletePointSelectionStrategy implements RenderablePointSelectionStrategy {
|
||
|
|
readonly name = 'complete' as const
|
||
|
|
|
||
|
|
select(context: RenderablePointSelectionContext): WaveformPoint[] {
|
||
|
|
const { start, end } = selectionBounds(context.range, context.points.length)
|
||
|
|
return context.points.slice(start, end)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class PeakPreservingPointSelectionStrategy implements RenderablePointSelectionStrategy {
|
||
|
|
readonly name = 'peak-preserving' as const
|
||
|
|
|
||
|
|
select(context: RenderablePointSelectionContext): WaveformPoint[] {
|
||
|
|
const { points, range, domain, width, options } = context
|
||
|
|
const { start, end } = selectionBounds(range, points.length)
|
||
|
|
const visibleCount = end - start
|
||
|
|
if (visibleCount <= 0) return []
|
||
|
|
|
||
|
|
const domainStart = Math.min(domain[0], domain[1])
|
||
|
|
const domainEnd = Math.max(domain[0], domain[1])
|
||
|
|
const maximumPointCount = Math.max(4, Math.floor(width * options.maxPointsPerPixel))
|
||
|
|
const bucketCount = Math.max(1, Math.floor(maximumPointCount / 4))
|
||
|
|
const result: WaveformPoint[] = []
|
||
|
|
const span = domainEnd - domainStart || 1
|
||
|
|
const bucketIndexes = Array.from({ length: 4 }, () => -1)
|
||
|
|
let activeBucket = -1
|
||
|
|
let firstIndex = -1
|
||
|
|
let lastIndex = -1
|
||
|
|
let minimumIndex = -1
|
||
|
|
let maximumIndex = -1
|
||
|
|
|
||
|
|
const addBucketIndex = (index: number, count: number) => {
|
||
|
|
if (index < 0) return count
|
||
|
|
for (let position = 0; position < count; position += 1) {
|
||
|
|
if (bucketIndexes[position] === index) return count
|
||
|
|
}
|
||
|
|
bucketIndexes[count] = index
|
||
|
|
return count + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
const flushBucket = () => {
|
||
|
|
if (firstIndex < 0) return
|
||
|
|
let count = 0
|
||
|
|
count = addBucketIndex(firstIndex, count)
|
||
|
|
count = addBucketIndex(minimumIndex, count)
|
||
|
|
count = addBucketIndex(maximumIndex, count)
|
||
|
|
count = addBucketIndex(lastIndex, count)
|
||
|
|
for (let index = 1; index < count; index += 1) {
|
||
|
|
const value = bucketIndexes[index]
|
||
|
|
let position = index - 1
|
||
|
|
while (position >= 0 && bucketIndexes[position] > value) {
|
||
|
|
bucketIndexes[position + 1] = bucketIndexes[position]
|
||
|
|
position -= 1
|
||
|
|
}
|
||
|
|
bucketIndexes[position + 1] = value
|
||
|
|
}
|
||
|
|
for (let index = 0; index < count; index += 1) {
|
||
|
|
pushUniquePoint(result, points[bucketIndexes[index]])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pushUniquePoint(result, points[start])
|
||
|
|
for (let index = range.start; index < range.end; index += 1) {
|
||
|
|
const point = points[index]
|
||
|
|
const bucket = Math.min(
|
||
|
|
bucketCount - 1,
|
||
|
|
Math.max(0, Math.floor(((point.x - domainStart) / span) * bucketCount)),
|
||
|
|
)
|
||
|
|
if (bucket !== activeBucket) {
|
||
|
|
flushBucket()
|
||
|
|
activeBucket = bucket
|
||
|
|
firstIndex = index
|
||
|
|
lastIndex = index
|
||
|
|
minimumIndex = index
|
||
|
|
maximumIndex = index
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
lastIndex = index
|
||
|
|
if (point.y < points[minimumIndex].y) minimumIndex = index
|
||
|
|
if (point.y > points[maximumIndex].y) maximumIndex = index
|
||
|
|
}
|
||
|
|
flushBucket()
|
||
|
|
pushUniquePoint(result, points[end - 1])
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RenderablePointSelectionStrategyRequest {
|
||
|
|
visibleCount: number
|
||
|
|
width: number
|
||
|
|
options: ResolvedWaveformRenderingOptions
|
||
|
|
}
|
||
|
|
|
||
|
|
export class RenderablePointSelectionStrategyFactory {
|
||
|
|
private readonly complete = new CompletePointSelectionStrategy()
|
||
|
|
private readonly peakPreserving = new PeakPreservingPointSelectionStrategy()
|
||
|
|
|
||
|
|
resolve(request: RenderablePointSelectionStrategyRequest): RenderablePointSelectionStrategy {
|
||
|
|
const maximumPointCount = Math.max(
|
||
|
|
4,
|
||
|
|
Math.floor(request.width * request.options.maxPointsPerPixel),
|
||
|
|
)
|
||
|
|
const shouldUseCompletePoints =
|
||
|
|
!request.options.downsample ||
|
||
|
|
request.visibleCount <= request.options.downsampleThreshold ||
|
||
|
|
request.visibleCount <= maximumPointCount
|
||
|
|
return shouldUseCompletePoints ? this.complete : this.peakPreserving
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const defaultRenderablePointSelectionStrategyFactory = new RenderablePointSelectionStrategyFactory()
|
||
|
|
|
||
|
|
export function resolveRenderablePointSelectionStrategy(
|
||
|
|
request: RenderablePointSelectionStrategyRequest,
|
||
|
|
): RenderablePointSelectionStrategy {
|
||
|
|
return defaultRenderablePointSelectionStrategyFactory.resolve(request)
|
||
|
|
}
|