piano roll improvements
This commit is contained in:
parent
1fcefab966
commit
422f97382b
187
src/main.js
187
src/main.js
|
|
@ -10508,11 +10508,108 @@ function piano() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function pianoRoll() {
|
function pianoRoll() {
|
||||||
|
// Create container for piano roll and properties panel
|
||||||
|
let container = document.createElement("div");
|
||||||
|
container.className = "piano-roll-container";
|
||||||
|
container.style.position = "relative";
|
||||||
|
container.style.width = "100%";
|
||||||
|
container.style.height = "100%";
|
||||||
|
container.style.display = "flex";
|
||||||
|
|
||||||
let canvas = document.createElement("canvas");
|
let canvas = document.createElement("canvas");
|
||||||
canvas.className = "piano-roll";
|
canvas.className = "piano-roll";
|
||||||
|
canvas.style.flex = "1";
|
||||||
|
|
||||||
|
// Create properties panel
|
||||||
|
let propertiesPanel = document.createElement("div");
|
||||||
|
propertiesPanel.className = "piano-roll-properties";
|
||||||
|
propertiesPanel.style.display = "flex";
|
||||||
|
propertiesPanel.style.gap = "15px";
|
||||||
|
propertiesPanel.style.padding = "10px";
|
||||||
|
propertiesPanel.style.backgroundColor = "#1e1e1e";
|
||||||
|
propertiesPanel.style.borderLeft = "1px solid #333";
|
||||||
|
propertiesPanel.style.alignItems = "center";
|
||||||
|
propertiesPanel.style.fontSize = "12px";
|
||||||
|
propertiesPanel.style.color = "#ccc";
|
||||||
|
|
||||||
|
// Create property sections
|
||||||
|
const createPropertySection = (label, isEditable = false) => {
|
||||||
|
const section = document.createElement("div");
|
||||||
|
section.style.display = "flex";
|
||||||
|
section.style.flexDirection = "column";
|
||||||
|
section.style.gap = "5px";
|
||||||
|
|
||||||
|
const labelEl = document.createElement("label");
|
||||||
|
labelEl.textContent = label;
|
||||||
|
labelEl.style.fontSize = "11px";
|
||||||
|
labelEl.style.color = "#999";
|
||||||
|
section.appendChild(labelEl);
|
||||||
|
|
||||||
|
if (isEditable) {
|
||||||
|
const inputContainer = document.createElement("div");
|
||||||
|
inputContainer.style.display = "flex";
|
||||||
|
inputContainer.style.gap = "5px";
|
||||||
|
inputContainer.style.alignItems = "center";
|
||||||
|
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "number";
|
||||||
|
input.style.width = "45px";
|
||||||
|
input.style.padding = "3px";
|
||||||
|
input.style.backgroundColor = "#2a2a2a";
|
||||||
|
input.style.border = "1px solid #444";
|
||||||
|
input.style.borderRadius = "3px";
|
||||||
|
input.style.color = "#ccc";
|
||||||
|
input.style.fontSize = "12px";
|
||||||
|
input.style.boxSizing = "border-box";
|
||||||
|
inputContainer.appendChild(input);
|
||||||
|
|
||||||
|
const slider = document.createElement("input");
|
||||||
|
slider.type = "range";
|
||||||
|
slider.style.flex = "1";
|
||||||
|
slider.style.minWidth = "80px";
|
||||||
|
inputContainer.appendChild(slider);
|
||||||
|
|
||||||
|
section.appendChild(inputContainer);
|
||||||
|
return { section, input, slider };
|
||||||
|
} else {
|
||||||
|
const value = document.createElement("span");
|
||||||
|
value.style.color = "#fff";
|
||||||
|
value.textContent = "-";
|
||||||
|
section.appendChild(value);
|
||||||
|
return { section, value };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pitchSection = createPropertySection("Pitch");
|
||||||
|
const velocitySection = createPropertySection("Velocity", true);
|
||||||
|
const modulationSection = createPropertySection("Modulation", true);
|
||||||
|
|
||||||
|
// Configure velocity slider
|
||||||
|
velocitySection.input.min = 1;
|
||||||
|
velocitySection.input.max = 127;
|
||||||
|
velocitySection.slider.min = 1;
|
||||||
|
velocitySection.slider.max = 127;
|
||||||
|
|
||||||
|
// Configure modulation slider
|
||||||
|
modulationSection.input.min = 0;
|
||||||
|
modulationSection.input.max = 127;
|
||||||
|
modulationSection.slider.min = 0;
|
||||||
|
modulationSection.slider.max = 127;
|
||||||
|
|
||||||
|
propertiesPanel.appendChild(pitchSection.section);
|
||||||
|
propertiesPanel.appendChild(velocitySection.section);
|
||||||
|
propertiesPanel.appendChild(modulationSection.section);
|
||||||
|
|
||||||
|
container.appendChild(canvas);
|
||||||
|
container.appendChild(propertiesPanel);
|
||||||
|
|
||||||
// Create the piano roll editor widget
|
// Create the piano roll editor widget
|
||||||
canvas.pianoRollEditor = new PianoRollEditor(0, 0, 0, 0);
|
canvas.pianoRollEditor = new PianoRollEditor(0, 0, 0, 0);
|
||||||
|
canvas.pianoRollEditor.propertiesPanel = {
|
||||||
|
pitch: pitchSection.value,
|
||||||
|
velocity: { input: velocitySection.input, slider: velocitySection.slider },
|
||||||
|
modulation: { input: modulationSection.input, slider: modulationSection.slider }
|
||||||
|
};
|
||||||
|
|
||||||
function updateCanvasSize() {
|
function updateCanvasSize() {
|
||||||
const canvasStyles = window.getComputedStyle(canvas);
|
const canvasStyles = window.getComputedStyle(canvas);
|
||||||
|
|
@ -10533,6 +10630,30 @@ function pianoRoll() {
|
||||||
|
|
||||||
// Render the piano roll
|
// Render the piano roll
|
||||||
canvas.pianoRollEditor.draw(ctx);
|
canvas.pianoRollEditor.draw(ctx);
|
||||||
|
|
||||||
|
// Update properties panel layout based on aspect ratio
|
||||||
|
const containerWidth = container.offsetWidth;
|
||||||
|
const containerHeight = container.offsetHeight;
|
||||||
|
const isWide = containerWidth > containerHeight;
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
// Side layout
|
||||||
|
container.style.flexDirection = "row";
|
||||||
|
propertiesPanel.style.flexDirection = "column";
|
||||||
|
propertiesPanel.style.width = "240px";
|
||||||
|
propertiesPanel.style.height = "auto";
|
||||||
|
propertiesPanel.style.borderLeft = "1px solid #333";
|
||||||
|
propertiesPanel.style.borderTop = "none";
|
||||||
|
propertiesPanel.style.alignItems = "stretch";
|
||||||
|
} else {
|
||||||
|
// Bottom layout
|
||||||
|
container.style.flexDirection = "column";
|
||||||
|
propertiesPanel.style.flexDirection = "row";
|
||||||
|
propertiesPanel.style.width = "auto";
|
||||||
|
propertiesPanel.style.height = "60px";
|
||||||
|
propertiesPanel.style.borderLeft = "none";
|
||||||
|
propertiesPanel.style.borderTop = "1px solid #333";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store references in context for global access and playback updates
|
// Store references in context for global access and playback updates
|
||||||
|
|
@ -10543,7 +10664,7 @@ function pianoRoll() {
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
});
|
});
|
||||||
resizeObserver.observe(canvas);
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
// Pointer event handlers (works with mouse and touch)
|
// Pointer event handlers (works with mouse and touch)
|
||||||
canvas.addEventListener("pointerdown", (e) => {
|
canvas.addEventListener("pointerdown", (e) => {
|
||||||
|
|
@ -10585,7 +10706,69 @@ function pianoRoll() {
|
||||||
// Prevent text selection
|
// Prevent text selection
|
||||||
canvas.addEventListener("selectstart", (e) => e.preventDefault());
|
canvas.addEventListener("selectstart", (e) => e.preventDefault());
|
||||||
|
|
||||||
return canvas;
|
// Add event handlers for velocity and modulation inputs/sliders
|
||||||
|
const syncInputSlider = (input, slider) => {
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
const value = parseInt(input.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
slider.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
slider.addEventListener("input", () => {
|
||||||
|
input.value = slider.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
syncInputSlider(velocitySection.input, velocitySection.slider);
|
||||||
|
syncInputSlider(modulationSection.input, modulationSection.slider);
|
||||||
|
|
||||||
|
// Handle property changes
|
||||||
|
const updateNoteProperty = (property, value) => {
|
||||||
|
const clipData = canvas.pianoRollEditor.getSelectedClip();
|
||||||
|
if (!clipData || !clipData.clip || !clipData.clip.notes) return;
|
||||||
|
|
||||||
|
if (canvas.pianoRollEditor.selectedNotes.size === 0) return;
|
||||||
|
|
||||||
|
for (const noteIndex of canvas.pianoRollEditor.selectedNotes) {
|
||||||
|
if (noteIndex >= 0 && noteIndex < clipData.clip.notes.length) {
|
||||||
|
const note = clipData.clip.notes[noteIndex];
|
||||||
|
if (property === "velocity") {
|
||||||
|
note.velocity = value;
|
||||||
|
} else if (property === "modulation") {
|
||||||
|
note.modulation = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.pianoRollEditor.syncNotesToBackend(clipData);
|
||||||
|
updateCanvasSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
velocitySection.input.addEventListener("change", (e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
if (!isNaN(value) && value >= 1 && value <= 127) {
|
||||||
|
updateNoteProperty("velocity", value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
velocitySection.slider.addEventListener("change", (e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
updateNoteProperty("velocity", value);
|
||||||
|
});
|
||||||
|
|
||||||
|
modulationSection.input.addEventListener("change", (e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
if (!isNaN(value) && value >= 0 && value <= 127) {
|
||||||
|
updateNoteProperty("modulation", value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
modulationSection.slider.addEventListener("change", (e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
updateNoteProperty("modulation", value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
function presetBrowser() {
|
function presetBrowser() {
|
||||||
|
|
|
||||||
483
src/widgets.js
483
src/widgets.js
|
|
@ -1390,6 +1390,18 @@ class TimelineWindowV2 extends Widget {
|
||||||
trackHeight - 10
|
trackHeight - 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Highlight selected MIDI clip
|
||||||
|
if (isMIDI && context.pianoRollEditor && clip.clipId === context.pianoRollEditor.selectedClipId) {
|
||||||
|
ctx.strokeStyle = '#6fdc6f' // Bright green for selected MIDI clip
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
ctx.strokeRect(
|
||||||
|
startX,
|
||||||
|
y + 5,
|
||||||
|
clipWidth,
|
||||||
|
trackHeight - 10
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Draw clip name if there's enough space
|
// Draw clip name if there's enough space
|
||||||
const minWidthForLabel = 40
|
const minWidthForLabel = 40
|
||||||
if (clipWidth >= minWidthForLabel) {
|
if (clipWidth >= minWidthForLabel) {
|
||||||
|
|
@ -2265,6 +2277,16 @@ class TimelineWindowV2 extends Widget {
|
||||||
// Select the track
|
// Select the track
|
||||||
this.selectTrack(track)
|
this.selectTrack(track)
|
||||||
|
|
||||||
|
// If this is a MIDI clip, update piano roll selection
|
||||||
|
if (audioClipInfo.isMIDI && context.pianoRollEditor) {
|
||||||
|
context.pianoRollEditor.selectedClipId = audioClipInfo.clip.clipId
|
||||||
|
context.pianoRollEditor.selectedNotes.clear()
|
||||||
|
// Trigger piano roll redraw to show the selection change
|
||||||
|
if (context.pianoRollRedraw) {
|
||||||
|
context.pianoRollRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start audio clip dragging
|
// Start audio clip dragging
|
||||||
const clickTime = this.timelineState.pixelToTime(adjustedX)
|
const clickTime = this.timelineState.pixelToTime(adjustedX)
|
||||||
this.draggingAudioClip = {
|
this.draggingAudioClip = {
|
||||||
|
|
@ -2866,7 +2888,8 @@ class TimelineWindowV2 extends Widget {
|
||||||
return {
|
return {
|
||||||
clip: clip,
|
clip: clip,
|
||||||
clipIndex: i,
|
clipIndex: i,
|
||||||
audioTrack: audioTrack
|
audioTrack: audioTrack,
|
||||||
|
isMIDI: audioTrack.type === 'midi'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5372,10 +5395,12 @@ class PianoRollEditor extends Widget {
|
||||||
|
|
||||||
// Interaction state
|
// Interaction state
|
||||||
this.selectedNotes = new Set() // Set of note indices
|
this.selectedNotes = new Set() // Set of note indices
|
||||||
this.dragMode = null // null, 'move', 'resize-left', 'resize-right', 'create'
|
this.selectedClipId = null // Currently selected clip ID for editing
|
||||||
|
this.dragMode = null // null, 'move', 'resize', 'create', 'select'
|
||||||
this.dragStartX = 0
|
this.dragStartX = 0
|
||||||
this.dragStartY = 0
|
this.dragStartY = 0
|
||||||
this.creatingNote = null // Temporary note being created
|
this.creatingNote = null // Temporary note being created
|
||||||
|
this.selectionRect = null // Rectangle for multi-select {startX, startY, endX, endY}
|
||||||
this.isDragging = false
|
this.isDragging = false
|
||||||
|
|
||||||
// Note preview playback state
|
// Note preview playback state
|
||||||
|
|
@ -5387,10 +5412,24 @@ class PianoRollEditor extends Widget {
|
||||||
this.autoScrollEnabled = true // Auto-scroll to follow playhead during playback
|
this.autoScrollEnabled = true // Auto-scroll to follow playhead during playback
|
||||||
this.lastPlayheadTime = 0 // Track last playhead position
|
this.lastPlayheadTime = 0 // Track last playhead position
|
||||||
|
|
||||||
|
// Properties panel state
|
||||||
|
this.propertyInputs = {} // Will hold references to input elements
|
||||||
|
|
||||||
// Start timer to check for note duration expiry
|
// Start timer to check for note duration expiry
|
||||||
this.checkNoteDurationTimer = setInterval(() => this.checkNoteDuration(), 50)
|
this.checkNoteDurationTimer = setInterval(() => this.checkNoteDuration(), 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the dimensions of the piano roll grid area (excluding keyboard)
|
||||||
|
// Note: Properties panel is outside the canvas now, so we don't subtract it here
|
||||||
|
getGridBounds() {
|
||||||
|
return {
|
||||||
|
left: this.keyboardWidth,
|
||||||
|
top: 0,
|
||||||
|
width: this.width - this.keyboardWidth,
|
||||||
|
height: this.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkNoteDuration() {
|
checkNoteDuration() {
|
||||||
if (this.playingNote !== null && this.playingNoteMaxDuration !== null && this.playingNoteStartTime !== null) {
|
if (this.playingNote !== null && this.playingNoteMaxDuration !== null && this.playingNoteStartTime !== null) {
|
||||||
const elapsed = (Date.now() - this.playingNoteStartTime) / 1000
|
const elapsed = (Date.now() - this.playingNoteStartTime) / 1000
|
||||||
|
|
@ -5410,23 +5449,46 @@ class PianoRollEditor extends Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the currently selected MIDI clip from context
|
// Get all MIDI clips and the selected clip from the first MIDI track
|
||||||
getSelectedClip() {
|
getMidiClipsData() {
|
||||||
if (typeof context === 'undefined' || !context.activeObject || !context.activeObject.audioTracks) {
|
if (typeof context === 'undefined' || !context.activeObject || !context.activeObject.audioTracks) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the first MIDI track with a selected clip
|
// Find the first MIDI track
|
||||||
for (const track of context.activeObject.audioTracks) {
|
for (const track of context.activeObject.audioTracks) {
|
||||||
if (track.type === 'midi' && track.clips && track.clips.length > 0) {
|
if (track.type === 'midi' && track.clips && track.clips.length > 0) {
|
||||||
// For now, just return the first clip on the first MIDI track
|
// If no clip is selected, default to the first clip
|
||||||
// TODO: Add proper clip selection mechanism
|
if (this.selectedClipId === null && track.clips.length > 0) {
|
||||||
return { clip: track.clips[0], trackId: track.audioTrackId }
|
this.selectedClipId = track.clips[0].clipId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the selected clip
|
||||||
|
let selectedClip = track.clips.find(c => c.clipId === this.selectedClipId)
|
||||||
|
|
||||||
|
// If selected clip not found (maybe deleted), select first clip
|
||||||
|
if (!selectedClip && track.clips.length > 0) {
|
||||||
|
selectedClip = track.clips[0]
|
||||||
|
this.selectedClipId = selectedClip.clipId
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allClips: track.clips,
|
||||||
|
selectedClip: selectedClip,
|
||||||
|
trackId: track.audioTrackId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the currently selected MIDI clip (for backward compatibility)
|
||||||
|
getSelectedClip() {
|
||||||
|
const data = this.getMidiClipsData()
|
||||||
|
if (!data || !data.selectedClip) return null
|
||||||
|
return { clip: data.selectedClip, trackId: data.trackId }
|
||||||
|
}
|
||||||
|
|
||||||
hitTest(x, y) {
|
hitTest(x, y) {
|
||||||
return x >= 0 && x <= this.width && y >= 0 && y <= this.height
|
return x >= 0 && x <= this.width && y >= 0 && y <= this.height
|
||||||
}
|
}
|
||||||
|
|
@ -5453,7 +5515,22 @@ class PianoRollEditor extends Widget {
|
||||||
return time * this.pixelsPerSecond - this.scrollX + this.keyboardWidth
|
return time * this.pixelsPerSecond - this.scrollX + this.keyboardWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find note at screen position
|
// Find which clip contains the given time
|
||||||
|
findClipAtTime(time) {
|
||||||
|
const clipsData = this.getMidiClipsData()
|
||||||
|
if (!clipsData || !clipsData.allClips) return null
|
||||||
|
|
||||||
|
for (const clip of clipsData.allClips) {
|
||||||
|
const clipStart = clip.startTime || 0
|
||||||
|
const clipEnd = clipStart + (clip.duration || 0)
|
||||||
|
if (time >= clipStart && time <= clipEnd) {
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find note at screen position (only searches selected clip)
|
||||||
findNoteAtPosition(x, y) {
|
findNoteAtPosition(x, y) {
|
||||||
const clipData = this.getSelectedClip()
|
const clipData = this.getSelectedClip()
|
||||||
if (!clipData || !clipData.clip.notes) {
|
if (!clipData || !clipData.clip.notes) {
|
||||||
|
|
@ -5462,12 +5539,14 @@ class PianoRollEditor extends Widget {
|
||||||
|
|
||||||
const note = this.screenToNote(y)
|
const note = this.screenToNote(y)
|
||||||
const time = this.screenToTime(x)
|
const time = this.screenToTime(x)
|
||||||
|
const clipStartTime = clipData.clip.startTime || 0
|
||||||
|
const clipLocalTime = time - clipStartTime
|
||||||
|
|
||||||
// Search in reverse order so we find top-most notes first
|
// Search in reverse order so we find top-most notes first
|
||||||
for (let i = clipData.clip.notes.length - 1; i >= 0; i--) {
|
for (let i = clipData.clip.notes.length - 1; i >= 0; i--) {
|
||||||
const n = clipData.clip.notes[i]
|
const n = clipData.clip.notes[i]
|
||||||
const noteMatches = Math.round(n.note) === Math.round(note)
|
const noteMatches = Math.round(n.note) === Math.round(note)
|
||||||
const timeInRange = time >= n.start_time && time <= (n.start_time + n.duration)
|
const timeInRange = clipLocalTime >= n.start_time && clipLocalTime <= (n.start_time + n.duration)
|
||||||
|
|
||||||
if (noteMatches && timeInRange) {
|
if (noteMatches && timeInRange) {
|
||||||
return i
|
return i
|
||||||
|
|
@ -5485,7 +5564,9 @@ class PianoRollEditor extends Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
const note = clipData.clip.notes[noteIndex]
|
const note = clipData.clip.notes[noteIndex]
|
||||||
const noteEndX = this.timeToScreenX(note.start_time + note.duration)
|
const clipStartTime = clipData.clip.startTime || 0
|
||||||
|
const globalEndTime = clipStartTime + note.start_time + note.duration
|
||||||
|
const noteEndX = this.timeToScreenX(globalEndTime)
|
||||||
|
|
||||||
// Consider clicking within 8 pixels of the right edge as resize
|
// Consider clicking within 8 pixels of the right edge as resize
|
||||||
return Math.abs(x - noteEndX) < 8
|
return Math.abs(x - noteEndX) < 8
|
||||||
|
|
@ -5508,6 +5589,22 @@ class PianoRollEditor extends Widget {
|
||||||
const note = this.screenToNote(y)
|
const note = this.screenToNote(y)
|
||||||
const time = this.screenToTime(x)
|
const time = this.screenToTime(x)
|
||||||
|
|
||||||
|
// Check if clicking on a different clip and switch to it
|
||||||
|
const clickedClip = this.findClipAtTime(time)
|
||||||
|
if (clickedClip && clickedClip.clipId !== this.selectedClipId) {
|
||||||
|
this.selectedClipId = clickedClip.clipId
|
||||||
|
this.selectedNotes.clear()
|
||||||
|
// Redraw to show the new selection
|
||||||
|
if (context.timelineWidget) {
|
||||||
|
context.timelineWidget.requestRedraw()
|
||||||
|
}
|
||||||
|
// Don't start dragging/editing on the same click that switches clips
|
||||||
|
this.isDragging = false
|
||||||
|
this._globalEvents.delete("mousemove")
|
||||||
|
this._globalEvents.delete("mouseup")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if clicking on an existing note
|
// Check if clicking on an existing note
|
||||||
const noteIndex = this.findNoteAtPosition(x, y)
|
const noteIndex = this.findNoteAtPosition(x, y)
|
||||||
|
|
||||||
|
|
@ -5547,21 +5644,28 @@ class PianoRollEditor extends Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clicking on empty space - start creating a new note
|
// Clicking on empty space
|
||||||
|
const isShiftHeld = this.lastClickEvent?.shiftKey || false
|
||||||
|
|
||||||
|
if (isShiftHeld) {
|
||||||
|
// Shift+click: Start creating a new note
|
||||||
this.dragMode = 'create'
|
this.dragMode = 'create'
|
||||||
this.selectedNotes.clear()
|
this.selectedNotes.clear()
|
||||||
|
|
||||||
// Create a temporary note for preview
|
// Create a temporary note for preview (store in clip-local time)
|
||||||
|
const clipData = this.getSelectedClip()
|
||||||
|
const clipStartTime = clipData?.clip?.startTime || 0
|
||||||
|
const clipLocalTime = time - clipStartTime
|
||||||
|
|
||||||
const newNoteValue = Math.round(note)
|
const newNoteValue = Math.round(note)
|
||||||
this.creatingNote = {
|
this.creatingNote = {
|
||||||
note: newNoteValue,
|
note: newNoteValue,
|
||||||
start_time: time,
|
start_time: clipLocalTime,
|
||||||
duration: 0.1, // Minimum duration
|
duration: 0.1, // Minimum duration
|
||||||
velocity: 100
|
velocity: 100
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play preview of the new note
|
// Play preview of the new note
|
||||||
const clipData = this.getSelectedClip()
|
|
||||||
if (clipData) {
|
if (clipData) {
|
||||||
this.playingNote = newNoteValue
|
this.playingNote = newNoteValue
|
||||||
this.playingNoteMaxDuration = null // No max duration for creating notes
|
this.playingNoteMaxDuration = null // No max duration for creating notes
|
||||||
|
|
@ -5573,6 +5677,17 @@ class PianoRollEditor extends Widget {
|
||||||
velocity: 100
|
velocity: 100
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Regular click: Start selection rectangle
|
||||||
|
this.dragMode = 'select'
|
||||||
|
this.selectedNotes.clear()
|
||||||
|
this.selectionRect = {
|
||||||
|
startX: x,
|
||||||
|
startY: y,
|
||||||
|
endX: x,
|
||||||
|
endY: y
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5596,7 +5711,9 @@ class PianoRollEditor extends Widget {
|
||||||
// Extend the note being created
|
// Extend the note being created
|
||||||
if (this.creatingNote) {
|
if (this.creatingNote) {
|
||||||
const currentTime = this.screenToTime(x)
|
const currentTime = this.screenToTime(x)
|
||||||
const duration = Math.max(0.1, currentTime - this.creatingNote.start_time)
|
const clipStartTime = clipData.clip.startTime || 0
|
||||||
|
const clipLocalTime = currentTime - clipStartTime
|
||||||
|
const duration = Math.max(0.1, clipLocalTime - this.creatingNote.start_time)
|
||||||
this.creatingNote.duration = duration
|
this.creatingNote.duration = duration
|
||||||
}
|
}
|
||||||
} else if (this.dragMode === 'move') {
|
} else if (this.dragMode === 'move') {
|
||||||
|
|
@ -5658,7 +5775,9 @@ class PianoRollEditor extends Widget {
|
||||||
if (this.resizingNoteIndex >= 0 && this.resizingNoteIndex < clipData.clip.notes.length) {
|
if (this.resizingNoteIndex >= 0 && this.resizingNoteIndex < clipData.clip.notes.length) {
|
||||||
const note = clipData.clip.notes[this.resizingNoteIndex]
|
const note = clipData.clip.notes[this.resizingNoteIndex]
|
||||||
const currentTime = this.screenToTime(x)
|
const currentTime = this.screenToTime(x)
|
||||||
const newDuration = Math.max(0.1, currentTime - note.start_time)
|
const clipStartTime = clipData.clip.startTime || 0
|
||||||
|
const clipLocalTime = currentTime - clipStartTime
|
||||||
|
const newDuration = Math.max(0.1, clipLocalTime - note.start_time)
|
||||||
note.duration = newDuration
|
note.duration = newDuration
|
||||||
|
|
||||||
// Trigger timeline redraw to show updated notes
|
// Trigger timeline redraw to show updated notes
|
||||||
|
|
@ -5666,6 +5785,15 @@ class PianoRollEditor extends Widget {
|
||||||
context.timelineWidget.requestRedraw()
|
context.timelineWidget.requestRedraw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (this.dragMode === 'select') {
|
||||||
|
// Update selection rectangle
|
||||||
|
if (this.selectionRect) {
|
||||||
|
this.selectionRect.endX = x
|
||||||
|
this.selectionRect.endY = y
|
||||||
|
|
||||||
|
// Update selected notes based on rectangle
|
||||||
|
this.updateSelectionFromRect(clipData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5675,6 +5803,31 @@ class PianoRollEditor extends Widget {
|
||||||
|
|
||||||
const clipData = this.getSelectedClip()
|
const clipData = this.getSelectedClip()
|
||||||
|
|
||||||
|
// Check if this was a simple click (not a drag) on empty space
|
||||||
|
if (this.dragMode === 'select' && this.dragStartX !== undefined && this.dragStartY !== undefined) {
|
||||||
|
const dragDistance = Math.sqrt(
|
||||||
|
Math.pow(x - this.dragStartX, 2) + Math.pow(y - this.dragStartY, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If drag distance is minimal (< 5 pixels), treat it as a click to reposition playhead
|
||||||
|
if (dragDistance < 5) {
|
||||||
|
const time = this.screenToTime(x)
|
||||||
|
|
||||||
|
// Set playhead position
|
||||||
|
if (context.activeObject) {
|
||||||
|
context.activeObject.currentTime = time
|
||||||
|
|
||||||
|
// Request redraws to show the new playhead position
|
||||||
|
if (context.timelineWidget) {
|
||||||
|
context.timelineWidget.requestRedraw()
|
||||||
|
}
|
||||||
|
if (context.pianoRollRedraw) {
|
||||||
|
context.pianoRollRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop playing note
|
// Stop playing note
|
||||||
if (this.playingNote !== null && clipData) {
|
if (this.playingNote !== null && clipData) {
|
||||||
invoke('audio_send_midi_note_off', {
|
invoke('audio_send_midi_note_off', {
|
||||||
|
|
@ -5728,6 +5881,7 @@ class PianoRollEditor extends Widget {
|
||||||
this.isDragging = false
|
this.isDragging = false
|
||||||
this.dragMode = null
|
this.dragMode = null
|
||||||
this.creatingNote = null
|
this.creatingNote = null
|
||||||
|
this.selectionRect = null
|
||||||
this.resizingNoteIndex = -1
|
this.resizingNoteIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5751,6 +5905,76 @@ class PianoRollEditor extends Widget {
|
||||||
this.autoScrollEnabled = false
|
this.autoScrollEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keydown(e) {
|
||||||
|
// Handle delete/backspace to delete selected notes
|
||||||
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
if (this.selectedNotes.size > 0) {
|
||||||
|
const clipData = this.getSelectedClip()
|
||||||
|
if (clipData && clipData.clip && clipData.clip.notes) {
|
||||||
|
// Convert set to sorted array in reverse order to avoid index shifting
|
||||||
|
const indicesToDelete = Array.from(this.selectedNotes).sort((a, b) => b - a)
|
||||||
|
|
||||||
|
for (const index of indicesToDelete) {
|
||||||
|
if (index >= 0 && index < clipData.clip.notes.length) {
|
||||||
|
clipData.clip.notes.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
this.selectedNotes.clear()
|
||||||
|
|
||||||
|
// Sync to backend
|
||||||
|
this.syncNotesToBackend(clipData)
|
||||||
|
|
||||||
|
// Trigger redraws
|
||||||
|
if (context.timelineWidget) {
|
||||||
|
context.timelineWidget.requestRedraw()
|
||||||
|
}
|
||||||
|
if (context.pianoRollRedraw) {
|
||||||
|
context.pianoRollRedraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectionFromRect(clipData) {
|
||||||
|
if (!clipData || !clipData.clip || !clipData.clip.notes || !this.selectionRect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const clipStartTime = clipData.clip.startTime || 0
|
||||||
|
this.selectedNotes.clear()
|
||||||
|
|
||||||
|
// Get rectangle bounds
|
||||||
|
const minX = Math.min(this.selectionRect.startX, this.selectionRect.endX)
|
||||||
|
const maxX = Math.max(this.selectionRect.startX, this.selectionRect.endX)
|
||||||
|
const minY = Math.min(this.selectionRect.startY, this.selectionRect.endY)
|
||||||
|
const maxY = Math.max(this.selectionRect.startY, this.selectionRect.endY)
|
||||||
|
|
||||||
|
// Convert to time/note coordinates
|
||||||
|
const minTime = this.screenToTime(minX)
|
||||||
|
const maxTime = this.screenToTime(maxX)
|
||||||
|
const minNote = this.screenToNote(maxY) // Note: Y is inverted
|
||||||
|
const maxNote = this.screenToNote(minY)
|
||||||
|
|
||||||
|
// Check each note
|
||||||
|
for (let i = 0; i < clipData.clip.notes.length; i++) {
|
||||||
|
const note = clipData.clip.notes[i]
|
||||||
|
const noteGlobalStart = clipStartTime + note.start_time
|
||||||
|
const noteGlobalEnd = noteGlobalStart + note.duration
|
||||||
|
|
||||||
|
// Check if note overlaps with selection rectangle
|
||||||
|
const timeOverlaps = noteGlobalEnd >= minTime && noteGlobalStart <= maxTime
|
||||||
|
const noteOverlaps = note.note >= minNote && note.note <= maxNote
|
||||||
|
|
||||||
|
if (timeOverlaps && noteOverlaps) {
|
||||||
|
this.selectedNotes.add(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
syncNotesToBackend(clipData) {
|
syncNotesToBackend(clipData) {
|
||||||
// Convert notes to backend format: (start_time, note, velocity, duration)
|
// Convert notes to backend format: (start_time, note, velocity, duration)
|
||||||
const notes = clipData.clip.notes.map(n => [
|
const notes = clipData.clip.notes.map(n => [
|
||||||
|
|
@ -5810,14 +6034,124 @@ class PianoRollEditor extends Widget {
|
||||||
// Draw grid
|
// Draw grid
|
||||||
this.drawGrid(ctx, this.width, this.height)
|
this.drawGrid(ctx, this.width, this.height)
|
||||||
|
|
||||||
// Draw notes if we have a selected clip
|
// Draw clip boundaries
|
||||||
const selected = this.getSelectedClip()
|
const clipsData = this.getMidiClipsData()
|
||||||
if (selected && selected.clip && selected.clip.notes) {
|
if (clipsData && clipsData.allClips) {
|
||||||
this.drawNotes(ctx, this.width, this.height, selected.clip)
|
this.drawClipBoundaries(ctx, this.width, this.height, clipsData.allClips)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw notes for all clips in the track
|
||||||
|
if (clipsData && clipsData.allClips) {
|
||||||
|
// Draw non-selected clips first (at lower opacity)
|
||||||
|
for (const clip of clipsData.allClips) {
|
||||||
|
if (clip.clipId !== this.selectedClipId && clip.notes) {
|
||||||
|
this.drawNotes(ctx, this.width, this.height, clip, 0.3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw selected clip on top (at full opacity)
|
||||||
|
if (clipsData.selectedClip && clipsData.selectedClip.notes) {
|
||||||
|
this.drawNotes(ctx, this.width, this.height, clipsData.selectedClip, 1.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw playhead
|
// Draw playhead
|
||||||
this.drawPlayhead(ctx, this.width, this.height)
|
this.drawPlayhead(ctx, this.width, this.height)
|
||||||
|
|
||||||
|
// Draw selection rectangle
|
||||||
|
if (this.selectionRect) {
|
||||||
|
this.drawSelectionRect(ctx, this.width, this.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update HTML properties panel
|
||||||
|
this.updatePropertiesPanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSelectionRect(ctx, width, height) {
|
||||||
|
if (!this.selectionRect) return
|
||||||
|
|
||||||
|
const gridLeft = this.keyboardWidth
|
||||||
|
const minX = Math.max(gridLeft, Math.min(this.selectionRect.startX, this.selectionRect.endX))
|
||||||
|
const maxX = Math.min(width, Math.max(this.selectionRect.startX, this.selectionRect.endX))
|
||||||
|
const minY = Math.max(0, Math.min(this.selectionRect.startY, this.selectionRect.endY))
|
||||||
|
const maxY = Math.min(height, Math.max(this.selectionRect.startY, this.selectionRect.endY))
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
|
||||||
|
// Draw filled rectangle with transparency
|
||||||
|
ctx.fillStyle = 'rgba(100, 150, 255, 0.2)'
|
||||||
|
ctx.fillRect(minX, minY, maxX - minX, maxY - minY)
|
||||||
|
|
||||||
|
// Draw border
|
||||||
|
ctx.strokeStyle = 'rgba(100, 150, 255, 0.6)'
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeRect(minX, minY, maxX - minX, maxY - minY)
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePropertiesPanel() {
|
||||||
|
// Update the HTML properties panel with current selection
|
||||||
|
if (!this.propertiesPanel) return
|
||||||
|
|
||||||
|
const clipData = this.getSelectedClip()
|
||||||
|
const properties = this.getSelectedNoteProperties(clipData)
|
||||||
|
|
||||||
|
// Update pitch (display-only)
|
||||||
|
this.propertiesPanel.pitch.textContent = properties.pitch || '-'
|
||||||
|
|
||||||
|
// Update velocity
|
||||||
|
if (properties.velocity !== null) {
|
||||||
|
this.propertiesPanel.velocity.input.value = properties.velocity
|
||||||
|
this.propertiesPanel.velocity.slider.value = properties.velocity
|
||||||
|
} else {
|
||||||
|
this.propertiesPanel.velocity.input.value = ''
|
||||||
|
this.propertiesPanel.velocity.slider.value = 64 // Default middle value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update modulation
|
||||||
|
if (properties.modulation !== null) {
|
||||||
|
this.propertiesPanel.modulation.input.value = properties.modulation
|
||||||
|
this.propertiesPanel.modulation.slider.value = properties.modulation
|
||||||
|
} else {
|
||||||
|
this.propertiesPanel.modulation.input.value = ''
|
||||||
|
this.propertiesPanel.modulation.slider.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedNoteProperties(clipData) {
|
||||||
|
if (!clipData || !clipData.clip || !clipData.clip.notes || this.selectedNotes.size === 0) {
|
||||||
|
return { pitch: null, velocity: null, modulation: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedIndices = Array.from(this.selectedNotes)
|
||||||
|
const notes = selectedIndices.map(i => clipData.clip.notes[i]).filter(n => n)
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
return { pitch: null, velocity: null, modulation: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all selected notes have the same values
|
||||||
|
const firstNote = notes[0]
|
||||||
|
const allSamePitch = notes.every(n => n.note === firstNote.note)
|
||||||
|
const allSameVelocity = notes.every(n => n.velocity === firstNote.velocity)
|
||||||
|
const allSameModulation = notes.every(n => (n.modulation || 0) === (firstNote.modulation || 0))
|
||||||
|
|
||||||
|
// Convert MIDI note number to name
|
||||||
|
const noteName = allSamePitch ? this.midiNoteToName(firstNote.note) : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
pitch: noteName,
|
||||||
|
velocity: allSameVelocity ? firstNote.velocity : null,
|
||||||
|
modulation: allSameModulation ? (firstNote.modulation || 0) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
midiNoteToName(midiNote) {
|
||||||
|
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||||
|
const octave = Math.floor(midiNote / 12) - 1
|
||||||
|
const noteName = noteNames[midiNote % 12]
|
||||||
|
return `${noteName}${octave} (${midiNote})`
|
||||||
}
|
}
|
||||||
|
|
||||||
drawKeyboard(ctx, width, height) {
|
drawKeyboard(ctx, width, height) {
|
||||||
|
|
@ -5850,17 +6184,19 @@ class PianoRollEditor extends Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawGrid(ctx, width, height) {
|
drawGrid(ctx, width, height) {
|
||||||
const gridLeft = this.keyboardWidth
|
const gridBounds = this.getGridBounds()
|
||||||
const gridWidth = width - gridLeft
|
const gridLeft = gridBounds.left
|
||||||
|
const gridWidth = gridBounds.width
|
||||||
|
const gridHeight = gridBounds.height
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.rect(gridLeft, 0, gridWidth, height)
|
ctx.rect(gridLeft, 0, gridWidth, gridHeight)
|
||||||
ctx.clip()
|
ctx.clip()
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
ctx.fillStyle = backgroundColor
|
ctx.fillStyle = backgroundColor
|
||||||
ctx.fillRect(gridLeft, 0, gridWidth, height)
|
ctx.fillRect(gridLeft, 0, gridWidth, gridHeight)
|
||||||
|
|
||||||
// Draw horizontal lines (note separators)
|
// Draw horizontal lines (note separators)
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'
|
||||||
|
|
@ -5910,7 +6246,7 @@ class PianoRollEditor extends Widget {
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
drawNotes(ctx, width, height, clip) {
|
drawClipBoundaries(ctx, width, height, clips) {
|
||||||
const gridLeft = this.keyboardWidth
|
const gridLeft = this.keyboardWidth
|
||||||
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
@ -5918,13 +6254,73 @@ class PianoRollEditor extends Widget {
|
||||||
ctx.rect(gridLeft, 0, width - gridLeft, height)
|
ctx.rect(gridLeft, 0, width - gridLeft, height)
|
||||||
ctx.clip()
|
ctx.clip()
|
||||||
|
|
||||||
// Draw existing notes
|
// Draw background highlight for selected clip
|
||||||
ctx.fillStyle = '#6fdc6f'
|
const selectedClip = clips.find(c => c.clipId === this.selectedClipId)
|
||||||
|
if (selectedClip) {
|
||||||
|
const clipStart = selectedClip.startTime || 0
|
||||||
|
const clipEnd = clipStart + (selectedClip.duration || 0)
|
||||||
|
const startX = Math.max(gridLeft, this.timeToScreenX(clipStart))
|
||||||
|
const endX = Math.min(width, this.timeToScreenX(clipEnd))
|
||||||
|
|
||||||
|
if (endX > startX) {
|
||||||
|
ctx.fillStyle = 'rgba(111, 220, 111, 0.05)' // Very subtle green tint
|
||||||
|
ctx.fillRect(startX, 0, endX - startX, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw start and end lines for each clip
|
||||||
|
for (const clip of clips) {
|
||||||
|
const clipStart = clip.startTime || 0
|
||||||
|
const clipEnd = clipStart + (clip.duration || 0)
|
||||||
|
const isSelected = clip.clipId === this.selectedClipId
|
||||||
|
|
||||||
|
// Use brighter green for selected clip, dimmer for others
|
||||||
|
const color = isSelected ? 'rgba(111, 220, 111, 0.5)' : 'rgba(111, 220, 111, 0.2)'
|
||||||
|
const lineWidth = isSelected ? 2 : 1
|
||||||
|
|
||||||
|
ctx.strokeStyle = color
|
||||||
|
ctx.lineWidth = lineWidth
|
||||||
|
|
||||||
|
// Draw clip start line
|
||||||
|
const startX = this.timeToScreenX(clipStart)
|
||||||
|
if (startX >= gridLeft && startX <= width) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(startX, 0)
|
||||||
|
ctx.lineTo(startX, height)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw clip end line
|
||||||
|
const endX = this.timeToScreenX(clipEnd)
|
||||||
|
if (endX >= gridLeft && endX <= width) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(endX, 0)
|
||||||
|
ctx.lineTo(endX, height)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawNotes(ctx, width, height, clip, opacity = 1.0) {
|
||||||
|
const gridLeft = this.keyboardWidth
|
||||||
|
const clipStartTime = clip.startTime || 0
|
||||||
|
const isSelectedClip = clip.clipId === this.selectedClipId
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.globalAlpha = opacity
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(gridLeft, 0, width - gridLeft, height)
|
||||||
|
ctx.clip()
|
||||||
|
|
||||||
|
// Draw existing notes at their global timeline position
|
||||||
for (let i = 0; i < clip.notes.length; i++) {
|
for (let i = 0; i < clip.notes.length; i++) {
|
||||||
const note = clip.notes[i]
|
const note = clip.notes[i]
|
||||||
|
|
||||||
const x = this.timeToScreenX(note.start_time)
|
// Convert note time to global timeline time
|
||||||
|
const globalTime = clipStartTime + note.start_time
|
||||||
|
const x = this.timeToScreenX(globalTime)
|
||||||
const y = this.noteToScreenY(note.note)
|
const y = this.noteToScreenY(note.note)
|
||||||
const noteWidth = note.duration * this.pixelsPerSecond
|
const noteWidth = note.duration * this.pixelsPerSecond
|
||||||
const noteHeight = this.noteHeight - 2
|
const noteHeight = this.noteHeight - 2
|
||||||
|
|
@ -5934,11 +6330,24 @@ class PianoRollEditor extends Widget {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Highlight selected notes
|
// Calculate brightness based on velocity (1-127)
|
||||||
if (this.selectedNotes.has(i)) {
|
// Map velocity to brightness range: 0.35 (min) to 1.0 (max)
|
||||||
ctx.fillStyle = '#8ffc8f'
|
const velocity = note.velocity || 100
|
||||||
|
const brightness = 0.35 + (velocity / 127) * 0.65
|
||||||
|
|
||||||
|
// Highlight selected notes (only for selected clip)
|
||||||
|
if (isSelectedClip && this.selectedNotes.has(i)) {
|
||||||
|
// Selected note: brighter green with velocity-based brightness
|
||||||
|
const r = Math.round(143 * brightness)
|
||||||
|
const g = Math.round(252 * brightness)
|
||||||
|
const b = Math.round(143 * brightness)
|
||||||
|
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = '#6fdc6f'
|
// Normal note: velocity-based brightness
|
||||||
|
const r = Math.round(111 * brightness)
|
||||||
|
const g = Math.round(220 * brightness)
|
||||||
|
const b = Math.round(111 * brightness)
|
||||||
|
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillRect(x, y, noteWidth, noteHeight)
|
ctx.fillRect(x, y, noteWidth, noteHeight)
|
||||||
|
|
@ -5948,9 +6357,11 @@ class PianoRollEditor extends Widget {
|
||||||
ctx.strokeRect(x, y, noteWidth, noteHeight)
|
ctx.strokeRect(x, y, noteWidth, noteHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw note being created
|
// Draw note being created (only for selected clip)
|
||||||
if (this.creatingNote) {
|
if (this.creatingNote && isSelectedClip) {
|
||||||
const x = this.timeToScreenX(this.creatingNote.start_time)
|
// Note being created is in clip-local time, convert to global
|
||||||
|
const globalTime = clipStartTime + this.creatingNote.start_time
|
||||||
|
const x = this.timeToScreenX(globalTime)
|
||||||
const y = this.noteToScreenY(this.creatingNote.note)
|
const y = this.noteToScreenY(this.creatingNote.note)
|
||||||
const noteWidth = this.creatingNote.duration * this.pixelsPerSecond
|
const noteWidth = this.creatingNote.duration * this.pixelsPerSecond
|
||||||
const noteHeight = this.noteHeight - 2
|
const noteHeight = this.noteHeight - 2
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue