UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

144 lines (143 loc) 5.18 kB
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()); } } }