refactor: zero lead-in, split time ruler + tempo track
- leadInMargin=0, time 0 at left edge, only 8px PADDING_LEFT - TimelineRuler simplified: time-only labels, 40px height - TempoTrackLane re-added to layout below time ruler (40px) - WaveformLane PADDING_LEFT=8/zoom, leadIn=0
This commit is contained in:
+42
-90
@@ -418,7 +418,7 @@ const WaveformLane = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
const drawWidth = Math.min(timelineWidth, viewportWidth);
|
const drawWidth = Math.min(timelineWidth, viewportWidth);
|
||||||
const leadInMargin = (60.0 / (parseFloat(bpm) || 120)) * 4;
|
const leadInMargin = 0;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
@@ -441,9 +441,9 @@ const WaveformLane = ({
|
|||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
const beatDuration = (60.0 / (parseFloat(bpm) || 120));
|
const beatDuration = (60.0 / (parseFloat(bpm) || 120));
|
||||||
const barDuration = beatDuration * 4;
|
const barDuration = beatDuration * 4;
|
||||||
const leadIn = barDuration;
|
const leadIn = 0;
|
||||||
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
|
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
|
||||||
const PADDING_LEFT = barDuration; // 1-bar margin on left
|
const PADDING_LEFT = 8 / zoom;
|
||||||
const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
|
const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
|
||||||
const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
|
const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
|
||||||
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
|
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
|
||||||
@@ -1195,11 +1195,10 @@ const WaveformLane = ({
|
|||||||
};
|
};
|
||||||
const TimelineRuler = ({
|
const TimelineRuler = ({
|
||||||
bpm, zoom, timelineWidth, viewportWidth,
|
bpm, zoom, timelineWidth, viewportWidth,
|
||||||
onPlayheadSet, snapValue, onRulerMouseDown, scrollLeft, canvasRedrawCount,
|
onPlayheadSet, snapValue, onRulerMouseDown, scrollLeft, canvasRedrawCount
|
||||||
leadInMargin: propLeadIn
|
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
const RULER_HEIGHT = 48;
|
const RULER_HEIGHT = 40;
|
||||||
const drawWidth = Math.min(timelineWidth, viewportWidth);
|
const drawWidth = Math.min(timelineWidth, viewportWidth);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -1222,68 +1221,13 @@ const TimelineRuler = ({
|
|||||||
ctx.moveTo(0, height - 0.5);
|
ctx.moveTo(0, height - 0.5);
|
||||||
ctx.lineTo(drawWidth, height - 0.5);
|
ctx.lineTo(drawWidth, height - 0.5);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
const beatDuration = 60 / bpm;
|
|
||||||
const barDuration = beatDuration * 4;
|
const PADDING_LEFT = 8 / zoom;
|
||||||
const leadIn = propLeadIn !== undefined ? propLeadIn : barDuration;
|
const tStart = scrollLeftVal / zoom - PADDING_LEFT;
|
||||||
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
|
const tEnd = (scrollLeftVal + drawWidth) / zoom + PADDING_LEFT;
|
||||||
const PADDING_LEFT = barDuration;
|
const CLIP_BUFFER = 400;
|
||||||
const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
|
|
||||||
const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
|
// Draw time duration labels with drag-selection markers
|
||||||
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
|
|
||||||
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;
|
|
||||||
if (isBar) {
|
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
|
|
||||||
ctx.lineWidth = 1.5;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(localX, 0);
|
|
||||||
ctx.lineTo(localX, height);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
|
||||||
ctx.font = 'bold 10px Inter, sans-serif';
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.fillText(`${Math.floor((beatNum - 1) / 4)}`, localX + 4, 13);
|
|
||||||
} else {
|
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
|
|
||||||
ctx.lineWidth = 0.8;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(localX, 0);
|
|
||||||
ctx.lineTo(localX, height);
|
|
||||||
ctx.stroke();
|
|
||||||
if (zoom >= 40) {
|
|
||||||
const bar = Math.floor((beatNum - 1) / 4);
|
|
||||||
const beat = ((beatNum - 1) % 4) + 1;
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
|
||||||
ctx.font = '7px Inter, sans-serif';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(`${bar}.${beat}`, localX, height - 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (snapValue && snapValue !== 'free') {
|
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
|
|
||||||
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 firstSnap = Math.floor(tStart / snapInterval) * snapInterval;
|
|
||||||
for (let t = firstSnap; t <= tEnd; t += 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)
|
|
||||||
const minTimePx = 60;
|
const minTimePx = 60;
|
||||||
const rawSecInt = Math.max(1, Math.ceil(minTimePx / zoom));
|
const rawSecInt = Math.max(1, Math.ceil(minTimePx / zoom));
|
||||||
const timePowers = [1, 2, 5, 10, 30, 60];
|
const timePowers = [1, 2, 5, 10, 30, 60];
|
||||||
@@ -1299,17 +1243,18 @@ const TimelineRuler = ({
|
|||||||
ctx.moveTo(localX, 0);
|
ctx.moveTo(localX, 0);
|
||||||
ctx.lineTo(localX, height);
|
ctx.lineTo(localX, height);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.fillStyle = 'rgba(255, 180, 100, 0.55)';
|
ctx.strokeStyle = 'rgba(255, 180, 100, 0.3)';
|
||||||
ctx.font = 'bold 8px monospace';
|
ctx.lineWidth = 1.5;
|
||||||
ctx.textAlign = 'left';
|
ctx.beginPath();
|
||||||
ctx.fillText(formatTimeSimple(t), localX + 4, 11);
|
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]);
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
|
||||||
ctx.font = 'bold 10px Inter, sans-serif';
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillText(`${bpm} BPM`, drawWidth - 6, 13);
|
|
||||||
}, [bpm, zoom, timelineWidth, viewportWidth, snapValue, scrollLeft, canvasRedrawCount]);
|
|
||||||
return React.createElement(React.Fragment, null, React.createElement("div", {
|
return React.createElement(React.Fragment, null, React.createElement("div", {
|
||||||
key: "virtual-spacer-ruler",
|
key: "virtual-spacer-ruler",
|
||||||
style: { width: `${timelineWidth}px`, height: '1px', pointerEvents: 'none' }
|
style: { width: `${timelineWidth}px`, height: '1px', pointerEvents: 'none' }
|
||||||
@@ -1320,8 +1265,7 @@ const TimelineRuler = ({
|
|||||||
onMouseDown: e => {
|
onMouseDown: e => {
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left + (scrollLeft || 0);
|
const x = e.clientX - rect.left + (scrollLeft || 0);
|
||||||
const leadIn = propLeadIn !== undefined ? propLeadIn : (60 / bpm) * 4;
|
const time = Math.max(0, x / zoom);
|
||||||
const time = Math.max(0, x / zoom - leadIn);
|
|
||||||
if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
|
if (e.shiftKey) { e.preventDefault(); e.stopPropagation(); }
|
||||||
if (onRulerMouseDown) onRulerMouseDown(e);
|
if (onRulerMouseDown) onRulerMouseDown(e);
|
||||||
else onPlayheadSet(time, e.shiftKey);
|
else onPlayheadSet(time, e.shiftKey);
|
||||||
@@ -1359,9 +1303,9 @@ const TempoTrackLane = ({
|
|||||||
ctx.fillRect(0, 0, drawWidth, height);
|
ctx.fillRect(0, 0, drawWidth, height);
|
||||||
const beatDuration = 60 / bpm;
|
const beatDuration = 60 / bpm;
|
||||||
const barDuration = beatDuration * 4;
|
const barDuration = beatDuration * 4;
|
||||||
const leadIn = propLeadIn !== undefined ? propLeadIn : barDuration;
|
const leadIn = propLeadIn !== undefined ? propLeadIn : 0;
|
||||||
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
|
const CLIP_BUFFER = Math.max(400, barDuration * zoom + 200);
|
||||||
const PADDING_LEFT = barDuration; // 1-bar margin on left
|
const PADDING_LEFT = 8 / zoom;
|
||||||
const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
|
const tStart = (scrollLeftVal - leadIn * zoom) / zoom - PADDING_LEFT;
|
||||||
const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
|
const tEnd = (scrollLeftVal + drawWidth - leadIn * zoom) / zoom + PADDING_LEFT;
|
||||||
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
|
const firstBeat = Math.floor(tStart / beatDuration) * beatDuration;
|
||||||
@@ -1462,8 +1406,7 @@ const TempoTrackLane = ({
|
|||||||
onMouseDown: e => {
|
onMouseDown: e => {
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left + (scrollLeft || 0);
|
const x = e.clientX - rect.left + (scrollLeft || 0);
|
||||||
const leadIn = (60 / bpm) * 4;
|
const time = Math.max(0, x / zoom);
|
||||||
const time = Math.max(0, x / zoom - leadIn);
|
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -8682,9 +8625,9 @@ const App = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Computed Values ──
|
// ── Computed Values ──
|
||||||
const leadInMargin = useMemo(() => (60.0 / (parseInt(bpm) || 120)) * 4, [bpm]);
|
const leadInMargin = 0;
|
||||||
const leadInMarginRef = useRef(leadInMargin);
|
const leadInMarginRef = useRef(0);
|
||||||
leadInMarginRef.current = leadInMargin;
|
leadInMarginRef.current = 0;
|
||||||
const maxDuration = useMemo(() => {
|
const maxDuration = useMemo(() => {
|
||||||
const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
|
const secPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
|
||||||
const cur = activeTracks;
|
const cur = activeTracks;
|
||||||
@@ -15017,12 +14960,22 @@ const App = () => {
|
|||||||
onRulerMouseDown: handleRulerMouseDown,
|
onRulerMouseDown: handleRulerMouseDown,
|
||||||
scrollLeft: scrollLeft,
|
scrollLeft: scrollLeft,
|
||||||
canvasRedrawCount: canvasRedrawCount
|
canvasRedrawCount: canvasRedrawCount
|
||||||
|
}), /*#__PURE__*/React.createElement(TempoTrackLane, {
|
||||||
|
bpm: parseInt(bpm) || 120,
|
||||||
|
zoom: zoom,
|
||||||
|
timelineWidth: timelineWidth,
|
||||||
|
viewportWidth: viewportWidth,
|
||||||
|
onPlayheadSet: handlePlayheadSet,
|
||||||
|
snapValue: snapValue,
|
||||||
|
onRulerMouseDown: handleRulerMouseDown,
|
||||||
|
scrollLeft: scrollLeft,
|
||||||
|
canvasRedrawCount: canvasRedrawCount
|
||||||
}), selectionMode === 'global' && selLeft !== null && selRight !== null && selRight > selLeft && /*#__PURE__*/React.createElement("div", {
|
}), selectionMode === 'global' && selLeft !== null && selRight !== null && selRight > selLeft && /*#__PURE__*/React.createElement("div", {
|
||||||
className: "absolute inset-0 pointer-events-none z-20",
|
className: "absolute inset-0 pointer-events-none z-20",
|
||||||
style: {
|
style: {
|
||||||
left: `${selLeft * zoom}px`,
|
left: `${selLeft * zoom}px`,
|
||||||
width: `${(selRight - selLeft) * zoom}px`,
|
width: `${(selRight - selLeft) * zoom}px`,
|
||||||
top: '48px'
|
top: '80px'
|
||||||
}
|
}
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__PURE__*/React.createElement("div", {
|
||||||
className: "w-full h-full bg-amber-500/10",
|
className: "w-full h-full bg-amber-500/10",
|
||||||
@@ -15533,8 +15486,7 @@ const App = () => {
|
|||||||
handlePlayheadSet(t);
|
handlePlayheadSet(t);
|
||||||
},
|
},
|
||||||
scrollLeft: scrollLeft,
|
scrollLeft: scrollLeft,
|
||||||
canvasRedrawCount: canvasRedrawCount,
|
canvasRedrawCount: canvasRedrawCount
|
||||||
leadInMargin: 0
|
|
||||||
}), /*#__PURE__*/React.createElement("div", {
|
}), /*#__PURE__*/React.createElement("div", {
|
||||||
className: "flex-1 flex flex-col relative bg-[#111111] min-h-full"
|
className: "flex-1 flex flex-col relative bg-[#111111] min-h-full"
|
||||||
}, vTrack && /*#__PURE__*/React.createElement("div", {
|
}, vTrack && /*#__PURE__*/React.createElement("div", {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -92,6 +92,12 @@
|
|||||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### [2026-07-25 10:44] Task: Zero lead-in, time ruler + tempo track split layout
|
||||||
|
- **Tóm tắt thay đổi:** Xóa leadInMargin (về 0) — time 0 tại mép trái, chỉ còn 8px PADDING_LEFT. TimelineRuler đơn giản thành time-only ruler (40px, chỉ hiển thị time labels). Thêm TempoTrackLane ngay dưới (40px, bars/beats/BPM). Selection overlay top từ 48→80px. Đồng bộ WaveformLane PADDING_LEFT=8/zoom.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||||
|
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||||
|
---
|
||||||
|
|
||||||
### [2026-07-25 10:18] Task: Lead-in margin, bar 0-indexed, grid clip fix
|
### [2026-07-25 10:18] Task: Lead-in margin, bar 0-indexed, grid clip fix
|
||||||
- **Tóm tắt thay đổi:** Thêm leadInMargin (1 bar) đẩy timeline content sang phải — bar 0, time 0s tại mép margin. Playhead bắt đầu từ đó. Bar numbering 0-indexed (bar 0 thay vì 1). Sửa CLIP_BUFFER từ 200px → max(400, barWidth+200) để bar number không bị che. Đồng bộ leadInMargin vào tStart/tEnd canvas + mouse handlers (ruler, clip drag, section resize).
|
- **Tóm tắt thay đổi:** Thêm leadInMargin (1 bar) đẩy timeline content sang phải — bar 0, time 0s tại mép margin. Playhead bắt đầu từ đó. Bar numbering 0-indexed (bar 0 thay vì 1). Sửa CLIP_BUFFER từ 200px → max(400, barWidth+200) để bar number không bị che. Đồng bộ leadInMargin vào tStart/tEnd canvas + mouse handlers (ruler, clip drag, section resize).
|
||||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||||
|
|||||||
Reference in New Issue
Block a user