From 1f96799a9c4a1d58083560ca3629bbbb2a8ce38f Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sat, 21 Oct 2023 15:01:50 +0200 Subject: [PATCH] Refactoring: cleaning up main.ts file --- src/API.ts | 126 +- src/Documentation.ts | 50 +- src/DomElements.ts | 83 ++ src/EditorSetup.ts | 71 ++ src/Evaluator.ts | 8 +- src/{AppSettings.ts => FileManagement.ts} | 148 ++- src/IO/MidiConnection.ts | 184 ++- src/InterfaceLogic.ts | 434 +++++++ src/KeyActions.ts | 160 +++ src/WindowBehavior.ts | 38 + src/main.ts | 1269 +++------------------ 11 files changed, 1343 insertions(+), 1228 deletions(-) create mode 100644 src/DomElements.ts rename src/{AppSettings.ts => FileManagement.ts} (61%) create mode 100644 src/InterfaceLogic.ts create mode 100644 src/KeyActions.ts create mode 100644 src/WindowBehavior.ts diff --git a/src/API.ts b/src/API.ts index e05e963..1c604b6 100644 --- a/src/API.ts +++ b/src/API.ts @@ -11,7 +11,11 @@ import { SoundEvent } from "./classes/SoundEvent"; import { MidiEvent } from "./classes/MidiEvent"; import { LRUCache } from "lru-cache"; import { InputOptions, Player } from "./classes/ZPlayer"; -import { template_universes } from "./AppSettings"; +import { + loadUniverse, + openUniverseModal, + template_universes, +} from "./FileManagement"; import { samples, initAudioOnFirstClick, @@ -71,8 +75,8 @@ export class UserAPI { } _loadUniverseFromInterface = (universe: string) => { - this.app.loadUniverse(universe as string); - this.app.openBuffersModal(); + loadUniverse(this.app, universe as string); + openUniverseModal(); }; _deleteUniverseFromInterface = (universe: string) => { @@ -141,11 +145,11 @@ export class UserAPI { console.log(error); clearTimeout(this.errorTimeoutID); clearTimeout(this.printTimeoutID); - this.app.error_line.innerHTML = error as string; - this.app.error_line.style.color = "color-red-800"; - this.app.error_line.classList.remove("hidden"); + this.app.interface.error_line.innerHTML = error as string; + this.app.interface.error_line.style.color = "color-red-800"; + this.app.interface.error_line.classList.remove("hidden"); this.errorTimeoutID = setTimeout( - () => this.app.error_line.classList.add("hidden"), + () => this.app.interface.error_line.classList.add("hidden"), 2000 ); }; @@ -154,11 +158,11 @@ export class UserAPI { console.log(message); clearTimeout(this.printTimeoutID); clearTimeout(this.errorTimeoutID); - this.app.error_line.innerHTML = message as string; - this.app.error_line.style.color = "white"; - this.app.error_line.classList.remove("hidden"); + this.app.interface.error_line.innerHTML = message as string; + this.app.interface.error_line.style.color = "white"; + this.app.interface.error_line.classList.remove("hidden"); this.printTimeoutID = setTimeout( - () => this.app.error_line.classList.add("hidden"), + () => this.app.interface.error_line.classList.add("hidden"), 4000 ); }; @@ -591,8 +595,9 @@ export class UserAPI { root: number | string, scale: number | string, channel: number = 0, - port: number | string = (this.MidiConnection.currentOutputIndex || 0), - soundOff: boolean = false): void => { + port: number | string = this.MidiConnection.currentOutputIndex || 0, + soundOff: boolean = false + ): void => { /** * Sends given scale to midi output for visual aid */ @@ -600,47 +605,53 @@ export class UserAPI { this.hide_scale(root, scale, channel, port); const scaleNotes = getAllScaleNotes(scale, root); // Send each scale note to current midi out - scaleNotes.forEach(note => { + scaleNotes.forEach((note) => { this.MidiConnection.sendMidiOn(note, channel, 1, port); if (soundOff) this.MidiConnection.sendAllSoundOff(channel, port); }); this.scale_aid = scale; } - } + }; public hide_scale = ( // @ts-ignore root: number | string = 0, - // @ts-ignore + // @ts-ignore scale: number | string = 0, channel: number = 0, - port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => { + port: number | string = this.MidiConnection.currentOutputIndex || 0 + ): void => { /** * Hides all notes by sending all notes off to midi output */ const allNotes = Array.from(Array(128).keys()); // Send each scale note to current midi out - allNotes.forEach(note => { + allNotes.forEach((note) => { this.MidiConnection.sendMidiOff(note, channel, port); }); this.scale_aid = undefined; + }; - } - - midi_notes_off = (channel: number = 0, port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => { + midi_notes_off = ( + channel: number = 0, + port: number | string = this.MidiConnection.currentOutputIndex || 0 + ): void => { /** * Sends all notes off to midi output */ this.MidiConnection.sendAllNotesOff(channel, port); - } + }; - midi_sound_off = (channel: number = 0, port: number | string = (this.MidiConnection.currentOutputIndex || 0)): void => { + midi_sound_off = ( + channel: number = 0, + port: number | string = this.MidiConnection.currentOutputIndex || 0 + ): void => { /** * Sends all sound off to midi output */ this.MidiConnection.sendAllSoundOff(channel, port); - } + }; // ============================================================= // Ziffers related functions @@ -1255,7 +1266,6 @@ export class UserAPI { denominator = this.meter; - // ============================================================= // Fill // ============================================================= @@ -1276,7 +1286,9 @@ export class UserAPI { const nArray = Array.isArray(n) ? n : [n]; const results: boolean[] = nArray.map( (value) => - (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % Math.floor(value * this.ppqn()) === 0 + (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % + Math.floor(value * this.ppqn()) === + 0 ); return results.some((value) => value === true); }; @@ -1284,17 +1296,19 @@ export class UserAPI { public bar = (n: number | number[] = 1, nudge: number = 0): boolean => { /** - * Determine if the current pulse is on a specified bar, with optional nudge. - * @param n Single bar multiplier or array of bar multipliers - * @param nudge Offset in bars to nudge the bar forward or backward - * @returns True if the current pulse is on one of the specified bars (considering nudge), false otherwise - */ + * Determine if the current pulse is on a specified bar, with optional nudge. + * @param n Single bar multiplier or array of bar multipliers + * @param nudge Offset in bars to nudge the bar forward or backward + * @returns True if the current pulse is on one of the specified bars (considering nudge), false otherwise + */ const nArray = Array.isArray(n) ? n : [n]; const barLength = this.app.clock.time_signature[1] * this.ppqn(); const nudgeInPulses = Math.floor(nudge * barLength); const results: boolean[] = nArray.map( (value) => - (this.app.clock.pulses_since_origin - nudgeInPulses) % Math.floor(value * barLength) === 0 + (this.app.clock.pulses_since_origin - nudgeInPulses) % + Math.floor(value * barLength) === + 0 ); return results.some((value) => value === true); }; @@ -1302,11 +1316,11 @@ export class UserAPI { public pulse = (n: number | number[] = 1, nudge: number = 0): boolean => { /** - * Determine if the current pulse is on a specified pulse count, with optional nudge. - * @param n Single pulse count or array of pulse counts - * @param nudge Offset in pulses to nudge the pulse forward or backward - * @returns True if the current pulse is on one of the specified pulse counts (considering nudge), false otherwise - */ + * Determine if the current pulse is on a specified pulse count, with optional nudge. + * @param n Single pulse count or array of pulse counts + * @param nudge Offset in pulses to nudge the pulse forward or backward + * @returns True if the current pulse is on one of the specified pulse counts (considering nudge), false otherwise + */ const nArray = Array.isArray(n) ? n : [n]; const results: boolean[] = nArray.map( (value) => (this.app.clock.pulses_since_origin - nudge) % value === 0 @@ -1318,11 +1332,10 @@ export class UserAPI { public tick = (tick: number | number[], offset: number = 0): boolean => { const nArray = Array.isArray(tick) ? tick : [tick]; const results: boolean[] = nArray.map( - (value) => (this.app.clock.time_position.pulse === value + offset) + (value) => this.app.clock.time_position.pulse === value + offset ); - return results.some((value) => value === true) - } - + return results.some((value) => value === true); + }; // ============================================================= // Modulo based time filters @@ -1582,7 +1595,8 @@ export class UserAPI { * @returns A sine wave between -1 and 1 */ return ( - (Math.sin(this.app.clock.ctx.currentTime * Math.PI * 2 * freq) + offset) * times + (Math.sin(this.app.clock.ctx.currentTime * Math.PI * 2 * freq) + offset) * + times ); }; @@ -1595,7 +1609,7 @@ export class UserAPI { * @returns A sine wave between 0 and 1 * @see sine */ - return ((this.sine(freq, offset) + 1) / 2) * times + return ((this.sine(freq, offset) + 1) / 2) * times; }; saw = (freq: number = 1, times: number = 1, offset: number = 0): number => { @@ -1610,7 +1624,9 @@ export class UserAPI { * @see sine * @see noise */ - return (((this.app.clock.ctx.currentTime * freq) % 1) * 2 - 1 + offset) * times; + return ( + (((this.app.clock.ctx.currentTime * freq) % 1) * 2 - 1 + offset) * times + ); }; usaw = (freq: number = 1, times: number = 1, offset: number = 0): number => { @@ -1625,7 +1641,11 @@ export class UserAPI { return ((this.saw(freq, offset) + 1) / 2) * times; }; - triangle = (freq: number = 1, times: number = 1, offset: number = 0): number => { + triangle = ( + freq: number = 1, + times: number = 1, + offset: number = 0 + ): number => { /** * Returns a triangle wave between -1 and 1. * @@ -1638,7 +1658,11 @@ export class UserAPI { return (Math.abs(this.saw(freq, offset)) * 2 - 1) * times; }; - utriangle = (freq: number = 1, times: number = 1, offset: number = 0): number => { + utriangle = ( + freq: number = 1, + times: number = 1, + offset: number = 0 + ): number => { /** * Returns a triangle wave between 0 and 1. * @@ -1740,15 +1764,17 @@ export class UserAPI { }; public range = ( - inputY: number, yMin: number, - yMax: number, xMin: number, - xMax: number): number => { + inputY: number, + yMin: number, + yMax: number, + xMin: number, + xMax: number + ): number => { const percent = (inputY - yMin) / (yMax - yMin); const outputX = percent * (xMax - xMin) + xMin; return outputX; }; - limit = (value: number, min: number, max: number): number => { /** * Limits a value between a minimum and a maximum. diff --git a/src/Documentation.ts b/src/Documentation.ts index 6a35fbf..114d9e3 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -20,6 +20,18 @@ import { reference } from "./documentation/reference"; import { synths } from "./documentation/synths"; import { bonus } from "./documentation/bonus"; +// Setting up the Markdown converter with syntax highlighting +import showdown from "showdown"; +import showdownHighlight from "showdown-highlight"; +showdown.setFlavor("github"); +import { classMap } from "./DomElements"; +const bindings = Object.keys(classMap).map((key) => ({ + type: "output", + regex: new RegExp(`<${key}([^>]*)>`, "g"), + //@ts-ignore + replace: (match, p1) => `<${key} class="${classMap[key]}" ${p1}>`, +})); + export const key_shortcut = (shortcut: string): string => { return `${shortcut}`; }; @@ -38,8 +50,8 @@ export const makeExampleFactory = (application: Editor): Function => {
${description} - - + + \`\`\`javascript ${code} @@ -77,3 +89,37 @@ export const documentation_factory = (application: Editor) => { about: about(), }; }; + +export const showDocumentation = (app: Editor) => { + if (document.getElementById("app")?.classList.contains("hidden")) { + document.getElementById("app")?.classList.remove("hidden"); + document.getElementById("documentation")?.classList.add("hidden"); + app.exampleIsPlaying = false; + } else { + document.getElementById("app")?.classList.add("hidden"); + document.getElementById("documentation")?.classList.remove("hidden"); + // Load and convert Markdown content from the documentation file + updateDocumentationContent(app); + } +}; + +export const hideDocumentation = () => { + if (document.getElementById("app")?.classList.contains("hidden")) { + document.getElementById("app")?.classList.remove("hidden"); + document.getElementById("documentation")?.classList.add("hidden"); + } +}; + +export const updateDocumentationContent = (app: Editor) => { + const converter = new showdown.Converter({ + emoji: true, + moreStyling: true, + backslashEscapesHTMLTags: true, + extensions: [showdownHighlight({ auto_detection: true }), ...bindings], + }); + const converted_markdown = converter.makeHtml( + app.docs[app.currentDocumentationPane] + ); + document.getElementById("documentation-content")!.innerHTML = + converted_markdown; +}; diff --git a/src/DomElements.ts b/src/DomElements.ts new file mode 100644 index 0000000..c592c35 --- /dev/null +++ b/src/DomElements.ts @@ -0,0 +1,83 @@ +export type ElementMap = { + [key: string]: + | HTMLElement + | HTMLButtonElement + | HTMLDivElement + | HTMLInputElement + | HTMLSelectElement + | HTMLCanvasElement + | HTMLFormElement; +}; + +export const singleElements = { + topos_logo: "topos-logo", + fill_viewer: "fillviewer", + load_universe_button: "load-universe-button", + download_universe_button: "download-universes", + upload_universe_button: "upload-universes", + destroy_universes_button: "destroy-universes", + documentation_button: "doc-button-1", + eval_button: "eval-button-1", + local_button: "local-button", + global_button: "global-button", + init_button: "init-button", + note_button: "note-button", + settings_button: "settings-button", + close_settings_button: "close-settings-button", + close_universes_button: "close-universes-button", + universe_viewer: "universe-viewer", + buffer_modal: "modal-buffers", + buffer_search: "buffer-search", + universe_creator: "universe-creator", + local_script_tabs: "local-script-tabs", + font_size_input: "font-size-input", + font_family_selector: "font-family", + vim_mode_checkbox: "vim-mode", + line_numbers_checkbox: "show-line-numbers", + time_position_checkbox: "show-time-position", + tips_checkbox: "show-tips", + midi_clock_checkbox: "send-midi-clock", + midi_channels_scripts: "midi-channels-scripts", + midi_clock_ppqn: "midi-clock-ppqn-input", + load_demo_songs: "load-demo-songs", + normal_mode_button: "normal-mode", + vim_mode_button: "vim-mode", + share_button: "share-button", + audio_nudge_range: "audio_nudge", + dough_nudge_range: "dough_nudge", + error_line: "error_line", + hydra_canvas: "hydra-bg", +}; + +export const buttonGroups = { + play_buttons: ["play-button-1"], + stop_buttons: ["stop-button-1"], + clear_buttons: ["clear-button-1"], +}; + +export const classMap = { + h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", + h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", + h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-700 rounded-lg py-2 px-2 lg:mt-16", + ul: "text-underline pl-6", + li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal", + p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal", + warning: + "animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal", + a: "lg:text-2xl text-base text-orange-300", + code: "lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden", + icode: + "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600", + ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600", + blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4", + details: + "lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600", + summary: "font-semibold text-xl", + table: + "justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse", + thead: + "text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400", + th: "", + td: "", + tr: "", +}; diff --git a/src/EditorSetup.ts b/src/EditorSetup.ts index b2a50ba..5eae43e 100644 --- a/src/EditorSetup.ts +++ b/src/EditorSetup.ts @@ -1,5 +1,9 @@ +import { Prec } from "@codemirror/state"; +import { indentWithTab } from "@codemirror/commands"; import { keymap, + ViewUpdate, + lineNumbers, highlightSpecialChars, drawSelection, highlightActiveLine, @@ -9,6 +13,7 @@ import { highlightActiveLineGutter, } from "@codemirror/view"; import { Extension, EditorState } from "@codemirror/state"; +import { vim } from "@replit/codemirror-vim"; import { defaultHighlightStyle, syntaxHighlighting, @@ -23,6 +28,12 @@ import { closeBracketsKeymap, } from "@codemirror/autocomplete"; import { lintKeymap } from "@codemirror/lint"; +import { Compartment } from "@codemirror/state"; +import { Editor } from "./main"; +import { EditorView } from "codemirror"; +import { toposTheme } from "./themes/toposTheme"; +import { javascript } from "@codemirror/lang-javascript"; +import { inlineHoveringTips } from "./documentation/inlineHelp"; export const editorSetup: Extension = (() => [ highlightActiveLineGutter(), @@ -47,3 +58,63 @@ export const editorSetup: Extension = (() => [ ...lintKeymap, ]), ])(); + +export const installEditor = (app: Editor) => { + app.vimModeCompartment = new Compartment(); + app.hoveringCompartment = new Compartment(); + app.withLineNumbers = new Compartment(); + app.chosenLanguage = new Compartment(); + app.fontSize = new Compartment(); + const vimPlugin = app.settings.vimMode ? vim() : []; + const lines = app.settings.line_numbers ? lineNumbers() : []; + + const fontModif = EditorView.theme({ + "&": { + fontSize: `${app.settings.font_size}px`, + }, + $content: { + fontFamily: `${app.settings.font}, Menlo, Monaco, Lucida Console, monospace`, + fontSize: `${app.settings.font_size}px`, + }, + ".cm-gutters": { + fontSize: `${app.settings.font_size}px`, + }, + }); + + app.editorExtensions = [ + app.vimModeCompartment.of(vimPlugin), + app.withLineNumbers.of(lines), + app.fontSize.of(fontModif), + app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []), + editorSetup, + toposTheme, + app.chosenLanguage.of(javascript()), + EditorView.updateListener.of((v: ViewUpdate) => { + v; + }), + ]; + app.dynamicPlugins = new Compartment(); + app.state = EditorState.create({ + extensions: [ + ...app.editorExtensions, + EditorView.lineWrapping, + app.dynamicPlugins.of(app.userPlugins), + Prec.highest( + keymap.of([ + { + key: "Ctrl-Enter", + run: () => { + return true; + }, + }, + ]) + ), + keymap.of([indentWithTab]), + ], + doc: app.universes[app.selected_universe].global.candidate, + }); + app.view = new EditorView({ + parent: document.getElementById("editor") as HTMLElement, + state: app.state, + }); +}; diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 460474f..096b16b 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -1,5 +1,5 @@ import type { Editor } from "./main"; -import type { File } from "./AppSettings"; +import type { File } from "./FileManagement"; const delay = (ms: number) => new Promise((_, reject) => @@ -24,7 +24,7 @@ const tryCatchWrapper = ( ).call(application.api); resolve(true); } catch (error) { - application.error_line.innerHTML = error as string; + application.interface.error_line.innerHTML = error as string; console.log(error); resolve(false); } @@ -74,7 +74,7 @@ export const tryEvaluate = async ( } } } catch (error) { - application.error_line.innerHTML = error as string; + application.interface.error_line.innerHTML = error as string; console.log(error); } }; @@ -91,7 +91,7 @@ export const evaluate = async ( ]); if (code.evaluations) code.evaluations++; } catch (error) { - application.error_line.innerHTML = error as string; + application.interface.error_line.innerHTML = error as string; console.log(error); } }; diff --git a/src/AppSettings.ts b/src/FileManagement.ts similarity index 61% rename from src/AppSettings.ts rename to src/FileManagement.ts index c40c628..0b88229 100644 --- a/src/AppSettings.ts +++ b/src/FileManagement.ts @@ -1,5 +1,9 @@ import { tutorial_universe } from "./universes/tutorial"; - +import { gzipSync, decompressSync, strFromU8 } from "fflate"; +import { examples } from "./examples/excerpts"; +import { type Editor } from "./main"; +import { uniqueNamesGenerator, colors, animals } from "unique-names-generator"; +import { tryEvaluate } from "./Evaluator"; export type Universes = { [key: string]: Universe }; export interface Universe { @@ -62,9 +66,9 @@ export interface Settings { tips: boolean; send_clock: boolean; midi_channels_scripts: boolean; - midi_clock_input: string|undefined; + midi_clock_input: string | undefined; midi_clock_ppqn: number; - default_midi_input: string|undefined; + default_midi_input: string | undefined; } export const template_universe = { @@ -139,8 +143,8 @@ export class AppSettings { public tips: boolean = true; public send_clock: boolean = false; public midi_channels_scripts: boolean = true; - public midi_clock_input: string|undefined = undefined; - public default_midi_input: string|undefined = undefined; + public midi_clock_input: string | undefined = undefined; + public default_midi_input: string | undefined = undefined; public midi_clock_ppqn: number = 24; public load_demo_songs: boolean = true; @@ -225,3 +229,137 @@ export class AppSettings { localStorage.setItem("topos", JSON.stringify(this.data)); } } + +export const initializeSelectedUniverse = (app: Editor): void => { + /** + * Initializes the selected universe. If there is no selected universe, it + * will create a new one. If there is a selected universe, it will load it. + * + * @param app - The main application + * @returns void + */ + if (app.settings.load_demo_songs) { + let random_example = examples[Math.floor(Math.random() * examples.length)]; + app.selected_universe = "Welcome"; + app.universes[app.selected_universe].global.committed = random_example; + app.universes[app.selected_universe].global.candidate = random_example; + } else { + app.selected_universe = app.settings.selected_universe; + if (app.universes[app.selected_universe] === undefined) + app.universes[app.selected_universe] = structuredClone(template_universe); + } + app.interface.universe_viewer.innerHTML = `Topos: ${app.selected_universe}`; +}; + +export const emptyUrl = () => { + window.history.replaceState({}, document.title, "/"); +}; + +export const share = async (app: Editor) => { + async function bufferToBase64(buffer: Uint8Array) { + const base64url: string = await new Promise((r) => { + const reader = new FileReader(); + reader.onload = () => r(reader.result as string); + reader.readAsDataURL(new Blob([buffer])); + }); + return base64url.slice(base64url.indexOf(",") + 1); + } + let data = JSON.stringify({ + universe: app.settings.universes[app.selected_universe], + }); + let encoded_data = gzipSync(new TextEncoder().encode(data), { level: 9 }); + const hashed_table = await bufferToBase64(encoded_data); + const url = new URL(window.location.href); + url.searchParams.set("universe", hashed_table); + window.history.replaceState({}, "", url.toString()); + // Copy the text inside the text field + navigator.clipboard.writeText(url.toString()); +}; + +export const loadUniverserFromUrl = (app: Editor): void => { + /** + * Loads a universe from the URL bar. + * @param app - The main application + * @returns void + */ + // Loading from URL bar + let url = new URLSearchParams(window.location.search); + if (url !== undefined) { + let new_universe; + if (url !== null) { + const universeParam = url.get("universe"); + if (universeParam !== null) { + let data = Uint8Array.from(atob(universeParam), (c) => c.charCodeAt(0)); + new_universe = JSON.parse(strFromU8(decompressSync(data))); + const randomName: string = uniqueNamesGenerator({ + length: 2, + separator: "_", + dictionaries: [colors, animals], + }); + loadUniverse(app, randomName, new_universe["universe"]); + emptyUrl(); + } + } + } +}; + +export const loadUniverse = ( + app: Editor, + universeName: string, + universe: Universe = template_universe +): void => { + console.log(universeName, universe); + app.currentFile().candidate = app.view.state.doc.toString(); + + // Getting the new universe name and moving on + let selectedUniverse = universeName.trim(); + if (app.universes[selectedUniverse] === undefined) { + app.settings.universes[selectedUniverse] = universe; + app.universes[selectedUniverse] = universe; + } + app.selected_universe = selectedUniverse; + app.settings.selected_universe = app.selected_universe; + app.interface.universe_viewer.innerHTML = `Topos: ${selectedUniverse}`; + + // Updating the editor View to reflect the selected universe + app.updateEditorView(); + + // Evaluating the initialisation script for the selected universe + tryEvaluate(app, app.universes[app.selected_universe.toString()].init); +}; + +export const openUniverseModal = (): void => { + // If the modal is hidden, unhide it and hide the editor + if ( + document.getElementById("modal-buffers")!.classList.contains("invisible") + ) { + document.getElementById("editor")!.classList.add("invisible"); + document.getElementById("modal-buffers")!.classList.remove("invisible"); + document.getElementById("buffer-search")!.focus(); + } else { + closeUniverseModal(); + } +}; + +export const closeUniverseModal = (): void => { + // @ts-ignore + document.getElementById("buffer-search")!.value = ""; + document.getElementById("editor")!.classList.remove("invisible"); + document.getElementById("modal-buffers")!.classList.add("invisible"); +}; + +export const openSettingsModal = (): void => { + if ( + document.getElementById("modal-settings")!.classList.contains("invisible") + ) { + document.getElementById("editor")!.classList.add("invisible"); + document.getElementById("modal-settings")!.classList.remove("invisible"); + } else { + closeSettingsModal(); + } +}; + +export const closeSettingsModal = (): void => { + document.getElementById("editor")!.classList.remove("invisible"); + document.getElementById("modal-settings")!.classList.add("invisible"); +}; diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index fa281b1..71bb238 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,19 +1,19 @@ import { UserAPI } from "../API"; -import { AppSettings } from "../AppSettings"; +import { AppSettings } from "../FileManagement"; export type MidiNoteEvent = { note: number; velocity: number; channel: number; timestamp: number; -} +}; export type MidiCCEvent = { control: number; value: number; channel: number; timestamp: number; -} +}; export class MidiConnection { /** @@ -45,7 +45,8 @@ export class MidiConnection { public lastNote: MidiNoteEvent | undefined = undefined; public lastCC: { [control: number]: number } = {}; public lastNoteInChannel: { [channel: number]: MidiNoteEvent } = {}; - public lastCCInChannel: { [channel: number]: { [control: number]: number } } = {}; + public lastCCInChannel: { [channel: number]: { [control: number]: number } } = + {}; /* MIDI clock stuff */ private midiClockInputIndex: number | undefined = undefined; @@ -173,8 +174,12 @@ export class MidiConnection { * Updates the MIDI clock input select element with the available MIDI inputs. */ if (this.midiInputs.length > 0) { - const midiClockSelect = document.getElementById("midi-clock-input") as HTMLSelectElement; - const midiInputSelect = document.getElementById("default-midi-input") as HTMLSelectElement; + const midiClockSelect = document.getElementById( + "midi-clock-input" + ) as HTMLSelectElement; + const midiInputSelect = document.getElementById( + "default-midi-input" + ) as HTMLSelectElement; midiClockSelect.innerHTML = ""; midiInputSelect.innerHTML = ""; @@ -201,7 +206,9 @@ export class MidiConnection { }); if (this.settings.midi_clock_input) { - const clockMidiInputIndex = this.getMidiInputIndex(this.settings.midi_clock_input); + const clockMidiInputIndex = this.getMidiInputIndex( + this.settings.midi_clock_input + ); midiClockSelect.value = clockMidiInputIndex.toString(); if (clockMidiInputIndex > 0) { this.midiClockInput = this.midiInputs[clockMidiInputIndex]; @@ -212,7 +219,9 @@ export class MidiConnection { } if (this.settings.default_midi_input) { - const defaultMidiInputIndex = this.getMidiInputIndex(this.settings.default_midi_input); + const defaultMidiInputIndex = this.getMidiInputIndex( + this.settings.default_midi_input + ); midiInputSelect.value = defaultMidiInputIndex.toString(); if (defaultMidiInputIndex > 0) { this.currentInputIndex = defaultMidiInputIndex; @@ -226,16 +235,25 @@ export class MidiConnection { midiClockSelect.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; if (value === "-1") { - if (this.midiClockInput && this.midiClockInputIndex != this.currentInputIndex) this.midiClockInput.onmidimessage = null; + if ( + this.midiClockInput && + this.midiClockInputIndex != this.currentInputIndex + ) + this.midiClockInput.onmidimessage = null; this.midiClockInput = undefined; this.settings.midi_clock_input = undefined; } else { const clockInputIndex = parseInt(value); this.midiClockInputIndex = clockInputIndex; - if (this.midiClockInput && this.midiClockInputIndex != this.currentInputIndex) this.midiClockInput.onmidimessage = null; + if ( + this.midiClockInput && + this.midiClockInputIndex != this.currentInputIndex + ) + this.midiClockInput.onmidimessage = null; this.midiClockInput = this.midiInputs[clockInputIndex]; this.registerMidiInputListener(clockInputIndex); - this.settings.midi_clock_input = this.midiClockInput.name || undefined; + this.settings.midi_clock_input = + this.midiClockInput.name || undefined; } }); @@ -243,17 +261,25 @@ export class MidiConnection { midiInputSelect.addEventListener("change", (event) => { const value = (event.target as HTMLSelectElement).value; if (value === "-1") { - if (this.currentInputIndex && this.currentInputIndex != this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex); + if ( + this.currentInputIndex && + this.currentInputIndex != this.midiClockInputIndex + ) + this.unregisterMidiInputListener(this.currentInputIndex); this.currentInputIndex = undefined; this.settings.default_midi_input = undefined; } else { - if (this.currentInputIndex && this.currentInputIndex != this.midiClockInputIndex) this.unregisterMidiInputListener(this.currentInputIndex); + if ( + this.currentInputIndex && + this.currentInputIndex != this.midiClockInputIndex + ) + this.unregisterMidiInputListener(this.currentInputIndex); this.currentInputIndex = parseInt(value); this.registerMidiInputListener(this.currentInputIndex); - this.settings.default_midi_input = this.midiInputs[this.currentInputIndex].name || undefined; + this.settings.default_midi_input = + this.midiInputs[this.currentInputIndex].name || undefined; } }); - } } @@ -290,36 +316,60 @@ export class MidiConnection { } /* DEFAULT MIDI INPUT */ if (input.name === this.settings.default_midi_input) { - // If message is one of note ons - if (message.data[0] >= 0x90 && message.data[0] <= 0x9F) { + if (message.data[0] >= 0x90 && message.data[0] <= 0x9f) { const channel = message.data[0] - 0x90 + 1; const note = message.data[1]; const velocity = message.data[2]; - this.lastNote = { note, velocity, channel, timestamp: event.timeStamp }; - this.lastNoteInChannel[channel] = { note, velocity, channel, timestamp: event.timeStamp }; + this.lastNote = { + note, + velocity, + channel, + timestamp: event.timeStamp, + }; + this.lastNoteInChannel[channel] = { + note, + velocity, + channel, + timestamp: event.timeStamp, + }; if (this.settings.midi_channels_scripts) this.api.script(channel); - this.pushToMidiInputBuffer({ note, velocity, channel, timestamp: event.timeStamp }); - this.activeNotes.push({ note, velocity, channel, timestamp: event.timeStamp }); + this.pushToMidiInputBuffer({ + note, + velocity, + channel, + timestamp: event.timeStamp, + }); + this.activeNotes.push({ + note, + velocity, + channel, + timestamp: event.timeStamp, + }); const sticky = this.removeFromStickyNotes(note, channel); - if (!sticky) this.stickyNotes.push({ note, velocity, channel, timestamp: event.timeStamp }); + if (!sticky) + this.stickyNotes.push({ + note, + velocity, + channel, + timestamp: event.timeStamp, + }); } // If note off - if (message.data[0] >= 0x80 && message.data[0] <= 0x8F) { + if (message.data[0] >= 0x80 && message.data[0] <= 0x8f) { const channel = message.data[0] - 0x80 + 1; const note = message.data[1]; this.removeFromActiveNotes(note, channel); } // If message is one of CCs - if (message.data[0] >= 0xB0 && message.data[0] <= 0xBF) { - - const channel = message.data[0] - 0xB0 + 1; + if (message.data[0] >= 0xb0 && message.data[0] <= 0xbf) { + const channel = message.data[0] - 0xb0 + 1; const control = message.data[1]; const value = message.data[2]; @@ -333,13 +383,15 @@ export class MidiConnection { //console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`); - this.pushToMidiCCBuffer({ control, value, channel, timestamp: event.timeStamp }); - + this.pushToMidiCCBuffer({ + control, + value, + channel, + timestamp: event.timeStamp, + }); } - - } - } + }; } } } @@ -347,16 +399,22 @@ export class MidiConnection { /* Methods for handling active midi notes */ public removeFromActiveNotes(note: number, channel: number): void { - const index = this.activeNotes.findIndex((e) => e.note === note && e.channel === channel); + const index = this.activeNotes.findIndex( + (e) => e.note === note && e.channel === channel + ); if (index >= 0) this.activeNotes.splice(index, 1); } public removeFromStickyNotes(note: number, channel: number): boolean { - const index = this.stickyNotes.findIndex((e) => e.note === note && e.channel === channel); + const index = this.stickyNotes.findIndex( + (e) => e.note === note && e.channel === channel + ); if (index >= 0) { this.stickyNotes.splice(index, 1); return true; - } else { return false; } + } else { + return false; + } } public stickyNotesFromChannel(channel: number): MidiNoteEvent[] { @@ -425,7 +483,6 @@ export class MidiConnection { } } - public onMidiClock(timestamp: number): void { /** * Called when a MIDI clock message is received. @@ -434,7 +491,6 @@ export class MidiConnection { this.clockTicks += 1; if (this.lastTimestamp > 0) { - if (this.lastTimestamp === timestamp) { // This is error handling for odd MIDI clock messages with the same timestamp this.clockErrorCount += 1; @@ -452,12 +508,13 @@ export class MidiConnection { this.skipOnError = this.settings.midi_clock_ppqn / 4; timestamp = 0; // timestamp 0 == lastTimestamp 0 } else { - this.midiClockDelta = timestamp - this.lastTimestamp; - this.lastBPM = 60 * (1000 / this.midiClockDelta / this.settings.midi_clock_ppqn); + this.lastBPM = + 60 * (1000 / this.midiClockDelta / this.settings.midi_clock_ppqn); this.clockBuffer.push(this.lastBPM); - if (this.clockBuffer.length > this.clockBufferLength) this.clockBuffer.shift(); + if (this.clockBuffer.length > this.clockBufferLength) + this.clockBuffer.shift(); const estimatedBPM = this.estimatedBPM(); if (estimatedBPM !== this.roundedBPM) { @@ -465,13 +522,11 @@ export class MidiConnection { this.api.bpm(estimatedBPM); this.roundedBPM = estimatedBPM; } - } } } this.lastTimestamp = timestamp; - } public estimatedBPM(): number { @@ -523,7 +578,8 @@ export class MidiConnection { if (typeof output === "number") { if (output < 0 || output >= this.midiOutputs.length) { console.error( - `Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1 + `Invalid MIDI output index. Index must be in the range 0-${ + this.midiOutputs.length - 1 }.` ); return this.currentOutputIndex; @@ -552,7 +608,8 @@ export class MidiConnection { if (typeof input === "number") { if (input < 0 || input >= this.midiInputs.length) { console.error( - `Invalid MIDI input index. Index must be in the range 0-${this.midiInputs.length - 1 + `Invalid MIDI input index. Index must be in the range 0-${ + this.midiInputs.length - 1 }.` ); return -1; @@ -625,26 +682,35 @@ export class MidiConnection { } } - public sendMidiOn(note: number, channel: number, velocity: number, port: number | string = this.currentOutputIndex) { - /** + public sendMidiOn( + note: number, + channel: number, + velocity: number, + port: number | string = this.currentOutputIndex + ) { + /** * Sending Midi Note on message */ - if(typeof port === "string") port = this.getMidiOutputIndex(port); + if (typeof port === "string") port = this.getMidiOutputIndex(port); const output = this.midiOutputs[port]; note = Math.min(Math.max(note, 0), 127); if (output) { const noteOnMessage = [0x90 + channel, note, velocity]; output.send(noteOnMessage); - } else { + } else { console.error("MIDI output not available."); } } - sendMidiOff(note: number, channel: number, port: number | string = this.currentOutputIndex) { + sendMidiOff( + note: number, + channel: number, + port: number | string = this.currentOutputIndex + ) { /** - * Sending Midi Note off message + * Sending Midi Note off message */ - if(typeof port === "string") port = this.getMidiOutputIndex(port); + if (typeof port === "string") port = this.getMidiOutputIndex(port); const output = this.midiOutputs[port]; note = Math.min(Math.max(note, 0), 127); if (output) { @@ -655,11 +721,14 @@ export class MidiConnection { } } - sendAllNotesOff(channel: number, port: number | string = this.currentOutputIndex) { + sendAllNotesOff( + channel: number, + port: number | string = this.currentOutputIndex + ) { /** - * Sending Midi Note off message + * Sending Midi Note off message */ - if(typeof port === "string") port = this.getMidiOutputIndex(port); + if (typeof port === "string") port = this.getMidiOutputIndex(port); const output = this.midiOutputs[port]; if (output) { const noteOffMessage = [0xb0 + channel, 123, 0]; @@ -669,11 +738,14 @@ export class MidiConnection { } } - sendAllSoundOff(channel: number, port: number | string = this.currentOutputIndex) { + sendAllSoundOff( + channel: number, + port: number | string = this.currentOutputIndex + ) { /** - * Sending all sound off + * Sending all sound off */ - if(typeof port === "string") port = this.getMidiOutputIndex(port); + if (typeof port === "string") port = this.getMidiOutputIndex(port); const output = this.midiOutputs[port]; if (output) { const noteOffMessage = [0xb0 + channel, 120, 0]; diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts new file mode 100644 index 0000000..9b726fe --- /dev/null +++ b/src/InterfaceLogic.ts @@ -0,0 +1,434 @@ +import { EditorView } from "codemirror"; +import { vim } from "@replit/codemirror-vim"; +import { type Editor } from "./main"; +import { + documentation_factory, + hideDocumentation, + showDocumentation, + updateDocumentationContent, +} from "./Documentation"; +import { + type Universe, + template_universe, + template_universes, + loadUniverse, + emptyUrl, + share, + closeUniverseModal, + openUniverseModal, +} from "./FileManagement"; +import { loadSamples } from "./API"; +import { tryEvaluate } from "./Evaluator"; +import { inlineHoveringTips } from "./documentation/inlineHelp"; +import { lineNumbers } from "@codemirror/view"; + +export const installInterfaceLogic = (app: Editor) => { + (app.interface.line_numbers_checkbox as HTMLInputElement).checked = + app.settings.line_numbers; + (app.interface.time_position_checkbox as HTMLInputElement).checked = + app.settings.time_position; + (app.interface.tips_checkbox as HTMLInputElement).checked = app.settings.tips; + (app.interface.midi_clock_checkbox as HTMLInputElement).checked = + app.settings.send_clock; + (app.interface.midi_channels_scripts as HTMLInputElement).checked = + app.settings.midi_channels_scripts; + (app.interface.midi_clock_ppqn as HTMLInputElement).value = + app.settings.midi_clock_ppqn.toString(); + if (!app.settings.time_position) { + (app.interface.timeviewer as HTMLElement).classList.add("hidden"); + } + (app.interface.load_demo_songs as HTMLInputElement).checked = + app.settings.load_demo_songs; + + const tabs = document.querySelectorAll('[id^="tab-"]'); + // Iterate over the tabs with an index + for (let i = 0; i < tabs.length; i++) { + tabs[i].addEventListener("click", (event) => { + // Updating the CSS accordingly + tabs[i].classList.add("bg-orange-300"); + for (let j = 0; j < tabs.length; j++) { + if (j != i) tabs[j].classList.remove("bg-orange-300"); + } + app.currentFile().candidate = app.view.state.doc.toString(); + + let tab = event.target as HTMLElement; + let tab_id = tab.id.split("-")[1]; + app.local_index = parseInt(tab_id); + app.updateEditorView(); + }); + } + + app.interface.topos_logo.addEventListener("click", () => { + hideDocumentation(); + app.updateKnownUniversesView(); + openUniverseModal(); + }); + + app.buttonElements.play_buttons.forEach((button) => { + button.addEventListener("click", () => { + if (app.isPlaying) { + app.setButtonHighlighting("pause", true); + app.isPlaying = !app.isPlaying; + app.clock.pause(); + app.api.MidiConnection.sendStopMessage(); + } else { + app.setButtonHighlighting("play", true); + app.isPlaying = !app.isPlaying; + app.clock.start(); + app.api.MidiConnection.sendStartMessage(); + } + }); + }); + + app.buttonElements.clear_buttons.forEach((button) => { + button.addEventListener("click", () => { + app.setButtonHighlighting("clear", true); + if (confirm("Do you want to reset the current universe?")) { + app.universes[app.selected_universe] = + structuredClone(template_universe); + app.updateEditorView(); + } + }); + }); + + app.interface.documentation_button.addEventListener("click", () => { + showDocumentation(app); + }); + + app.interface.destroy_universes_button.addEventListener("click", () => { + if (confirm("Do you want to destroy all universes?")) { + app.universes = { + ...template_universes, + }; + app.updateKnownUniversesView(); + } + }); + + app.interface.audio_nudge_range.addEventListener("input", () => { + app.clock.nudge = parseInt( + (app.interface.audio_nudge_range as HTMLInputElement).value + ); + }); + + app.interface.dough_nudge_range.addEventListener("input", () => { + app.dough_nudge = parseInt( + (app.interface.dough_nudge_range as HTMLInputElement).value + ); + }); + + app.interface.upload_universe_button.addEventListener("click", () => { + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = ".json"; + + fileInput.addEventListener("change", (event) => { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.readAsText(file, "UTF-8"); + + reader.onload = (evt) => { + const data = JSON.parse(evt.target!.result as string); + for (const [key, value] of Object.entries(data)) { + app.universes[key] = value as Universe; + } + }; + reader.onerror = (evt) => { + console.error("An error occurred reading the file:", evt); + }; + } + }); + + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + }); + + app.interface.download_universe_button.addEventListener("click", () => { + // Trigger save of the universe before downloading + app.settings.saveApplicationToLocalStorage(app.universes, app.settings); + + // Generate a file name based on timestamp + let fileName = `topos-universes-${Date.now()}.json`; + + // Create Blob and Object URL + const blob = new Blob([JSON.stringify(app.settings.universes)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + + // Create a temporary anchor and trigger download + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + + // Revoke the Object URL to free resources + URL.revokeObjectURL(url); + }); + + app.interface.load_universe_button.addEventListener("click", () => { + let query = (app.interface.buffer_search as HTMLInputElement).value; + if (query.length > 2 && query.length < 20 && !query.includes(" ")) { + loadUniverse(app, query); + app.settings.selected_universe = query; + (app.interface.buffer_search as HTMLInputElement).value = ""; + closeUniverseModal(); + app.view.focus(); + emptyUrl(); + } + }); + + app.interface.eval_button.addEventListener("click", () => { + app.currentFile().candidate = app.view.state.doc.toString(); + app.flashBackground("#404040", 200); + }); + + app.buttonElements.stop_buttons.forEach((button) => { + button.addEventListener("click", () => { + app.setButtonHighlighting("stop", true); + app.isPlaying = false; + app.clock.stop(); + }); + }); + + app.interface.local_button.addEventListener("click", () => + app.changeModeFromInterface("local") + ); + app.interface.global_button.addEventListener("click", () => + app.changeModeFromInterface("global") + ); + app.interface.init_button.addEventListener("click", () => + app.changeModeFromInterface("init") + ); + app.interface.note_button.addEventListener("click", () => + app.changeModeFromInterface("notes") + ); + + app.interface.font_family_selector.addEventListener("change", () => { + //@ts-ignore + let new_font = (app.interface.font_family_selector as HTMLSelectElement) + .value; + }); + + app.interface.font_size_input.addEventListener("input", () => { + let new_value: string | number = ( + app.interface.font_size_input as HTMLInputElement + ).value; + app.settings.font_size = parseInt(new_value); + }); + + app.interface.settings_button.addEventListener("click", () => { + // Populate the font family selector + const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement; + doughNudgeRange.value = app.dough_nudge.toString(); + // @ts-ignore + const doughNumber = document.getElementById( + "doughnumber" + ) as HTMLInputElement; + doughNumber.value = app.dough_nudge.toString(); + (app.interface.font_family_selector as HTMLSelectElement).value = + app.settings.font; + + if (app.settings.font_size === null) { + app.settings.font_size = 12; + } + const fontSizeInput = app.interface.font_size_input as HTMLInputElement; + fontSizeInput.value = app.settings.font_size.toString(); + + // Get the right value to update graphical widgets + const lineNumbersCheckbox = app.interface + .line_numbers_checkbox as HTMLInputElement; + lineNumbersCheckbox.checked = app.settings.line_numbers; + const timePositionCheckbox = app.interface + .time_position_checkbox as HTMLInputElement; + timePositionCheckbox.checked = app.settings.time_position; + const tipsCheckbox = app.interface.tips_checkbox as HTMLInputElement; + tipsCheckbox.checked = app.settings.tips; + const midiClockCheckbox = app.interface + .midi_clock_checkbox as HTMLInputElement; + midiClockCheckbox.checked = app.settings.send_clock; + const midiChannelsScripts = app.interface + .midi_channels_scripts as HTMLInputElement; + midiChannelsScripts.checked = app.settings.midi_channels_scripts; + const midiClockPpqn = app.interface.midi_clock_ppqn as HTMLInputElement; + midiClockPpqn.value = app.settings.midi_clock_ppqn.toString(); + const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement; + loadDemoSongs.checked = app.settings.load_demo_songs; + const vimModeCheckbox = app.interface.vim_mode_checkbox as HTMLInputElement; + vimModeCheckbox.checked = app.settings.vimMode; + + let modal_settings = document.getElementById("modal-settings"); + let editor = document.getElementById("editor"); + modal_settings?.classList.remove("invisible"); + + editor?.classList.add("invisible"); + }); + + app.interface.close_settings_button.addEventListener("click", () => { + let modal_settings = document.getElementById("modal-settings"); + let editor = document.getElementById("editor"); + modal_settings?.classList.add("invisible"); + editor?.classList.remove("invisible"); + // Update the font size once again + app.view.dispatch({ + effects: app.fontSize.reconfigure( + EditorView.theme({ + "&": { fontSize: app.settings.font_size + "px" }, + "&content": { + fontFamily: app.settings.font, + fontSize: app.settings.font_size + "px", + }, + ".cm-gutters": { fontSize: app.settings.font_size + "px" }, + }) + ), + }); + }); + + app.interface.close_universes_button.addEventListener("click", () => { + openUniverseModal(); + }); + + app.interface.share_button.addEventListener("click", async () => { + // trigger a manual save + app.currentFile().candidate = app.view.state.doc.toString(); + app.currentFile().committed = app.view.state.doc.toString(); + app.settings.saveApplicationToLocalStorage(app.universes, app.settings); + // encode as a blob! + await share(app); + }); + + app.interface.vim_mode_checkbox.addEventListener("change", () => { + let checked = (app.interface.vim_mode_checkbox as HTMLInputElement).checked + ? true + : false; + app.settings.vimMode = checked; + app.view.dispatch({ + effects: app.vimModeCompartment.reconfigure(checked ? vim() : []), + }); + }); + + app.interface.line_numbers_checkbox.addEventListener("change", () => { + let lineNumbersCheckbox = app.interface + .line_numbers_checkbox as HTMLInputElement; + let checked = lineNumbersCheckbox.checked ? true : false; + app.settings.line_numbers = checked; + app.view.dispatch({ + effects: app.withLineNumbers.reconfigure(checked ? [lineNumbers()] : []), + }); + }); + + app.interface.time_position_checkbox.addEventListener("change", () => { + let timeviewer = document.getElementById("timeviewer") as HTMLElement; + let checked = (app.interface.time_position_checkbox as HTMLInputElement) + .checked + ? true + : false; + app.settings.time_position = checked; + checked + ? timeviewer.classList.remove("hidden") + : timeviewer.classList.add("hidden"); + }); + + app.interface.tips_checkbox.addEventListener("change", () => { + let checked = (app.interface.tips_checkbox as HTMLInputElement).checked + ? true + : false; + app.settings.tips = checked; + app.view.dispatch({ + effects: app.hoveringCompartment.reconfigure( + checked ? inlineHoveringTips : [] + ), + }); + }); + + app.interface.midi_clock_checkbox.addEventListener("change", () => { + let checked = (app.interface.midi_clock_checkbox as HTMLInputElement) + .checked + ? true + : false; + app.settings.send_clock = checked; + }); + + app.interface.midi_channels_scripts.addEventListener("change", () => { + let checked = (app.interface.midi_channels_scripts as HTMLInputElement) + .checked + ? true + : false; + app.settings.midi_channels_scripts = checked; + }); + + app.interface.midi_clock_ppqn.addEventListener("change", () => { + let value = parseInt( + (app.interface.midi_clock_ppqn as HTMLInputElement).value + ); + app.settings.midi_clock_ppqn = value; + }); + + app.interface.load_demo_songs.addEventListener("change", () => { + let checked = (app.interface.load_demo_songs as HTMLInputElement).checked + ? true + : false; + app.settings.load_demo_songs = checked; + }); + + app.interface.universe_creator.addEventListener("submit", (event) => { + event.preventDefault(); + + let data = new FormData(app.interface.universe_creator as HTMLFormElement); + let universeName = data.get("universe") as string | null; + + if (universeName) { + if (universeName.length > 2 && universeName.length < 20) { + loadUniverse(app, universeName); + app.settings.selected_universe = universeName; + (app.interface.buffer_search as HTMLInputElement).value = ""; + closeUniverseModal(); + app.view.focus(); + } + } + }); + + tryEvaluate(app, app.universes[app.selected_universe.toString()].init); + + [ + "introduction", + "interface", + "interaction", + "code", + "time", + "sound", + "samples", + "synths", + "chaining", + "patterns", + "ziffers", + "midi", + "functions", + "lfos", + "probabilities", + "variables", + // "reference", + "shortcuts", + "about", + "bonus", + ].forEach((e) => { + let name = `docs_` + e; + document.getElementById(name)!.addEventListener("click", async () => { + if (name !== "docs_samples") { + app.currentDocumentationPane = e; + updateDocumentationContent(app); + } else { + console.log("Loading samples!"); + await loadSamples().then(() => { + app.docs = documentation_factory(app); + app.currentDocumentationPane = e; + updateDocumentationContent(app); + }); + } + }); + }); +}; diff --git a/src/KeyActions.ts b/src/KeyActions.ts new file mode 100644 index 0000000..893d403 --- /dev/null +++ b/src/KeyActions.ts @@ -0,0 +1,160 @@ +import { type Editor } from "./main"; +import { vim } from "@replit/codemirror-vim"; +import { tryEvaluate } from "./Evaluator"; +import { hideDocumentation, showDocumentation } from "./Documentation"; +import { openSettingsModal, openUniverseModal } from "./FileManagement"; + +export const registerFillKeys = (app: Editor) => { + document.addEventListener("keydown", (event) => { + if (event.altKey) { + app.fill = true; + app.interface.fill_viewer.classList.remove("invisible"); + } + }); + + document.addEventListener("keyup", (event) => { + if (event.key === "Alt") { + app.fill = false; + app.interface.fill_viewer.classList.add("invisible"); + } + }); +}; + +export const registerOnKeyDown = (app: Editor) => { + window.addEventListener("keydown", (event) => { + if (event.key === "Tab") { + event.preventDefault(); + } + + if (event.ctrlKey && event.key === "s") { + event.preventDefault(); + app.setButtonHighlighting("stop", true); + app.clock.stop(); + } + + if (event.ctrlKey && event.key === "p") { + event.preventDefault(); + if (app.isPlaying) { + app.isPlaying = false; + app.setButtonHighlighting("pause", true); + app.clock.pause(); + } else { + app.isPlaying = true; + app.setButtonHighlighting("play", true); + app.clock.start(); + } + } + + // Ctrl + Shift + V: Vim Mode + if ( + (event.key === "v" || event.key === "V") && + event.ctrlKey && + event.shiftKey + ) { + app.settings.vimMode = !app.settings.vimMode; + event.preventDefault(); + app.userPlugins = app.settings.vimMode ? [] : [vim()]; + app.view.dispatch({ + effects: app.dynamicPlugins.reconfigure(app.userPlugins), + }); + } + + // Ctrl + Enter or Return: Evaluate + if ((event.key === "Enter" || event.key === "Return") && event.ctrlKey) { + event.preventDefault(); + app.currentFile().candidate = app.view.state.doc.toString(); + app.flashBackground("#404040", 200); + } + + // Evaluate (bis) + if ( + (event.key === "Enter" && event.shiftKey) || + (event.key === "e" && event.ctrlKey) + ) { + event.preventDefault(); // Prevents the addition of a new line + app.currentFile().candidate = app.view.state.doc.toString(); + app.flashBackground("#404040", 200); + } + + // Force evaluation + if (event.key === "Enter" && event.shiftKey && event.ctrlKey) { + event.preventDefault(); + app.currentFile().candidate = app.view.state.doc.toString(); + tryEvaluate(app, app.currentFile()); + app.flashBackground("#404040", 200); + } + + // app is the modal to switch between universes + if (event.ctrlKey && event.key === "b") { + event.preventDefault(); + hideDocumentation(); + app.updateKnownUniversesView(); + openUniverseModal(); + } + + // app is the modal that opens up the settings + if (event.shiftKey && event.key === "Escape") { + openSettingsModal(); + } + + if (event.ctrlKey && event.key === "l") { + event.preventDefault(); + app.changeModeFromInterface("local"); + hideDocumentation(); + app.view.focus(); + } + + if (event.ctrlKey && event.key === "n") { + event.preventDefault(); + app.changeModeFromInterface("notes"); + hideDocumentation(); + app.view.focus(); + } + + if (event.ctrlKey && event.key === "g") { + event.preventDefault(); + app.changeModeFromInterface("global"); + hideDocumentation(); + app.view.focus(); + } + + if (event.ctrlKey && event.key === "i") { + event.preventDefault(); + app.changeModeFromInterface("init"); + hideDocumentation(); + app.changeToLocalBuffer(0); + app.view.focus(); + } + + if (event.ctrlKey && event.key === "d") { + event.preventDefault(); + showDocumentation(app); + } + + [112, 113, 114, 115, 116, 117, 118, 119, 120].forEach((keycode, index) => { + if (event.keyCode === keycode) { + event.preventDefault(); + if (event.ctrlKey) { + event.preventDefault(); + app.api.script(keycode - 111); + } else { + event.preventDefault(); + app.changeModeFromInterface("local"); + app.changeToLocalBuffer(index); + hideDocumentation(); + } + } + }); + + if (event.keyCode == 121) { + event.preventDefault(); + app.changeModeFromInterface("global"); + hideDocumentation(); + } + if (event.keyCode == 122) { + event.preventDefault(); + app.changeModeFromInterface("init"); + hideDocumentation(); + } + }); +}; diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts new file mode 100644 index 0000000..fe3a6bd --- /dev/null +++ b/src/WindowBehavior.ts @@ -0,0 +1,38 @@ +import { type Editor } from "./main"; + +export const installWindowBehaviors = ( + app: Editor, + window: Window, + preventMultipleTabs: boolean = false +) => { + window.addEventListener("beforeunload", () => { + // @ts-ignore + event.preventDefault(); + // Iterate over all local files and set the candidate to the committed + app.currentFile().candidate = app.view.state.doc.toString(); + app.currentFile().committed = app.view.state.doc.toString(); + app.settings.saveApplicationToLocalStorage(app.universes, app.settings); + app.clock.stop(); + return null; + }); + + if (preventMultipleTabs) { + localStorage.openpages = Date.now(); + window.addEventListener( + "storage", + function (e) { + if (e.key == "openpages") { + // Listen if anybody else is opening the same page! + localStorage.page_available = Date.now(); + } + if (e.key == "page_available") { + document.getElementById("all")!.classList.add("invisible"); + alert( + "Topos is already opened in another tab. Close this tab now to prevent data loss." + ); + } + }, + false + ); + } +}; diff --git a/src/main.ts b/src/main.ts index a638010..b67211a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,317 +1,84 @@ -import { uniqueNamesGenerator, colors, animals } from "unique-names-generator"; -import { examples } from "./examples/excerpts"; import { EditorState, Compartment } from "@codemirror/state"; -import { ViewUpdate, lineNumbers, keymap } from "@codemirror/view"; import { javascript } from "@codemirror/lang-javascript"; -import { inlineHoveringTips } from "./documentation/inlineHelp"; -import { toposTheme } from "./themes/toposTheme"; import { markdown } from "@codemirror/lang-markdown"; -import { Extension, Prec } from "@codemirror/state"; -import { indentWithTab } from "@codemirror/commands"; -import { vim } from "@replit/codemirror-vim"; -import { AppSettings, Universe } from "./AppSettings"; -import { editorSetup } from "./EditorSetup"; +import { Extension } from "@codemirror/state"; +import { + initializeSelectedUniverse, + AppSettings, + Universe, + loadUniverserFromUrl, +} from "./FileManagement"; +import { singleElements, buttonGroups, ElementMap } from "./DomElements"; +import { registerFillKeys, registerOnKeyDown } from "./KeyActions"; +import { installEditor } from "./EditorSetup"; import { documentation_factory } from "./Documentation"; import { EditorView } from "codemirror"; import { Clock } from "./Clock"; import { loadSamples, UserAPI } from "./API"; import { makeArrayExtensions } from "./ArrayExtensions"; import "./style.css"; -import { - Universes, - File, - template_universe, - template_universes, -} from "./AppSettings"; +import { Universes, File, template_universes } from "./FileManagement"; import { tryEvaluate } from "./Evaluator"; // @ts-ignore -import { gzipSync, decompressSync, strFromU8 } from "fflate"; - -// Importing showdown and setting up the markdown converter import showdown from "showdown"; -showdown.setFlavor("github"); -import showdownHighlight from "showdown-highlight"; import { makeStringExtensions } from "./StringExtensions"; - -// Broadcast that you're opening a page. -// localStorage.openpages = Date.now(); -// window.addEventListener( -// "storage", -// function (e) { -// if (e.key == "openpages") { -// // Listen if anybody else is opening the same page! -// localStorage.page_available = Date.now(); -// } -// if (e.key == "page_available") { -// document.getElementById("all")!.classList.add("invisible"); -// alert( -// "Topos is already opened in another tab. Close this tab now to prevent data loss." -// ); -// } -// }, -// false -// ); - -const classMap = { - h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", - h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", - h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-700 rounded-lg py-2 px-2 lg:mt-16", - ul: "text-underline pl-6", - li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal", - p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal", - warning: - "animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal", - a: "lg:text-2xl text-base text-orange-300", - code: "lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden", - icode: - "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600", - ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600", - - blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4", - details: - "lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600", - summary: "font-semibold text-xl", - table: - "justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse", - thead: - "text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400", - th: "", - td: "", - tr: "", -}; -const bindings = Object.keys(classMap).map((key) => ({ - type: "output", - regex: new RegExp(`<${key}([^>]*)>`, "g"), - //@ts-ignore - replace: (match, p1) => `<${key} class="${classMap[key]}" ${p1}>`, -})); +import { installInterfaceLogic } from "./InterfaceLogic"; +import { installWindowBehaviors } from "./WindowBehavior"; export class Editor { + // Universes and settings + settings: AppSettings = new AppSettings(); universes: Universes = template_universes; + selected_universe: string = "Welcome"; + fill: boolean = false; - selected_universe: string; local_index: number = 1; + + // Editor logic editor_mode: "global" | "local" | "init" | "notes" = "global"; - fontSize: Compartment; - withLineNumbers: Compartment; - vimModeCompartment: Compartment; - hoveringCompartment: Compartment; - chosenLanguage: Compartment; + fontSize!: Compartment; + withLineNumbers!: Compartment; + vimModeCompartment!: Compartment; + hoveringCompartment!: Compartment; + chosenLanguage!: Compartment; + dynamicPlugins!: Compartment; currentDocumentationPane: string = "introduction"; exampleCounter: number = 0; exampleIsPlaying: boolean = false; - - settings: AppSettings = new AppSettings(); editorExtensions: Extension[] = []; userPlugins: Extension[] = []; - state: EditorState; - api: UserAPI; + state!: EditorState; + view!: EditorView; selectedExample: string | null = ""; docs: { [key: string]: string } = {}; + public _mouseX: number = 0; + public _mouseY: number = 0; + show_error: boolean = false; + buttonElements: Record = {}; + interface: ElementMap = {}; + + // UserAPI + api: UserAPI; // Audio stuff audioContext: AudioContext; - dough_nudge: number = 20; - view: EditorView; clock: Clock; + dough_nudge: number = 20; manualPlay: boolean = false; isPlaying: boolean = false; - // Mouse position - public _mouseX: number = 0; - public _mouseY: number = 0; - - // Topos Logo - topos_logo: HTMLElement = document.getElementById( - "topos-logo" - ) as HTMLElement; - - // Fillviewer - fill_viewer: HTMLElement = document.getElementById( - "fillviewer" - ) as HTMLElement; - - // Transport elements - play_buttons: HTMLButtonElement[] = [ - document.getElementById("play-button-1") as HTMLButtonElement, - ]; - stop_buttons: HTMLButtonElement[] = [ - document.getElementById("stop-button-1") as HTMLButtonElement, - //document.getElementById("stop-button-2") as HTMLButtonElement, - ]; - clear_buttons: HTMLButtonElement[] = [ - document.getElementById("clear-button-1") as HTMLButtonElement, - //document.getElementById("clear-button-2") as HTMLButtonElement, - ]; - load_universe_button: HTMLButtonElement = document.getElementById( - "load-universe-button" - ) as HTMLButtonElement; - - download_universe_button: HTMLButtonElement = document.getElementById( - "download-universes" - ) as HTMLButtonElement; - - upload_universe_button: HTMLButtonElement = document.getElementById( - "upload-universes" - ) as HTMLButtonElement; - - destroy_universes_button: HTMLButtonElement = document.getElementById( - "destroy-universes" - ) as HTMLButtonElement; - - documentation_button: HTMLButtonElement = document.getElementById( - "doc-button-1" - ) as HTMLButtonElement; - eval_button: HTMLButtonElement = document.getElementById( - "eval-button-1" - ) as HTMLButtonElement; - - // Script selection elements - local_button: HTMLButtonElement = document.getElementById( - "local-button" - ) as HTMLButtonElement; - global_button: HTMLButtonElement = document.getElementById( - "global-button" - ) as HTMLButtonElement; - init_button: HTMLButtonElement = document.getElementById( - "init-button" - ) as HTMLButtonElement; - note_button: HTMLButtonElement = document.getElementById( - "note-button" - ) as HTMLButtonElement; - settings_button: HTMLButtonElement = document.getElementById( - "settings-button" - ) as HTMLButtonElement; - close_settings_button: HTMLButtonElement = document.getElementById( - "close-settings-button" - ) as HTMLButtonElement; - close_universes_button: HTMLButtonElement = document.getElementById( - "close-universes-button" - ) as HTMLButtonElement; - - universe_viewer: HTMLDivElement = document.getElementById( - "universe-viewer" - ) as HTMLDivElement; - - // Buffer modal - buffer_modal: HTMLDivElement = document.getElementById( - "modal-buffers" - ) as HTMLDivElement; - buffer_search: HTMLInputElement = document.getElementById( - "buffer-search" - ) as HTMLInputElement; - universe_creator: HTMLFormElement = document.getElementById( - "universe-creator" - ) as HTMLFormElement; - - // Local script tabs - local_script_tabs: HTMLDivElement = document.getElementById( - "local-script-tabs" - ) as HTMLDivElement; - - // Font Size Slider - font_size_input: HTMLInputElement = document.getElementById( - "font-size-input" - ) as HTMLInputElement; - - // Font Family Selector - font_family_selector: HTMLSelectElement = document.getElementById( - "font-family" - ) as HTMLSelectElement; - - // Vim mode checkbox - vim_mode_checkbox: HTMLInputElement = document.getElementById( - "vim-mode" - ) as HTMLInputElement; - - // Line Numbers checkbox - line_numbers_checkbox: HTMLInputElement = document.getElementById( - "show-line-numbers" - ) as HTMLInputElement; - - // Time Position checkbox - time_position_checkbox: HTMLInputElement = document.getElementById( - "show-time-position" - ) as HTMLInputElement; - - // Hovering tips checkbox - tips_checkbox: HTMLInputElement = document.getElementById( - "show-tips" - ) as HTMLInputElement; - - midi_clock_checkbox: HTMLInputElement = document.getElementById( - "send-midi-clock" - ) as HTMLInputElement; - - midi_channels_scripts: HTMLInputElement = document.getElementById( - "midi-channels-scripts" - ) as HTMLInputElement; - - midi_clock_ppqn: HTMLSelectElement = document.getElementById( - "midi-clock-ppqn-input" - ) as HTMLSelectElement; - - // Loading demo songs when starting - load_demo_songs: HTMLInputElement = document.getElementById( - "load-demo-songs" - ) as HTMLInputElement; - - // Editor mode selection - normal_mode_button: HTMLButtonElement = document.getElementById( - "normal-mode" - ) as HTMLButtonElement; - vim_mode_button: HTMLButtonElement = document.getElementById( - "vim-mode" - ) as HTMLButtonElement; - - // Share button - share_button: HTMLElement = document.getElementById( - "share-button" - ) as HTMLElement; - - // Audio nudge range - audio_nudge_range: HTMLInputElement = document.getElementById( - "audio_nudge" - ) as HTMLInputElement; - - // Dough nudge range - dough_nudge_range: HTMLInputElement = document.getElementById( - "dough_nudge" - ) as HTMLInputElement; - - // Error line - error_line: HTMLElement = document.getElementById( - "error_line" - ) as HTMLElement; - show_error: boolean = false; - - // Hydra integration - hydra_canvas: HTMLCanvasElement = document.getElementById( - "hydra-bg" - ) as HTMLCanvasElement; - //@ts-ignore - public hydra_backend = new Hydra({ - canvas: this.hydra_canvas, - detectAudio: false, - enableStreamCapture: false, - }); - public hydra: any = this.hydra_backend.synth; + // Hydra + public hydra_backend: any; + public hydra: any; constructor() { // ================================================================================ - // Loading the settings + // Build user interface // ================================================================================ - this.line_numbers_checkbox.checked = this.settings.line_numbers; - this.time_position_checkbox.checked = this.settings.time_position; - this.tips_checkbox.checked = this.settings.tips; - this.midi_clock_checkbox.checked = this.settings.send_clock; - this.midi_channels_scripts.checked = this.settings.midi_channels_scripts; - this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); - if (!this.settings.time_position) { - document.getElementById("timeviewer")!.classList.add("hidden"); - } - this.load_demo_songs.checked = this.settings.load_demo_songs; + this.initializeElements(); + this.initializeButtonGroups(); + this.initializeHydra(); // ================================================================================ // Loading the universe from local storage @@ -321,20 +88,7 @@ export class Editor { ...this.settings.universes, ...template_universes, }; - - if (this.settings.load_demo_songs) { - let random_example = - examples[Math.floor(Math.random() * examples.length)]; - this.selected_universe = "Welcome"; - this.universes[this.selected_universe].global.committed = random_example; - this.universes[this.selected_universe].global.candidate = random_example; - } else { - this.selected_universe = this.settings.selected_universe; - if (this.universes[this.selected_universe] === undefined) - this.universes[this.selected_universe] = - structuredClone(template_universe); - } - this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`; + initializeSelectedUniverse(this); // ================================================================================ // Audio context and clock @@ -351,768 +105,111 @@ export class Editor { makeArrayExtensions(this.api); makeStringExtensions(this.api); - // ================================================================================ - // CodeMirror Management - // ================================================================================ - - this.vimModeCompartment = new Compartment(); - this.hoveringCompartment = new Compartment(); - this.withLineNumbers = new Compartment(); - this.chosenLanguage = new Compartment(); - this.fontSize = new Compartment(); - const vimPlugin = this.settings.vimMode ? vim() : []; - const lines = this.settings.line_numbers ? lineNumbers() : []; - const fontModif = EditorView.theme({ - "&": { - fontSize: `${this.settings.font_size}px`, - }, - $content: { - fontFamily: `${this.settings.font}, Menlo, Monaco, Lucida Console, monospace`, - fontSize: `${this.settings.font_size}px`, - }, - ".cm-gutters": { - fontSize: `${this.settings.font_size}px`, - }, - }); - - this.editorExtensions = [ - this.vimModeCompartment.of(vimPlugin), - this.withLineNumbers.of(lines), - this.fontSize.of(fontModif), - this.hoveringCompartment.of(this.settings.tips ? inlineHoveringTips : []), - editorSetup, - toposTheme, - this.chosenLanguage.of(javascript()), - EditorView.updateListener.of((v: ViewUpdate) => { - v; - }), - ]; - - let dynamicPlugins = new Compartment(); - - // ================================================================================ - // Building the documentation - let pre_loading = async () => { - await loadSamples(); - }; - pre_loading(); - this.docs = documentation_factory(this); - // ================================================================================ - - // ================================================================================ - // Application event listeners - // ================================================================================ - // - document.addEventListener('keydown', (event) => { - if (event.altKey) { - this.fill = true; - this.fill_viewer.classList.remove("invisible"); - } - }); - - document.addEventListener('keyup', (event) => { - if (event.key === 'Alt') { - this.fill = false; - this.fill_viewer.classList.add("invisible"); - } - }); - - window.addEventListener("keydown", (event: KeyboardEvent) => { - if (event.key === "Tab") { - event.preventDefault(); - } - - if (event.ctrlKey && event.key === "s") { - event.preventDefault(); - this.setButtonHighlighting("stop", true); - this.clock.stop(); - } - - if (event.ctrlKey && event.key === "p") { - event.preventDefault(); - if (this.isPlaying) { - this.isPlaying = false; - this.setButtonHighlighting("pause", true); - this.clock.pause(); - } else { - this.isPlaying = true; - this.setButtonHighlighting("play", true); - this.clock.start(); - } - } - - // Ctrl + Shift + V: Vim Mode - if ( - (event.key === "v" || event.key === "V") && - event.ctrlKey && - event.shiftKey - ) { - this.settings.vimMode = !this.settings.vimMode; - event.preventDefault(); - this.userPlugins = this.settings.vimMode ? [] : [vim()]; - this.view.dispatch({ - effects: dynamicPlugins.reconfigure(this.userPlugins), - }); - } - - // Ctrl + Enter or Return: Evaluate the hovered code block - if ((event.key === "Enter" || event.key === "Return") && event.ctrlKey) { - event.preventDefault(); - this.currentFile().candidate = this.view.state.doc.toString(); - this.flashBackground("#404040", 200); - } - - // Shift + Enter or Ctrl + E: evaluate the line - if ( - (event.key === "Enter" && event.shiftKey) || - (event.key === "e" && event.ctrlKey) - ) { - event.preventDefault(); // Prevents the addition of a new line - this.currentFile().candidate = this.view.state.doc.toString(); - this.flashBackground("#404040", 200); - } - - // Shift + Ctrl + Enter: Evaluate the currently visible code block - if (event.key === "Enter" && event.shiftKey && event.ctrlKey) { - event.preventDefault(); - this.currentFile().candidate = this.view.state.doc.toString(); - tryEvaluate(this, this.currentFile()); - this.flashBackground("#404040", 200); - } - - // This is the modal to switch between universes - if (event.ctrlKey && event.key === "b") { - event.preventDefault(); - this.hideDocumentation(); - this.updateKnownUniversesView(); - this.openBuffersModal(); - } - - // This is the modal that opens up the settings - if (event.shiftKey && event.key === "Escape") { - this.openSettingsModal(); - } - - if (event.ctrlKey && event.key === "l") { - event.preventDefault(); - this.changeModeFromInterface("local"); - this.hideDocumentation(); - this.view.focus(); - } - - if (event.ctrlKey && event.key === "n") { - event.preventDefault(); - this.changeModeFromInterface("notes"); - this.hideDocumentation(); - this.view.focus(); - } - - if (event.ctrlKey && event.key === "g") { - event.preventDefault(); - this.changeModeFromInterface("global"); - this.hideDocumentation(); - this.view.focus(); - } - - if (event.ctrlKey && event.key === "i") { - event.preventDefault(); - this.changeModeFromInterface("init"); - this.hideDocumentation(); - this.changeToLocalBuffer(0); - this.view.focus(); - } - - if (event.ctrlKey && event.key === "d") { - event.preventDefault(); - this.showDocumentation(); - } - - [112, 113, 114, 115, 116, 117, 118, 119, 120].forEach( - (keycode, index) => { - if (event.keyCode === keycode) { - event.preventDefault(); - if (event.ctrlKey) { - event.preventDefault(); - this.api.script(keycode - 111); - } else { - event.preventDefault(); - this.changeModeFromInterface("local"); - this.changeToLocalBuffer(index); - this.hideDocumentation(); - } - } - } - ); - - if (event.keyCode == 121) { - event.preventDefault(); - this.changeModeFromInterface("global"); - this.hideDocumentation(); - } - if (event.keyCode == 122) { - event.preventDefault(); - this.changeModeFromInterface("init"); - this.hideDocumentation(); - } - }); - - // ================================================================================ - // Interface buttons - // ================================================================================ - - const tabs = document.querySelectorAll('[id^="tab-"]'); - // Iterate over the tabs with an index - for (let i = 0; i < tabs.length; i++) { - tabs[i].addEventListener("click", (event) => { - // Updating the CSS accordingly - tabs[i].classList.add("bg-orange-300"); - for (let j = 0; j < tabs.length; j++) { - if (j != i) tabs[j].classList.remove("bg-orange-300"); - } - this.currentFile().candidate = this.view.state.doc.toString(); - - let tab = event.target as HTMLElement; - let tab_id = tab.id.split("-")[1]; - this.local_index = parseInt(tab_id); - this.updateEditorView(); - }); - } - - this.topos_logo.addEventListener("click", () => { - this.hideDocumentation(); - this.updateKnownUniversesView(); - this.openBuffersModal(); - }); - - this.play_buttons.forEach((button) => { - button.addEventListener("click", () => { - if (this.isPlaying) { - this.setButtonHighlighting("pause", true); - this.isPlaying = !this.isPlaying; - this.clock.pause(); - this.api.MidiConnection.sendStopMessage(); - } else { - this.setButtonHighlighting("play", true); - this.isPlaying = !this.isPlaying; - this.clock.start(); - this.api.MidiConnection.sendStartMessage(); - } - }); - }); - - this.clear_buttons.forEach((button) => { - button.addEventListener("click", () => { - this.setButtonHighlighting("clear", true); - if (confirm("Do you want to reset the current universe?")) { - this.universes[this.selected_universe] = - structuredClone(template_universe); - this.updateEditorView(); - } - }); - }); - - this.documentation_button.addEventListener("click", () => { - this.showDocumentation(); - }); - - this.destroy_universes_button.addEventListener("click", () => { - if (confirm("Do you want to destroy all universes?")) { - this.universes = { - ...template_universes, - }; - this.updateKnownUniversesView(); - } - }); - - this.audio_nudge_range.addEventListener("input", () => { - this.clock.nudge = parseInt(this.audio_nudge_range.value); - }); - - this.dough_nudge_range.addEventListener("input", () => { - this.dough_nudge = parseInt(this.dough_nudge_range.value); - }); - - this.upload_universe_button.addEventListener("click", () => { - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = ".json"; - - fileInput.addEventListener("change", (event) => { - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.readAsText(file, "UTF-8"); - - reader.onload = (evt) => { - const data = JSON.parse(evt.target!.result as string); - for (const [key, value] of Object.entries(data)) { - this.universes[key] = value as Universe; - } - }; - reader.onerror = (evt) => { - console.error("An error occurred reading the file:", evt); - }; - } - }); - - document.body.appendChild(fileInput); - fileInput.click(); - document.body.removeChild(fileInput); - }); - - this.download_universe_button.addEventListener("click", () => { - // Trigger save of the universe before downloading - this.settings.saveApplicationToLocalStorage( - this.universes, - this.settings - ); - - // Generate a file name based on timestamp - let fileName = `topos-universes-${Date.now()}.json`; - - // Create Blob and Object URL - const blob = new Blob([JSON.stringify(this.settings.universes)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - - // Create a temporary anchor and trigger download - const a = document.createElement("a"); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - - // Revoke the Object URL to free resources - URL.revokeObjectURL(url); - }); - - this.load_universe_button.addEventListener("click", () => { - let query = this.buffer_search.value; - if (query.length > 2 && query.length < 20 && !query.includes(" ")) { - this.loadUniverse(query); - this.settings.selected_universe = query; - this.buffer_search.value = ""; - this.closeBuffersModal(); - this.view.focus(); - this.emptyUrl(); - } - }); - - this.eval_button.addEventListener("click", () => { - this.currentFile().candidate = this.view.state.doc.toString(); - this.flashBackground("#404040", 200); - }); - - this.stop_buttons.forEach((button) => { - button.addEventListener("click", () => { - this.setButtonHighlighting("stop", true); - this.isPlaying = false; - this.clock.stop(); - }); - }); - - this.local_button.addEventListener("click", () => - this.changeModeFromInterface("local") - ); - this.global_button.addEventListener("click", () => - this.changeModeFromInterface("global") - ); - this.init_button.addEventListener("click", () => - this.changeModeFromInterface("init") - ); - this.note_button.addEventListener("click", () => - this.changeModeFromInterface("notes") - ); - - this.font_family_selector.addEventListener("change", () => { - let new_font = this.font_family_selector.value; - this.settings.font = new_font; - let new_font_size = EditorView.theme({ - "&": { fontSize: this.settings.font_size + "px" }, - "&content": { - fontFamily: new_font, - fontSize: this.settings.font_size + "px", - }, - ".cm-gutters": { fontSize: this.settings.font_size + "px" }, - }); - this.view.dispatch({ - effects: this.fontSize.reconfigure(new_font_size), - }); - }); - - this.font_size_input.addEventListener("input", () => { - let new_value: string | number = this.font_size_input.value; - this.settings.font_size = Math.max(8, Math.min(48, parseInt(new_value))); - - let new_font_size = EditorView.theme({ - "&": { fontSize: new_value + "px" }, - "&content": { - fontFamily: this.settings.font, - fontSize: new_value + "px", - }, - ".cm-gutters": { fontSize: new_value + "px" }, - }); - this.view.dispatch({ - effects: this.fontSize.reconfigure(new_font_size), - }); - this.settings.font_size = parseInt(new_value); - }); - - this.settings_button.addEventListener("click", () => { - // Populate the font family selector - this.dough_nudge_range.value = this.dough_nudge.toString(); - // @ts-ignore - document.getElementById("doughnumber")!.value = - this.dough_nudge.toString(); - this.font_family_selector.value = this.settings.font; - - if (this.settings.font_size === null) { - this.settings.font_size = 12; - } - this.font_size_input.value = this.settings.font_size.toString(); - - // Get the right value to update graphical widgets - this.line_numbers_checkbox.checked = this.settings.line_numbers; - this.time_position_checkbox.checked = this.settings.time_position; - this.tips_checkbox.checked = this.settings.tips; - this.midi_clock_checkbox.checked = this.settings.send_clock; - this.midi_channels_scripts.checked = this.settings.midi_channels_scripts; - this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString(); - this.load_demo_songs.checked = this.settings.load_demo_songs; - this.vim_mode_checkbox.checked = this.settings.vimMode; - - let modal_settings = document.getElementById("modal-settings"); - let editor = document.getElementById("editor"); - modal_settings?.classList.remove("invisible"); - - editor?.classList.add("invisible"); - }); - - this.close_settings_button.addEventListener("click", () => { - let modal_settings = document.getElementById("modal-settings"); - let editor = document.getElementById("editor"); - modal_settings?.classList.add("invisible"); - editor?.classList.remove("invisible"); - // Update the font size once again - this.view.dispatch({ - effects: this.fontSize.reconfigure( - EditorView.theme({ - "&": { fontSize: this.settings.font_size + "px" }, - "&content": { - fontFamily: this.settings.font, - fontSize: this.settings.font_size + "px", - }, - ".cm-gutters": { fontSize: this.settings.font_size + "px" }, - }) - ), - }); - }); - - this.close_universes_button.addEventListener("click", () => { - this.openBuffersModal(); - }); - - this.share_button.addEventListener("click", async () => { - // trigger a manual save - this.currentFile().candidate = app.view.state.doc.toString(); - this.currentFile().committed = app.view.state.doc.toString(); - this.settings.saveApplicationToLocalStorage(app.universes, app.settings); - // encode as a blob! - await this.share(); - }); - - this.vim_mode_checkbox.addEventListener("change", () => { - let checked = this.vim_mode_checkbox.checked ? true : false; - this.settings.vimMode = checked; - this.view.dispatch({ - effects: this.vimModeCompartment.reconfigure(checked ? vim() : []), - }); - }); - - this.line_numbers_checkbox.addEventListener("change", () => { - let checked = this.line_numbers_checkbox.checked ? true : false; - this.settings.line_numbers = checked; - this.view.dispatch({ - effects: this.withLineNumbers.reconfigure( - checked ? [lineNumbers()] : [] - ), - }); - }); - - this.time_position_checkbox.addEventListener("change", () => { - let timeviewer = document.getElementById("timeviewer") as HTMLElement; - let checked = this.time_position_checkbox.checked ? true : false; - this.settings.time_position = checked; - checked - ? timeviewer.classList.remove("hidden") - : timeviewer.classList.add("hidden"); - }); - - this.tips_checkbox.addEventListener("change", () => { - let checked = this.tips_checkbox.checked ? true : false; - this.settings.tips = checked; - this.view.dispatch({ - effects: this.hoveringCompartment.reconfigure( - checked ? inlineHoveringTips : [] - ), - }); - }); - - this.midi_clock_checkbox.addEventListener("change", () => { - let checked = this.midi_clock_checkbox.checked ? true : false; - this.settings.send_clock = checked; - }); - - this.midi_channels_scripts.addEventListener("change", () => { - let checked = this.midi_channels_scripts.checked ? true : false; - this.settings.midi_channels_scripts = checked; - }); - - this.midi_clock_ppqn.addEventListener("change", () => { - let value = parseInt(this.midi_clock_ppqn.value); - this.settings.midi_clock_ppqn = value; - }); - - this.load_demo_songs.addEventListener("change", () => { - let checked = this.load_demo_songs.checked ? true : false; - this.settings.load_demo_songs = checked; - }); - - this.universe_creator.addEventListener("submit", (event) => { - event.preventDefault(); - - let data = new FormData(this.universe_creator); - let universeName = data.get("universe") as string | null; - - if (universeName) { - if (universeName.length > 2 && universeName.length < 20) { - this.loadUniverse(universeName); - this.settings.selected_universe = universeName; - this.buffer_search.value = ""; - this.closeBuffersModal(); - this.view.focus(); - } - } - }); - - tryEvaluate(this, this.universes[this.selected_universe.toString()].init); - - [ - "introduction", - "interface", - "interaction", - "code", - "time", - "sound", - "samples", - "synths", - "chaining", - "patterns", - "ziffers", - "midi", - "functions", - "lfos", - "probabilities", - "variables", - // "reference", - "shortcuts", - "about", - "bonus", - ].forEach((e) => { - let name = `docs_` + e; - document.getElementById(name)!.addEventListener("click", async () => { - if (name !== "docs_samples") { - this.currentDocumentationPane = e; - this.updateDocumentationContent(); - } else { - console.log("Loading samples!"); - await loadSamples().then(() => { - this.docs = documentation_factory(this); - this.currentDocumentationPane = e; - this.updateDocumentationContent(); - }); - } - }); - }); - // Passing the API to the User Object.entries(this.api).forEach(([name, value]) => { (globalThis as Record)[name] = value; }); - this.state = EditorState.create({ - extensions: [ - ...this.editorExtensions, - EditorView.lineWrapping, - dynamicPlugins.of(this.userPlugins), - Prec.highest( - keymap.of([ - { - key: "Ctrl-Enter", - run: () => { - return true; - }, - }, - ]) - ), - keymap.of([indentWithTab]), - ], - doc: this.universes[this.selected_universe].global.candidate, - }); + // ================================================================================ + // Building Documentation + // ================================================================================ - this.view = new EditorView({ - parent: document.getElementById("editor") as HTMLElement, - state: this.state, - }); + let pre_loading = async () => { + await loadSamples(); + }; + pre_loading(); + this.docs = documentation_factory(this); + // ================================================================================ + // Application event listeners + // ================================================================================ + + registerFillKeys(this); + registerOnKeyDown(this); + installInterfaceLogic(this); + + // ================================================================================ + // Building CodeMirror Editor + // ================================================================================ + + installEditor(this); + + // First evaluation of the init file + tryEvaluate(this, this.universes[this.selected_universe.toString()].init); + + // Changing to global when opening this.changeModeFromInterface("global"); - // Loading from URL bar - let url = new URLSearchParams(window.location.search); - if (url !== undefined) { - let new_universe; - if (url !== null) { - const universeParam = url.get("universe"); - if (universeParam !== null) { - let data = Uint8Array.from(atob(universeParam), (c) => - c.charCodeAt(0) - ); - new_universe = JSON.parse(strFromU8(decompressSync(data))); - const randomName: string = uniqueNamesGenerator({ - length: 2, - separator: "_", - dictionaries: [colors, animals], - }); - this.loadUniverse(randomName, new_universe["universe"]); - this.emptyUrl(); - this.emptyUrl(); - } - } - } + // Loading universe from URL (if needed) + loadUniverserFromUrl(this); + } - this.hydra = this.hydra_backend.synth; + private getBuffer(type: string): any { + const universe = this.universes[this.selected_universe.toString()]; + return type === "locals" + ? universe[type][this.local_index] + : universe[type as keyof Universe]; } get note_buffer() { - return this.universes[this.selected_universe.toString()].notes; + return this.getBuffer("notes"); } get example_buffer() { - return this.universes[this.selected_universe.toString()].example; + return this.getBuffer("example"); } get global_buffer() { - return this.universes[this.selected_universe.toString()].global; + return this.getBuffer("global"); } get init_buffer() { - return this.universes[this.selected_universe.toString()].init; + return this.getBuffer("init"); } get local_buffer() { - return this.universes[this.selected_universe.toString()].locals[ - this.local_index - ]; + return this.getBuffer("locals"); } - emptyUrl = () => { - window.history.replaceState({}, document.title, "/"); - }; - - parseHash = (hash: string) => { - return JSON.parse(hash); - }; - updateKnownUniversesView = () => { - let itemTemplate = document.getElementById("ui-known-universe-item-template") as HTMLTemplateElement; + let itemTemplate = document.getElementById( + "ui-known-universe-item-template" + ) as HTMLTemplateElement; if (!itemTemplate) { - console.warn("Missing template #ui-known-universe-item-template") - return + console.warn("Missing template #ui-known-universe-item-template"); + return; } - let existing_universes = document.getElementById("existing-universes") + let existing_universes = document.getElementById("existing-universes"); if (!existing_universes) { - console.warn("Missing element #existing-universes") - return + console.warn("Missing element #existing-universes"); + return; } - let list = document.createElement("ul") - list.className = "lg:h-80 lg:w-80 lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800" + let list = document.createElement("ul"); + list.className = + "lg:h-80 lg:w-80 lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800"; - list.append(...Object.keys(this.universes) - .map(it => { - let item = itemTemplate.content.cloneNode(true) as DocumentFragment - let api = (window as unknown as UserAPI) // It's dirty but okey - item.querySelector(".universe-name")!.textContent = it - item.querySelector(".load-universe")?.addEventListener("click", () => api._loadUniverseFromInterface(it)) - item.querySelector(".delete-universe")?.addEventListener("click", () => api._deleteUniverseFromInterface(it)) - return item - })) - - existing_universes.innerHTML = "" - existing_universes.append(list) - }; - - async share() { - async function bufferToBase64(buffer: Uint8Array) { - const base64url: string = await new Promise((r) => { - const reader = new FileReader(); - reader.onload = () => r(reader.result as string); - reader.readAsDataURL(new Blob([buffer])); - }); - return base64url.slice(base64url.indexOf(",") + 1); - } - - let data = JSON.stringify({ - universe: this.settings.universes[this.selected_universe], - }); - let encoded_data = gzipSync(new TextEncoder().encode(data)); - // TODO make this async - // TODO maybe try with compression level 9 - const hashed_table = await bufferToBase64(encoded_data); - const url = new URL(window.location.href); - url.searchParams.set("universe", hashed_table); - window.history.replaceState({}, "", url.toString()); - // Copy the text inside the text field - navigator.clipboard.writeText(url.toString()); - } - - showDocumentation() { - if (document.getElementById("app")?.classList.contains("hidden")) { - document.getElementById("app")?.classList.remove("hidden"); - document.getElementById("documentation")?.classList.add("hidden"); - this.exampleIsPlaying = false; - } else { - document.getElementById("app")?.classList.add("hidden"); - document.getElementById("documentation")?.classList.remove("hidden"); - // Load and convert Markdown content from the documentation file - this.updateDocumentationContent(); - } - } - - hideDocumentation() { - if (document.getElementById("app")?.classList.contains("hidden")) { - document.getElementById("app")?.classList.remove("hidden"); - document.getElementById("documentation")?.classList.add("hidden"); - } - } - - updateDocumentationContent() { - const converter = new showdown.Converter({ - emoji: true, - moreStyling: true, - backslashEscapesHTMLTags: true, - extensions: [showdownHighlight({ auto_detection: true }), ...bindings], - }); - const converted_markdown = converter.makeHtml( - this.docs[this.currentDocumentationPane] + list.append( + ...Object.keys(this.universes).map((it) => { + let item = itemTemplate.content.cloneNode(true) as DocumentFragment; + let api = window as unknown as UserAPI; // It's dirty but okey + item.querySelector(".universe-name")!.textContent = it; + item + .querySelector(".load-universe") + ?.addEventListener("click", () => api._loadUniverseFromInterface(it)); + item + .querySelector(".delete-universe") + ?.addEventListener("click", () => + api._deleteUniverseFromInterface(it) + ); + return item; + }) ); - document.getElementById("documentation-content")!.innerHTML = - converted_markdown; - } + + existing_universes.innerHTML = ""; + existing_universes.append(list); + }; changeToLocalBuffer(i: number) { // Updating the CSS accordingly @@ -1129,10 +226,10 @@ export class Editor { changeModeFromInterface(mode: "global" | "local" | "init" | "notes") { const interface_buttons: HTMLElement[] = [ - this.local_button, - this.global_button, - this.init_button, - this.note_button, + this.interface.local_button, + this.interface.global_button, + this.interface.init_button, + this.interface.note_button, ]; let changeColor = (button: HTMLElement) => { @@ -1151,36 +248,36 @@ export class Editor { switch (mode) { case "local": - if (this.local_script_tabs.classList.contains("hidden")) { - this.local_script_tabs.classList.remove("hidden"); + if (this.interface.local_script_tabs.classList.contains("hidden")) { + this.interface.local_script_tabs.classList.remove("hidden"); } this.editor_mode = "local"; this.local_index = 0; - document.getElementById('editor')!.style.height = 'calc(100% - 100px)'; + document.getElementById("editor")!.style.height = "calc(100% - 100px)"; this.changeToLocalBuffer(this.local_index); - changeColor(this.local_button); + changeColor(this.interface.local_button); break; case "global": - if (!this.local_script_tabs.classList.contains("hidden")) { - this.local_script_tabs.classList.add("hidden"); + if (!this.interface.local_script_tabs.classList.contains("hidden")) { + this.interface.local_script_tabs.classList.add("hidden"); } this.editor_mode = "global"; - document.getElementById('editor')!.style.height = '100%'; - changeColor(this.global_button); + document.getElementById("editor")!.style.height = "100%"; + changeColor(this.interface.global_button); break; case "init": - if (!this.local_script_tabs.classList.contains("hidden")) { - this.local_script_tabs.classList.add("hidden"); + if (!this.interface.local_script_tabs.classList.contains("hidden")) { + this.interface.local_script_tabs.classList.add("hidden"); } this.editor_mode = "init"; - changeColor(this.init_button); + changeColor(this.interface.init_button); break; case "notes": - if (!this.local_script_tabs.classList.contains("hidden")) { - this.local_script_tabs.classList.add("hidden"); + if (!this.interface.local_script_tabs.classList.contains("hidden")) { + this.interface.local_script_tabs.classList.add("hidden"); } this.editor_mode = "notes"; - changeColor(this.note_button); + changeColor(this.interface.note_button); break; } @@ -1270,9 +367,6 @@ export class Editor { }); } - /** - * @returns The current file being edited - */ currentFile(): File { switch (this.editor_mode) { case "global": @@ -1286,69 +380,6 @@ export class Editor { } } - /** - * @param universeName: The name of the universe to load - */ - loadUniverse( - universeName: string, - universe: Universe = template_universe - ): void { - console.log(universeName, universe); - this.currentFile().candidate = this.view.state.doc.toString(); - - // Getting the new universe name and moving on - let selectedUniverse = universeName.trim(); - if (this.universes[selectedUniverse] === undefined) { - this.settings.universes[selectedUniverse] = universe; - this.universes[selectedUniverse] = universe; - } - this.selected_universe = selectedUniverse; - this.settings.selected_universe = this.selected_universe; - this.universe_viewer.innerHTML = `Topos: ${selectedUniverse}`; - - // Updating the editor View to reflect the selected universe - this.updateEditorView(); - - // Evaluating the initialisation script for the selected universe - tryEvaluate(this, this.universes[this.selected_universe.toString()].init); - } - - openSettingsModal(): void { - if ( - document.getElementById("modal-settings")!.classList.contains("invisible") - ) { - document.getElementById("editor")!.classList.add("invisible"); - document.getElementById("modal-settings")!.classList.remove("invisible"); - } else { - this.closeSettingsModal(); - } - } - - closeSettingsModal(): void { - document.getElementById("editor")!.classList.remove("invisible"); - document.getElementById("modal-settings")!.classList.add("invisible"); - } - - openBuffersModal(): void { - // If the modal is hidden, unhide it and hide the editor - if ( - document.getElementById("modal-buffers")!.classList.contains("invisible") - ) { - document.getElementById("editor")!.classList.add("invisible"); - document.getElementById("modal-buffers")!.classList.remove("invisible"); - document.getElementById("buffer-search")!.focus(); - } else { - this.closeBuffersModal(); - } - } - - closeBuffersModal(): void { - // @ts-ignore - document.getElementById("buffer-search")!.value = ""; - document.getElementById("editor")!.classList.remove("invisible"); - document.getElementById("modal-buffers")!.classList.add("invisible"); - } - /** * @param color the color to flash the background * @param duration the duration of the flash @@ -1371,17 +402,33 @@ export class Editor { ); }, duration); } + + private initializeElements(): void { + for (const [key, value] of Object.entries(singleElements)) { + this.interface[key] = document.getElementById( + value + ) as ElementMap[keyof ElementMap]; + } + } + + private initializeButtonGroups(): void { + for (const [key, ids] of Object.entries(buttonGroups)) { + this.buttonElements[key] = ids.map( + (id) => document.getElementById(id) as HTMLButtonElement + ); + } + } + + private initializeHydra(): void { + //@ts-ignore + this.hydra_backend = new Hydra({ + canvas: this.interface.hydra_canvas as HTMLCanvasElement, + detectAudio: false, + enableStreamCapture: false, + }); + this.hydra = this.hydra_backend.synth; + } } let app = new Editor(); - -window.addEventListener("beforeunload", () => { - // @ts-ignore - event.preventDefault(); - // Iterate over all local files and set the candidate to the committed - app.currentFile().candidate = app.view.state.doc.toString(); - app.currentFile().committed = app.view.state.doc.toString(); - app.settings.saveApplicationToLocalStorage(app.universes, app.settings); - app.clock.stop(); - return null; -}); +installWindowBehaviors(app, window, false);