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);