fix: minor UI adjustments

This commit is contained in:
Xiaohan-Tian
2026-05-08 16:16:34 -07:00
parent 31e02b4149
commit bd067cae64
3 changed files with 170 additions and 17 deletions
+9
View File
@@ -265,6 +265,15 @@
background: rgba(0, 0, 0, 0.35);
}
.track-automation-center-line {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.18);
pointer-events: none;
}
.track-automation-svg {
position: absolute;
inset: 0;
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
vi.mock('../../stores/projectStore', () => ({
useProjectStore: (selector: (state: {
selectedTrackAutomationPointIds: string[];
updateTrack: () => Promise<void>;
refreshProjectState: () => void;
bumpTrackAutomationRedrawVersion: () => void;
}) => unknown) => selector({
selectedTrackAutomationPointIds: [],
updateTrack: async () => {},
refreshProjectState: () => {},
bumpTrackAutomationRedrawVersion: () => {},
}),
}));
const { __trackAutomationTestUtils } = await import('./TrackAutomationLane');
describe('TrackAutomationLane mapping', () => {
it('maps volume 0.0dB to the visual midpoint and back', () => {
const laneHeight = 120;
const middle = __trackAutomationTestUtils.getLaneMetrics(laneHeight).middle;
expect(__trackAutomationTestUtils.volumeToY(0, laneHeight)).toBe(middle);
expect(__trackAutomationTestUtils.yToVolume(middle, laneHeight)).toBe(0);
});
it('maps the volume bounds to the top and bottom limits', () => {
const laneHeight = 120;
const { top, bottom } = __trackAutomationTestUtils.getLaneMetrics(laneHeight);
expect(__trackAutomationTestUtils.volumeToY(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, laneHeight)).toBe(top);
expect(__trackAutomationTestUtils.volumeToY(AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB, laneHeight)).toBe(bottom);
});
it('maps pan center to a reachable 1-pixel midpoint band', () => {
const laneHeight = 120;
const middle = __trackAutomationTestUtils.getLaneMetrics(laneHeight).middle;
expect(__trackAutomationTestUtils.panToY(0, laneHeight)).toBe(middle);
expect(__trackAutomationTestUtils.yToPan(middle, laneHeight)).toBe(0);
expect(__trackAutomationTestUtils.yToPan(middle - 0.5, laneHeight)).toBe(0);
expect(__trackAutomationTestUtils.yToPan(middle + 0.5, laneHeight)).toBe(0);
});
it('formats volume and pan labels with the refined representation', () => {
expect(__trackAutomationTestUtils.formatAutomationValue('volume', 0)).toBe('+0.0dB');
expect(__trackAutomationTestUtils.formatAutomationValue('volume', AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB)).toBe('−∞');
expect(__trackAutomationTestUtils.formatAutomationValue('pan', -1)).toBe('-64');
expect(__trackAutomationTestUtils.formatAutomationValue('pan', 0)).toBe('+0');
expect(__trackAutomationTestUtils.formatAutomationValue('pan', 1)).toBe('+63');
});
});
+105 -15
View File
@@ -42,17 +42,103 @@ const TRACK_AUTOMATION_COLORS: Record<TrackAutomationType, string> = {
function formatAutomationValue(automationType: TrackAutomationType, value: number): string {
if (automationType === 'volume') {
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}`;
if (value <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB) {
return '−∞';
}
const magnitude = Math.round(Math.abs(value) * 100);
if (magnitude === 0) {
return 'C';
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}dB`;
}
return `${value < 0 ? 'L' : 'R'}${magnitude}`;
const logicPanValue = value <= 0
? Math.round(value * 64)
: Math.round(value * 63);
return `${logicPanValue >= 0 ? '+' : ''}${logicPanValue}`;
}
function getLaneMetrics(laneHeight: number) {
const top = LANE_PADDING_Y;
const bottom = laneHeight - LANE_PADDING_Y;
const middle = Math.round((top + bottom) / 2);
return { top, middle, bottom };
}
function volumeToY(value: number, laneHeight: number): number {
const clamped = Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, value)
);
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped >= 0) {
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB;
return middle - (middle - top) * normalized;
}
const normalized = clamped / AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB;
return middle + (bottom - middle) * normalized;
}
function yToVolume(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
if (clampedY <= middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.min(
AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB,
Math.max(0, normalized * AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB)
);
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(
AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB,
Math.min(0, normalized * AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB)
);
}
function panToY(value: number, laneHeight: number): number {
const clamped = Math.max(-1, Math.min(1, value));
const { top, middle, bottom } = getLaneMetrics(laneHeight);
if (clamped === 0) {
return middle;
}
if (clamped > 0) {
return middle - (middle - top) * clamped;
}
return middle + (bottom - middle) * Math.abs(clamped);
}
function yToPan(y: number, laneHeight: number): number {
const { top, middle, bottom } = getLaneMetrics(laneHeight);
const clampedY = Math.min(bottom, Math.max(top, y));
// Reserve a full pixel around the midpoint for exact center.
if (Math.abs(clampedY - middle) <= 0.5) {
return 0;
}
if (clampedY < middle) {
const normalized = middle === top ? 0 : (middle - clampedY) / (middle - top);
return Math.max(0, Math.min(1, normalized));
}
const normalized = bottom === middle ? 0 : (clampedY - middle) / (bottom - middle);
return Math.max(-1, Math.min(0, -normalized));
}
export const __trackAutomationTestUtils = {
formatAutomationValue,
getLaneMetrics,
volumeToY,
yToVolume,
panToY,
yToPan,
};
const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
track,
automationType,
@@ -133,20 +219,20 @@ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
const toY = (value: number): number => {
const laneHeight = laneRef.current?.clientHeight ?? 120;
const usableHeight = laneHeight - LANE_PADDING_Y * 2;
const normalized = (value - minValue) / (maxValue - minValue);
return laneHeight - LANE_PADDING_Y - normalized * usableHeight;
if (automationType === 'volume') {
return volumeToY(value, laneHeight);
}
return panToY(value, laneHeight);
};
const toValue = (y: number): number => {
const laneHeight = laneRef.current?.clientHeight ?? 120;
const usableHeight = laneHeight - LANE_PADDING_Y * 2;
const clampedY = Math.min(laneHeight - LANE_PADDING_Y, Math.max(LANE_PADDING_Y, y));
const normalized = (laneHeight - LANE_PADDING_Y - clampedY) / usableHeight;
const rawValue = minValue + normalized * (maxValue - minValue);
return automationType === 'volume'
? Math.max(AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB, Math.min(AUDIO_INTERFACE_CONSTANTS.MAX_TRACK_VOLUME_DB, rawValue))
: Math.max(-1, Math.min(1, rawValue));
if (automationType === 'volume') {
return yToVolume(y, laneHeight);
}
return yToPan(y, laneHeight);
};
const renderedPoints = points.map(point => {
@@ -502,6 +588,10 @@ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
onDoubleClick={(event) => { void handleBackgroundDoubleClick(event); }}
>
<div className="track-automation-overlay" />
<div
className="track-automation-center-line"
style={{ top: `${getLaneMetrics(laneRef.current?.clientHeight ?? 120).middle}px` }}
/>
<svg
className="track-automation-svg"
width="100%"