Files
audio-ui/components/automation/AutomationPoint.tsx
Sebastian Krüger 9b1eedc379 feat: Ableton Live-style effects and complete automation system
Enhanced visual design:
- Improved device rack container with darker background and inner shadow
- Device cards now have rounded corners, shadows, and colored indicators
- Better visual separation between enabled/disabled effects
- Active devices highlighted with accent border

Complete automation infrastructure (Phase 9):
- Created comprehensive type system for automation lanes and points
- Implemented AutomationPoint component with drag-and-drop editing
- Implemented AutomationHeader with mode controls (Read/Write/Touch/Latch)
- Implemented AutomationLane with canvas-based curve rendering
- Integrated automation lanes into Track component below effects
- Created automation playback engine with real-time interpolation
- Added automation data persistence to localStorage

Automation features:
- Add/remove automation points by clicking/double-clicking
- Drag points to change time and value
- Multiple automation modes (Read, Write, Touch, Latch)
- Linear and step curve types (bezier planned)
- Adjustable lane height (60-180px)
- Show/hide automation per lane
- Real-time value display at playhead
- Color-coded lanes by parameter type
- Keyboard delete support (Delete/Backspace)

Track type updates:
- Added automation field to Track interface
- Updated track creation to initialize empty automation
- Updated localStorage save/load to include automation data

Files created:
- components/automation/AutomationPoint.tsx
- components/automation/AutomationHeader.tsx
- components/automation/AutomationLane.tsx
- lib/audio/automation/utils.ts (helper functions)
- lib/audio/automation/playback.ts (playback engine)
- types/automation.ts (complete type system)

Files modified:
- components/effects/EffectDevice.tsx (Ableton-style visual improvements)
- components/tracks/Track.tsx (automation lanes integration)
- types/track.ts (automation field added)
- lib/audio/track-utils.ts (automation initialization)
- lib/hooks/useMultiTrack.ts (automation persistence)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 16:30:01 +01:00

124 lines
3.3 KiB
TypeScript

'use client';
import * as React from 'react';
import { cn } from '@/lib/utils/cn';
import type { AutomationPoint as AutomationPointType } from '@/types/automation';
export interface AutomationPointProps {
point: AutomationPointType;
x: number; // Pixel position
y: number; // Pixel position
isSelected?: boolean;
onDragStart?: (pointId: string, startX: number, startY: number) => void;
onDrag?: (pointId: string, deltaX: number, deltaY: number) => void;
onDragEnd?: (pointId: string) => void;
onClick?: (pointId: string, event: React.MouseEvent) => void;
onDoubleClick?: (pointId: string) => void;
}
export function AutomationPoint({
point,
x,
y,
isSelected = false,
onDragStart,
onDrag,
onDragEnd,
onClick,
onDoubleClick,
}: AutomationPointProps) {
const [isDragging, setIsDragging] = React.useState(false);
const dragStartRef = React.useRef({ x: 0, y: 0 });
const handleMouseDown = React.useCallback(
(e: React.MouseEvent) => {
if (e.button !== 0) return; // Only left click
e.stopPropagation();
setIsDragging(true);
dragStartRef.current = { x: e.clientX, y: e.clientY };
if (onDragStart) {
onDragStart(point.id, e.clientX, e.clientY);
}
},
[point.id, onDragStart]
);
const handleClick = React.useCallback(
(e: React.MouseEvent) => {
if (!isDragging && onClick) {
onClick(point.id, e);
}
},
[isDragging, point.id, onClick]
);
const handleDoubleClick = React.useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
if (onDoubleClick) {
onDoubleClick(point.id);
}
},
[point.id, onDoubleClick]
);
// Global mouse handlers
React.useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const deltaX = e.clientX - dragStartRef.current.x;
const deltaY = e.clientY - dragStartRef.current.y;
if (onDrag) {
onDrag(point.id, deltaX, deltaY);
}
// Update drag start position for next delta calculation
dragStartRef.current = { x: e.clientX, y: e.clientY };
};
const handleMouseUp = () => {
if (isDragging) {
setIsDragging(false);
if (onDragEnd) {
onDragEnd(point.id);
}
}
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, point.id, onDrag, onDragEnd]);
return (
<div
className={cn(
'absolute rounded-full cursor-pointer transition-all select-none',
'hover:scale-125',
isDragging ? 'scale-125 z-10' : 'z-0',
isSelected
? 'w-3 h-3 bg-primary border-2 border-background shadow-lg'
: 'w-2.5 h-2.5 bg-primary/80 border border-background shadow-md'
)}
style={{
left: x - (isSelected || isDragging ? 6 : 5),
top: y - (isSelected || isDragging ? 6 : 5),
}}
onMouseDown={handleMouseDown}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
title={`Time: ${point.time.toFixed(3)}s, Value: ${point.value.toFixed(3)}`}
/>
);
}