UNPKG

@stringsync/vexml

Version:

MusicXML to Vexflow

204 lines (203 loc) 7.74 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Cursor = void 0; const events = __importStar(require("../events")); const util = __importStar(require("../util")); const spatial_1 = require("../spatial"); const scroller_1 = require("./scroller"); const fastcursorframelocator_1 = require("./fastcursorframelocator"); const bsearchcursorframelocator_1 = require("./bsearchcursorframelocator"); const duration_1 = require("./duration"); const lazycursorstatehintprovider_1 = require("./lazycursorstatehintprovider"); const emptycursorframe_1 = require("./emptycursorframe"); const hintdescriber_1 = require("./hintdescriber"); // 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; class Cursor { path; locator; scroller; elementDescriber; topic = new events.Topic(); index = 0; alpha = 0; // interpolation factor, ranging from 0 to 1 previousFrame = new emptycursorframe_1.EmptyCursorFrame(); constructor(path, locator, scroller, elementDescriber) { this.path = path; this.locator = locator; this.scroller = scroller; this.elementDescriber = elementDescriber; } static create(path, scrollContainer, elementDescriber) { const bSearchLocator = new bsearchcursorframelocator_1.BSearchCursorFrameLocator(path); const fastLocator = new fastcursorframelocator_1.FastCursorFrameLocator(path, bSearchLocator); const scroller = new scroller_1.Scroller(scrollContainer); return new Cursor(path, fastLocator, scroller, elementDescriber); } iterable() { // Clone the cursor to avoid modifying the index of this instance. const cursor = new Cursor(this.path, this.locator, this.scroller, this.elementDescriber); return new CursorIterator(cursor); } getCurrentState() { 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 hintDescriber = new hintdescriber_1.HintDescriber(this.elementDescriber); const hints = new lazycursorstatehintprovider_1.LazyCursorStateHintProvider(frame, this.previousFrame, hintDescriber); return { index, hasNext, hasPrevious, frame, rect, hints, }; } next() { if (this.index === this.path.getFrames().length - 1) { this.update(this.index, { alpha: 1 }); } else { this.update(this.index + 1, { alpha: 0 }); } } previous() { this.update(this.index - 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(); } getCurrentFrame() { return this.path.getFrames().at(this.index) ?? new emptycursorframe_1.EmptyCursorFrame(); } getScrollPoint() { const cursorRect = this.getCurrentState().rect; const x = cursorRect.center().x; const y = cursorRect.y; return new spatial_1.Point(x, y); } normalize(timeMs) { const ms = util.clamp(0, this.getDuration().ms, timeMs); return duration_1.Duration.ms(ms); } getDuration() { return this.path.getFrames().at(-1)?.tRange.end ?? duration_1.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 spatial_1.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.index || alpha !== this.alpha) { this.previousFrame = this.getCurrentFrame(); this.index = index; this.alpha = alpha; this.topic.publish('change', this.getCurrentState()); } } } exports.Cursor = Cursor; class CursorIterator { cursor; constructor(cursor) { this.cursor = cursor; } [Symbol.iterator]() { return { next: () => { const state = this.cursor.getCurrentState(); const done = !state.hasNext; if (!done) { this.cursor.next(); } return { value: state, done }; }, }; } }