scope rotation

This commit is contained in:
Boof2015
2026-07-10 21:08:29 -04:00
parent 4a9b325532
commit 7a1a799266
20 changed files with 695 additions and 191 deletions
+19 -9
View File
@@ -1,10 +1,12 @@
import { useEffect, useRef, type JSX } from 'react'
import { useEffect, useLayoutEffect, useRef, type JSX } from 'react'
import { Oscilloscope } from '../renderer/visualizers/Oscilloscope'
import type { ScopeSettings } from '../types/settings'
import type { ResolvedOscilloscopeTheme } from '../types/theme'
import type { BridgeOscilloscopeAnalyzer } from './BridgeOscilloscopeAnalyzer'
import type { PluginWebViewDataSource } from './PluginWebViewDataSource'
import { oscilloscopeSettingsToOptions } from './oscilloscopeOptions'
import { getScopeCanvasTransformStyle } from '../renderer/scopeCanvasTransform'
import { applyPluginScopeCanvasLayout } from './scopeCanvasLayout'
interface OscilloscopeScopeProps {
dataSource: PluginWebViewDataSource
@@ -22,6 +24,9 @@ export default function OscilloscopeScope({
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const vizRef = useRef<Oscilloscope | null>(null)
const rotationRef = useRef(settings.rotation)
const applySizeRef = useRef<(() => void) | null>(null)
rotationRef.current = settings.rotation
useEffect(() => {
const container = containerRef.current
@@ -36,16 +41,12 @@ export default function OscilloscopeScope({
vizRef.current = viz
const applySize = (): void => {
const rect = container.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const pixelWidth = Math.max(1, Math.floor(rect.width * dpr))
const pixelHeight = Math.max(1, Math.floor(rect.height * dpr))
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth
canvas.height = pixelHeight
const { changed } = applyPluginScopeCanvasLayout(container, canvas, rotationRef.current)
if (changed) {
viz.resize()
}
}
applySizeRef.current = applySize
applySize()
viz.start()
@@ -54,6 +55,7 @@ export default function OscilloscopeScope({
return () => {
observer.disconnect()
applySizeRef.current = null
viz.dispose()
vizRef.current = null
}
@@ -65,9 +67,17 @@ export default function OscilloscopeScope({
vizRef.current?.setOptions(oscilloscopeSettingsToOptions(settings, theme))
}, [settings, theme])
useLayoutEffect(() => {
applySizeRef.current?.()
}, [settings.rotation])
return (
<div ref={containerRef} className="spectrum-scope">
<canvas ref={canvasRef} className="spectrum-scope__canvas" />
<canvas
ref={canvasRef}
className="spectrum-scope__canvas"
style={getScopeCanvasTransformStyle(settings.rotation, settings.mirrorHorizontal)}
/>
</div>
)
}
+24 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, type JSX } from 'react'
import { useEffect, useLayoutEffect, useRef, type JSX } from 'react'
import { Spectrogram } from '../renderer/visualizers/Spectrogram'
import type { ScopeSettings } from '../types/settings'
import type { ResolvedSpectrogramTheme } from '../types/theme'
@@ -6,6 +6,8 @@ import type { BridgeSpectrogramAnalyzer } from './BridgeSpectrogramAnalyzer'
import type { PluginWebViewDataSource } from './PluginWebViewDataSource'
import { spectrogramSettingsToOptions } from './spectrogramOptions'
import { resolveScrollingCanvasSize } from './scrollingCanvas'
import { getScopeCanvasTransformStyle } from '../renderer/scopeCanvasTransform'
import { applyPluginScopeCanvasLayout } from './scopeCanvasLayout'
interface SpectrogramScopeProps {
dataSource: PluginWebViewDataSource
@@ -23,6 +25,9 @@ export default function SpectrogramScope({
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const vizRef = useRef<Spectrogram | null>(null)
const rotationRef = useRef(settings.rotation)
const applySizeRef = useRef<(() => void) | null>(null)
rotationRef.current = settings.rotation
useEffect(() => {
const container = containerRef.current
@@ -37,15 +42,17 @@ export default function SpectrogramScope({
vizRef.current = viz
const applySize = (): void => {
const rect = container.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const { width, height } = resolveScrollingCanvasSize(rect.width, rect.height, dpr)
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
const { changed } = applyPluginScopeCanvasLayout(
container,
canvas,
rotationRef.current,
resolveScrollingCanvasSize,
)
if (changed) {
viz.resize()
}
}
applySizeRef.current = applySize
applySize()
viz.start()
@@ -54,6 +61,7 @@ export default function SpectrogramScope({
return () => {
observer.disconnect()
applySizeRef.current = null
viz.dispose()
vizRef.current = null
}
@@ -65,9 +73,17 @@ export default function SpectrogramScope({
vizRef.current?.setOptions(spectrogramSettingsToOptions(settings, theme))
}, [settings, theme])
useLayoutEffect(() => {
applySizeRef.current?.()
}, [settings.rotation])
return (
<div ref={containerRef} className="spectrum-scope">
<canvas ref={canvasRef} className="spectrum-scope__canvas" />
<canvas
ref={canvasRef}
className="spectrum-scope__canvas"
style={getScopeCanvasTransformStyle(settings.rotation, settings.mirrorHorizontal)}
/>
</div>
)
}
+26 -8
View File
@@ -6,10 +6,11 @@ import type { SpectrumPeakInfo } from '../types/spectrum'
import type { BridgeSpectrumAnalyzer } from './BridgeSpectrumAnalyzer'
import type { PluginWebViewDataSource } from './PluginWebViewDataSource'
import { spectrumSettingsToOptions } from './spectrumOptions'
import { getScopeCanvasTransformStyle } from '../renderer/scopeCanvasTransform'
import { applyPluginScopeCanvasLayout } from './scopeCanvasLayout'
import {
formatSpectrumPeakDb,
formatSpectrumPeakFrequency,
measureCanvasResizeState,
resolveFollowingPeakOverlayStyle,
type CanvasResizeState,
type SizeMeasurement,
@@ -32,11 +33,14 @@ export default function SpectrumScope({
const canvasRef = useRef<HTMLCanvasElement>(null)
const analyzerRef = useRef<SpectrumAnalyzer | null>(null)
const resizeStateRef = useRef<CanvasResizeState | null>(null)
const rotationRef = useRef(settings.rotation)
const applySizeRef = useRef<(() => void) | null>(null)
const peakOverlayRef = useRef<HTMLDivElement | null>(null)
const [peak, setPeak] = useState<SpectrumPeakInfo | null>(null)
const [overlaySize, setOverlaySize] = useState<SizeMeasurement | null>(null)
const peakMode = settings.peakInfoMode
rotationRef.current = settings.rotation
// Create the analyzer once per data source / shim.
useEffect(() => {
@@ -54,14 +58,13 @@ export default function SpectrumScope({
analyzerRef.current = analyzer
const applySize = (): void => {
const state = measureCanvasResizeState(container)
resizeStateRef.current = state
if (canvas.width !== state.pixelWidth || canvas.height !== state.pixelHeight) {
canvas.width = state.pixelWidth
canvas.height = state.pixelHeight
const { changed, layout } = applyPluginScopeCanvasLayout(container, canvas, rotationRef.current)
resizeStateRef.current = layout
if (changed) {
analyzer.resize()
}
}
applySizeRef.current = applySize
applySize()
analyzer.start()
@@ -70,6 +73,7 @@ export default function SpectrumScope({
return () => {
observer.disconnect()
applySizeRef.current = null
analyzer.dispose()
analyzerRef.current = null
}
@@ -87,6 +91,10 @@ export default function SpectrumScope({
})
}, [settings, theme])
useLayoutEffect(() => {
applySizeRef.current?.()
}, [settings.rotation])
// Measure the overlay so "following" placement can avoid the screen edges.
useLayoutEffect(() => {
const overlay = peakOverlayRef.current
@@ -107,12 +115,22 @@ export default function SpectrumScope({
const showPeak = peakMode !== 'off' && peak !== null
const overlayStyle: CSSProperties | undefined =
peakMode === 'following' && peak
? resolveFollowingPeakOverlayStyle(peak, resizeStateRef.current, overlaySize)
? resolveFollowingPeakOverlayStyle(
peak,
resizeStateRef.current,
overlaySize,
settings.rotation,
settings.mirrorHorizontal,
)
: undefined
return (
<div ref={containerRef} className="spectrum-scope">
<canvas ref={canvasRef} className="spectrum-scope__canvas" />
<canvas
ref={canvasRef}
className="spectrum-scope__canvas"
style={getScopeCanvasTransformStyle(settings.rotation, settings.mirrorHorizontal)}
/>
{showPeak && peak && (
<div
ref={peakMode === 'following' ? peakOverlayRef : null}
+24 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, type JSX } from 'react'
import { useEffect, useLayoutEffect, useRef, type JSX } from 'react'
import { Waveform } from '../renderer/visualizers/Waveform'
import type { ScopeSettings } from '../types/settings'
import type { ResolvedWaveformTheme } from '../types/theme'
@@ -6,6 +6,8 @@ import type { BridgeWaveformAnalyzer } from './BridgeWaveformAnalyzer'
import type { PluginWebViewDataSource } from './PluginWebViewDataSource'
import { waveformSettingsToOptions } from './waveformOptions'
import { resolveScrollingCanvasSize } from './scrollingCanvas'
import { getScopeCanvasTransformStyle } from '../renderer/scopeCanvasTransform'
import { applyPluginScopeCanvasLayout } from './scopeCanvasLayout'
interface WaveformScopeProps {
dataSource: PluginWebViewDataSource
@@ -23,6 +25,9 @@ export default function WaveformScope({
const containerRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const vizRef = useRef<Waveform | null>(null)
const rotationRef = useRef(settings.rotation)
const applySizeRef = useRef<(() => void) | null>(null)
rotationRef.current = settings.rotation
useEffect(() => {
const container = containerRef.current
@@ -37,15 +42,17 @@ export default function WaveformScope({
vizRef.current = viz
const applySize = (): void => {
const rect = container.getBoundingClientRect()
const dpr = window.devicePixelRatio || 1
const { width, height } = resolveScrollingCanvasSize(rect.width, rect.height, dpr)
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
const { changed } = applyPluginScopeCanvasLayout(
container,
canvas,
rotationRef.current,
resolveScrollingCanvasSize,
)
if (changed) {
viz.resize()
}
}
applySizeRef.current = applySize
applySize()
viz.start()
@@ -54,6 +61,7 @@ export default function WaveformScope({
return () => {
observer.disconnect()
applySizeRef.current = null
viz.dispose()
vizRef.current = null
}
@@ -65,9 +73,17 @@ export default function WaveformScope({
vizRef.current?.setOptions(waveformSettingsToOptions(settings, theme))
}, [settings, theme])
useLayoutEffect(() => {
applySizeRef.current?.()
}, [settings.rotation])
return (
<div ref={containerRef} className="spectrum-scope">
<canvas ref={canvasRef} className="spectrum-scope__canvas" />
<canvas
ref={canvasRef}
className="spectrum-scope__canvas"
style={getScopeCanvasTransformStyle(settings.rotation, settings.mirrorHorizontal)}
/>
</div>
)
}
+23 -24
View File
@@ -1,5 +1,11 @@
import type { CSSProperties } from 'react'
import type { SpectrumPeakInfo } from '../types/spectrum'
import type { ScopeDisplayRotation } from '../types/scopeTransform'
import {
measureScopeCanvasLayout,
transformNormalizedScopePoint,
type ScopeCanvasLayout,
} from '../renderer/scopeCanvasTransform'
/**
* Peak-overlay positioning + formatting, mirroring ScopeModule.tsx so the plugin's
@@ -7,13 +13,7 @@ import type { SpectrumPeakInfo } from '../types/spectrum'
* copy (pure functions) so the plugin doesn't import the heavy ScopeModule.
*/
export interface CanvasResizeState {
cssWidth: number
cssHeight: number
pixelWidth: number
pixelHeight: number
dpr: number
}
export type CanvasResizeState = ScopeCanvasLayout
export interface SizeMeasurement {
width: number
@@ -45,25 +45,19 @@ export function formatSpectrumPeakFrequency(value: number): string {
return `${value.toFixed(2)}Hz`
}
export function measureCanvasResizeState(container: HTMLElement): CanvasResizeState {
const rect = container.getBoundingClientRect()
const cssWidth = Math.max(1, Math.floor(rect.width))
const cssHeight = Math.max(1, Math.floor(rect.height))
const dpr = window.devicePixelRatio || 1
return {
cssWidth,
cssHeight,
pixelWidth: Math.max(1, Math.floor(cssWidth * dpr)),
pixelHeight: Math.max(1, Math.floor(cssHeight * dpr)),
dpr,
}
export function measureCanvasResizeState(
container: HTMLElement,
rotation: ScopeDisplayRotation = 0,
): CanvasResizeState {
return measureScopeCanvasLayout(container, rotation)
}
export function resolveFollowingPeakOverlayStyle(
peakInfo: SpectrumPeakInfo,
resizeState: CanvasResizeState | null,
overlaySize: SizeMeasurement | null,
rotation: ScopeDisplayRotation = 0,
mirrorHorizontal = false,
): CSSProperties {
if (!resizeState) {
return {
@@ -72,12 +66,17 @@ export function resolveFollowingPeakOverlayStyle(
}
}
const width = resizeState.cssWidth
const height = resizeState.cssHeight
const width = resizeState.viewportCssWidth
const height = resizeState.viewportCssHeight
const overlayWidth = overlaySize?.width ?? SPECTRUM_PEAK_OVERLAY_FALLBACK_WIDTH_PX
const overlayHeight = overlaySize?.height ?? SPECTRUM_PEAK_OVERLAY_FALLBACK_HEIGHT_PX
const peakX = peakInfo.normalizedX * width
const peakY = peakInfo.normalizedY * height
const transformedPeak = transformNormalizedScopePoint(
{ x: peakInfo.normalizedX, y: peakInfo.normalizedY },
rotation,
mirrorHorizontal,
)
const peakX = transformedPeak.x * width
const peakY = transformedPeak.y * height
const maxLeft = Math.max(
SPECTRUM_PEAK_OVERLAY_MARGIN_PX,
width - overlayWidth - SPECTRUM_PEAK_OVERLAY_MARGIN_PX,
+34
View File
@@ -0,0 +1,34 @@
import type { ScopeDisplayRotation } from '../types/scopeTransform'
import {
measureScopeCanvasLayout,
type ScopeCanvasLayout,
} from '../renderer/scopeCanvasTransform'
type PixelSizeResolver = (
cssWidth: number,
cssHeight: number,
dpr: number,
) => { width: number; height: number }
export function applyPluginScopeCanvasLayout(
container: HTMLElement,
canvas: HTMLCanvasElement,
rotation: ScopeDisplayRotation,
pixelSizeResolver?: PixelSizeResolver,
): { changed: boolean; layout: ScopeCanvasLayout } {
const layout = measureScopeCanvasLayout(container, rotation)
const resolvedPixels = pixelSizeResolver
? pixelSizeResolver(layout.cssWidth, layout.cssHeight, layout.dpr)
: { width: layout.pixelWidth, height: layout.pixelHeight }
canvas.style.width = `${layout.cssWidth}px`
canvas.style.height = `${layout.cssHeight}px`
const changed = canvas.width !== resolvedPixels.width || canvas.height !== resolvedPixels.height
if (changed) {
canvas.width = resolvedPixels.width
canvas.height = resolvedPixels.height
}
return { changed, layout }
}
+1 -1
View File
@@ -20,7 +20,7 @@ export function spectrogramSettingsToOptions(
contrast: settings.contrast,
clarityMode: settings.clarityMode,
scaleMode: settings.scaleMode,
orientation: settings.orientation,
orientation: 'horizontal',
colorScheme: settings.colorScheme,
}
}
+2 -10
View File
@@ -3,21 +3,13 @@ import { DEFAULT_SCOPE_SETTINGS, type ScopeSettings } from '../types/settings'
import type { ScopeKind } from '../types/scope'
import type { PrismResolvedTheme } from '../types/theme'
import { createBundledThemes, createDefaultTheme, parseThemeFileContent, resolveTheme } from '../shared/themeState'
import { mergeScopeSettings as normalizeAllScopeSettings } from '../shared/profileState'
import { emitToHost, onHostEvent } from './juceBridge'
const DEFAULT_THEME = resolveTheme(createDefaultTheme())
function mergeScopeSettings<K extends ScopeKind>(kind: K, raw: unknown): ScopeSettings[K] {
const defaults = DEFAULT_SCOPE_SETTINGS[kind] as Record<string, unknown>
if (typeof raw !== 'object' || raw === null) return { ...defaults } as ScopeSettings[K]
const parsed = raw as Record<string, unknown>
const next: Record<string, unknown> = { ...defaults }
for (const key of Object.keys(defaults)) {
if (key in parsed && typeof parsed[key] === typeof defaults[key]) {
next[key] = parsed[key]
}
}
return next as ScopeSettings[K]
return normalizeAllScopeSettings({ [kind]: raw })[kind]
}
function resolveAppTheme(themeId: string, themeFile: string): PrismResolvedTheme {