feat(04-02): wire block interactions into TimelineCanvas

- Import CommandHistory and all 4 command classes from new modules
- hitTest() with 8px EDGE for resize-left/resize-right/move zones
- _hasOverlap() check for time range collision on same track
- snapToBeat() with 0.1s threshold and calibrationOffset
- _setupInteractions() with mousedown/mousemove/keydown listeners
- _startMove(): live drag preview, snap on mouseup, overlap rejection
- _startResize(): left/right edge handles, 0.5s min duration, snap
- handleDrop(): place block from palette with crypto.randomUUID()
- Keyboard: Delete, Ctrl+Z (undo), Ctrl+Shift+Z / Ctrl+Y (redo)
- app.js: dragstart on palette, dragover/dragleave/drop on canvas
- Ghost block rendered during dragover for visual feedback
- Resize handle indicators drawn on selected block
This commit is contained in:
Claude
2026-04-06 23:44:45 +00:00
parent 658c92f94d
commit 7e18ac6c84
2 changed files with 325 additions and 10 deletions

View File

@@ -304,6 +304,48 @@ function initTimeline() {
`).join('');
}
// Drag from palette to canvas
const canvasEl = document.getElementById('timeline-canvas');
palette?.addEventListener('dragstart', (e) => {
const tile = e.target.closest('.anim-tile');
if (!tile) return;
e.dataTransfer.setData('text/plain', tile.dataset.anim);
e.dataTransfer.effectAllowed = 'copy';
});
canvasEl?.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
// Show ghost preview for current drag position
const rect = canvasEl.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const trackIdx = timeline.yToTrackIndex(my);
const time = timeline.xToTime(mx);
timeline._dragState = {
ghost: true,
trackIdx: trackIdx,
time: Math.max(0, time),
duration: 4.0
};
});
canvasEl?.addEventListener('dragleave', () => {
timeline._dragState = null;
});
canvasEl?.addEventListener('drop', (e) => {
e.preventDefault();
const animType = e.dataTransfer.getData('text/plain');
if (!animType) return;
const rect = canvasEl.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
timeline.handleDrop(animType, mx, my);
timeline._dragState = null;
});
timeline.startRenderLoop();
}