Refactoring: cleaning up main.ts file

This commit is contained in:
2023-10-21 15:01:50 +02:00
parent 1341522f47
commit 1f96799a9c
11 changed files with 1343 additions and 1228 deletions

View File

@ -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.

View File

@ -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 `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
};
@ -38,8 +50,8 @@ export const makeExampleFactory = (application: Editor): Function => {
<details ${open ? "open" : ""}>
<summary >${description}
<button class="py-1 align-top text-base rounded-lg pl-2 pr-2 hover:bg-green-700 bg-green-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base rounded-lg pl-2 pr-2 hover:bg-neutral-600 bg-neutral-500 inline-block pl-2" onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base rounded-lg pl-2 pr-2 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-600 bg-neutral-500 inline-block pl-2" onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
</summary>
\`\`\`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;
};

83
src/DomElements.ts Normal file
View File

@ -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: "",
};

View File

@ -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,
});
};

View File

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

View File

@ -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");
};

View File

@ -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];

434
src/InterfaceLogic.ts Normal file
View File

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

160
src/KeyActions.ts Normal file
View File

@ -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();
}
});
};

38
src/WindowBehavior.ts Normal file
View File

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

File diff suppressed because it is too large Load Diff