diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 9537cbd..a74cce8 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -2,19 +2,19 @@ import { getAnalyser } from "superdough"; import { type Editor } from "./main"; -/** - * Draw a circle at a specific position on the canvas. - * @param {number} x - The x-coordinate of the circle's center. - * @param {number} y - The y-coordinate of the circle's center. - * @param {number} radius - The radius of the circle. - * @param {string} color - The fill color of the circle. - */ export const drawCircle = ( + /** + * Draw a circle at a specific position on the canvas. + * @param {number} x - The x-coordinate of the circle's center. + * @param {number} y - The y-coordinate of the circle's center. + * @param {number} radius - The radius of the circle. + * @param {string} color - The fill color of the circle. + */ app: Editor, x: number, y: number, radius: number, - color: string, + color: string ): void => { // @ts-ignore const canvas: HTMLCanvasElement = app.interface.feedback; @@ -28,15 +28,15 @@ export const drawCircle = ( ctx.closePath(); }; -/** - * Blinks a script indicator circle. - * @param script - The type of script. - * @param no - The shift amount multiplier. - */ export const blinkScript = ( + /** + * Blinks a script indicator circle. + * @param script - The type of script. + * @param no - The shift amount multiplier. + */ app: Editor, script: "local" | "global" | "init", - no?: number, + no?: number ) => { if (no !== undefined && no < 1 && no > 9) return; const blinkDuration = @@ -55,15 +55,15 @@ export const blinkScript = ( horizontalOffset + shift, app.interface.feedback.clientHeight - 15, 8, - "#fdba74", + "#fdba74" ); }; - /** - * Clears the circle at a given shift. - * @param shift - The pixel distance from the origin. - */ const _clearBlinker = (shift: number) => { + /** + * Clears the circle at a given shift. + * @param shift - The pixel distance from the origin. + */ const x = 50 + shift; const y = app.interface.feedback.clientHeight - 15; const radius = 8; @@ -91,17 +91,18 @@ export const blinkScript = ( 0, 0, (app.interface.feedback as HTMLCanvasElement).width, - (app.interface.feedback as HTMLCanvasElement).height, + (app.interface.feedback as HTMLCanvasElement).height ); }, blinkDuration); } }; -/** - * Manages animation updates using requestAnimationFrame. - * @param app - The Editor application context. - */ export const scriptBlinkers = () => { + /** + * Manages animation updates using requestAnimationFrame. + * @param app - The Editor application context. + */ + let lastFrameTime = Date.now(); const frameRate = 10; const minFrameDelay = 1000 / frameRate; @@ -134,15 +135,16 @@ export interface OscilloscopeConfig { let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' let lastRenderTime: number = 0; -/** - * Initializes and runs an oscilloscope using an AnalyzerNode. - * @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope. - * @param {OscilloscopeConfig} config - Configuration for the oscilloscope's appearance and behavior. - */ export const runOscilloscope = ( canvas: HTMLCanvasElement, - app: Editor, + app: Editor ): void => { + /** + * Runs the oscilloscope visualization on the provided canvas element. + * + * @param canvas - The HTMLCanvasElement on which to render the visualization. + * @param app - The Editor object containing the configuration for the oscilloscope. + */ let config = app.osc; let analyzer = getAnalyser(config.fftSize); let dataArray = new Float32Array(analyzer.frequencyBinCount); @@ -155,7 +157,7 @@ export const runOscilloscope = ( width: number, height: number, offset_height: number, - offset_width: number, + offset_width: number ) { const maxFPS = 30; const now = performance.now(); @@ -170,11 +172,11 @@ export const runOscilloscope = ( const performanceFactor = 1; const reducedDataSize = Math.floor( - freqDataArray.length * performanceFactor, + freqDataArray.length * performanceFactor ); const numBars = Math.min( reducedDataSize, - app.osc.orientation === "horizontal" ? width : height, + app.osc.orientation === "horizontal" ? width : height ); const barWidth = app.osc.orientation === "horizontal" ? width / numBars : height / numBars; @@ -187,7 +189,7 @@ export const runOscilloscope = ( for (let i = 0; i < numBars; i++) { barHeight = Math.floor( freqDataArray[Math.floor((i * freqDataArray.length) / numBars)] * - ((height / 256) * app.osc.size), + ((height / 256) * app.osc.size) ); if (app.osc.orientation === "horizontal") { @@ -195,7 +197,7 @@ export const runOscilloscope = ( x + offset_width, (height - barHeight) / 2 + offset_height, barWidth + 1, - barHeight, + barHeight ); x += barWidth; } else { @@ -203,7 +205,7 @@ export const runOscilloscope = ( (width - barHeight) / 2 + offset_width, y + offset_height, barHeight, - barWidth + 1, + barWidth + 1 ); y += barWidth; } @@ -232,12 +234,19 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT, + HEIGHT + 2 * OFFSET_HEIGHT ); return; } if (analyzer.fftSize !== app.osc.fftSize) { + // Disconnect and release the old analyzer if it exists + if (analyzer) { + analyzer.disconnect(); + analyzer = null; // Release the reference for garbage collection + } + + // Create a new analyzer with the updated FFT size analyzer = getAnalyser(app.osc.fftSize); dataArray = new Float32Array(analyzer.frequencyBinCount); } @@ -252,7 +261,7 @@ export const runOscilloscope = ( -OFFSET_WIDTH, -OFFSET_HEIGHT, WIDTH + 2 * OFFSET_WIDTH, - HEIGHT + 2 * OFFSET_HEIGHT, + HEIGHT + 2 * OFFSET_HEIGHT ); } canvasCtx.lineWidth = app.osc.thickness; diff --git a/src/Clock.ts b/src/Clock.ts index c184e1f..e0c3a8b 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -48,10 +48,7 @@ export class Clock { lastPlayPressTime: number; totalPauseTime: number; - constructor( - public app: Editor, - ctx: AudioContext, - ) { + constructor(public app: Editor, ctx: AudioContext) { this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_signature = [4, 4]; this.logicalTime = 0; @@ -77,6 +74,12 @@ export class Clock { } convertTicksToTimeposition(ticks: number): TimePosition { + /** + * Converts ticks to a TimePosition object. + * @param ticks The number of ticks to convert. + * @returns The TimePosition object representing the converted ticks. + */ + const beatsPerBar = this.app.clock.time_signature[0]; const ppqnPosition = ticks % this.app.clock.ppqn; const beatNumber = Math.floor(ticks / this.app.clock.ppqn); diff --git a/src/Documentation.ts b/src/Documentation.ts index 7dac040..b480491 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -47,7 +47,7 @@ export const makeExampleFactory = (application: Editor): Function => { const make_example = ( description: string, code: string, - open: boolean = false, + open: boolean = false ) => { const codeId = `codeExample${application.exampleCounter++}`; // Store the code snippet in the data structure @@ -70,7 +70,11 @@ export const makeExampleFactory = (application: Editor): Function => { }; export const documentation_factory = (application: Editor) => { - // Initialize a data structure to store code examples by their unique IDs + /** + * Creates the documentation for the given application. + * @param application The editor application. + * @returns An object containing various documentation sections. + */ application.api.codeExamples = {}; return { @@ -109,6 +113,10 @@ export const documentation_factory = (application: Editor) => { }; export const showDocumentation = (app: Editor) => { + /** + * Shows or hides the documentation based on the current state of the app. + * @param app - The Editor instance. + */ if (document.getElementById("app")?.classList.contains("hidden")) { document.getElementById("app")?.classList.remove("hidden"); document.getElementById("documentation")?.classList.add("hidden"); @@ -129,6 +137,9 @@ export const showDocumentation = (app: Editor) => { }; export const hideDocumentation = () => { + /** + * Hides the documentation section and shows the main application. + */ if (document.getElementById("app")?.classList.contains("hidden")) { document.getElementById("app")?.classList.remove("hidden"); document.getElementById("documentation")?.classList.add("hidden"); @@ -136,6 +147,12 @@ export const hideDocumentation = () => { }; export const updateDocumentationContent = (app: Editor, bindings: any) => { + /** + * Updates the content of the documentation pane with the converted markdown. + * + * @param app - The editor application. + * @param bindings - Additional bindings for the showdown converter. + */ const converter = new showdown.Converter({ emoji: true, moreStyling: true, @@ -143,7 +160,7 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => { extensions: [showdownHighlight({ auto_detection: true }), ...bindings], }); const converted_markdown = converter.makeHtml( - app.docs[app.currentDocumentationPane], + app.docs[app.currentDocumentationPane] ); document.getElementById("documentation-content")!.innerHTML = converted_markdown; diff --git a/src/DomElements.ts b/src/DomElements.ts index 8689eca..a7844e0 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -63,6 +63,12 @@ export const buttonGroups = { //@ts-ignore export const createDocumentationStyle = (app: Editor) => { + /** + * Creates a documentation style object. + * @param {Editor} app - The editor object. + * @returns {Object} - The documentation style object. + */ + return { 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 border-b-4 pt-4 pb-3 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 border-b-2 pt-12 pb-3 px-2", diff --git a/src/Evaluator.ts b/src/Evaluator.ts index b210dba..7c5b586 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -8,11 +8,19 @@ const codeReplace = (code: string): string => { const tryCatchWrapper = async ( application: Editor, - code: string, + code: string ): Promise => { + /** + * Wraps the provided code in a try-catch block and executes it. + * + * @param application - The editor application. + * @param code - The code to be executed. + * @returns A promise that resolves to a boolean indicating whether the code executed successfully or not. + */ + try { await new Function(`"use strict"; ${codeReplace(code)}`).call( - application.api, + application.api ); return true; } catch (error) { @@ -26,18 +34,32 @@ const cache = new Map(); const MAX_CACHE_SIZE = 40; const addFunctionToCache = (code: string, fn: Function) => { + /** + * Adds a function to the cache. + * @param code - The code associated with the function. + * @param fn - The function to be added to the cache. + */ if (cache.size >= MAX_CACHE_SIZE) { cache.delete(cache.keys().next().value); } cache.set(code, fn); }; -// Optimized evaluate function with reduced complexity export const tryEvaluate = async ( application: Editor, code: File, - timeout = 5000, + timeout = 5000 ): Promise => { + /** + * Tries to evaluate the provided code within a specified timeout period. + * Increments the evaluation count of the code file. + * If the code is valid, updates the committed code and adds the evaluated function to the cache. + * If the code is invalid, retries the evaluation. + * @param application - The editor application. + * @param code - The code file to evaluate. + * @param timeout - The timeout period in milliseconds (default: 5000). + * @returns A Promise that resolves when the evaluation is complete. + */ code.evaluations!++; const candidateCode = code.candidate; @@ -55,7 +77,7 @@ export const tryEvaluate = async ( if (isCodeValid) { code.committed = code.candidate; const newFunction = new Function( - `"use strict"; ${codeReplace(wrappedCode)}`, + `"use strict"; ${codeReplace(wrappedCode)}` ); addFunctionToCache(candidateCode, newFunction); } else { @@ -71,8 +93,16 @@ export const tryEvaluate = async ( export const evaluate = async ( application: Editor, code: File, - timeout = 1000, + timeout = 1000 ): Promise => { + /** + * Evaluates the given code using the provided application and timeout. + * @param application The editor application. + * @param code The code file to evaluate. + * @param timeout The timeout value in milliseconds (default: 1000). + * @returns A Promise that resolves when the evaluation is complete. + */ + try { await Promise.race([ tryCatchWrapper(application, code.committed as string), @@ -87,7 +117,7 @@ export const evaluate = async ( export const evaluateOnce = async ( application: Editor, - code: string, + code: string ): Promise => { /** * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. diff --git a/src/FileManagement.ts b/src/FileManagement.ts index 213308e..093af7a 100644 --- a/src/FileManagement.ts +++ b/src/FileManagement.ts @@ -154,7 +154,7 @@ export class AppSettings { constructor() { const settingsFromStorage = JSON.parse( - localStorage.getItem("topos") || "{}", + localStorage.getItem("topos") || "{}" ); if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) { @@ -210,7 +210,7 @@ export class AppSettings { saveApplicationToLocalStorage( universes: Universes, - settings: Settings, + settings: Settings ): void { /** * Main method to store the application to local storage. @@ -273,6 +273,11 @@ export const emptyUrl = () => { }; export const share = async (app: Editor) => { + /** + * Shares the current state of the app by generating a URL with encoded data and copying it to the clipboard. + * @param app - The Editor instance representing the app. + * @returns A Promise that resolves to void. + */ async function bufferToBase64(buffer: Uint8Array) { const base64url: string = await new Promise((r) => { const reader = new FileReader(); @@ -323,8 +328,20 @@ export const loadUniverserFromUrl = (app: Editor): void => { export const loadUniverse = ( app: Editor, universeName: string, - universe: Universe = template_universe, + universe: Universe = template_universe ): void => { + /** + * Loads a universe into the application. + * If the universe does not exist, a fresh clone of the template universe is created and added to the application. + * The references to the selected universe are updated in the application settings. + * The editor view is updated to reflect the selected universe. + * The initialization script for the selected universe is evaluated. + * + * @param app - The Editor application instance. + * @param universeName - The name of the universe to load. + * @param universe - The template universe to clone if the specified universe does not exist. + */ + let selectedUniverse = universeName.trim(); if (app.universes[selectedUniverse] === undefined) { // Pushing a freshly cloned template universe to: @@ -346,7 +363,11 @@ export const loadUniverse = ( }; export const openUniverseModal = (): void => { - // If the modal is hidden, unhide it and hide the editor + /** + * Opens the universe modal. + * If the modal is hidden, it unhides it and hides the editor. + * If the modal is already visible, it closes the modal. + */ if ( document.getElementById("modal-buffers")!.classList.contains("invisible") ) { @@ -359,6 +380,9 @@ export const openUniverseModal = (): void => { }; export const closeUniverseModal = (): void => { + /** + * Closes the universe modal and performs necessary actions. + */ // @ts-ignore document.getElementById("buffer-search")!.value = ""; document.getElementById("editor")!.classList.remove("invisible"); @@ -366,6 +390,9 @@ export const closeUniverseModal = (): void => { }; export const openSettingsModal = (): void => { + /** + * Opens the settings modal. + */ if ( document.getElementById("modal-settings")!.classList.contains("invisible") ) { @@ -377,6 +404,9 @@ export const openSettingsModal = (): void => { }; export const closeSettingsModal = (): void => { + /** + * Closes the settings modal and performs necessary actions. + */ document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("modal-settings")!.classList.add("invisible"); }; diff --git a/src/main.ts b/src/main.ts index 1d3721a..26990cc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,6 +101,19 @@ export class Editor { public hydra: any; constructor() { + /** + * This is the entry point of the application. The Editor instance is created when the page is loaded. + * It is responsible for: + * - Initializing the user interface + * - Loading the universe from local storage + * - Initializing the audio context and the clock + * - Building the user API + * - Building the documentation + * - Installing event listeners + * - Building the CodeMirror editor + * - Evaluating the init file + */ + // ================================================================================ // Build user interface // ================================================================================ @@ -194,6 +207,11 @@ export class Editor { } private getBuffer(type: string): any { + /** + * Retrieves the buffer based on the specified type. + * @param type - The type of buffer to retrieve. + * @returns The buffer object. + */ const universe = this.universes[this.selected_universe.toString()]; return type === "locals" ? universe[type][this.local_index] @@ -221,6 +239,12 @@ export class Editor { } updateKnownUniversesView = () => { + /** + * Updates the known universes view. + * This function generates and populates a list of known universes based on the data stored in the 'universes' property. + * It retrieves the necessary HTML elements and template, creates the list, and attaches event listeners to the generated items. + * If any required elements or templates are missing, warning messages are logged and the function returns early. + */ let itemTemplate = document.getElementById( "ui-known-universe-item-template" ) as HTMLTemplateElement; @@ -261,7 +285,13 @@ export class Editor { }; changeToLocalBuffer(i: number) { - // Updating the CSS accordingly + /** + * Changes the local buffer based on the provided index. + * Updates the CSS accordingly by adding a specific class to the selected tab and removing it from other tabs. + * Updates the local index and updates the editor view. + * + * @param i The index of the tab to change the local buffer to. + */ const tabs = document.querySelectorAll('[id^="tab-"]'); const tab = tabs[i] as HTMLElement; tab.classList.add("bg-orange-300"); @@ -274,6 +304,11 @@ export class Editor { } changeModeFromInterface(mode: "global" | "local" | "init" | "notes") { + /** + * Changes the mode of the interface. + * + * @param mode - The mode to change to. Can be one of "global", "local", "init", or "notes". + */ const interface_buttons: HTMLElement[] = [ this.interface.local_button, this.interface.global_button, @@ -345,6 +380,12 @@ export class Editor { button: "play" | "pause" | "stop" | "clear", highlight: boolean ) { + /** + * Sets the highlighting for a specific button. + * + * @param button - The button to highlight ("play", "pause", "stop", or "clear"). + * @param highlight - A boolean indicating whether to highlight the button or not. + */ document.getElementById("play-label")!.textContent = button !== "pause" ? "Pause" : "Play"; if (button !== "pause") { @@ -429,12 +470,12 @@ export class Editor { } } - /** - * Flashes the background of the view and its gutters. - * @param {string} color - The color to set. - * @param {number} duration - Duration in milliseconds to maintain the color. - */ flashBackground(color: string, duration: number): void { + /** + * Flashes the background of the view and its gutters. + * @param {string} color - The color to set. + * @param {number} duration - Duration in milliseconds to maintain the color. + */ const domElement = this.view.dom; const gutters = domElement.getElementsByClassName( "cm-gutter" @@ -480,6 +521,12 @@ export class Editor { } private loadHydraSynthAsync(): void { + /** + * Loads the Hydra Synth asynchronously by creating a script element + * and appending it to the document head. * Once the script is + * loaded successfully, it initializes the Hydra Synth. If there + * is an error loading the script, it logs an error message. + */ var script = document.createElement("script"); script.src = "https://unpkg.com/hydra-synth"; script.async = true; @@ -494,6 +541,9 @@ export class Editor { } private initializeHydra(): void { + /** + * Initializes the Hydra backend and sets up the Hydra synth. + */ // @ts-ignore this.hydra_backend = new Hydra({ canvas: this.interface.hydra_canvas as HTMLCanvasElement, @@ -506,6 +556,11 @@ export class Editor { } private setCanvas(canvas: HTMLCanvasElement): void { + /** + * Sets the canvas element and configures its size and context. + * + * @param canvas - The HTMLCanvasElement to set. + */ if (!canvas) return; const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1;