@@ -33,11 +33,10 @@ interface ChatMessage {
3333 from : 'user' | 'system' ;
3434}
3535
36- interface PianoRollInstance {
36+ interface OpenRoll {
3737 padId : string ;
3838 x : number ;
3939 y : number ;
40- notes : Set < string > ;
4140}
4241
4342const initialPads : BeatPad [ ] = [
@@ -51,29 +50,22 @@ const initialPads: BeatPad[] = [
5150 } ) ) ,
5251] ;
5352
54- // Pre-seed two piano rolls so the canvas looks alive
55- const initialPianoRolls : PianoRollInstance [ ] = [
56- {
57- padId : 'pad-0' , // Kick
58- x : 500 ,
59- y : - 200 ,
60- notes : new Set ( [
61- 'C3-0' , 'C3-4' , 'C3-8' , 'C3-12' ,
62- 'C3-16' , 'C3-20' , 'C3-24' , 'C3-28' ,
63- 'E3-2' , 'E3-10' , 'E3-18' , 'E3-26' ,
64- ] ) ,
65- } ,
66- {
67- padId : 'pad-1' , // Snare
68- x : 500 ,
69- y : 360 ,
70- notes : new Set ( [
71- 'D4-4' , 'D4-12' , 'D4-20' , 'D4-28' ,
72- 'E4-6' , 'E4-14' , 'E4-22' , 'E4-30' ,
73- 'F#4-7' , 'F#4-15' , 'F#4-23' , 'F#4-31' ,
74- ] ) ,
75- } ,
76- ] ;
53+ // Pre-seed notes so pads have content even before opening their roll
54+ const initialPadNotes = new Map < string , Set < string > > ( [
55+ [ 'pad-0' , new Set ( [
56+ 'C3-0' , 'C3-4' , 'C3-8' , 'C3-12' ,
57+ 'C3-16' , 'C3-20' , 'C3-24' , 'C3-28' ,
58+ 'E3-2' , 'E3-10' , 'E3-18' , 'E3-26' ,
59+ ] ) ] ,
60+ [ 'pad-1' , new Set ( [
61+ 'D4-4' , 'D4-12' , 'D4-20' , 'D4-28' ,
62+ 'E4-6' , 'E4-14' , 'E4-22' , 'E4-30' ,
63+ 'F#4-7' , 'F#4-15' , 'F#4-23' , 'F#4-31' ,
64+ ] ) ] ,
65+ ] ) ;
66+
67+ // Only one piano roll open at start
68+ const initialOpenRoll : OpenRoll = { padId : 'pad-0' , x : 500 , y : - 200 } ;
7769
7870const MIN_ZOOM = 0.25 ;
7971const MAX_ZOOM = 2 ;
@@ -88,7 +80,8 @@ export default function Canvas002() {
8880 > ( null ) ;
8981 const [ pads , setPads ] = useState ( initialPads ) ;
9082 const [ activePad , setActivePad ] = useState < string | null > ( null ) ;
91- const [ pianoRolls , setPianoRolls ] = useState ( initialPianoRolls ) ;
83+ const [ padNotes , setPadNotes ] = useState ( initialPadNotes ) ;
84+ const [ openRoll , setOpenRoll ] = useState < OpenRoll | null > ( initialOpenRoll ) ;
9285 const [ messages , setMessages ] = useState < ChatMessage [ ] > ( [
9386 { id : 'welcome' , text : 'Type a message to drop a new beat pad onto the canvas.' , from : 'system' } ,
9487 ] ) ;
@@ -131,9 +124,8 @@ export default function Canvas002() {
131124 e . stopPropagation ( ) ;
132125 } , [ pads , camera ] ) ;
133126
134- const handlePianoRollDragStart = useCallback ( ( e : MouseEvent , padId : string ) => {
135- const pr = pianoRolls . find ( ( p ) => p . padId === padId ) ;
136- if ( ! pr ) return ;
127+ const handlePianoRollDragStart = useCallback ( ( e : MouseEvent ) => {
128+ if ( ! openRoll ) return ;
137129 const rect = containerRef . current ?. getBoundingClientRect ( ) ;
138130 if ( ! rect ) return ;
139131
@@ -142,14 +134,14 @@ export default function Canvas002() {
142134
143135 setDragging ( {
144136 type : 'pianoroll' ,
145- padId,
146- offsetX : canvasX - pr . x ,
147- offsetY : canvasY - pr . y ,
137+ padId : openRoll . padId ,
138+ offsetX : canvasX - openRoll . x ,
139+ offsetY : canvasY - openRoll . y ,
148140 } ) ;
149141
150142 e . preventDefault ( ) ;
151143 e . stopPropagation ( ) ;
152- } , [ pianoRolls , camera ] ) ;
144+ } , [ openRoll , camera ] ) ;
153145
154146 const handleMouseMove = useCallback ( ( e : MouseEvent ) => {
155147 if ( ! dragging ) return ;
@@ -176,12 +168,10 @@ export default function Canvas002() {
176168 if ( ! rect ) return ;
177169 const canvasX = ( e . clientX - rect . left - rect . width / 2 ) / camera . zoom - camera . x ;
178170 const canvasY = ( e . clientY - rect . top - rect . height / 2 ) / camera . zoom - camera . y ;
179- setPianoRolls ( ( prev ) =>
180- prev . map ( ( pr ) =>
181- pr . padId === dragging . padId
182- ? { ...pr , x : canvasX - dragging . offsetX , y : canvasY - dragging . offsetY }
183- : pr
184- )
171+ setOpenRoll ( ( prev ) =>
172+ prev && prev . padId === dragging . padId
173+ ? { ...prev , x : canvasX - dragging . offsetX , y : canvasY - dragging . offsetY }
174+ : prev
185175 ) ;
186176 }
187177 } , [ dragging , camera ] ) ;
@@ -205,37 +195,32 @@ export default function Canvas002() {
205195 setTimeout ( ( ) => setActivePad ( null ) , 150 ) ;
206196 } , [ ] ) ;
207197
208- // --- Piano roll management ---
198+ // --- Piano roll management (only one open at a time) ---
209199 const openPianoRoll = useCallback ( ( padId : string ) => {
210- if ( pianoRolls . some ( ( pr ) => pr . padId === padId ) ) return ;
200+ if ( openRoll ?. padId === padId ) return ;
211201 const pad = pads . find ( ( p ) => p . id === padId ) ;
212202 if ( ! pad ) return ;
213203
214- setPianoRolls ( ( prev ) => [
215- ...prev ,
216- {
217- padId,
218- x : pad . x + pad . size + 40 ,
219- y : pad . y - 50 ,
220- notes : new Set ( ) ,
221- } ,
222- ] ) ;
223- } , [ pianoRolls , pads ] ) ;
204+ setOpenRoll ( {
205+ padId,
206+ x : pad . x + pad . size + 40 ,
207+ y : pad . y - 50 ,
208+ } ) ;
209+ } , [ openRoll , pads ] ) ;
224210
225- const closePianoRoll = useCallback ( ( padId : string ) => {
226- setPianoRolls ( ( prev ) => prev . filter ( ( pr ) => pr . padId !== padId ) ) ;
211+ const closePianoRoll = useCallback ( ( ) => {
212+ setOpenRoll ( null ) ;
227213 } , [ ] ) ;
228214
229215 const handleToggleNote = useCallback ( ( padId : string , noteKey : string ) => {
230- setPianoRolls ( ( prev ) =>
231- prev . map ( ( pr ) => {
232- if ( pr . padId !== padId ) return pr ;
233- const next = new Set ( pr . notes ) ;
234- if ( next . has ( noteKey ) ) next . delete ( noteKey ) ;
235- else next . add ( noteKey ) ;
236- return { ...pr , notes : next } ;
237- } )
238- ) ;
216+ setPadNotes ( ( prev ) => {
217+ const next = new Map ( prev ) ;
218+ const notes = new Set ( next . get ( padId ) || [ ] ) ;
219+ if ( notes . has ( noteKey ) ) notes . delete ( noteKey ) ;
220+ else notes . add ( noteKey ) ;
221+ next . set ( padId , notes ) ;
222+ return next ;
223+ } ) ;
239224 } , [ ] ) ;
240225
241226 // --- Chat ---
@@ -272,7 +257,7 @@ export default function Canvas002() {
272257 } , [ input ] ) ;
273258
274259 // --- Helpers for rendering ---
275- const hasPianoRoll = ( padId : string ) => pianoRolls . some ( ( pr ) => pr . padId === padId ) ;
260+ const hasPianoRoll = ( padId : string ) => openRoll ?. padId === padId ;
276261
277262 return (
278263 < div className = "canvas-002 h-dvh overflow-hidden bg-[#0c0a14] select-none font-mono" >
@@ -299,23 +284,23 @@ export default function Canvas002() {
299284 transformOrigin : '0 0' ,
300285 } }
301286 >
302- { /* Tether lines between pads and their piano rolls */ }
287+ { /* Tether line between pad and its piano roll */ }
303288 < svg
304289 className = "absolute pointer-events-none"
305290 style = { { left : 0 , top : 0 , overflow : 'visible' , zIndex : 0 } }
306291 width = "0"
307292 height = "0"
308293 >
309- { pianoRolls . map ( ( pr ) => {
310- const pad = pads . find ( ( p ) => p . id === pr . padId ) ;
294+ { openRoll && ( ( ) => {
295+ const pad = pads . find ( ( p ) => p . id === openRoll . padId ) ;
311296 if ( ! pad ) return null ;
312297 const padCx = pad . x + pad . size / 2 ;
313298 const padCy = pad . y + pad . size / 2 ;
314- const prAx = pr . x ;
315- const prAy = pr . y + 20 ;
299+ const prAx = openRoll . x ;
300+ const prAy = openRoll . y + 20 ;
316301 const midX = ( padCx + prAx ) / 2 ;
317302 return (
318- < g key = { pr . padId } >
303+ < g >
319304 < path
320305 d = { `M ${ padCx } ${ padCy } C ${ midX } ${ padCy } ${ midX } ${ prAy } ${ prAx } ${ prAy } ` }
321306 stroke = { pad . color . glow }
@@ -328,7 +313,7 @@ export default function Canvas002() {
328313 < circle cx = { prAx } cy = { prAy } r = { 4 } fill = { pad . color . glow } fillOpacity = { 0.4 } />
329314 </ g >
330315 ) ;
331- } ) }
316+ } ) ( ) }
332317 </ svg >
333318
334319 { pads . map ( ( pad , i ) => (
@@ -358,7 +343,8 @@ export default function Canvas002() {
358343 onMouseDown = { ( e ) => {
359344 e . stopPropagation ( ) ;
360345 setPads ( ( prev ) => prev . filter ( ( p ) => p . id !== pad . id ) ) ;
361- setPianoRolls ( ( prev ) => prev . filter ( ( pr ) => pr . padId !== pad . id ) ) ;
346+ setOpenRoll ( ( prev ) => prev ?. padId === pad . id ? null : prev ) ;
347+ setPadNotes ( ( prev ) => { const next = new Map ( prev ) ; next . delete ( pad . id ) ; return next ; } ) ;
362348 } }
363349 >
364350 ×
@@ -395,26 +381,26 @@ export default function Canvas002() {
395381 </ div >
396382 ) ) }
397383
398- { /* Piano rolls */ }
399- { pianoRolls . map ( ( pr , i ) => {
400- const pad = pads . find ( ( p ) => p . id === pr . padId ) ;
384+ { /* Piano roll (single) */ }
385+ { openRoll && ( ( ) => {
386+ const pad = pads . find ( ( p ) => p . id === openRoll . padId ) ;
401387 if ( ! pad ) return null ;
402388 return (
403389 < PianoRoll
404- key = { pr . padId }
405- x = { pr . x }
406- y = { pr . y }
407- zIndex = { pads . length + i + 1 }
390+ key = { openRoll . padId }
391+ x = { openRoll . x }
392+ y = { openRoll . y }
393+ zIndex = { pads . length + 1 }
408394 padLabel = { pad . color . label }
409395 padBg = { pad . color . bg }
410396 padGlow = { pad . color . glow }
411- notes = { pr . notes }
412- onToggleNote = { ( noteKey ) => handleToggleNote ( pr . padId , noteKey ) }
413- onTitleBarMouseDown = { ( e ) => handlePianoRollDragStart ( e , pr . padId ) }
414- onClose = { ( ) => closePianoRoll ( pr . padId ) }
397+ notes = { padNotes . get ( openRoll . padId ) || new Set ( ) }
398+ onToggleNote = { ( noteKey ) => handleToggleNote ( openRoll . padId , noteKey ) }
399+ onTitleBarMouseDown = { handlePianoRollDragStart }
400+ onClose = { closePianoRoll }
415401 />
416402 ) ;
417- } ) }
403+ } ) ( ) }
418404 </ div >
419405 </ div >
420406
0 commit comments