mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-18 11:34:19 +02:00
update vu needles
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user