update vu needles

This commit is contained in:
Boof2015
2026-08-16 21:47:43 -04:00
parent 04f254512c
commit c73dac4fc1
5 changed files with 334 additions and 77 deletions
+3
View File
@@ -1,5 +1,8 @@
import { StrictMode, type JSX } from 'react'
import { createRoot } from 'react-dom/client'
import '@fontsource/jetbrains-mono/latin-400.css'
import '@fontsource/jetbrains-mono/latin-600.css'
import '@fontsource/jetbrains-mono/latin-700.css'
import '../renderer/styles/globals.css'
import './styles.css'
import ScopeApp from './ScopeApp'
+2
View File
@@ -9,6 +9,8 @@ import '@fontsource/inter/400.css'
import '@fontsource/inter/500.css'
import '@fontsource/inter/600.css'
import '@fontsource/jetbrains-mono/400.css'
import '@fontsource/jetbrains-mono/latin-600.css'
import '@fontsource/jetbrains-mono/latin-700.css'
import { SCOPE_KINDS, type ScopeKind } from '../types/scope'
import { bootstrapWindowBackgroundFromQuery } from './windowBackground'
+210 -75
View File
@@ -83,8 +83,8 @@ const INITIAL_NATIVE_SNAPSHOT: VUMeterNativeSnapshot = {
const NEEDLE_VISUAL_SMOOTHING_SECONDS = 0.065
const NEEDLE_PEAK_DECAY_DB_PER_SECOND = 12
const NEEDLE_PIVOT_FRACTION = 0.91
const NEEDLE_STEREO_GAP_FRACTION = 0.14
const NEEDLE_PIVOT_FRACTION = 0.885
const NEEDLE_STEREO_GAP_FRACTION = 0.12
export const VU_NEEDLE_FACE_WIDTH_CSS_PX = 560
export const VU_NEEDLE_FACE_HEIGHT_CSS_PX = 360
@@ -184,6 +184,14 @@ export const CLASSIC_VU_LABELS: ReadonlyArray<{ vu: number; label: string }> = [
{ vu: 3, label: '+3' },
]
function isClassicVuMajorLabel(vu: number): boolean {
return vu <= -10 || vu === 0 || vu > 0
}
function shouldShowClassicVuLabel(vu: number): boolean {
return isClassicVuMajorLabel(vu) || vu === -5 || vu === -3 || vu === -1
}
const CLASSIC_VU_ANCHORS: ReadonlyArray<{ vu: number; position: number }> = [
{ vu: -20, position: 0 },
{ vu: -10, position: 0.24 },
@@ -759,6 +767,7 @@ export class VUMeter {
labelAlpha = 0.6,
minLabelSize = 8,
maxLabelSize = 20,
outlineAlpha = 0,
): void {
const centerX = x + w / 2
const corr = Math.max(-1, Math.min(1, this.correlation))
@@ -796,6 +805,12 @@ export class VUMeter {
ctx.fillText('Ø', centerX, y + h / 2)
ctx.textAlign = 'right'
ctx.fillText('+1', x + w - 2, y + h / 2)
if (outlineAlpha > 0 && w > 1 && h > 1) {
ctx.strokeStyle = alphaColor(this.options.scaleColor, outlineAlpha)
ctx.lineWidth = 1
ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1)
}
}
private drawNeedleMode(width: number, height: number): void {
@@ -817,19 +832,32 @@ export class VUMeter {
const gap = 4 * faceScale
const meterAreaHeight = Math.max(1, faceHeight - corrHeight - gap)
const sidePadding = 11 * faceScale
const labelFontSize = 23.5 * faceScale
const outerArcWidth = 22 * faceScale
const innerArcWidth = 17 * faceScale
const needleWidth = 3.25 * faceScale
const labelFontSize = 26 * faceScale
const outerArcWidth = 18 * faceScale
const innerArcWidth = 13.5 * faceScale
const needleWidth = 3 * faceScale
const pivotY = meterAreaHeight * NEEDLE_PIVOT_FRACTION
const tickOutward = 5 * faceScale
const topPadding = 10 * faceScale
const horizontalLabelAllowance = labelFontSize * 0.32
const verticalLabelAllowance = labelFontSize * 0.62
const radiusXByWidth = (faceWidth / 2 - sidePadding) / spanCos
- outerArcWidth / 2
- tickOutward
- horizontalLabelAllowance
const labelOffset = outerArcWidth / 2 + tickOutward + labelFontSize * 0.68
let radiusXByWidth = Number.POSITIVE_INFINITY
for (const { vu, label } of CLASSIC_VU_LABELS) {
if (!shouldShowClassicVuLabel(vu)) continue
const isMajor = isClassicVuMajorLabel(vu)
ctx.font = `${isMajor ? 700 : 600} ${isMajor ? labelFontSize : labelFontSize * 0.92}px "JetBrains Mono", monospace`
const halfLabelWidth = ctx.measureText(label).width / 2
const angle = this.vuToNeedleAngle(vu, startAngle, endAngle)
const horizontalDirection = Math.abs(Math.cos(angle))
if (horizontalDirection <= 1e-6) continue
radiusXByWidth = Math.min(
radiusXByWidth,
(faceWidth / 2 - sidePadding - halfLabelWidth) / horizontalDirection - labelOffset,
)
}
if (!Number.isFinite(radiusXByWidth)) {
radiusXByWidth = (faceWidth / 2 - sidePadding) / spanCos - labelOffset
}
const radiusYByHeight = pivotY
- outerArcWidth / 2
- tickOutward
@@ -869,14 +897,14 @@ export class VUMeter {
outerArcWidth,
innerArcWidth,
)
this.drawSharedNeedleScale(ctx, centerX, centerY, outerRadiusX, outerRadiusY, outerArcWidth, startAngle, endAngle, labelFontSize, faceWidth, sidePadding, tickOutward)
this.drawSharedNeedleScale(ctx, centerX, centerY, outerRadiusX, outerRadiusY, outerArcWidth, startAngle, endAngle, labelFontSize, tickOutward)
this.drawNeedlePeakMarkers(ctx, centerX, centerY, outerRadiusX, outerRadiusY, innerRadiusX, innerRadiusY, startAngle, endAngle, readoutReadings, outerArcWidth, innerArcWidth)
this.drawSharedNeedles(ctx, centerX, centerY, outerRadiusX, outerRadiusY, startAngle, endAngle, visualReadings, needleWidth)
this.drawNeedleReadouts(ctx, readoutReadings, meterAreaHeight, faceWidth, sidePadding, labelFontSize)
this.drawNeedleReadouts(ctx, readoutReadings, meterAreaHeight, faceWidth, sidePadding, faceScale)
const barLeft = Math.max(10 * faceScale, sidePadding)
const barWidth = Math.max(1, faceWidth - barLeft * 2)
this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb, 0.72, 0.76, 8 * faceScale, 18 * faceScale)
this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb, 0.72, 0.76, 8 * faceScale, 18 * faceScale, 0.24)
ctx.restore()
}
@@ -918,30 +946,63 @@ export class VUMeter {
if (!mainReading) return
ctx.lineCap = 'butt'
ctx.strokeStyle = alphaColor(this.getNeedleColor(mainReading), 0.052)
ctx.strokeStyle = alphaColor(this.getNeedleColor(mainReading), 0.075)
ctx.lineWidth = outerWidth
ctx.beginPath()
ctx.ellipse(centerX, centerY, outerRadiusX, outerRadiusY, 0, startAngle, hotAngle)
ctx.stroke()
ctx.strokeStyle = alphaColor(this.options.clipColor, 0.085)
ctx.strokeStyle = alphaColor(this.options.clipColor, 0.15)
ctx.beginPath()
ctx.ellipse(centerX, centerY, outerRadiusX, outerRadiusY, 0, hotAngle, endAngle)
ctx.stroke()
if (leftReading) {
ctx.strokeStyle = alphaColor(this.getNeedleColor(leftReading), 0.04)
ctx.strokeStyle = alphaColor(this.getNeedleColor(leftReading), 0.06)
ctx.lineWidth = innerWidth
ctx.beginPath()
ctx.ellipse(centerX, centerY, innerRadiusX, innerRadiusY, 0, startAngle, hotAngle)
ctx.stroke()
ctx.strokeStyle = alphaColor(this.options.clipColor, 0.065)
ctx.strokeStyle = alphaColor(this.options.clipColor, 0.12)
ctx.beginPath()
ctx.ellipse(centerX, centerY, innerRadiusX, innerRadiusY, 0, hotAngle, endAngle)
ctx.stroke()
this.drawNeedleLevelArc(ctx, leftReading, centerX, centerY, innerRadiusX, innerRadiusY, innerWidth, startAngle, hotAngle, endAngle, 0.72)
this.drawNeedleLevelArc(ctx, leftReading, centerX, centerY, innerRadiusX, innerRadiusY, innerWidth, startAngle, hotAngle, endAngle, 0.78)
}
this.drawNeedleLevelArc(ctx, mainReading, centerX, centerY, outerRadiusX, outerRadiusY, outerWidth, startAngle, hotAngle, endAngle, 0.95)
this.drawNeedleLevelArc(ctx, mainReading, centerX, centerY, outerRadiusX, outerRadiusY, outerWidth, startAngle, hotAngle, endAngle, 0.96)
if (leftReading) {
this.drawNeedleRailOutline(ctx, centerX, centerY, innerRadiusX, innerRadiusY, innerWidth, startAngle, endAngle, 0.14)
}
this.drawNeedleRailOutline(ctx, centerX, centerY, outerRadiusX, outerRadiusY, outerWidth, startAngle, endAngle, 0.18)
}
private drawNeedleRailOutline(
ctx: CanvasRenderingContext2D,
centerX: number,
centerY: number,
radiusX: number,
radiusY: number,
railWidth: number,
startAngle: number,
endAngle: number,
alpha: number,
): void {
ctx.strokeStyle = alphaColor(this.options.scaleColor, alpha)
ctx.lineWidth = Math.max(1, railWidth * 0.065)
ctx.lineCap = 'butt'
for (const offset of [-railWidth / 2, railWidth / 2]) {
ctx.beginPath()
ctx.ellipse(
centerX,
centerY,
Math.max(1, radiusX + offset),
Math.max(1, radiusY + offset),
0,
startAngle,
endAngle,
)
ctx.stroke()
}
}
private drawNeedleLevelArc(
@@ -990,8 +1051,6 @@ export class VUMeter {
startAngle: number,
endAngle: number,
labelFontSize: number,
width: number,
sidePadding: number,
tickOutward: number,
): void {
const averageRadius = (radiusX + radiusY) / 2
@@ -1002,14 +1061,16 @@ export class VUMeter {
for (const { vu, label } of CLASSIC_VU_LABELS) {
const angle = this.vuToNeedleAngle(vu, startAngle, endAngle)
const isHot = vu >= 0
const isMajor = vu <= -10 || vu === 0 || vu > 0
const shouldShowLabel = isMajor || vu === -5 || vu === -3 || vu === -2 || vu === -1
const isMajor = isClassicVuMajorLabel(vu)
const shouldShowLabel = shouldShowClassicVuLabel(vu)
const inward = isMajor ? majorInward : minorInward
ctx.strokeStyle = isHot
? alphaColor(this.options.clipColor, 0.92)
: alphaColor(this.options.scaleColor, 0.72)
ctx.lineWidth = isMajor ? Math.max(2.25, averageRadius * 0.014) : Math.max(1.15, averageRadius * 0.008)
? alphaColor(this.options.clipColor, 1)
: alphaColor(this.options.scaleColor, isMajor ? 0.96 : 0.82)
ctx.lineWidth = isMajor
? Math.max(2.6, averageRadius * 0.016)
: Math.max(1.5, averageRadius * 0.0095)
const tickStart = this.needleEllipsePoint(centerX, centerY, radiusX, radiusY, angle, outerWidth / 2 + tickOutward)
const tickEnd = this.needleEllipsePoint(centerX, centerY, radiusX, radiusY, angle, outerWidth / 2 - inward)
ctx.beginPath()
@@ -1018,46 +1079,22 @@ export class VUMeter {
ctx.stroke()
if (shouldShowLabel) {
const labelPoint = this.needleEllipsePoint(centerX, centerY, radiusX, radiusY, angle, outerWidth / 2 + tickOutward + labelFontSize * 0.64)
const x = clamp(labelPoint.x, sidePadding, width - sidePadding)
const y = labelPoint.y
const labelPoint = this.needleEllipsePoint(centerX, centerY, radiusX, radiusY, angle, outerWidth / 2 + tickOutward + labelFontSize * 0.68)
ctx.fillStyle = isHot
? alphaColor(this.options.clipColor, 0.98)
: alphaColor(this.options.labelColor, 0.9)
ctx.font = `${isMajor ? '700 ' : ''}${isMajor ? labelFontSize : labelFontSize * 0.9}px "JetBrains Mono", monospace`
? alphaColor(this.options.clipColor, 1)
: alphaColor(this.options.labelColor, 1)
ctx.font = `${isMajor ? 700 : 600} ${isMajor ? labelFontSize : labelFontSize * 0.92}px "JetBrains Mono", monospace`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(label, x, y)
}
}
const percentFontSize = labelFontSize * 0.62
ctx.font = `${percentFontSize}px "JetBrains Mono", monospace`
ctx.fillStyle = alphaColor(this.options.labelColor, 0.48)
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const percentMarks = [
{ vu: -20, label: '0' },
{ vu: -10, label: '20' },
{ vu: -5, label: '40' },
{ vu: -3, label: '60' },
{ vu: -1, label: '80' },
{ vu: 0, label: '100' },
]
for (const { vu, label } of percentMarks) {
const angle = this.vuToNeedleAngle(vu, startAngle, endAngle)
const x = centerX + Math.cos(angle) * (radiusX * 0.55)
const y = centerY + Math.sin(angle) * (radiusY * 0.55)
if (y < centerY - 4) {
ctx.fillText(label, x, y)
ctx.fillText(label, labelPoint.x, labelPoint.y)
}
}
const titleFontSize = Math.min(labelFontSize * 0.88, averageRadius * 0.095)
const titleY = centerY - radiusY * 0.14
if (titleY > 4) {
ctx.fillStyle = alphaColor(this.options.labelColor, 0.48)
ctx.font = `${titleFontSize}px "JetBrains Mono", monospace`
ctx.fillStyle = alphaColor(this.options.labelColor, 0.66)
ctx.font = `700 ${titleFontSize}px "JetBrains Mono", monospace`
ctx.fillText('VU', centerX, titleY)
}
}
@@ -1132,13 +1169,11 @@ export class VUMeter {
const leftReading = readings.length > 1 ? readings[0] : null
const averageRadius = (radiusX + radiusY) / 2
if (leftReading) {
this.drawSharedNeedle(ctx, leftReading, centerX, centerY, radiusX, radiusY, startAngle, endAngle, 0.5, needleWidth * 0.72)
this.drawSharedNeedle(ctx, leftReading, centerX, centerY, radiusX, radiusY, startAngle, endAngle, 0.62, needleWidth * 0.76)
}
if (mainReading) {
this.drawSharedNeedle(ctx, mainReading, centerX, centerY, radiusX, radiusY, startAngle, endAngle, 0.96, needleWidth)
const pivotSize = Math.max(2.3, averageRadius * 0.015)
ctx.fillStyle = alphaColor(this.getNeedleColor(mainReading), 0.65)
ctx.fillRect(centerX - pivotSize, centerY - pivotSize, pivotSize * 2, pivotSize * 2)
this.drawNeedleHub(ctx, centerX, centerY, averageRadius, mainReading)
}
}
@@ -1162,6 +1197,14 @@ export class VUMeter {
const baseX = centerX - Math.cos(angle) * counterWeight
const baseY = centerY - Math.sin(angle) * counterWeight
ctx.strokeStyle = alphaColor(this.options.scaleColor, alpha * 0.32)
ctx.lineWidth = lineWidth + Math.max(1.25, averageRadius * 0.008)
ctx.lineCap = 'round'
ctx.beginPath()
ctx.moveTo(baseX, baseY)
ctx.lineTo(tip.x, tip.y)
ctx.stroke()
ctx.strokeStyle = alphaColor(color, alpha)
ctx.lineWidth = lineWidth
ctx.lineCap = 'round'
@@ -1176,37 +1219,129 @@ export class VUMeter {
ctx.fill()
}
private drawNeedleHub(
ctx: CanvasRenderingContext2D,
centerX: number,
centerY: number,
averageRadius: number,
reading: VUNeedleReading,
): void {
const outerRadius = Math.max(2.5, averageRadius * 0.031)
ctx.fillStyle = alphaColor(this.options.labelColor, 0.12)
ctx.strokeStyle = alphaColor(this.options.scaleColor, 0.72)
ctx.lineWidth = Math.max(1, averageRadius * 0.007)
ctx.beginPath()
ctx.arc(centerX, centerY, outerRadius, 0, Math.PI * 2)
ctx.fill()
ctx.stroke()
ctx.fillStyle = alphaColor(this.getNeedleColor(reading), 0.94)
ctx.beginPath()
ctx.arc(centerX, centerY, outerRadius * 0.42, 0, Math.PI * 2)
ctx.fill()
}
private drawNeedleReadouts(
ctx: CanvasRenderingContext2D,
readings: VUNeedleReading[],
meterAreaHeight: number,
width: number,
sidePadding: number,
labelFontSize: number,
faceScale: number,
): void {
const mainReading = readings[readings.length - 1]
if (!mainReading) return
const leftReading = readings.length > 1 ? readings[0] : null
const fontSize = labelFontSize * 0.88
const y = meterAreaHeight - 3
ctx.font = `${fontSize}px "JetBrains Mono", monospace`
ctx.textBaseline = 'bottom'
const plateHeight = 31 * faceScale
const plateY = meterAreaHeight - plateHeight - faceScale
if (!leftReading) {
ctx.fillStyle = alphaColor(this.getNeedleColor(mainReading), 0.94)
ctx.textAlign = 'center'
ctx.fillText(`${this.formatNeedleDb(mainReading.db)} dB`, width / 2, y)
const plateWidth = Math.min(170 * faceScale, width - sidePadding * 2)
this.drawNeedleReadoutPlate(
ctx,
mainReading,
null,
(width - plateWidth) / 2,
plateY,
plateWidth,
plateHeight,
faceScale,
)
return
}
ctx.fillStyle = alphaColor(this.getNeedleColor(leftReading), 0.88)
ctx.textAlign = 'left'
ctx.fillText(`L ${this.formatNeedleDb(leftReading.db)} dB`, sidePadding, y)
const centerGap = 52 * faceScale
const plateWidth = Math.min(
148 * faceScale,
Math.max(1, (width - sidePadding * 2 - centerGap) / 2),
)
this.drawNeedleReadoutPlate(
ctx,
leftReading,
'L',
sidePadding,
plateY,
plateWidth,
plateHeight,
faceScale,
)
this.drawNeedleReadoutPlate(
ctx,
mainReading,
'R',
width - sidePadding - plateWidth,
plateY,
plateWidth,
plateHeight,
faceScale,
)
}
ctx.fillStyle = alphaColor(this.getNeedleColor(mainReading), 0.96)
private drawNeedleReadoutPlate(
ctx: CanvasRenderingContext2D,
reading: VUNeedleReading,
channelLabel: 'L' | 'R' | null,
x: number,
y: number,
width: number,
height: number,
faceScale: number,
): void {
const channelColor = this.getNeedleColor(reading)
const padding = 8 * faceScale
const centerY = y + height / 2 + 0.5 * faceScale
ctx.fillStyle = alphaColor(this.options.labelColor, 0.055)
ctx.fillRect(x, y, width, height)
ctx.fillStyle = alphaColor(channelColor, 0.72)
ctx.fillRect(x, y, width, Math.max(1, 1.5 * faceScale))
ctx.strokeStyle = alphaColor(this.options.scaleColor, 0.24)
ctx.lineWidth = Math.max(1, faceScale)
ctx.strokeRect(x + 0.5 * faceScale, y + 0.5 * faceScale, width - faceScale, height - faceScale)
ctx.textBaseline = 'middle'
if (channelLabel) {
ctx.fillStyle = alphaColor(channelColor, 0.82)
ctx.font = `700 ${13.5 * faceScale}px "JetBrains Mono", monospace`
ctx.textAlign = 'left'
ctx.fillText(channelLabel, x + padding, centerY)
}
ctx.fillStyle = alphaColor(channelColor, 0.98)
ctx.font = `600 ${19.5 * faceScale}px "JetBrains Mono", monospace`
ctx.textAlign = channelLabel ? 'left' : 'center'
ctx.fillText(
this.formatNeedleDb(reading.db),
channelLabel ? x + padding + 24 * faceScale : x + width / 2 - 10 * faceScale,
centerY,
)
ctx.fillStyle = alphaColor(this.options.labelColor, 0.56)
ctx.font = `600 ${11.5 * faceScale}px "JetBrains Mono", monospace`
ctx.textAlign = 'right'
ctx.fillText(`${this.formatNeedleDb(mainReading.db)} dB R`, width - sidePadding, y)
ctx.fillText('dB', x + width - padding, centerY)
}
private formatNeedleDb(db: number): string {
+116 -2
View File
@@ -609,6 +609,7 @@ interface FakeCanvasRecorder {
lineStrokes: Array<{
commands: Array<{ kind: 'moveTo' | 'lineTo'; x: number; y: number }>
strokeStyle: string
lineWidth: number
}>
strokeRects: Array<{ x: number; y: number; width: number; height: number; lineDash: number[] }>
arcs: Array<{
@@ -647,6 +648,7 @@ function createFakeCanvasContext(recorder: FakeCanvasRecorder | null = null): Ca
let currentLineDash: number[] = []
let currentFillStyle = ''
let currentStrokeStyle = ''
let currentLineWidth = 1
let currentFont = ''
let currentCompositeOperation: GlobalCompositeOperation = 'source-over'
let currentPath: Array<{ kind: 'moveTo' | 'lineTo'; x: number; y: number }> = []
@@ -674,7 +676,11 @@ function createFakeCanvasContext(recorder: FakeCanvasRecorder | null = null): Ca
},
stroke() {
if (currentPath.length > 0) {
recorder?.lineStrokes.push({ commands: [...currentPath], strokeStyle: currentStrokeStyle })
recorder?.lineStrokes.push({
commands: [...currentPath],
strokeStyle: currentStrokeStyle,
lineWidth: currentLineWidth,
})
}
},
strokeRect(x: number, y: number, width: number, height: number) {
@@ -683,6 +689,7 @@ function createFakeCanvasContext(recorder: FakeCanvasRecorder | null = null): Ca
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise = false) {
recorder?.arcs.push({ x, y, radius, startAngle, endAngle, anticlockwise, lineDash: [...currentLineDash] })
},
ellipse() {},
drawImage(...args: unknown[]) {
recorder?.drawImageCalls.push({ compositeOperation: currentCompositeOperation, args })
},
@@ -710,7 +717,12 @@ function createFakeCanvasContext(recorder: FakeCanvasRecorder | null = null): Ca
const fontSize = fontSizeMatch ? Number(fontSizeMatch[1]) : 12
return { width: String(text).length * fontSize * 0.62 } as TextMetrics
},
lineWidth: 1,
get lineWidth() {
return currentLineWidth
},
set lineWidth(value: number) {
currentLineWidth = value
},
get font() {
return currentFont
},
@@ -3805,6 +3817,108 @@ test('VUMeter needle face layout stays fixed instead of scaling up', () => {
assert.equal(retinaNarrow.y, 180)
})
test('VUMeter needle face renders a readable primary scale without redundant inner percentages', () => {
const dataSource = {
getPendingVUMeterSamples: () => [],
getSampleRate: () => 48000,
isPlaying: () => false,
subscribeToSessionChanges: () => () => {},
}
const renderNeedleFace = (
width: number,
height: number,
needleChannels: 'stereo' | 'combined',
): FakeCanvasRecorder => {
const recorder = createFakeCanvasRecorder()
const meter = new VUMeter(createFakeCanvas(recorder, width, height), {
mode: 'needle',
needleChannels,
dataSource,
nativeAnalyzer: null,
labelColor: 'rgb(180, 200, 220)',
scaleColor: 'rgb(90, 100, 110)',
clipColor: 'rgb(250, 80, 70)',
})
try {
;(meter as unknown as { drawFrame: () => void }).drawFrame()
} finally {
meter.dispose()
}
return recorder
}
const full = renderNeedleFace(560, 360, 'stereo')
const fullColdEndpoint = full.fillTexts.find((text) => text.text === '20' && text.font.startsWith('700 '))
const fullMajor = full.fillTexts.find((text) => text.text === '+3')
const fullIntermediate = full.fillTexts.find((text) => text.text === '5')
const fullLeftLabel = full.fillTexts.find((text) => text.text === 'L')
const fullRightLabel = full.fillTexts.find((text) => text.text === 'R')
const fullReadoutValue = full.fillTexts.find((text) => text.text === '-∞')
const fullReadoutUnit = full.fillTexts.find((text) => text.text === 'dB')
assert.equal(fullMajor?.font, '700 26px "JetBrains Mono", monospace')
assert.equal(fullMajor?.fillStyle, 'rgba(250, 80, 70, 1)')
assert.equal(fullIntermediate?.font, '600 23.92px "JetBrains Mono", monospace')
assert.equal(fullIntermediate?.fillStyle, 'rgba(180, 200, 220, 1)')
assert.equal(fullLeftLabel?.font, '700 13.5px "JetBrains Mono", monospace')
assert.equal(fullRightLabel?.font, '700 13.5px "JetBrains Mono", monospace')
assert.equal(fullReadoutValue?.font, '600 19.5px "JetBrains Mono", monospace')
assert.equal(fullReadoutUnit?.font, '600 11.5px "JetBrains Mono", monospace')
assert.equal(full.fillTexts.some((text) => ['40', '60', '80', '100'].includes(text.text)), false)
assert.equal(full.fillTexts.some((text) => text.font.startsWith('500 ')), false)
assert.equal(
full.fillRects.filter((rect) => rect.fillStyle === 'rgba(180, 200, 220, 0.055)').length,
2,
)
assert.ok(full.arcs.some((arc) => arc.radius > 5))
const fullScaleTicks = full.lineStrokes.slice(0, 12)
const fullColdTickStart = fullScaleTicks[0]?.commands[0]
const fullHotTickStart = fullScaleTicks[11]?.commands[0]
assert.equal(fullColdTickStart?.kind, 'moveTo')
assert.equal(fullHotTickStart?.kind, 'moveTo')
assert.ok((fullColdEndpoint?.x ?? Number.POSITIVE_INFINITY) < (fullColdTickStart?.x ?? Number.NEGATIVE_INFINITY))
assert.ok((fullMajor?.x ?? Number.NEGATIVE_INFINITY) > (fullHotTickStart?.x ?? Number.POSITIVE_INFINITY))
const majorTick = full.lineStrokes.find((stroke) => stroke.strokeStyle === 'rgba(90, 100, 110, 0.96)')
const minorTick = full.lineStrokes.find((stroke) => stroke.strokeStyle === 'rgba(90, 100, 110, 0.82)')
assert.ok(majorTick)
assert.ok(minorTick)
assert.ok(majorTick.lineWidth > minorTick.lineWidth)
const compact = renderNeedleFace(280, 180, 'combined')
const compactColdEndpoint = compact.fillTexts.find((text) => text.text === '20' && text.font.startsWith('700 '))
const compactMajor = compact.fillTexts.find((text) => text.text === '+3')
const compactIntermediate = compact.fillTexts.find((text) => text.text === '5')
const compactReadoutValue = compact.fillTexts.find((text) => text.text === '-∞')
const compactReadoutUnit = compact.fillTexts.find((text) => text.text === 'dB')
const compactPrimaryLabels = compact.fillTexts.filter((text) => (
text.font === '700 13px "JetBrains Mono", monospace'
|| text.font === '600 11.96px "JetBrains Mono", monospace'
))
assert.equal(compactMajor?.font, '700 13px "JetBrains Mono", monospace')
assert.equal(compactIntermediate?.font, '600 11.96px "JetBrains Mono", monospace')
assert.equal(compactReadoutValue?.font, '600 9.75px "JetBrains Mono", monospace')
assert.equal(compactReadoutUnit?.font, '600 5.75px "JetBrains Mono", monospace')
assert.equal(compact.fillTexts.some((text) => ['40', '60', '80', '100'].includes(text.text)), false)
assert.equal(compactPrimaryLabels.length, 9)
for (const label of compactPrimaryLabels) {
const fontSize = Number(label.font.match(/(\d+(?:\.\d+)?)px/)?.[1] ?? 0)
const halfLabelWidth = label.text.length * fontSize * 0.62 / 2
assert.ok(label.x - halfLabelWidth >= 5.5)
assert.ok(label.x + halfLabelWidth <= 274.5)
assert.ok(label.y >= 0 && label.y <= 180)
}
const compactScaleTicks = compact.lineStrokes.slice(0, 12)
const compactColdTickStart = compactScaleTicks[0]?.commands[0]
const compactHotTickStart = compactScaleTicks[11]?.commands[0]
assert.ok((compactColdEndpoint?.x ?? Number.POSITIVE_INFINITY) < (compactColdTickStart?.x ?? Number.NEGATIVE_INFINITY))
assert.ok((compactMajor?.x ?? Number.NEGATIVE_INFINITY) > (compactHotTickStart?.x ?? Number.POSITIVE_INFINITY))
})
test('VUMeter shared needle helpers switch between stereo needles and combined RMS', () => {
const stereo = resolveVUNeedleReadings({
leftDb: -6,
+3
View File
@@ -72,6 +72,9 @@ export default defineConfig({
build: {
outDir: resolve(__dirname, 'plugin/webview-dist'),
emptyOutDir: true,
// The plugin ships as one embedded HTML resource. Keep the meter's Latin
// font files inside the inlined stylesheet so no runtime asset URLs remain.
assetsInlineLimit: 64 * 1024,
// Stable (unhashed) asset names so the C++ resource provider can map them
// deterministically and CMake's embedded BinaryData symbols stay stable.
rollupOptions: {