UNPKG

smoosic

Version:

<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i

454 lines (442 loc) 16.1 kB
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic) // Copyright (c) Aaron David Newman 2021. import { smoSerialize } from '../common/serializationHelpers'; import { _MidiWriter } from '../common/midiWriter'; import { dynamicCtorInit } from './dynamicInit'; import { SmoConfiguration, SmoConfigurationParams } from './configuration'; import { SmoScore } from '../smo/data/score'; import { UndoBuffer } from '../smo/xform/undo'; import { XmlToSmo } from '../smo/mxml/xmlToSmo'; import { SuiRenderState } from '../render/sui/renderState'; import { SuiScoreViewOperations } from '../render/sui/scoreViewOperations'; import { SuiOscillator } from '../render/audio/oscillator'; import { SuiSampleMedia } from '../render/audio/samples'; import { SuiTracker } from '../render/sui/tracker'; import { ArialFont } from '../styles/font_metrics/arial_metrics'; import { TimesFont } from '../styles/font_metrics/times_metrics'; import { Commissioner_MediumFont } from '../styles/font_metrics/Commissioner-Medium-Metrics'; import { Concert_OneFont } from '../styles/font_metrics/ConcertOne-Regular'; import { MerriweatherFont } from '../styles/font_metrics/Merriweather-Regular'; import { SourceSansProFont } from '../styles/font_metrics/ssp-sans-metrics'; import { SourceSerifProFont } from '../styles/font_metrics/ssp-serif-metrics'; import { SuiXhrLoader } from '../ui/fileio/xhrLoader'; import { SuiMenuManager } from '../ui/menus/manager'; import { BrowserEventSource } from '../ui/eventSource'; import { SmoTranslationEditor } from '../ui/i18n/translationEditor'; import { SmoTranslator } from '../ui/i18n/language'; import { RibbonButtons } from '../ui/buttons/ribbon'; import { PromiseHelpers } from '../common/promiseHelpers'; import { SuiDom } from './dom'; import { SuiKeyCommands } from './keyCommands'; import { SuiEventHandler } from './eventHandler'; import { KeyBinding, ModalEventHandlerProxy, isTrackerKeyAction, isEditorKeyAction } from './common'; import { SmoMeasure } from '../smo/data/measure'; import { getDomContainer } from '../common/htmlHelpers'; import { SuiHelp } from '../ui/help'; import { VexFlow } from '../common/vex'; import { TextFormatter } from '../common/textformatter'; declare var $: any; export interface pairType { [key: string]: string } /** * Score renderer instance * @internal */ export interface SuiRendererInstance { view: SuiScoreViewOperations; eventSource: BrowserEventSource; undoBuffer: UndoBuffer; renderer: SuiRenderState; } /** * Global instance for debugging * @internal */ export interface SuiInstance { view: SuiScoreViewOperations; eventSource: BrowserEventSource; undoBuffer: UndoBuffer; tracker: SuiTracker; keyCommands: SuiKeyCommands; menus: SuiMenuManager; eventHandler: SuiEventHandler; ribbon: RibbonButtons } const VF = VexFlow; /** * Parse query string for application * @category SuiApplication */ export class QueryParser { pairs: pairType[] = []; queryPair(str: string): pairType { var i = 0; const ar = str.split('='); const rv: pairType = {}; for (i = 0; i < ar.length - 1; i += 2) { const name = decodeURIComponent(ar[i]); rv[name] = decodeURIComponent(ar[i + 1]); } return rv; } constructor() { let i: number = 0; if (window.location.search) { const cmd = window.location.search.substring(1, window.location.search.length); const cmds = cmd.split('&'); for (i = 0; i < cmds.length; ++i) { const cmd = cmds[i]; this.pairs.push(this.queryPair(cmd)); } } } } /** SuiApplication * main entry point of application. Based on the configuration, * either start the default UI, or initialize library mode and * await further instructions. * @category SuiApplication */ export class SuiApplication { scoreLibrary: any; instance: SuiInstance | null = null; config: SmoConfiguration; score: SmoScore | null = null; view: SuiScoreViewOperations | null = null; domElement: HTMLElement; static async configure(params: Partial<SmoConfigurationParams>): Promise<SuiApplication> { const config: SmoConfiguration = new SmoConfiguration(params); (window as any).SmoConfig = config; const application = new SuiApplication(config); SuiApplication.registerFonts(); return application.initialize(); } constructor(config: SmoConfiguration) { this.config = config; this.domElement = this._getDomContainer(); } _getDomContainer(): HTMLElement { const el = getDomContainer(this.config.scoreDomContainer); if (typeof(el) === 'undefined') { throw 'scoreDomContainer is a required config parameter'; } return el; } static instance: SuiInstance; // Init for applications that create a score but don't create the application right away. // we need to create the dynamic constructors static initSync() { dynamicCtorInit(); } /** // Different applications can create their own key bindings, these are the defaults. // Many editor commands can be reached by a single keystroke. For more advanced things there // are menus. */ static get keyBindingDefaults(): KeyBinding[] { var editorKeys = SuiEventHandler.editorKeyBindingDefaults; let unknownKeyAction: boolean = false; editorKeys.forEach((key) => { key.module = 'keyCommands'; if (!isEditorKeyAction(key.action)) { console.error(`unknown key action ${key.action} in configuration`); unknownKeyAction = true; } }); var trackerKeys = SuiEventHandler.trackerKeyBindingDefaults; trackerKeys.forEach((key) => { key.module = 'tracker' if (!isTrackerKeyAction(key.action)) { console.error(`unknown key action ${key.action} in configuration`); unknownKeyAction = true; } }); if (unknownKeyAction) { throw(`unknown key action in configuration`); } return trackerKeys.concat(editorKeys); } /** * Initialize the library according to instruction in config object: * 1. Try to load a new score * 2. If in application mode, start the UI. If in translation mode, start translation * @returns */ initialize(): Promise<SuiApplication> { dynamicCtorInit(); const samplePromise: Promise<any> = SuiSampleMedia.samplePromise(SuiOscillator.audio); const self = this; // Hide header at the top of some applications $('#link-hdr button').off('click').on('click', () => { $('#link-hdr').addClass('hide'); }); const createScore = (): Promise<any> => { return self.createScore(); } const startApplication = () => { if (self.config.mode === 'translate') { self._startApplication(); } else if (self.config.mode === 'application') { self._startApplication(); } else { // library mode. self.createView(self.score!); } } const render = () => { return self.view?.renderer.renderPromise(); } const rv = new Promise<SuiApplication>((resolve: any) => { samplePromise.then(createScore).then(startApplication).then(render) .then( () => { resolve(self); }); }); return rv; } /** * Create the initial score we use to populate the UI etc: * 0. if translation mode, return empty promise, it won't be used anyway * 1. if remoteScore is set in config, try to load from remote * 2. if initialScore is set, use that * 3. if a score is saved locally with quick save (browser local cache), use that * 4. if all else fails, return an 'empty' score. * @returns promise for a remote load. If a local load, will resolve immediately */ async createScore(): Promise<SmoScore | null> { if (this.config.mode === 'translate') { return PromiseHelpers.emptyPromise(); } if (this.config.remoteScore) { const loader = new SuiXhrLoader(this.config.remoteScore); const file = await loader.loadAsync(); this.score = this._tryParse(file as string); return this.score; } else if (this.config.initialScore) { if (typeof(this.config.initialScore) === 'string') { this.score = this._tryParse(this.config.initialScore); return (this.score); } else { this.score = this.config.initialScore; return null; } } else { const localScore = localStorage.getItem(smoSerialize.localScore); if (localScore) { this.score = this._tryParse(localScore); } else { this.score = SmoScore.getDefaultScore(SmoScore.defaults, null); if (this.config.mode === 'application') { SuiHelp.displayHelp(); } } } return this.score; } _tryParse(scoreJson: string) { try { if (scoreJson[0] === '<') { const parser = new DOMParser(); const xml = parser.parseFromString(scoreJson, 'text/xml'); return XmlToSmo.convert(xml); } return SmoScore.deserialize(scoreJson); } catch (exp) { console.warn('could not parse score'); return SmoScore.getDefaultScore(SmoScore.defaults, SmoMeasure.defaults); } } _startApplication() { // Initialize the midi writer library _MidiWriter(); const queryString = new QueryParser(); const languageSelect = queryString.pairs.find((x) => x['language']) ?? {'language': 'en'} if (this.config.mode === 'translate') { this._deferCreateTranslator(); return; } if (languageSelect) { SuiApplication._deferLanguageSelection(languageSelect.language); } this.createUi(); } createView(score: SmoScore): SuiRendererInstance | null { let sdc: HTMLElement = this.domElement; const svgContainer = document.createElement('div'); $(svgContainer).attr('id', 'boo').addClass('musicContainer'); $(sdc).append(svgContainer); const undoBuffer = new UndoBuffer(); const view = new SuiScoreViewOperations(this.config, svgContainer, score, sdc as HTMLElement, undoBuffer); const eventSource = new BrowserEventSource(); eventSource.setRenderElement(svgContainer); this.view = view; view.startRenderingEngine(); return { view, eventSource, undoBuffer, renderer: view.renderer }; } /** * Convenience constructor, take the score and render it in the * configured rendering space. */ createUi() { const viewObj: SuiRendererInstance | null = this.createView(this.score!); if (!viewObj) { return; } const view = this.view!; const tracker = view.tracker; const eventSource = new BrowserEventSource(); // events come from the browser UI. const undoBuffer = viewObj.undoBuffer; const completeNotifier = new ModalEventHandlerProxy(eventSource); const menus = new SuiMenuManager({ view, eventSource, completeNotifier, undoBuffer }); const ribbon = new RibbonButtons({ config: this.config, ribbons: this.config.ribbonLayout, ribbonButtons: this.config.buttonDefinition, menus: menus, completeNotifier, view: view, eventSource: eventSource, tracker: view.tracker }); const keyCommands = new SuiKeyCommands ({ view, slashMode: true, completeNotifier, tracker, eventSource }); const eventHandler = new SuiEventHandler({ view, eventSource, tracker, keyCommands, menus, completeNotifier, keyBindings: SuiApplication.keyBindingDefaults, config: this.config }); this.instance = { view, eventSource, eventHandler, undoBuffer, tracker, ribbon, keyCommands, menus } SuiApplication.instance = this.instance; completeNotifier.handler = eventHandler; eventSource.setRenderElement(view.renderer.elementId); // eslint-disable-next-line SuiApplication.instance = this.instance; ribbon.display(); SuiDom.splash(this.config); } static async loadMusicFont(face: string, url: string) { const new_font = new FontFace('Bravura', `url(${url})`); const loadedFace = await new_font.load(); document.fonts.add(loadedFace); } static async registerFonts() { TextFormatter.registerInfo({ name: ArialFont.name, resolution: ArialFont.resolution, glyphs: ArialFont.glyphs, family: ArialFont.fontFamily, serifs: false, monospaced: false, italic: true, bold: true, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Built-in sans font', }); TextFormatter.registerInfo({ name: TimesFont.name, resolution: TimesFont.resolution, glyphs: TimesFont.glyphs, family: TimesFont.fontFamily, serifs: false, monospaced: false, italic: true, bold: true, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Built-in serif font', }); TextFormatter.registerInfo({ name: Commissioner_MediumFont.name, resolution: Commissioner_MediumFont.resolution, glyphs: Commissioner_MediumFont.glyphs, family: Commissioner_MediumFont.fontFamily, serifs: false, monospaced: false, italic: false, bold: false, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Low-contrast sans-serif text font', }); TextFormatter.registerInfo({ name: Concert_OneFont.name, resolution: Concert_OneFont.resolution, glyphs: Concert_OneFont.glyphs, family: Concert_OneFont.fontFamily, serifs: false, monospaced: false, italic: false, bold: false, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Rounded grotesque typeface inspired by 19th century 3D l', }); TextFormatter.registerInfo({ name: MerriweatherFont.name, resolution: MerriweatherFont.resolution, glyphs: MerriweatherFont.glyphs, family: MerriweatherFont.fontFamily, serifs: true, monospaced: false, italic: false, bold: false, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Serif screen font from Sorkin Type', }); TextFormatter.registerInfo({ name: SourceSansProFont.name, resolution: SourceSansProFont.resolution, glyphs: SourceSansProFont.glyphs, family: SourceSansProFont.fontFamily, serifs: false, monospaced: false, italic: false, bold: false, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Open source Sans screen font from Adobe', }); TextFormatter.registerInfo({ name: SourceSerifProFont.name, resolution: SourceSerifProFont.resolution, glyphs: SourceSerifProFont.glyphs, family: SourceSerifProFont.fontFamily, serifs: false, monospaced: false, italic: false, bold: false, maxSizeGlyph: 'H', superscriptOffset: 0.66, subscriptOffset: 0.66, description: 'Open source Serif screen font from Adobe', }); // await SuiApplication.loadMusicFont('Bravura', '../styles/fonts/Bravura_1.392.woff'); // await SuiApplication.loadMusicFont('Bravura', '../styles/fonts/Bravura_1.392.woff'); } _deferCreateTranslator() { SuiDom.createUiDom(this.config.scoreDomContainer); setTimeout(() => { SmoTranslationEditor.startEditor(this.config.language); }, 1); } static _deferLanguageSelection(lang: string) { setTimeout(() => { SmoTranslator.setLanguage(lang); }, 1); } }