Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 49 additions & 9 deletions src/elements/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,33 @@ export class Note {
return this.noteRender.rect;
}

getPitch(): Pitch {
const note = this.document.getNote(this.noteRender.key);
return {
step: note.pitch.step,
octave: note.pitch.octave,
accidentalCode: note.accidental?.code ?? null,
};
/** Returns the subtype of the note. */
getSubtype(): 'note' | 'chord' {
return this.noteRender.subtype;
}

sharesACurveWith(note: Note): boolean {
return this.noteRender.curveIds.some((curveId) => note.noteRender.curveIds.includes(curveId));
/** Returns the pitches of the note. */
getPitches(): Pitch[] {
switch (this.getSubtype()) {
case 'note':
return this.getNotePitches();
case 'chord':
return this.getChordPitches();
}
}

/** Returns whether the note contains an equivalent pitch to another note. */
containsEquivalentPitch(otherNote: Note): boolean {
// Let N be the number of pitches a note has. This algorithm has a time complexity of O(N^2), but N should always
// be small (<10).
return this.getPitches().some((pitch) =>
otherNote.getPitches().some((otherPitch) => this.isPitchEqivalent(pitch, otherPitch))
);
}

/** Returns whether the note is connected to another note via a curve (tie or slur). */
sharesACurveWith(otherNote: Note): boolean {
return this.noteRender.curveIds.some((curveId) => otherNote.noteRender.curveIds.includes(curveId));
}

/** Returns the measure beat that this note starts on. */
Expand All @@ -57,4 +73,28 @@ export class Note {
getAbsoluteMeasureIndex(): number {
return this.document.getAbsoluteMeasureIndex(this.noteRender.key);
}

private getNotePitches(): Pitch[] {
const note = this.document.getNote(this.noteRender.key);
return [
{
step: note.pitch.step,
octave: note.pitch.octave,
accidentalCode: note.accidental?.code ?? null,
},
];
}

private getChordPitches(): Pitch[] {
const chord = this.document.getChord(this.noteRender.key);
return chord.notes.map((note) => ({
step: note.pitch.step,
octave: note.pitch.octave,
accidentalCode: note.accidental?.code ?? null,
}));
}

private isPitchEqivalent(a: Pitch, b: Pitch): boolean {
return a.step === b.step && a.octave === b.octave && a.accidentalCode === b.accidentalCode;
}
}
4 changes: 2 additions & 2 deletions src/elements/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class Score {
const timeline = this.getTimelines().find((timeline) => timeline.getPartIndex() === partIndex);
util.assertDefined(timeline);

const frames = playback.CursorFrame.create(this.log, this, timeline, span);
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span);
const path = new playback.CursorPath(partIndex, frames);
const cursor = playback.Cursor.create(path, this.getScrollContainer());

Expand Down Expand Up @@ -369,7 +369,7 @@ export class Score {
const timeline = timelines.find((timeline) => timeline.getPartIndex() === partIndex);
util.assertDefined(timeline);
const span = { fromPartIndex: partIndex, toPartIndex: partIndex };
const frames = playback.CursorFrame.create(this.log, this, timeline, span);
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span);
const path = new playback.CursorPath(partIndex, frames);
paths.push(path);
}
Expand Down
91 changes: 54 additions & 37 deletions src/playback/cursor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as events from '@/events';
import * as util from '@/util';
import { Rect, Point } from '@/spatial';
import { CursorFrame } from './cursorframe';
import { Scroller } from './scroller';
import { CursorFrameLocator } from './types';
import { CursorFrame, CursorFrameLocator, CursorStateHintProvider } from './types';
import { FastCursorFrameLocator } from './fastcursorframelocator';
import { BSearchCursorFrameLocator } from './bsearchcursorframelocator';
import { Duration } from './duration';
import { CursorPath } from './cursorpath';
import { LazyCursorStateHintProvider } from './lazycursorstatehintprovider';
import { EmptyCursorFrame } from './emptycursorframe';

// NOTE: At 2px and below, there is some antialiasing issues on higher resolutions. The cursor will appear to "pulse" as
// it moves. This will happen even when rounding the position.
Expand All @@ -19,6 +20,7 @@ export type CursorState = {
hasPrevious: boolean;
rect: Rect;
frame: CursorFrame;
hints: CursorStateHintProvider;
};

export type CursorEventMap = {
Expand All @@ -28,11 +30,10 @@ export type CursorEventMap = {
export class Cursor {
private topic = new events.Topic<CursorEventMap>();

private currentIndex = 0;
private currentAlpha = 0; // interpolation factor, ranging from 0 to 1
private index = 0;
private alpha = 0; // interpolation factor, ranging from 0 to 1

private previousIndex = -1;
private previousAlpha = -1;
private previousFrame: CursorFrame = new EmptyCursorFrame();

private constructor(private path: CursorPath, private locator: CursorFrameLocator, private scroller: Scroller) {}

Expand All @@ -43,27 +44,40 @@ export class Cursor {
return new Cursor(path, fastLocator, scroller);
}

getCurrentState(): CursorState {
return this.getState(this.currentIndex, this.currentAlpha);
iterable(): Iterable<CursorState> {
// Clone the cursor to avoid modifying the index of this instance.
const cursor = new Cursor(this.path, this.locator, this.scroller);
return new CursorIterator(cursor);
}

getPreviousState(): CursorState | null {
if (this.previousIndex === -1 || this.previousAlpha === -1) {
return null;
}
return this.getState(this.previousIndex, this.previousAlpha);
getCurrentState(): CursorState {
const index = this.index;
const hasNext = index < this.path.getFrames().length - 1;
const hasPrevious = index > 0;
const frame = this.getCurrentFrame();
const rect = this.getCursorRect(frame, this.alpha);
const hints = new LazyCursorStateHintProvider(frame, this.previousFrame);

return {
index,
hasNext,
hasPrevious,
frame,
rect,
hints,
};
}

next(): void {
if (this.currentIndex === this.path.getFrames().length - 1) {
this.update(this.currentIndex, { alpha: 1 });
if (this.index === this.path.getFrames().length - 1) {
this.update(this.index, { alpha: 1 });
} else {
this.update(this.currentIndex + 1, { alpha: 0 });
this.update(this.index + 1, { alpha: 0 });
}
}

previous(): void {
this.update(this.currentIndex - 1, { alpha: 0 });
this.update(this.index - 1, { alpha: 0 });
}

goTo(index: number): void {
Expand Down Expand Up @@ -125,21 +139,8 @@ export class Cursor {
this.topic.unsubscribeAll();
}

private getState(index: number, alpha: number): CursorState {
const frame = this.path.getFrames().at(index);
util.assertDefined(frame);

const rect = this.getCursorRect(frame, alpha);
const hasNext = index < this.path.getFrames().length - 1;
const hasPrevious = index > 0;

return {
index,
hasNext,
hasPrevious,
rect,
frame,
};
private getCurrentFrame(): CursorFrame {
return this.path.getFrames().at(this.index) ?? new EmptyCursorFrame();
}

private getScrollPoint(): Point {
Expand Down Expand Up @@ -171,12 +172,28 @@ export class Cursor {
alpha = util.clamp(0, 1, alpha);
// Round to 3 decimal places to avoid overloading the event system with redundant updates.
alpha = Math.round(alpha * 1000) / 1000;
if (index !== this.currentIndex || alpha !== this.currentAlpha) {
this.previousIndex = this.currentIndex;
this.previousAlpha = this.currentAlpha;
this.currentIndex = index;
this.currentAlpha = alpha;
if (index !== this.index || alpha !== this.alpha) {
this.previousFrame = this.getCurrentFrame();
this.index = index;
this.alpha = alpha;
this.topic.publish('change', this.getCurrentState());
}
}
}

class CursorIterator implements Iterable<CursorState> {
constructor(private cursor: Cursor) {}

[Symbol.iterator](): Iterator<CursorState> {
return {
next: () => {
const state = this.cursor.getCurrentState();
const done = !state.hasNext;
if (!done) {
this.cursor.next();
}
return { value: state, done };
},
};
}
}
2 changes: 1 addition & 1 deletion src/playback/cursorpath.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CursorFrame } from './cursorframe';
import { CursorFrame } from './types';

/** A collection of cursor frames for a given part index.. */
export class CursorPath {
Expand Down
Loading