fix: use DOM scrollLeftVal for clip/section/MIDI rendering in WaveformLane
6 render positions (clip, marker, section, MIDI, note, selection) were using React prop scrollLeft instead of DOM scrollLeftVal, causing grid-vs-item desync during fast auto-scroll.
This commit is contained in:
@@ -515,7 +515,7 @@ const WaveformLane = ({
|
||||
if (clipEndTime < tStart || clipStartTime > tEnd) return;
|
||||
const xStartGlobal = clipStartTime * zoom;
|
||||
const wClip = duration * zoom;
|
||||
const xStartLocal = xStartGlobal - scrollLeft;
|
||||
const xStartLocal = xStartGlobal - scrollLeftVal;
|
||||
const xEndLocal = xStartLocal + wClip;
|
||||
|
||||
// 1. Draw Clip Layer Background & Border
|
||||
@@ -548,7 +548,7 @@ const WaveformLane = ({
|
||||
// Draw markers
|
||||
if (markers && markers.length > 0) {
|
||||
markers.forEach(m => {
|
||||
const mxLocal = m.time * zoom - scrollLeft;
|
||||
const mxLocal = m.time * zoom - scrollLeftVal;
|
||||
if (mxLocal >= 0 && mxLocal <= drawWidth) {
|
||||
ctx.fillStyle = '#fbbf24';
|
||||
ctx.fillRect(mxLocal - 1, 0, 2, height);
|
||||
@@ -693,7 +693,7 @@ const WaveformLane = ({
|
||||
// Draw sections
|
||||
const sections = track.sections || [];
|
||||
sections.forEach(sec => {
|
||||
const secStartLocal = sec.start * zoom - scrollLeft; // use secStartLocal NOT secStart
|
||||
const secStartLocal = sec.start * zoom - scrollLeftVal; // use secStartLocal NOT secStart
|
||||
const secWidth = sec.duration * zoom;
|
||||
if (secStartLocal + secWidth < 0 || secStartLocal > drawWidth) return;
|
||||
ctx.fillStyle = sec.color ? sec.color + '44' : 'rgba(6, 182, 212, 0.25)';
|
||||
@@ -772,7 +772,7 @@ const WaveformLane = ({
|
||||
// Draw MIDI items
|
||||
const midiItems = track.midiItems || [];
|
||||
midiItems.forEach(midi => {
|
||||
const midiStartLocal = midi.startTime * zoom - scrollLeft;
|
||||
const midiStartLocal = midi.startTime * zoom - scrollLeftVal;
|
||||
const midiWidth = midi.duration * zoom;
|
||||
if (midiStartLocal + midiWidth < 0 || midiStartLocal > drawWidth) return;
|
||||
|
||||
@@ -799,7 +799,7 @@ const WaveformLane = ({
|
||||
midiNotes.forEach(note => {
|
||||
const noteStartSec = (note.start_beat || 0) * secondsPerBeat;
|
||||
const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * secondsPerBeat);
|
||||
const noteStartLocal = (midi.startTime + noteStartSec) * zoom - scrollLeft;
|
||||
const noteStartLocal = (midi.startTime + noteStartSec) * zoom - scrollLeftVal;
|
||||
const nw = noteDurSec * zoom;
|
||||
if (noteStartLocal + nw < midiStartLocal || noteStartLocal > midiStartLocal + midiWidth) return;
|
||||
|
||||
@@ -815,7 +815,7 @@ const WaveformLane = ({
|
||||
|
||||
// Selection highlight - local selection on this track
|
||||
if (selectionMode === 'local' && localSelectionTrackId === track.id && localSelLeft !== null && localSelRight !== null && localSelRight > localSelLeft) {
|
||||
const hlLeftLocal = localSelLeft * zoom - scrollLeft;
|
||||
const hlLeftLocal = localSelLeft * zoom - scrollLeftVal;
|
||||
const hlWidth = (localSelRight - localSelLeft) * zoom;
|
||||
ctx.fillStyle = 'rgba(245, 158, 11, 0.15)';
|
||||
ctx.fillRect(hlLeftLocal, 0, hlWidth, height);
|
||||
|
||||
@@ -21,10 +21,10 @@ const totalSamples=this.pcmChunks.reduce((sum,chunk)=>sum+chunk.length,0);if(tot
|
||||
}}const VolumeKnob=({value,onChange,min=0,max=1})=>{const[isDragging,setIsDragging]=useState(false);const startY=useRef(0);const startValue=useRef(0);const rotation=useMemo(()=>{const percent=(value-min)/(max-min);return-135+percent*270;},[value,min,max]);const handleMouseDown=e=>{setIsDragging(true);startY.current=e.clientY;startValue.current=value;document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleMouseMove=e=>{const deltaY=startY.current-e.clientY;const sensitivity=0.005;const newValue=Math.max(min,Math.min(max,startValue.current+deltaY*sensitivity));onChange(parseFloat(newValue.toFixed(2)));};const handleMouseUp=()=>{setIsDragging(false);document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};return/*#__PURE__*/React.createElement("div",{className:"knob-container cursor-ns-resize flex flex-col items-center",onMouseDown:handleMouseDown,title:`Volume: ${Math.round(value*100)}%`},/*#__PURE__*/React.createElement("svg",{className:"w-7 h-7",viewBox:"0 0 40 40"},/*#__PURE__*/React.createElement("circle",{cx:"20",cy:"20",r:"16",fill:"#141414",stroke:"#444",strokeWidth:"2"}),/*#__PURE__*/React.createElement("g",{transform:`rotate(${rotation} 20 20)`,className:"knob-dial"},/*#__PURE__*/React.createElement("line",{x1:"20",y1:"20",x2:"20",y2:"6",stroke:"#ef4444",strokeWidth:"3",strokeLinecap:"round"}))));};const WaveformLane=({track,zoom,timelineWidth,viewportWidth,onSelectRange,onPlayheadSet,isSelected,onSelectTrack,markers,selectionMode,localSelectionTrackId,localSelectionStart,currentTime,getLocalAnchor,onClearLocalSelection,onSetSelectionMode,onSetSelectionStart,onSetSelectionEnd,onSetCurrentTime,onSetLocalSelectionTrackId,onSetLocalSelectionStart,onSetLocalSelectionEnd,localSelLeft,localSelRight,onTrackLaneMouseDown,onContextMenu,onClipDragStart,onClipStretchStart,onSectionItemDragStart,onSectionItemResizeStart,onEditSectionInTab,onEditMidiInTab,onSelectionEdgeDragStart,setSelectedClipId,selectedClipId,activeTool,onSplitTrackAtTime,onEditClipInSubTab,snapValue,bpm,scrollLeft,recordingState,recTempMidiNotes,recTempAudioBuffer,recStartTimelineTime,canvasRedrawCount})=>{const canvasRef=useRef(null);const drawWidth=Math.min(timelineWidth,viewportWidth);const leadInMargin=0;useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;let scrollLeftVal=scrollLeft||0;let el=canvas.parentElement;while(el){if(el.scrollLeft!==undefined&&(el.scrollWidth>el.clientWidth||el.scrollLeft>0)){scrollLeftVal=el.scrollLeft;break;}el=el.parentElement;}const vWidth=viewportWidth||1200;const height=canvas.parentElement?canvas.parentElement.clientHeight:96;canvas.width=Math.min(Math.round(drawWidth*dpr),32768);canvas.height=Math.min(Math.round(height*dpr),32768);ctx.scale(dpr,dpr);ctx.imageSmoothingEnabled=false;canvas.style.width=`${drawWidth}px`;canvas.style.height=`${height}px`;ctx.fillStyle=isSelected?'#2a2a2a':track.id%2===0?'#181818':'#1d1d1d';ctx.fillRect(0,0,drawWidth,height);// Grid lines based on Snap value
|
||||
ctx.strokeStyle='rgba(255, 255, 255, 0.03)';ctx.lineWidth=1;const beatDuration=60.0/(parseFloat(bpm)||120);const barDuration=beatDuration*4;const leadIn=0;const CLIP_BUFFER=Math.max(400,barDuration*zoom+200);const PADDING_LEFT=0;const tStart=(scrollLeftVal-leadIn*zoom)/zoom-PADDING_LEFT;const tEnd=(scrollLeftVal+drawWidth-leadIn*zoom)/zoom+CLIP_BUFFER/zoom;const firstBeat=Math.floor(tStart/beatDuration)*beatDuration;let snapDivisor=1;if(snapValue&&snapValue!=='free'){if(snapValue==='4')snapDivisor=4;else if(snapValue==='1')snapDivisor=1;else if(snapValue==='1/2')snapDivisor=0.5;else if(snapValue==='1/4')snapDivisor=0.25;else if(snapValue==='1/8')snapDivisor=0.125;else if(snapValue==='1/16')snapDivisor=0.0625;else if(snapValue==='1/32')snapDivisor=0.03125;}for(let t=firstBeat;t<=tEnd;t+=beatDuration){const beatNum=Math.floor(t/beatDuration)+1;const isBar=beatNum%4===1;const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;ctx.strokeStyle=isBar?'rgba(255, 255, 255, 0.12)':'rgba(255, 255, 255, 0.04)';ctx.lineWidth=isBar?1.2:0.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();if(isBar&&zoom>=2){ctx.fillStyle='rgba(255, 255, 255, 0.15)';ctx.font='bold 7px Inter, sans-serif';ctx.textAlign='left';ctx.fillText(`${Math.floor((beatNum-1)/4)}`,localX+2,10);}}// Draw waveform lane
|
||||
const clips=[...(track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[])];if(recordingState==='RECORDING'&&track.isArmed&&track.inputSource?.deviceType==='MICROPHONE'&&recTempAudioBuffer){clips.push({id:'rec_temp_'+track.id,buffer:recTempAudioBuffer,startTime:recStartTimelineTime,name:'[GHI ÂM...]',speed:1.0,isTemp:true});}if(clips.length>0){clips.forEach(clip=>{const numChannels=clip.buffer.numberOfChannels||1;const dataL=clip.buffer.getChannelData(0);const dataR=numChannels>=2?clip.buffer.getChannelData(1):dataL;const sampleRate=clip.buffer.sampleRate;const totalSamples=dataL.length;const originalDuration=totalSamples/sampleRate;const clipSpeed=clip.speed||1.0;const duration=originalDuration/clipSpeed;const clipStartTime=clip.startTime||0;const clipEndTime=clipStartTime+duration;// Culling: Skip rendering if clip is outside visible viewport window
|
||||
if(clipEndTime<tStart||clipStartTime>tEnd)return;const xStartGlobal=clipStartTime*zoom;const wClip=duration*zoom;const xStartLocal=xStartGlobal-scrollLeft;const xEndLocal=xStartLocal+wClip;// 1. Draw Clip Layer Background & Border
|
||||
if(clipEndTime<tStart||clipStartTime>tEnd)return;const xStartGlobal=clipStartTime*zoom;const wClip=duration*zoom;const xStartLocal=xStartGlobal-scrollLeftVal;const xEndLocal=xStartLocal+wClip;// 1. Draw Clip Layer Background & Border
|
||||
const clipIdentifier=clip.id==='default'?'default_'+track.id:clip.id;const isClipSelected=selectedClipId&&selectedClipId.trackId===track.id&&selectedClipId.clipId===clipIdentifier;ctx.fillStyle=clip.isTemp?'rgba(239, 68, 68, 0.25)':isClipSelected?track.color?track.color+'44':'rgba(6, 182, 212, 0.30)':track.color?track.color+'22':'rgba(6, 182, 212, 0.12)';ctx.strokeStyle=clip.isTemp?'#ef4444':isClipSelected?'#fbbf24':track.color||'#06b6d4';ctx.lineWidth=isClipSelected?1:1.5;const clipTop=4;const clipHeight=height-8;ctx.beginPath();if(ctx.roundRect){ctx.roundRect(xStartLocal,clipTop,wClip,clipHeight,4);}else{ctx.rect(xStartLocal,clipTop,wClip,clipHeight);}ctx.fill();ctx.stroke();// 2. Draw Clip Label & Speed Label
|
||||
ctx.fillStyle='#e4e4e7';ctx.font='bold 9px sans-serif';ctx.fillText(clip.name||'Clip',Math.max(xStartLocal+8,8),clipTop+12);if(clipSpeed!==1.0){ctx.fillStyle='#fbbf24';ctx.font='bold 8px sans-serif';ctx.fillText(`Speed: ${(clipSpeed*100).toFixed(1)}%`,Math.max(xStartLocal+8,8),clipTop+22);}// Draw markers
|
||||
if(markers&&markers.length>0){markers.forEach(m=>{const mxLocal=m.time*zoom-scrollLeft;if(mxLocal>=0&&mxLocal<=drawWidth){ctx.fillStyle='#fbbf24';ctx.fillRect(mxLocal-1,0,2,height);}});}// 3. Peak / Vector Waveform Drawing (Sound Forge Dual Stereo Channel Split Match)
|
||||
if(markers&&markers.length>0){markers.forEach(m=>{const mxLocal=m.time*zoom-scrollLeftVal;if(mxLocal>=0&&mxLocal<=drawWidth){ctx.fillStyle='#fbbf24';ctx.fillRect(mxLocal-1,0,2,height);}});}// 3. Peak / Vector Waveform Drawing (Sound Forge Dual Stereo Channel Split Match)
|
||||
const drawXStartLocal=Math.max(0,Math.floor(xStartLocal));const drawXEndLocal=Math.min(drawWidth,Math.ceil(xEndLocal));const samplesPerPixel=sampleRate/zoom*clipSpeed;const isStereo=numChannels>=2;const channelConfigs=isStereo?[{data:dataL,mid:height/4,chHeight:height/2-8,label:'1'},{data:dataR,mid:3*height/4,chHeight:height/2-8,label:'2'}]:[{data:dataL,mid:height/2,chHeight:clipHeight-12,label:'MONO'}];if(isStereo){ctx.strokeStyle='rgba(255, 255, 255, 0.12)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(xStartLocal,height/2);ctx.lineTo(xStartLocal+wClip,height/2);ctx.stroke();}channelConfigs.forEach(ch=>{const data=ch.data;const mid=ch.mid;const peakRatio=ch.chHeight*0.42;// Decibel Amplitude Grid Lines (+6.0dB, -Inf, -6.0dB)
|
||||
const yPlus6=mid-peakRatio*0.8;const yMinus6=mid+peakRatio*0.8;ctx.strokeStyle='rgba(255, 255, 255, 0.08)';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(xStartLocal,yPlus6);ctx.lineTo(xStartLocal+wClip,yPlus6);ctx.stroke();ctx.beginPath();ctx.moveTo(xStartLocal,mid);ctx.lineTo(xStartLocal+wClip,mid);ctx.stroke();ctx.beginPath();ctx.moveTo(xStartLocal,yMinus6);ctx.lineTo(xStartLocal+wClip,yMinus6);ctx.stroke();// Decibel Text Labels (Sound Forge Monospace)
|
||||
ctx.fillStyle='#71717a';ctx.font='8px monospace';const labelX=Math.max(xStartLocal+4,4);ctx.fillText('+6.0',labelX,yPlus6-2);ctx.fillText('-Inf',labelX,mid-2);ctx.fillText('-6.0',labelX,yMinus6+8);// Sound Forge Channel ID Badge (1 or 2)
|
||||
@@ -33,13 +33,13 @@ ctx.lineWidth=1.2;if(samplesPerPixel<4){// High-zoom continuous vector line rend
|
||||
const visibleTStart=(drawXStartLocal-xStartLocal)/zoom;const visibleTEnd=(drawXEndLocal-xStartLocal)/zoom;const startSample=Math.max(0,Math.floor(visibleTStart*clipSpeed*sampleRate));const endSample=Math.min(totalSamples,Math.ceil(visibleTEnd*clipSpeed*sampleRate));ctx.beginPath();let first=true;const maxSamples=50000;const vectorStep=Math.max(1,Math.floor((endSample-startSample)/maxSamples));for(let i=startSample;i<endSample;i+=vectorStep){const sampleTime=i/sampleRate/clipSpeed;const pxLocal=xStartLocal+sampleTime*zoom;const y=mid-data[i]*peakRatio;if(first){ctx.moveTo(pxLocal,y);first=false;}else{ctx.lineTo(pxLocal,y);}}ctx.stroke();// Draw granular sample nodes only at extreme zoom (samplesPerPixel < 0.3)
|
||||
if(samplesPerPixel<0.3){const maxNodes=5000;const step=Math.max(1,Math.floor((endSample-startSample)/maxNodes));ctx.fillStyle='#6ee7b7';let drawn=0;for(let i=startSample;i<endSample&&drawn<maxNodes;i+=step){const sampleTime=i/sampleRate/clipSpeed;const pxLocal=xStartLocal+sampleTime*zoom;const y=mid-data[i]*peakRatio;ctx.fillRect(pxLocal-1,y-1,2,2);drawn++;}}}else{// Coarse view vertical peak min/max bars per pixel
|
||||
for(let pxLocal=drawXStartLocal;pxLocal<drawXEndLocal;pxLocal++){const timeInClip=(pxLocal-xStartLocal)/zoom*clipSpeed;const sampleIdx=Math.floor(timeInClip*sampleRate);const chunkSize=Math.max(1,Math.floor(samplesPerPixel));const chunkStart=Math.max(0,sampleIdx-Math.floor(chunkSize/2));const chunkEnd=Math.min(totalSamples,chunkStart+chunkSize);let maxVal=0;for(let i=chunkStart;i<chunkEnd;i++){const abs=Math.abs(data[i]);if(abs>maxVal)maxVal=abs;}const peakHeight=maxVal*peakRatio;ctx.beginPath();ctx.moveTo(pxLocal,mid-peakHeight);ctx.lineTo(pxLocal,mid+peakHeight);ctx.stroke();}}});});}else{ctx.fillStyle='#444';ctx.font='12px Inter, sans-serif';ctx.textAlign='center';ctx.fillText('Kéo thả hoặc tải file âm thanh vào Track này',drawWidth/2,height/2);}// Draw sections
|
||||
const sections=track.sections||[];sections.forEach(sec=>{const secStartLocal=sec.start*zoom-scrollLeft;// use secStartLocal NOT secStart
|
||||
const sections=track.sections||[];sections.forEach(sec=>{const secStartLocal=sec.start*zoom-scrollLeftVal;// use secStartLocal NOT secStart
|
||||
const secWidth=sec.duration*zoom;if(secStartLocal+secWidth<0||secStartLocal>drawWidth)return;ctx.fillStyle=sec.color?sec.color+'44':'rgba(6, 182, 212, 0.25)';ctx.fillRect(secStartLocal,2,secWidth,height-4);ctx.strokeStyle=sec.color||'#06b6d4';ctx.lineWidth=1;ctx.setLineDash([4,4]);ctx.strokeRect(secStartLocal,2,secWidth,height-4);ctx.setLineDash([]);ctx.fillStyle='#e4e4e7';ctx.font='bold 9px sans-serif';ctx.fillText(sec.name||'Section',Math.max(secStartLocal+4,4),14);// Draw sub-tracks within section
|
||||
const subTracks=sec.tracks||[];const subTrackCount=Math.min(subTracks.length,4);const subTrackHeight=(height-20)/Math.max(1,subTrackCount);const subColors=['#fbbf24','#a78bfa','#ec4899','#10b981'];for(let stIdx=0;stIdx<subTrackCount;stIdx++){const sub=subTracks[stIdx];if(!sub)continue;const subY=20+stIdx*subTrackHeight;ctx.fillStyle=sub.color?sub.color+'22':subColors[stIdx]+'22';ctx.fillRect(secStartLocal+1,subY,secWidth-2,subTrackHeight-1);// Draw clips as waveform bars
|
||||
const subClips=sub.clips||[];subClips.forEach(cl=>{if(!cl.buffer)return;const sr=cl.buffer.sampleRate;const bufData=cl.buffer.getChannelData(0);const bufLen=bufData.length;const clStartLocal=cl.startTime||0;const clDurLocal=bufLen/sr/(cl.speed||1.0);const clStartMain=secStartLocal+clStartLocal*zoom;const clW=Math.max(1,clDurLocal*zoom);const peakSamples=Math.max(10,Math.min(100,Math.floor(clW/3)));const step=Math.max(1,Math.floor(bufLen/peakSamples));for(let p=0;p<peakSamples;p++){const sIdx=p*step;let maxVal=0;for(let j=0;j<step&&sIdx+j<bufLen;j++){const abs=Math.abs(bufData[sIdx+j]);if(abs>maxVal)maxVal=abs;}const bx=clStartMain+p/peakSamples*clW;const barH=Math.max(1,maxVal*(subTrackHeight*0.7));const barY=subY+(subTrackHeight-barH)/2;ctx.fillStyle=subColors[stIdx]+'99';ctx.fillRect(bx,barY,Math.max(1,clW/peakSamples),barH);}});// Draw MIDI items as colored note bars
|
||||
const subMidi=sub.midiItems||[];subMidi.forEach(item=>{const itemStartLocal=secStartLocal+(item.startTime||0)*zoom;const itemDurLocal=(item.duration||1)*zoom;const notes=item.notes||[];const pitchMin=36;const pitchMax=84;notes.forEach(note=>{const beatSec=60.0/(parseInt(bpm)||120);const noteStartSec=(note.start_beat||0)*beatSec;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*beatSec);const noteStartLocal=itemStartLocal+noteStartSec*zoom;const nw=noteDurSec*zoom;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=subY+2+(1.0-pitchFrac)*(subTrackHeight-6);const nh=Math.max(4,(subTrackHeight-6)/(pitchMax-pitchMin)*3);ctx.fillStyle=subColors[stIdx]+'cc';ctx.fillRect(Math.max(noteStartLocal,secStartLocal+2),ny,Math.max(2,nw),nh);});});}});// Draw MIDI items
|
||||
const midiItems=track.midiItems||[];midiItems.forEach(midi=>{const midiStartLocal=midi.startTime*zoom-scrollLeft;const midiWidth=midi.duration*zoom;if(midiStartLocal+midiWidth<0||midiStartLocal>drawWidth)return;const isRecordingItem=midi.name==='Recording...';if(isRecordingItem&&Math.random()<0.05){console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length||0}`);}ctx.fillStyle=isRecordingItem?'rgba(239, 68, 68, 0.2)':'#a78bfa33';ctx.fillRect(midiStartLocal,2,midiWidth,height-4);ctx.strokeStyle=isRecordingItem?'#ef4444':'#a78bfa';ctx.lineWidth=1.5;ctx.strokeRect(midiStartLocal,2,midiWidth,height-4);ctx.fillStyle=isRecordingItem?'#fca5a5':'#c4b5fd';ctx.font='bold 9px sans-serif';ctx.fillText(isRecordingItem?'[Ghi MIDI...]':midi.name||'MIDI',Math.max(midiStartLocal+4,4),14);const midiNotes=midi.notes||[];if(midiNotes.length>0){const secondsPerBeat=60.0/(parseInt(bpm)||120);const pitchMin=36;const pitchMax=84;midiNotes.forEach(note=>{const noteStartSec=(note.start_beat||0)*secondsPerBeat;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*secondsPerBeat);const noteStartLocal=(midi.startTime+noteStartSec)*zoom-scrollLeft;const nw=noteDurSec*zoom;if(noteStartLocal+nw<midiStartLocal||noteStartLocal>midiStartLocal+midiWidth)return;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=18+(1.0-pitchFrac)*(height-26);const nh=Math.max(6,(height-26)/(pitchMax-pitchMin)*4);ctx.fillStyle=isRecordingItem?'#10b981':'#a78bfa';ctx.fillRect(Math.max(noteStartLocal,midiStartLocal+2),ny,Math.max(2,nw),nh);});}});// Selection highlight - local selection on this track
|
||||
if(selectionMode==='local'&&localSelectionTrackId===track.id&&localSelLeft!==null&&localSelRight!==null&&localSelRight>localSelLeft){const hlLeftLocal=localSelLeft*zoom-scrollLeft;const hlWidth=(localSelRight-localSelLeft)*zoom;ctx.fillStyle='rgba(245, 158, 11, 0.15)';ctx.fillRect(hlLeftLocal,0,hlWidth,height);ctx.strokeStyle='#f59e0b';ctx.lineWidth=1;ctx.strokeRect(hlLeftLocal,0,hlWidth,height);}},[track,zoom,timelineWidth,viewportWidth,isSelected,markers,selectionMode,localSelectionTrackId,localSelLeft,localSelRight,snapValue,bpm,scrollLeft,recordingState,recTempMidiNotes,recStartTimelineTime,canvasRedrawCount,currentTime]);return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{key:"virtual-spacer",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseMove:e=>{if(!canvasRef.current)return;const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// 1. Shift+Drag (TOP PRIORITY): Range selection on track waveform
|
||||
const midiItems=track.midiItems||[];midiItems.forEach(midi=>{const midiStartLocal=midi.startTime*zoom-scrollLeftVal;const midiWidth=midi.duration*zoom;if(midiStartLocal+midiWidth<0||midiStartLocal>drawWidth)return;const isRecordingItem=midi.name==='Recording...';if(isRecordingItem&&Math.random()<0.05){console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length||0}`);}ctx.fillStyle=isRecordingItem?'rgba(239, 68, 68, 0.2)':'#a78bfa33';ctx.fillRect(midiStartLocal,2,midiWidth,height-4);ctx.strokeStyle=isRecordingItem?'#ef4444':'#a78bfa';ctx.lineWidth=1.5;ctx.strokeRect(midiStartLocal,2,midiWidth,height-4);ctx.fillStyle=isRecordingItem?'#fca5a5':'#c4b5fd';ctx.font='bold 9px sans-serif';ctx.fillText(isRecordingItem?'[Ghi MIDI...]':midi.name||'MIDI',Math.max(midiStartLocal+4,4),14);const midiNotes=midi.notes||[];if(midiNotes.length>0){const secondsPerBeat=60.0/(parseInt(bpm)||120);const pitchMin=36;const pitchMax=84;midiNotes.forEach(note=>{const noteStartSec=(note.start_beat||0)*secondsPerBeat;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*secondsPerBeat);const noteStartLocal=(midi.startTime+noteStartSec)*zoom-scrollLeftVal;const nw=noteDurSec*zoom;if(noteStartLocal+nw<midiStartLocal||noteStartLocal>midiStartLocal+midiWidth)return;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=18+(1.0-pitchFrac)*(height-26);const nh=Math.max(6,(height-26)/(pitchMax-pitchMin)*4);ctx.fillStyle=isRecordingItem?'#10b981':'#a78bfa';ctx.fillRect(Math.max(noteStartLocal,midiStartLocal+2),ny,Math.max(2,nw),nh);});}});// Selection highlight - local selection on this track
|
||||
if(selectionMode==='local'&&localSelectionTrackId===track.id&&localSelLeft!==null&&localSelRight!==null&&localSelRight>localSelLeft){const hlLeftLocal=localSelLeft*zoom-scrollLeftVal;const hlWidth=(localSelRight-localSelLeft)*zoom;ctx.fillStyle='rgba(245, 158, 11, 0.15)';ctx.fillRect(hlLeftLocal,0,hlWidth,height);ctx.strokeStyle='#f59e0b';ctx.lineWidth=1;ctx.strokeRect(hlLeftLocal,0,hlWidth,height);}},[track,zoom,timelineWidth,viewportWidth,isSelected,markers,selectionMode,localSelectionTrackId,localSelLeft,localSelRight,snapValue,bpm,scrollLeft,recordingState,recTempMidiNotes,recStartTimelineTime,canvasRedrawCount,currentTime]);return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{key:"virtual-spacer",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseMove:e=>{if(!canvasRef.current)return;const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// 1. Shift+Drag (TOP PRIORITY): Range selection on track waveform
|
||||
if(e.shiftKey&&e.buttons>0){e.preventDefault();e.stopPropagation();const currentAnchor=getLocalAnchor?getLocalAnchor():null;const anchor=currentAnchor!==null&¤tAnchor!==undefined?currentAnchor:localSelectionStart!==null?localSelectionStart:currentTime;const selS=Math.min(anchor,time);const selE=Math.max(anchor,time);if(onSetSelectionMode)onSetSelectionMode('local');if(onSetLocalSelectionTrackId)onSetLocalSelectionTrackId(track.id);if(onSetLocalSelectionStart)onSetLocalSelectionStart(selS);if(onSetLocalSelectionEnd)onSetLocalSelectionEnd(selE);if(onSetSelectionStart)onSetSelectionStart(selS);if(onSetSelectionEnd)onSetSelectionEnd(selE);if(onSetCurrentTime)onSetCurrentTime(time);if(onSelectTrack)onSelectTrack(track.id);return;}// Check if hovering near local selection boundaries of this track
|
||||
const isLocal=selectionMode==='local'&&localSelectionTrackId===track.id;if(isLocal&&localSelLeft!==null&&localSelRight!==null){const leftPx=localSelLeft*zoom;const rightPx=localSelRight*zoom;const distToLeft=Math.abs(x-leftPx);const distToRight=Math.abs(x-rightPx);if(distToLeft<=5||distToRight<=5){canvasRef.current.style.cursor='ew-resize';return;}}// Check if hovering near right edge of a clip for time-stretching (Alt key required)
|
||||
const toleranceSec=8/zoom;const rightEdgeClip=clips.find(c=>{const duration=c.buffer.duration/(c.speed||1.0);return Math.abs(time-(c.startTime+duration))<=toleranceSec;});if(rightEdgeClip&&e.altKey){canvasRef.current.style.cursor='ew-resize';return;}// Check section/MIDI item hover for resize or drag
|
||||
|
||||
Reference in New Issue
Block a user