@stringsync/vexml
Version:
MusicXML to Vexflow
204 lines (203 loc) • 7.74 kB
JavaScript
"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 };
},
};
}
}