Skip to content

Commit

Permalink
Initial bookmark point integration
Browse files Browse the repository at this point in the history
 * Create bookmark cursor which is rendered inside the MIDI editor
 * Event handlers for creating, moving, deleting the bookmark
 * Synchronizing within MIDI editor groups, but not between multiple MIDI editor VCs.  TODO: fix that
 * TODO: integrate it with the restart button
  • Loading branch information
Ameobea committed Sep 21, 2024
1 parent 2ee69a9 commit 953ec7c
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 30 deletions.
1 change: 0 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClientProvider as ReactQueryProvider } from 'react-query';
import { Provider } from 'react-redux';
Expand Down
76 changes: 52 additions & 24 deletions src/midiEditor/Cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export class CursorGutter {
private isDragging = false;
private graphics: PIXI.Graphics;

private getPosBeats = (evt: FederatedPointerEvent): number => {
const xPx = evt.getLocalPosition(this.graphics).x - conf.PIANO_KEYBOARD_WIDTH;
return this.app.parentInstance.snapBeat(
Math.max(0, this.app.parentInstance.baseView.scrollHorizontalBeats + this.app.pxToBeats(xPx))
);
};

constructor(app: MIDIEditorUIInstance) {
this.app = app;

Expand All @@ -23,13 +30,7 @@ export class CursorGutter {
return;
}

const xPx = evt.getLocalPosition(g).x - conf.PIANO_KEYBOARD_WIDTH;
const xBeats = this.app.parentInstance.snapBeat(
Math.max(
0,
this.app.parentInstance.baseView.scrollHorizontalBeats + this.app.pxToBeats(xPx)
)
);
const xBeats = this.getPosBeats(evt);
this.app.parentInstance.playbackHandler.setCursorPosBeats(xBeats);
};

Expand All @@ -39,17 +40,18 @@ export class CursorGutter {
}
this.isDragging = true;

const xPx = evt.getLocalPosition(g).x - conf.PIANO_KEYBOARD_WIDTH;
const xBeats = this.app.parentInstance.snapBeat(
this.app.parentInstance.baseView.scrollHorizontalBeats + this.app.pxToBeats(xPx)
);
const xBeats = this.getPosBeats(evt);
this.app.parentInstance.playbackHandler.setCursorPosBeats(xBeats);

this.app.app.stage.on('pointermove', handlePointerMove);
this.app.addMouseUpCB(() => {
this.isDragging = false;
this.app.app.stage.off('pointermove', handlePointerMove);
});
}).on('rightclick', evt => {
const xBeats = this.getPosBeats(evt);
this.app.parentInstance.uiManager.bookmarkPosBeats.set(xBeats);
localStorage.bookmarkPosBeats = xBeats;
});
g.lineStyle(1, conf.LINE_BORDER_COLOR);
g.moveTo(this.app.width, conf.CURSOR_GUTTER_HEIGHT).lineTo(0.5, conf.CURSOR_GUTTER_HEIGHT);
Expand All @@ -70,7 +72,9 @@ export class Cursor {
protected posBeats = 0;
public graphics: PIXI.Graphics;
public dragData: FederatedPointerEvent | null = null;
protected color = conf.CURSOR_COLOR;
protected get color(): number {
return conf.CURSOR_COLOR;
}

public handleDrag(newPos: PIXI.Point) {
const normalizedX = newPos.x - conf.PIANO_KEYBOARD_WIDTH;
Expand Down Expand Up @@ -110,9 +114,12 @@ export class Cursor {
return g;
}

constructor(inst: MIDIEditorUIInstance) {
constructor(inst: MIDIEditorUIInstance, initialPosBeats?: number) {
this.app = inst;
this.graphics = this.buildGraphics();
if (typeof initialPosBeats === 'number') {
this.setPosBeats(initialPosBeats);
}
}

public setPosBeats(posBeats: number) {
Expand Down Expand Up @@ -145,13 +152,8 @@ export class Cursor {
}

export class LoopCursor extends Cursor {
protected color = conf.LOOP_CURSOR_COLOR;

constructor(inst: MIDIEditorUIInstance, loopPoint: number) {
super(inst);
this.graphics.destroy();
this.graphics = this.buildGraphics();
this.setPosBeats(loopPoint);
protected get color(): number {
return conf.LOOP_CURSOR_COLOR;
}

public handleDrag(newPos: PIXI.Point) {
Expand All @@ -166,9 +168,35 @@ export class LoopCursor extends Cursor {
),
0
);
const didUpdate = this.app.parentInstance.setLoopPoint(newPosBeats);
if (!didUpdate) {
return;
}
this.app.parentInstance.setLoopPoint(newPosBeats);
}
}

export class BookmarkCursor extends Cursor {
protected get color(): number {
return conf.BOOKMARK_CURSOR_COLOR;
}

constructor(inst: MIDIEditorUIInstance, initialPosBeats?: number) {
super(inst, initialPosBeats);
this.graphics.on('rightclick', () => {
this.app.parentInstance.uiManager.bookmarkPosBeats.set(null);
delete localStorage.bookmarkPosBeats;
});
}

public handleDrag(newPos: PIXI.Point) {
const normalizedX = newPos.x - conf.PIANO_KEYBOARD_WIDTH;
const newPosBeats = this.app.parentInstance.snapBeat(
Math.max(
this.app.pxToBeats(normalizedX) + this.app.parentInstance.baseView.scrollHorizontalBeats,
0
)
);

this.setPosBeats(newPosBeats);

localStorage.bookmarkPosBeats = newPosBeats;
this.app.parentInstance.uiManager.bookmarkPosBeats.set(newPosBeats);
}
}
42 changes: 40 additions & 2 deletions src/midiEditor/MIDIEditorUIInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
SerializedMIDIEditorInstance,
SerializedMIDILine,
} from 'src/midiEditor';
import { Cursor, CursorGutter, LoopCursor } from 'src/midiEditor/Cursor';
import { BookmarkCursor, Cursor, CursorGutter, LoopCursor } from 'src/midiEditor/Cursor';
import type { ManagedMIDIEditorUIInstance } from 'src/midiEditor/MIDIEditorUIManager';
import MIDINoteBox, {
type NoteDragHandle,
Expand All @@ -30,6 +30,7 @@ import { UnreachableError } from 'src/util';
import type { Unsubscribe } from 'redux';
import { subscribeToConnections, type ConnectionDescriptor } from 'src/redux/modules/vcmUtils';
import { MIDINode, type MIDINodeMetadata } from 'src/patchNetwork/midiNode';
import { get } from 'svelte/store';

export interface Note {
id: number;
Expand Down Expand Up @@ -95,6 +96,8 @@ export default class MIDIEditorUIInstance {
private pianoKeys: PianoKeys | undefined;
private cursorGutter: CursorGutter;
public loopCursor: LoopCursor | null;
private bookmarkCursor: BookmarkCursor | null = null;
private unsubBookmarkPosBeatsChanges: Unsubscribe;
private clipboard: { startPoint: number; length: number; lineIx: number }[] = [];
public noteMetadataByNoteID: Map<number, any> = new Map();
public vcId: string;
Expand Down Expand Up @@ -143,6 +146,11 @@ export default class MIDIEditorUIInstance {
backgroundColor: conf.BACKGROUND_COLOR,
});

this.handleBookmarkPosBeatsChange(get(parentInstance.uiManager.bookmarkPosBeats));
this.unsubBookmarkPosBeatsChanges = parentInstance.uiManager.bookmarkPosBeats.subscribe(
this.handleBookmarkPosBeatsChange
);

registerVcHideCb(this.vcId, this.onHiddenStatusChanged);
this.isHidden = getIsVcHidden(this.vcId);
this.onHiddenStatusChanged(this.isHidden);
Expand Down Expand Up @@ -202,9 +210,29 @@ export default class MIDIEditorUIInstance {
if (this.loopCursor) {
this.app.stage.addChild(this.loopCursor.graphics);
}
if (this.bookmarkCursor) {
this.app.stage.addChild(this.bookmarkCursor.graphics);
}
});
}

private handleBookmarkPosBeatsChange = (newBookmarkPosBeats: number | null) => {
if (typeof newBookmarkPosBeats === 'number') {
if (!this.bookmarkCursor) {
this.bookmarkCursor = new BookmarkCursor(this, newBookmarkPosBeats);
this.app.stage.addChild(this.bookmarkCursor.graphics);
} else {
this.bookmarkCursor.setPosBeats(newBookmarkPosBeats);
}
} else {
if (this.bookmarkCursor) {
this.app.stage.removeChild(this.bookmarkCursor.graphics);
this.bookmarkCursor.destroy();
this.bookmarkCursor = null;
}
}
};

private buildNoteLines = (linesWithIDs: readonly Note[][]): NoteLine[] =>
linesWithIDs.map((notes, lineIx) => new NoteLine(this, notes, lineIx));

Expand Down Expand Up @@ -285,13 +313,21 @@ export default class MIDIEditorUIInstance {
this.cursor = new Cursor(this);
this.app.stage.addChild(this.cursor.graphics);

// need to destroy and re-create since the height is different and the
// graphics are cached
if (this.loopCursor) {
this.app.stage.removeChild(this.loopCursor.graphics);
this.loopCursor.destroy();
const loopPoint = this.loopCursor.getPosBeats();
this.loopCursor = new LoopCursor(this, loopPoint);
this.app.stage.addChild(this.loopCursor.graphics);
}
if (this.bookmarkCursor) {
this.app.stage.removeChild(this.bookmarkCursor.graphics);
this.bookmarkCursor.destroy();
this.bookmarkCursor = null;
this.handleBookmarkPosBeatsChange(get(this.parentInstance.uiManager.bookmarkPosBeats));
}

this.handleViewChange();
}
Expand Down Expand Up @@ -652,7 +688,7 @@ export default class MIDIEditorUIInstance {
}
}

// Step 2: Move all small notes that are < half the beat snap interval where possible, leaving the in
// Step 2: Move all small notes that are < half the beat snap interval where possible, leaving them in
// place in case of conflicts. We do not change their lengths to avoid them collapsing into zero length.
for (const [lineIx, notes] of selectedNotesByLineIx.entries()) {
// Sort the notes to make them in order by start beat
Expand Down Expand Up @@ -1011,6 +1047,7 @@ export default class MIDIEditorUIInstance {
this.lines.forEach(line => line.handleViewChange());
this.cursor.handleViewChange();
this.loopCursor?.handleViewChange();
this.bookmarkCursor?.handleViewChange();
this.pianoKeys?.handleViewChange();
}

Expand Down Expand Up @@ -1192,6 +1229,7 @@ export default class MIDIEditorUIInstance {
document.removeEventListener('mouseup', this.eventHandlerCBs.mouseUp);
document.removeEventListener('wheel', this.eventHandlerCBs.wheel);
this.app.stage.off('pointermove', this.handlePointerMove);
this.unsubBookmarkPosBeatsChanges();
}

public onHiddenStatusChanged = (isHidden: boolean) => {
Expand Down
12 changes: 12 additions & 0 deletions src/midiEditor/MIDIEditorUIManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export class MIDIEditorUIManager {
height: window.innerHeight,
};
public scrollHorizontalPx: Writable<number>;
public bookmarkPosBeats: Writable<number | null> = writable(null);
private silentOutput: GainNode;
private ctx: AudioContext;
private vcId: string;
Expand Down Expand Up @@ -552,6 +553,17 @@ export class MIDIEditorUIManager {
this.silentOutput = new GainNode(ctx);
this.silentOutput.gain.value = 0;

if (localStorage.bookmarkPosBeats) {
try {
this.bookmarkPosBeats.set(parseFloat(localStorage.bookmarkPosBeats));
} catch (_err) {
console.warn(
'Failed to parse `bookmarkPosBeats` from localStorage; found: ',
localStorage.bookmarkPosBeats
);
}
}

const instances = initialState.instances.map(inst => {
if (inst.type === 'midiEditor') {
const instance = new ManagedMIDIEditorUIInstance(
Expand Down
11 changes: 8 additions & 3 deletions src/midiEditor/NoteLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ export default class NoteLine {
this.app.allNotesByID.set(note.id, noteBox);
this.notesByID.set(note.id, noteBox);
});

this.handleViewChange();
if (enableNoteCreation) {
this.installNoteCreationHandlers();
}
this.handleViewChange();
}

private handlePointerMove = (evt: FederatedPointerEvent) => {
Expand Down Expand Up @@ -133,7 +134,7 @@ export default class NoteLine {
};

private installNoteCreationHandlers() {
this.background.on('pointerdown', (evt: FederatedPointerEvent) => {
const handlePointerDown = (evt: FederatedPointerEvent) => {
if (evt.button !== 0 || this.app.selectionBoxButtonDown) {
return;
}
Expand Down Expand Up @@ -165,7 +166,10 @@ export default class NoteLine {
this.noteCreationState = null;
this.app.ungate(this.index);
});
});
};

this.background.on('pointerdown', handlePointerDown);
this.lines?.on('pointerdown', handlePointerDown);

this.app.app.stage.on('pointermove', this.handlePointerMove);
}
Expand Down Expand Up @@ -281,6 +285,7 @@ export default class NoteLine {
}

this.lines = this.buildLines();
this.lines.interactive = true;
this.container.addChild(this.lines);
this.container.setChildIndex(this.lines, 2);
this.lastWidthPx = this.app.width;
Expand Down
1 change: 1 addition & 0 deletions src/midiEditor/conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const CURSOR_CARET_HEIGHT = (Math.sqrt(3) / 2) * CURSOR_CARET_WIDTH;
export const CURSOR_GUTTER_HEIGHT = 18.5;
export const CURSOR_GUTTER_COLOR = 0x070707;
export const LOOP_CURSOR_COLOR = 0xff00ff;
export const BOOKMARK_CURSOR_COLOR = 0x30b9cf;
export const PIANO_KEYBOARD_WIDTH = 79.5;
export const BLACK_NOTE_COLOR = 0x383838;
export const WHITE_NOTE_COLOR = 0xe5e5e5;
Expand Down

0 comments on commit 953ec7c

Please sign in to comment.