fix: minor UI adjustments
This commit is contained in:
@@ -265,6 +265,15 @@
|
|||||||
background: rgba(0, 0, 0, 0.35);
|
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 {
|
.track-automation-svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -42,17 +42,103 @@ const TRACK_AUTOMATION_COLORS: Record<TrackAutomationType, string> = {
|
|||||||
|
|
||||||
function formatAutomationValue(automationType: TrackAutomationType, value: number): string {
|
function formatAutomationValue(automationType: TrackAutomationType, value: number): string {
|
||||||
if (automationType === 'volume') {
|
if (automationType === 'volume') {
|
||||||
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}`;
|
if (value <= AUDIO_INTERFACE_CONSTANTS.MIN_TRACK_VOLUME_DB) {
|
||||||
|
return '−∞';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value >= 0 ? '+' : ''}${value.toFixed(1)}dB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const magnitude = Math.round(Math.abs(value) * 100);
|
const logicPanValue = value <= 0
|
||||||
if (magnitude === 0) {
|
? Math.round(value * 64)
|
||||||
return 'C';
|
: Math.round(value * 63);
|
||||||
}
|
return `${logicPanValue >= 0 ? '+' : ''}${logicPanValue}`;
|
||||||
|
|
||||||
return `${value < 0 ? 'L' : 'R'}${magnitude}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> = ({
|
const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
|
||||||
track,
|
track,
|
||||||
automationType,
|
automationType,
|
||||||
@@ -133,20 +219,20 @@ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
|
|||||||
|
|
||||||
const toY = (value: number): number => {
|
const toY = (value: number): number => {
|
||||||
const laneHeight = laneRef.current?.clientHeight ?? 120;
|
const laneHeight = laneRef.current?.clientHeight ?? 120;
|
||||||
const usableHeight = laneHeight - LANE_PADDING_Y * 2;
|
if (automationType === 'volume') {
|
||||||
const normalized = (value - minValue) / (maxValue - minValue);
|
return volumeToY(value, laneHeight);
|
||||||
return laneHeight - LANE_PADDING_Y - normalized * usableHeight;
|
}
|
||||||
|
|
||||||
|
return panToY(value, laneHeight);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toValue = (y: number): number => {
|
const toValue = (y: number): number => {
|
||||||
const laneHeight = laneRef.current?.clientHeight ?? 120;
|
const laneHeight = laneRef.current?.clientHeight ?? 120;
|
||||||
const usableHeight = laneHeight - LANE_PADDING_Y * 2;
|
if (automationType === 'volume') {
|
||||||
const clampedY = Math.min(laneHeight - LANE_PADDING_Y, Math.max(LANE_PADDING_Y, y));
|
return yToVolume(y, laneHeight);
|
||||||
const normalized = (laneHeight - LANE_PADDING_Y - clampedY) / usableHeight;
|
}
|
||||||
const rawValue = minValue + normalized * (maxValue - minValue);
|
|
||||||
return automationType === 'volume'
|
return yToPan(y, laneHeight);
|
||||||
? 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));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderedPoints = points.map(point => {
|
const renderedPoints = points.map(point => {
|
||||||
@@ -502,6 +588,10 @@ const TrackAutomationLane: React.FC<TrackAutomationLaneProps> = ({
|
|||||||
onDoubleClick={(event) => { void handleBackgroundDoubleClick(event); }}
|
onDoubleClick={(event) => { void handleBackgroundDoubleClick(event); }}
|
||||||
>
|
>
|
||||||
<div className="track-automation-overlay" />
|
<div className="track-automation-overlay" />
|
||||||
|
<div
|
||||||
|
className="track-automation-center-line"
|
||||||
|
style={{ top: `${getLaneMetrics(laneRef.current?.clientHeight ?? 120).middle}px` }}
|
||||||
|
/>
|
||||||
<svg
|
<svg
|
||||||
className="track-automation-svg"
|
className="track-automation-svg"
|
||||||
width="100%"
|
width="100%"
|
||||||
|
|||||||
Reference in New Issue
Block a user