Add code documentation

This commit is contained in:
2023-11-26 13:32:38 +01:00
parent 626a8be77c
commit d717fc8410
7 changed files with 212 additions and 62 deletions

View File

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

View File

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

View File

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

View File

@ -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",

View File

@ -8,11 +8,19 @@ const codeReplace = (code: string): string => {
const tryCatchWrapper = async (
application: Editor,
code: string,
code: string
): Promise<boolean> => {
/**
* 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<string, Function>();
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<void> => {
/**
* 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<void> => {
/**
* 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<void> => {
/**
* Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper.

View File

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

View File

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