Skip to content

Commit 34360f9

Browse files
committed
Add new layouts
1 parent 24e3aff commit 34360f9

9 files changed

Lines changed: 1480 additions & 84 deletions

File tree

.claude/settings.local.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"Bash(curl -s https://raw.githubusercontent.com/actions/upload-pages-artifact/main/README.md)",
1010
"WebFetch(domain:vite.dev)",
1111
"Bash(curl:*)",
12-
"WebSearch"
12+
"WebSearch",
13+
"Bash(npx tsc:*)",
14+
"Bash(grep -E \"\\(\\\\.tsx|\\\\.css\\)$\")"
1315
]
1416
}
1517
}

src/layouts/canvas-002/Canvas002.tsx

Lines changed: 69 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4342
const 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

7870
const MIN_ZOOM = 0.25;
7971
const 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
&times;
@@ -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

Comments
 (0)