fix: add bar markers to TimelineRuler aligned with TempoTrackLane
- TimelineRuler draws bar lines at bn * barDuration positions - Same integer beat loop as TempoTrackLane (bn from firstBarNum) - Bar number labels at center of ruler - Time markers still shown below bar markers - All use same bpm prop → always aligned with tempo track
This commit is contained in:
@@ -1243,6 +1243,27 @@ const TimelineRuler = ({
|
|||||||
const tStart = scrollLeftVal / zoom - PADDING_LEFT;
|
const tStart = scrollLeftVal / zoom - PADDING_LEFT;
|
||||||
const tEnd = (scrollLeftVal + drawWidth) / zoom + CLIP_BUFFER / zoom;
|
const tEnd = (scrollLeftVal + drawWidth) / zoom + CLIP_BUFFER / zoom;
|
||||||
|
|
||||||
|
// Draw bar markers (aligned with TempoTrackLane)
|
||||||
|
const beatDuration = 60 / bpm;
|
||||||
|
const barDuration = beatDuration * 4;
|
||||||
|
const firstBarNum = Math.floor(tStart / barDuration);
|
||||||
|
const lastBarNum = Math.ceil(tEnd / barDuration);
|
||||||
|
for (let bn = firstBarNum; bn <= lastBarNum; bn++) {
|
||||||
|
const t = bn * barDuration;
|
||||||
|
const localX = (t - tStart) * zoom;
|
||||||
|
if (localX < -CLIP_BUFFER || localX > drawWidth + CLIP_BUFFER) continue;
|
||||||
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
|
||||||
|
ctx.lineWidth = 1.2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(localX, 0);
|
||||||
|
ctx.lineTo(localX, height);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||||
|
ctx.font = 'bold 10px Inter, sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(`${bn}`, localX, 32);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw time duration labels with drag-selection markers
|
// Draw time duration labels with drag-selection markers
|
||||||
const minTimePx = 60;
|
const minTimePx = 60;
|
||||||
const rawSecInt = Math.max(1, Math.ceil(minTimePx / zoom));
|
const rawSecInt = Math.max(1, Math.ceil(minTimePx / zoom));
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ if(clickedClip&&(e.altKey||e.ctrlKey)){e.preventDefault();e.stopPropagation();if
|
|||||||
if(e.ctrlKey){if(onClearLocalSelection)onClearLocalSelection();if(onSetSelectionMode)onSetSelectionMode(null);if(onSetSelectionStart)onSetSelectionStart(null);if(onSetSelectionEnd)onSetSelectionEnd(null);return;}onPlayheadSet(time);if(onTrackLaneMouseDown){onTrackLaneMouseDown(track.id,time,e);}e.stopPropagation();},onDoubleClick:e=>{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}]:[];// Check double-click on section first
|
if(e.ctrlKey){if(onClearLocalSelection)onClearLocalSelection();if(onSetSelectionMode)onSetSelectionMode(null);if(onSetSelectionStart)onSetSelectionStart(null);if(onSetSelectionEnd)onSetSelectionEnd(null);return;}onPlayheadSet(time);if(onTrackLaneMouseDown){onTrackLaneMouseDown(track.id,time,e);}e.stopPropagation();},onDoubleClick:e=>{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}]:[];// Check double-click on section first
|
||||||
const dblSecItems=track.sections||[];let dblSecHit=null;for(const sec of dblSecItems){if(time>=sec.start&&time<sec.start+sec.duration){dblSecHit=sec;break;}}if(dblSecHit){e.preventDefault();e.stopPropagation();if(onEditSectionInTab)onEditSectionInTab(track.id,dblSecHit.id);return;}// Check double-click on MIDI item next
|
const dblSecItems=track.sections||[];let dblSecHit=null;for(const sec of dblSecItems){if(time>=sec.start&&time<sec.start+sec.duration){dblSecHit=sec;break;}}if(dblSecHit){e.preventDefault();e.stopPropagation();if(onEditSectionInTab)onEditSectionInTab(track.id,dblSecHit.id);return;}// Check double-click on MIDI item next
|
||||||
const dblMidiItems=track.midiItems||[];let dblMidiHit=null;for(const midi of dblMidiItems){if(time>=midi.startTime&&time<midi.startTime+midi.duration){dblMidiHit=midi;break;}}if(dblMidiHit){e.preventDefault();e.stopPropagation();if(onEditMidiInTab)onEditMidiInTab(track.id,dblMidiHit.id);return;}const clickedClip=clips.find(c=>time>=c.startTime&&time<c.startTime+c.buffer.duration/(c.speed||1.0));if(clickedClip){e.preventDefault();e.stopPropagation();if(onEditClipInSubTab){onEditClipInSubTab(track.id,clickedClip.id);}}},onContextMenu:e=>{e.preventDefault();e.stopPropagation();onSelectTrack(track.id);const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);// Detect section under cursor
|
const dblMidiItems=track.midiItems||[];let dblMidiHit=null;for(const midi of dblMidiItems){if(time>=midi.startTime&&time<midi.startTime+midi.duration){dblMidiHit=midi;break;}}if(dblMidiHit){e.preventDefault();e.stopPropagation();if(onEditMidiInTab)onEditMidiInTab(track.id,dblMidiHit.id);return;}const clickedClip=clips.find(c=>time>=c.startTime&&time<c.startTime+c.buffer.duration/(c.speed||1.0));if(clickedClip){e.preventDefault();e.stopPropagation();if(onEditClipInSubTab){onEditClipInSubTab(track.id,clickedClip.id);}}},onContextMenu:e=>{e.preventDefault();e.stopPropagation();onSelectTrack(track.id);const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);// Detect section under cursor
|
||||||
const secList=track.sections||[];let hitSectionId=null;for(const sec of secList){if(time>=sec.start&&time<sec.start+sec.duration){hitSectionId=sec.id;break;}}if(onContextMenu)onContextMenu(e,track.id,time,hitSectionId);}}));};const TimelineRuler=({bpm,zoom,timelineWidth,viewportWidth,onPlayheadSet,snapValue,onRulerMouseDown,scrollLeft,canvasRedrawCount})=>{const canvasRef=useRef(null);const RULER_HEIGHT=40;const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;let scrollLeftVal=scrollLeft||0;const height=RULER_HEIGHT;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='#242424';ctx.fillRect(0,0,drawWidth,height);ctx.strokeStyle='rgba(255,255,255,0.08)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,height-0.5);ctx.lineTo(drawWidth,height-0.5);ctx.stroke();const CLIP_BUFFER=400;const PADDING_LEFT=0;const tStart=scrollLeftVal/zoom-PADDING_LEFT;const tEnd=(scrollLeftVal+drawWidth)/zoom+CLIP_BUFFER/zoom;// Draw time duration labels with drag-selection markers
|
const secList=track.sections||[];let hitSectionId=null;for(const sec of secList){if(time>=sec.start&&time<sec.start+sec.duration){hitSectionId=sec.id;break;}}if(onContextMenu)onContextMenu(e,track.id,time,hitSectionId);}}));};const TimelineRuler=({bpm,zoom,timelineWidth,viewportWidth,onPlayheadSet,snapValue,onRulerMouseDown,scrollLeft,canvasRedrawCount})=>{const canvasRef=useRef(null);const RULER_HEIGHT=40;const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;let scrollLeftVal=scrollLeft||0;const height=RULER_HEIGHT;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='#242424';ctx.fillRect(0,0,drawWidth,height);ctx.strokeStyle='rgba(255,255,255,0.08)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,height-0.5);ctx.lineTo(drawWidth,height-0.5);ctx.stroke();const CLIP_BUFFER=400;const PADDING_LEFT=0;const tStart=scrollLeftVal/zoom-PADDING_LEFT;const tEnd=(scrollLeftVal+drawWidth)/zoom+CLIP_BUFFER/zoom;// Draw bar markers (aligned with TempoTrackLane)
|
||||||
|
const beatDuration=60/bpm;const barDuration=beatDuration*4;const firstBarNum=Math.floor(tStart/barDuration);const lastBarNum=Math.ceil(tEnd/barDuration);for(let bn=firstBarNum;bn<=lastBarNum;bn++){const t=bn*barDuration;const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;ctx.strokeStyle='rgba(255, 255, 255, 0.25)';ctx.lineWidth=1.2;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.fillStyle='rgba(255, 255, 255, 0.8)';ctx.font='bold 10px Inter, sans-serif';ctx.textAlign='center';ctx.fillText(`${bn}`,localX,32);}// Draw time duration labels with drag-selection markers
|
||||||
const minTimePx=60;const rawSecInt=Math.max(1,Math.ceil(minTimePx/zoom));const timePowers=[1,2,5,10,30,60];let secInterval=timePowers.find(p=>p>=rawSecInt)||120;if(secInterval*zoom<minTimePx)secInterval=Math.ceil(minTimePx/zoom);const firstSec=Math.floor(tStart/secInterval)*secInterval;for(let t=firstSec;t<=tEnd;t+=secInterval){const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;ctx.strokeStyle='rgba(255, 180, 100, 0.12)';ctx.lineWidth=0.8;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.strokeStyle='rgba(255, 180, 100, 0.3)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,10);ctx.stroke();ctx.fillStyle='rgba(255, 180, 100, 0.7)';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText(formatTimeSimple(t),localX,24);}},[bpm,zoom,timelineWidth,viewportWidth,scrollLeft,canvasRedrawCount]);return React.createElement(React.Fragment,null,React.createElement("div",{key:"virtual-spacer-ruler",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseDown:e=>{const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom);if(e.shiftKey){e.preventDefault();e.stopPropagation();}if(onRulerMouseDown)onRulerMouseDown(e);else onPlayheadSet(time,e.shiftKey);}}));};const TempoTrackLane=({bpm,zoom,timelineWidth,viewportWidth,onPlayheadSet,snapValue,onRulerMouseDown,scrollLeft,canvasRedrawCount,leadInMargin:propLeadIn})=>{const canvasRef=useRef(null);const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;// Use scrollLeft prop directly (DOM traversal broken by sticky wrapper)
|
const minTimePx=60;const rawSecInt=Math.max(1,Math.ceil(minTimePx/zoom));const timePowers=[1,2,5,10,30,60];let secInterval=timePowers.find(p=>p>=rawSecInt)||120;if(secInterval*zoom<minTimePx)secInterval=Math.ceil(minTimePx/zoom);const firstSec=Math.floor(tStart/secInterval)*secInterval;for(let t=firstSec;t<=tEnd;t+=secInterval){const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;ctx.strokeStyle='rgba(255, 180, 100, 0.12)';ctx.lineWidth=0.8;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.strokeStyle='rgba(255, 180, 100, 0.3)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,10);ctx.stroke();ctx.fillStyle='rgba(255, 180, 100, 0.7)';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText(formatTimeSimple(t),localX,24);}},[bpm,zoom,timelineWidth,viewportWidth,scrollLeft,canvasRedrawCount]);return React.createElement(React.Fragment,null,React.createElement("div",{key:"virtual-spacer-ruler",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseDown:e=>{const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom);if(e.shiftKey){e.preventDefault();e.stopPropagation();}if(onRulerMouseDown)onRulerMouseDown(e);else onPlayheadSet(time,e.shiftKey);}}));};const TempoTrackLane=({bpm,zoom,timelineWidth,viewportWidth,onPlayheadSet,snapValue,onRulerMouseDown,scrollLeft,canvasRedrawCount,leadInMargin:propLeadIn})=>{const canvasRef=useRef(null);const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;// Use scrollLeft prop directly (DOM traversal broken by sticky wrapper)
|
||||||
let scrollLeftVal=scrollLeft||0;const height=40;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='#1a1a2e';ctx.fillRect(0,0,drawWidth,height);const beatDuration=60/bpm;const barDuration=beatDuration*4;const leadIn=propLeadIn!==undefined?propLeadIn: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 firstBeatNum=Math.floor(tStart/beatDuration);const lastBeatNum=Math.ceil(tEnd/beatDuration);for(let bn=firstBeatNum;bn<=lastBeatNum;bn++){const t=bn*beatDuration;const beatNum=bn+1;const isBar=beatNum%4===1;const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;if(isBar){ctx.strokeStyle='rgba(255, 255, 255, 0.3)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.fillStyle='rgba(255, 255, 255, 0.7)';ctx.font='bold 9px Inter, sans-serif';ctx.textAlign='left';ctx.fillText(`${Math.floor((beatNum-1)/4)}`,localX+3,11);}else{ctx.strokeStyle='rgba(255, 255, 255, 0.08)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();}ctx.fillStyle='rgba(255, 255, 255, 0.35)';ctx.font='7px Inter, sans-serif';const bar=Math.floor((beatNum-1)/4);const beat=(beatNum-1)%4+1;ctx.fillText(`${bar}:${beat}`,localX+2,height-3);}// Draw snap sub-ticks at the bottom
|
let scrollLeftVal=scrollLeft||0;const height=40;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='#1a1a2e';ctx.fillRect(0,0,drawWidth,height);const beatDuration=60/bpm;const barDuration=beatDuration*4;const leadIn=propLeadIn!==undefined?propLeadIn: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 firstBeatNum=Math.floor(tStart/beatDuration);const lastBeatNum=Math.ceil(tEnd/beatDuration);for(let bn=firstBeatNum;bn<=lastBeatNum;bn++){const t=bn*beatDuration;const beatNum=bn+1;const isBar=beatNum%4===1;const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;if(isBar){ctx.strokeStyle='rgba(255, 255, 255, 0.3)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.fillStyle='rgba(255, 255, 255, 0.7)';ctx.font='bold 9px Inter, sans-serif';ctx.textAlign='left';ctx.fillText(`${Math.floor((beatNum-1)/4)}`,localX+3,11);}else{ctx.strokeStyle='rgba(255, 255, 255, 0.08)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();}ctx.fillStyle='rgba(255, 255, 255, 0.35)';ctx.font='7px Inter, sans-serif';const bar=Math.floor((beatNum-1)/4);const beat=(beatNum-1)%4+1;ctx.fillText(`${bar}:${beat}`,localX+2,height-3);}// Draw snap sub-ticks at the bottom
|
||||||
if(snapValue&&snapValue!=='free'){ctx.strokeStyle='rgba(255, 255, 255, 0.15)';ctx.lineWidth=0.8;let divisor=1;if(snapValue==='4')divisor=4;else if(snapValue==='1')divisor=1;else if(snapValue==='1/2')divisor=0.5;else if(snapValue==='1/4')divisor=0.25;else if(snapValue==='1/8')divisor=0.125;else if(snapValue==='1/16')divisor=0.0625;else if(snapValue==='1/32')divisor=0.03125;const snapInterval=beatDuration*divisor;if(snapInterval*zoom>=4){const firstSnapNum=Math.floor(tStart/snapInterval);const lastSnapNum=Math.ceil(tEnd/snapInterval);for(let sn=firstSnapNum;sn<=lastSnapNum;sn++){const t=sn*snapInterval;const onBeat=Math.abs(t/beatDuration-Math.round(t/beatDuration))<0.001;if(!onBeat){const localX=(t-tStart)*zoom;ctx.beginPath();ctx.moveTo(localX,height-6);ctx.lineTo(localX,height);ctx.stroke();}}}}// Draw time duration labels (restored from original time ruler)
|
if(snapValue&&snapValue!=='free'){ctx.strokeStyle='rgba(255, 255, 255, 0.15)';ctx.lineWidth=0.8;let divisor=1;if(snapValue==='4')divisor=4;else if(snapValue==='1')divisor=1;else if(snapValue==='1/2')divisor=0.5;else if(snapValue==='1/4')divisor=0.25;else if(snapValue==='1/8')divisor=0.125;else if(snapValue==='1/16')divisor=0.0625;else if(snapValue==='1/32')divisor=0.03125;const snapInterval=beatDuration*divisor;if(snapInterval*zoom>=4){const firstSnapNum=Math.floor(tStart/snapInterval);const lastSnapNum=Math.ceil(tEnd/snapInterval);for(let sn=firstSnapNum;sn<=lastSnapNum;sn++){const t=sn*snapInterval;const onBeat=Math.abs(t/beatDuration-Math.round(t/beatDuration))<0.001;if(!onBeat){const localX=(t-tStart)*zoom;ctx.beginPath();ctx.moveTo(localX,height-6);ctx.lineTo(localX,height);ctx.stroke();}}}}// Draw time duration labels (restored from original time ruler)
|
||||||
|
|||||||
Reference in New Issue
Block a user