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
18 changes: 12 additions & 6 deletions src/elements/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class Score {
private systems: System[]
) {}

/** Creates a Score from the given rendering context and score render tree. */
static create(
config: Config,
log: Logger,
Expand Down Expand Up @@ -64,6 +65,13 @@ export class Score {
return this.root.getScrollContainer();
}

/**
* Creates and registers a playback cursor scoped to a single part.
*
* @param opts.partIndex The part the cursor advances through. Defaults to the first part.
* @param opts.span The vertical range of parts the cursor visually spans. Defaults to spanning all parts.
* @returns The newly created cursor. The cursor is owned by the Score and will be cleaned up on `destroy()`.
*/
addCursor(opts?: { partIndex?: number; span?: playback.CursorVerticalSpan }): playback.Cursor {
const partCount = this.getPartCount();

Expand All @@ -80,8 +88,7 @@ export class Score {

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

this.cursors.push(cursor);

Expand Down Expand Up @@ -375,7 +382,7 @@ export class Score {

@util.memoize()
private getTimestampLocator(): playback.TimestampLocator {
const paths = new Array<playback.CursorPath>();
const partFrames = new Array<playback.CursorFrame[]>();
const timelines = this.getTimelines();

for (let partIndex = 0; partIndex < this.getPartCount(); partIndex++) {
Expand All @@ -384,10 +391,9 @@ export class Score {
const span = { fromPartIndex: partIndex, toPartIndex: partIndex };
const elementDescriber = playback.ElementDescriber.create(this, { partIndex });
const frames = playback.DefaultCursorFrame.create(this.log, this, timeline, span, elementDescriber);
const path = new playback.CursorPath(partIndex, frames);
paths.push(path);
partFrames.push(frames);
}

return playback.TimestampLocator.create(this, paths);
return playback.TimestampLocator.create(this, partFrames);
}
}
7 changes: 3 additions & 4 deletions src/playback/bsearchcursorframelocator.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import * as util from '@/util';
import { CursorFrameLocator } from './types';
import { CursorFrame, CursorFrameLocator } from './types';
import { Duration } from './duration';
import { CursorPath } from './cursorpath';

/**
* A CursorFrameLocator that uses binary search to locate the frame at a given time.
*/
export class BSearchCursorFrameLocator implements CursorFrameLocator {
constructor(private path: CursorPath) {}
constructor(private frames: CursorFrame[]) {}

locate(time: Duration): number | null {
const frames = this.path.getFrames();
const frames = this.frames;

let left = 0;
let right = frames.length - 1;
Expand Down
49 changes: 36 additions & 13 deletions src/playback/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { CursorFrame, CursorFrameLocator, CursorStateHintProvider } from './type
import { FastCursorFrameLocator } from './fastcursorframelocator';
import { BSearchCursorFrameLocator } from './bsearchcursorframelocator';
import { Duration } from './duration';
import { CursorPath } from './cursorpath';
import { LazyCursorStateHintProvider } from './lazycursorstatehintprovider';
import { EmptyCursorFrame } from './emptycursorframe';
import { ElementDescriber } from './elementdescriber';
Expand Down Expand Up @@ -38,28 +37,34 @@ export class Cursor {
private previousFrame: CursorFrame = new EmptyCursorFrame();

private constructor(
private path: CursorPath,
private frames: CursorFrame[],
private locator: CursorFrameLocator,
private scroller: Scroller,
private elementDescriber: ElementDescriber
) {}

static create(path: CursorPath, scrollContainer: HTMLElement, elementDescriber: ElementDescriber): Cursor {
const bSearchLocator = new BSearchCursorFrameLocator(path);
const fastLocator = new FastCursorFrameLocator(path, bSearchLocator);
/** Creates a Cursor over the given frames, scrolling within the provided container. */
static create(frames: CursorFrame[], scrollContainer: HTMLElement, elementDescriber: ElementDescriber): Cursor {
const bSearchLocator = new BSearchCursorFrameLocator(frames);
const fastLocator = new FastCursorFrameLocator(frames, bSearchLocator);
const scroller = new Scroller(scrollContainer);
return new Cursor(path, fastLocator, scroller, elementDescriber);
return new Cursor(frames, fastLocator, scroller, elementDescriber);
}

/**
* Returns an iterable that walks every frame from the current position to the end. Iteration uses
* an internal clone, so this cursor's position is not modified.
*/
iterable(): Iterable<CursorState> {
// Clone the cursor to avoid modifying the index of this instance.
const cursor = new Cursor(this.path, this.locator, this.scroller, this.elementDescriber);
const cursor = new Cursor(this.frames, this.locator, this.scroller, this.elementDescriber);
return new CursorIterator(cursor);
}

/** Returns the cursor's current state. */
getCurrentState(): CursorState {
const index = this.index;
const hasNext = index < this.path.getFrames().length - 1;
const hasNext = index < this.frames.length - 1;
const hasPrevious = index > 0;
const frame = this.getCurrentFrame();
const rect = this.getCursorRect(frame, this.alpha);
Expand All @@ -76,18 +81,26 @@ export class Cursor {
};
}

/** Returns the underlying frame array backing this cursor. */
getCursorFrames(): CursorFrame[] {
return this.frames;
}

/** Advances to the next frame, or completes the final frame (alpha = 1) if already at the end. */
next(): void {
if (this.index === this.path.getFrames().length - 1) {
if (this.index === this.frames.length - 1) {
this.update(this.index, { alpha: 1 });
} else {
this.update(this.index + 1, { alpha: 0 });
}
}

/** Moves to the previous frame. Clamped at the first frame. */
previous(): void {
this.update(this.index - 1, { alpha: 0 });
}

/** Jumps to the frame at the given frame index. Out-of-range indices are clamped. */
goTo(index: number): void {
this.update(index, { alpha: 0 });
}
Expand All @@ -105,7 +118,7 @@ export class Cursor {
const time = this.normalize(timestampMs);
const index = this.locator.locate(time);
util.assertNotNull(index, 'Cursor frame locator failed to find a frame.');
const entry = this.path.getFrames().at(index);
const entry = this.frames.at(index);
util.assertDefined(entry);

const left = entry.tRange.start;
Expand All @@ -115,16 +128,24 @@ export class Cursor {
this.update(index, { alpha });
}

/** Returns whether the cursor's current rect is fully within the scroll container's viewport. */
isFullyVisible(): boolean {
const cursorRect = this.getCurrentState().rect;
return this.scroller.isFullyVisible(cursorRect);
}

/** Scrolls the container so the cursor is centered horizontally and aligned to the top. */
scrollIntoView(behavior: ScrollBehavior = 'auto'): void {
const scrollPoint = this.getScrollPoint();
this.scroller.scrollTo(scrollPoint, behavior);
}

/**
* Subscribes a listener to a cursor event.
*
* @param opts.emitBootstrapEvent When true, immediately invokes the listener with the current state.
* @returns A subscription id that can be passed to `removeEventListener` to detach.
*/
addEventListener<N extends keyof CursorEventMap>(
name: N,
listener: events.EventListener<CursorEventMap[N]>,
Expand All @@ -137,18 +158,20 @@ export class Cursor {
return id;
}

/** Detaches one or more listeners by their subscription ids. */
removeEventListener(...ids: number[]): void {
for (const id of ids) {
this.topic.unsubscribe(id);
}
}

/** Detaches every listener registered on this cursor. */
removeAllEventListeners(): void {
this.topic.unsubscribeAll();
}

private getCurrentFrame(): CursorFrame {
return this.path.getFrames().at(this.index) ?? new EmptyCursorFrame();
return this.frames.at(this.index) ?? new EmptyCursorFrame();
}

private getScrollPoint(): Point {
Expand All @@ -164,7 +187,7 @@ export class Cursor {
}

private getDuration(): Duration {
return this.path.getFrames().at(-1)?.tRange.end ?? Duration.zero();
return this.frames.at(-1)?.tRange.end ?? Duration.zero();
}

private getCursorRect(frame: CursorFrame, alpha: number): Rect {
Expand All @@ -176,7 +199,7 @@ export class Cursor {
}

private update(index: number, { alpha }: { alpha: number }): void {
index = util.clamp(0, this.path.getFrames().length - 1, index);
index = util.clamp(0, this.frames.length - 1, index);
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;
Expand Down
14 changes: 0 additions & 14 deletions src/playback/cursorpath.ts

This file was deleted.

9 changes: 4 additions & 5 deletions src/playback/fastcursorframelocator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CursorPath } from './cursorpath';
import { Duration } from './duration';
import { CursorFrameLocator } from './types';
import { CursorFrame, CursorFrameLocator } from './types';

/**
* A CursorFrameLocator that uses O(1) time complexity to locate the frame at a given time before falling back to a more
Expand All @@ -9,10 +8,10 @@ import { CursorFrameLocator } from './types';
export class FastCursorFrameLocator implements CursorFrameLocator {
private index = 0;

constructor(private path: CursorPath, private fallback: CursorFrameLocator) {}
constructor(private frames: CursorFrame[], private fallback: CursorFrameLocator) {}

locate(time: Duration): number | null {
const frames = this.path.getFrames();
const frames = this.frames;

if (time.isLessThan(Duration.zero())) {
return this.update(0);
Expand Down Expand Up @@ -53,6 +52,6 @@ export class FastCursorFrameLocator implements CursorFrameLocator {
}

private getDuration(): Duration {
return this.path.getFrames().at(-1)?.tRange.end ?? Duration.zero();
return this.frames.at(-1)?.tRange.end ?? Duration.zero();
}
}
1 change: 0 additions & 1 deletion src/playback/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './cursor';
export * from './cursorpath';
export * from './duration';
export * from './durationrange';
export * from './timestamplocator';
Expand Down
13 changes: 5 additions & 8 deletions src/playback/timestamplocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as spatial from '@/spatial';
import * as elements from '@/elements';
import * as util from '@/util';
import { Duration } from './duration';
import { CursorPath } from './cursorpath';
import { CursorFrame } from './types';

type System = {
Expand All @@ -13,18 +12,16 @@ type System = {
export class TimestampLocator {
private constructor(private systems: System[]) {}

static create(score: elements.Score, paths: CursorPath[]): TimestampLocator {
static create(score: elements.Score, partFrames: CursorFrame[][]): TimestampLocator {
const systems = score.getSystems().map((system) => {
const yRange = new util.NumberRange(system.rect().top(), system.rect().bottom());

const frames = new Array<CursorFrame>();
for (const path of paths) {
for (const framesForPart of partFrames) {
frames.push(
...path
.getFrames()
.filter((frame) =>
frame.getActiveElements().some((element) => element.getSystemIndex() === system.getIndex())
)
...framesForPart.filter((frame) =>
frame.getActiveElements().some((element) => element.getSystemIndex() === system.getIndex())
)
);
}

Expand Down
Loading