fix: velocity flicker - grid canvas useLayoutEffect sync

This commit is contained in:
2026-07-26 10:29:23 +07:00
parent 0b7dd82407
commit d20fafb75e
3 changed files with 7 additions and 2 deletions
+1 -1
View File
@@ -4767,7 +4767,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
};
}, [notes, st.currentTime, pixelsPerBeat, st.id, totalBeats, bpm]);
React.useEffect(() => {
React.useLayoutEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
+1 -1
View File
@@ -137,7 +137,7 @@ const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const no
React.useEffect(()=>{const handleWheelRaw=e=>{if(e.ctrlKey){e.preventDefault();const zoomFactor=e.deltaY<0?1.15:0.85;setRollZoom(prev=>Math.max(15,Math.min(250,prev*zoomFactor)));}};const container=gridScrollRef.current;if(container){container.addEventListener('wheel',handleWheelRaw,{passive:false});}return()=>{if(container){container.removeEventListener('wheel',handleWheelRaw);}};},[]);// Alt + Scroll event listener: fastforward playhead + play notes
React.useEffect(()=>{const handleCanvasWheel=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const mx=e.clientX-rect.left;const my=e.clientY-rect.top;const pitch=127-Math.floor(my/NoteHeight);if(e.shiftKey){e.preventDefault();// Shift+scroll on note → change velocity
const clickedNote=notes.find(n=>pitch===n.pitch&&mx>=n.start_beat*pixelsPerBeat&&mx<(n.start_beat+n.duration_beats)*pixelsPerBeat);if(clickedNote){const delta=e.deltaY<0?0.05:-0.05;setNotes(prev=>prev.map(n=>n.id===clickedNote.id?{...n,velocity:Math.max(0.05,Math.min(1,(n.velocity||0.8)+delta))}:n));}else{// Shift+scroll on empty space → horizontal scroll
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,st.instrumentProgram,null);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,st.instrumentProgram,null);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows
for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);ctx.fillStyle=isBlack?'#1a1a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines
let snapBeats=0.25;if(snapVal==='1')snapBeats=4.0;else if(snapVal==='1/2')snapBeats=2.0;else if(snapVal==='1/4')snapBeats=1.0;else if(snapVal==='1/8')snapBeats=0.5;else if(snapVal==='1/16')snapBeats=0.25;else if(snapVal==='4')snapBeats=4.0;else if(snapVal==='1/32')snapBeats=0.125;for(let beat=0;beat<=viewBeats;beat+=snapBeats){const x=beat*pixelsPerBeat;if(x>drawWidth)break;const isBar=beat%timeSigNum===0;ctx.strokeStyle=isBar?'#444450':'#2d2d35';ctx.lineWidth=isBar?1.2:0.6;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}// Draw notes with velocity layer representation
notes.forEach(note=>{const x=note.start_beat*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=note.duration_beats*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);// Draw background of note
+5
View File
@@ -180,3 +180,8 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Direct scroll target: `container.scrollTop = target` trên mỗi mousemove khi pitch ở gần rìa.
---
### [2026-07-26 10:28] Task: Fix velocity flicker - grid canvas useLayoutEffect
- **Tóm tắt thay đổi:** Đổi grid canvas effect từ `React.useEffect``React.useLayoutEffect`. Nguyên nhân flicker: khi velocity thay đổi, grid canvas (useEffect) vẽ async sau paint → hiển thị velocity fill cũ 1 frame, trong khi CC canvas (useLayoutEffect) đã vẽ sync trước paint → hiển thị stem mới. Sự mismatch giữa 2 canvas tạo flicker. Fix: cả grid + CC canvas đều dùng useLayoutEffect, đồng bộ trước paint.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` pass.