@stringsync/vexml
Version:
MusicXML to Vexflow
144 lines (143 loc) • 5.18 kB
JavaScript
import * as events from '../events';
import * as util from '../util';
import { Rect, Point } from '../spatial';
import { Scroller } from './scroller';
import { FastCursorFrameLocator } from './fastcursorframelocator';
import { BSearchCursorFrameLocator } from './bsearchcursorframelocator';
import { Duration } from './duration';
// 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.
const CURSOR_WIDTH_PX = 3;
export class Cursor {
path;
locator;
scroller;
topic = new events.Topic();
currentIndex = 0;
currentAlpha = 0; // interpolation factor, ranging from 0 to 1
previousIndex = -1;
previousAlpha = -1;
constructor(path, locator, scroller) {
this.path = path;
this.locator = locator;
this.scroller = scroller;
}
static create(path, scrollContainer) {
const bSearchLocator = new BSearchCursorFrameLocator(path);
const fastLocator = new FastCursorFrameLocator(path, bSearchLocator);
const scroller = new Scroller(scrollContainer);
return new Cursor(path, fastLocator, scroller);
}
getCurrentState() {
return this.getState(this.currentIndex, this.currentAlpha);
}
getPreviousState() {
if (this.previousIndex === -1 || this.previousAlpha === -1) {
return null;
}
return this.getState(this.previousIndex, this.previousAlpha);
}
next() {
if (this.currentIndex === this.path.getFrames().length - 1) {
this.update(this.currentIndex, { alpha: 1 });
}
else {
this.update(this.currentIndex + 1, { alpha: 0 });
}
}
previous() {
this.update(this.currentIndex - 1, { alpha: 0 });
}
goTo(index) {
this.update(index, { alpha: 0 });
}
/** Snaps to the closest sequence entry step. */
snap(timeMs) {
const time = this.normalize(timeMs);
const index = this.locator.locate(time);
util.assertNotNull(index, 'Cursor frame locator failed to find a frame.');
this.update(index, { alpha: 0 });
}
/** Seeks to the exact position, interpolating as needed. */
seek(timestampMs) {
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);
util.assertDefined(entry);
const left = entry.tRange.start;
const right = entry.tRange.end;
const alpha = (time.ms - left.ms) / (right.ms - left.ms);
this.update(index, { alpha });
}
isFullyVisible() {
const cursorRect = this.getCurrentState().rect;
return this.scroller.isFullyVisible(cursorRect);
}
scrollIntoView(behavior = 'auto') {
const scrollPoint = this.getScrollPoint();
this.scroller.scrollTo(scrollPoint, behavior);
}
addEventListener(name, listener, opts) {
const id = this.topic.subscribe(name, listener);
if (opts?.emitBootstrapEvent) {
listener(this.getCurrentState());
}
return id;
}
removeEventListener(...ids) {
for (const id of ids) {
this.topic.unsubscribe(id);
}
}
removeAllEventListeners() {
this.topic.unsubscribeAll();
}
getState(index, alpha) {
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,
};
}
getScrollPoint() {
const cursorRect = this.getCurrentState().rect;
const x = cursorRect.center().x;
const y = cursorRect.y;
return new Point(x, y);
}
normalize(timeMs) {
const ms = util.clamp(0, this.getDuration().ms, timeMs);
return Duration.ms(ms);
}
getDuration() {
return this.path.getFrames().at(-1)?.tRange.end ?? Duration.zero();
}
getCursorRect(frame, alpha) {
const x = frame.xRange.lerp(alpha);
const y = frame.yRange.start;
const w = CURSOR_WIDTH_PX;
const h = frame.yRange.getSize();
return new Rect(x, y, w, h);
}
update(index, { alpha }) {
index = util.clamp(0, this.path.getFrames().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;
if (index !== this.currentIndex || alpha !== this.currentAlpha) {
this.previousIndex = this.currentIndex;
this.previousAlpha = this.currentAlpha;
this.currentIndex = index;
this.currentAlpha = alpha;
this.topic.publish('change', this.getCurrentState());
}
}
}