refactor(28-02): rewrite SessionTrackVU with direct DOM updates
- Remove useVuContext - directly import vuStore - Remove VuMeter component usage - render lights directly - Add RAF loop polling vuStore.getLevelSnapshot at ~60fps - Store light element refs for direct className assignment - Wrap with React.memo to prevent parent re-renders - Zero React re-renders for VU visual updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f8214854d9
commit
a020e27260
|
|
@ -1,44 +1,102 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef, memo } from 'react';
|
||||||
import { useVuContext } from '../../context/VuContext';
|
import { vuStore } from '../../stores/vuStore';
|
||||||
|
|
||||||
function SessionTrackVU({ lightCount, orientation, lightWidth, lightHeight, side, ptr, mixers }) {
|
const SessionTrackVU = memo(function SessionTrackVU({
|
||||||
const { VuMeter, updateVuState } = useVuContext();
|
lightCount = 16,
|
||||||
const ptrRef = useRef(ptr || `STV${Date.now()}`);
|
orientation = 'vertical',
|
||||||
|
lightWidth = 15,
|
||||||
|
lightHeight = 10,
|
||||||
|
mixers
|
||||||
|
}) {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const lightsRef = useRef([]);
|
||||||
const mixerIdRef = useRef(null);
|
const mixerIdRef = useRef(null);
|
||||||
|
const rafIdRef = useRef(null);
|
||||||
|
|
||||||
|
// Update mixerId when mixers prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const mixer = mixers?.vuMixer;
|
const mixer = mixers?.vuMixer;
|
||||||
|
mixerIdRef.current = mixer ? `${mixer.mode ? 'M' : 'P'}${mixer.id}` : null;
|
||||||
if (!mixer) {
|
|
||||||
mixerIdRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a unique ID for this VU meter
|
|
||||||
const mixerId = `${mixer.mode ? 'M' : 'P'}${mixer.id}`;
|
|
||||||
mixerIdRef.current = mixerId;
|
|
||||||
|
|
||||||
//console.debug("SessionTrackVU: VU registered for mixer", mixerId);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Cleanup: reset VU state when component unmounts or mixer changes
|
// Cleanup VU state on unmount
|
||||||
if (mixerIdRef.current) {
|
if (mixerIdRef.current) {
|
||||||
updateVuState(mixerIdRef.current, 0, false);
|
vuStore.removeLevel(mixerIdRef.current);
|
||||||
mixerIdRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [mixers, updateVuState]);
|
}, [mixers]);
|
||||||
|
|
||||||
// Use the React component for rendering
|
// Animation loop for direct DOM updates - separate from React lifecycle
|
||||||
|
useEffect(() => {
|
||||||
|
const updateVisuals = () => {
|
||||||
|
const mixerId = mixerIdRef.current;
|
||||||
|
|
||||||
|
if (!mixerId) {
|
||||||
|
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = vuStore.getLevelSnapshot(mixerId);
|
||||||
|
const level = data?.level ?? 0;
|
||||||
|
const clipping = data?.clipping ?? false;
|
||||||
|
const activeLights = Math.round(level * lightCount);
|
||||||
|
|
||||||
|
// Direct DOM manipulation - no React involvement
|
||||||
|
for (let i = 0; i < lightCount; i++) {
|
||||||
|
const light = lightsRef.current[i];
|
||||||
|
if (!light) continue;
|
||||||
|
|
||||||
|
const positionFromBottom = lightCount - 1 - i;
|
||||||
|
const isActive = positionFromBottom < activeLights;
|
||||||
|
|
||||||
|
// Use className assignment for efficient class manipulation
|
||||||
|
// (faster than classList.add/remove for multiple classes)
|
||||||
|
if (isActive) {
|
||||||
|
if (clipping || positionFromBottom >= Math.floor(lightCount * 0.75)) {
|
||||||
|
light.className = 'vu-light vu-bg-danger';
|
||||||
|
} else if (positionFromBottom >= Math.floor(lightCount * 0.5)) {
|
||||||
|
light.className = 'vu-light vu-bg-warning';
|
||||||
|
} else {
|
||||||
|
light.className = 'vu-light vu-bg-success';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
light.className = 'vu-light vu-bg-secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||||
|
};
|
||||||
|
|
||||||
|
rafIdRef.current = requestAnimationFrame(updateVisuals);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (rafIdRef.current) {
|
||||||
|
cancelAnimationFrame(rafIdRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [lightCount]);
|
||||||
|
|
||||||
|
// Render ONCE - never re-renders for VU updates
|
||||||
return (
|
return (
|
||||||
<VuMeter
|
<div ref={containerRef} className="vu">
|
||||||
mixerId={mixerIdRef.current}
|
<div className="d-flex flex-column" style={{ height: `${lightCount * (lightHeight + 1)}px` }}>
|
||||||
orientation={orientation}
|
{Array.from({ length: lightCount }).map((_, i) => (
|
||||||
lightCount={lightCount}
|
<div
|
||||||
lightWidth={lightWidth}
|
key={i}
|
||||||
lightHeight={lightHeight}
|
ref={el => lightsRef.current[i] = el}
|
||||||
/>
|
className="vu-light vu-bg-secondary"
|
||||||
|
style={{
|
||||||
|
height: `${lightHeight}px`,
|
||||||
|
width: '25px',
|
||||||
|
marginTop: '1px',
|
||||||
|
borderRadius: '2px',
|
||||||
|
border: '1px solid #eee'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export default SessionTrackVU;
|
export default SessionTrackVU;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue