diff --git a/src/API.ts b/src/API.ts index 6a7ead8..337740d 100644 --- a/src/API.ts +++ b/src/API.ts @@ -51,25 +51,24 @@ export class UserAPI { private errorTimeoutID: number = 0; private printTimeoutID: number = 0; - MidiConnection: MidiConnection = new MidiConnection(); load: samples; constructor(public app: Editor) {} - _loadUniverseFromInterface = (universe: string) => { - this.app.loadUniverse(universe as string); + _loadUniverseFromInterface = (universe: string) => { + this.app.loadUniverse(universe as string); this.app.openBuffersModal(); - } + }; - _deleteUniverseFromInterface = (universe: string) => { - delete this.app.universes[universe]; - this.app.settings.saveApplicationToLocalStorage( - this.app.universes, - this.app.settings - ); - this.app.updateKnownUniversesView(); - } + _deleteUniverseFromInterface = (universe: string) => { + delete this.app.universes[universe]; + this.app.settings.saveApplicationToLocalStorage( + this.app.universes, + this.app.settings + ); + this.app.updateKnownUniversesView(); + }; _playDocExample = (code?: string) => { this.play(); @@ -82,7 +81,6 @@ export class UserAPI { ); }; - _playDocExampleOnce = (code?: string) => { this.play(); console.log("Executing documentation example: " + this.app.selectedExample); @@ -98,7 +96,7 @@ export class UserAPI { 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.style.color = "color-red-800"; this.app.error_line.classList.remove("hidden"); this.errorTimeoutID = setTimeout( () => this.app.error_line.classList.add("hidden"), @@ -111,7 +109,7 @@ export class UserAPI { 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.style.color = "white"; this.app.error_line.classList.remove("hidden"); this.printTimeoutID = setTimeout( () => this.app.error_line.classList.add("hidden"), @@ -151,10 +149,10 @@ export class UserAPI { // Mouse functions // ============================================================= - onmousemove = (e: MouseEvent) => { - this.app._mouseX = e.clientX; - this.app._mouseY = e.clientY; - } + onmousemove = (e: MouseEvent) => { + this.app._mouseX = e.clientX; + this.app._mouseY = e.clientY; + }; public mouseX = (): number => { /** @@ -359,17 +357,16 @@ export class UserAPI { public z = ( input: string, options: InputOptions = {}, - id: number|string = "" + id: number | string = "" ) => { - - const zid = "z"+id.toString(); - const key = id==="" ? this.generateCacheKey(input, options) : zid; + const zid = "z" + id.toString(); + const key = id === "" ? this.generateCacheKey(input, options) : zid; let player; if (this.app.api.patternCache.has(key)) { player = this.app.api.patternCache.get(key) as Player; - if(player.input!==input) { + if (player.input !== input) { player = undefined; } } @@ -379,30 +376,47 @@ export class UserAPI { this.app.api.patternCache.set(key, player); } - if(typeof id === "number") player.zid = zid; - + if (typeof id === "number") player.zid = zid; + player.updateLastCallTime(); - + return player; }; - public z0 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 0); - public z1 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 1); - public z2 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 2); - public z3 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 3); - public z4 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 4); - public z5 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 5); - public z6 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 6); - public z7 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 7); - public z8 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 8); - public z9 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 9); - public z10 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 10); - public z11 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 11); - public z12 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 12); - public z13 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 13); - public z14 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 14); - public z15 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 15); - public z16 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 16); + public z0 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 0); + public z1 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 1); + public z2 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 2); + public z3 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 3); + public z4 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 4); + public z5 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 5); + public z6 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 6); + public z7 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 7); + public z8 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 8); + public z9 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 9); + public z10 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 10); + public z11 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 11); + public z12 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 12); + public z13 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 13); + public z14 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 14); + public z15 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 15); + public z16 = (input: string, opts: InputOptions = {}) => + this.z(input, opts, 16); // ============================================================= // Counter and iteration @@ -930,12 +944,15 @@ export class UserAPI { return current_chunk % 2 === 0; }; - public onbar = (bars: number[] | number, n: number = this.app.clock.time_signature[0]): boolean => { - let current_bar = (this.bar() % n) + 1; - return (typeof bars === "number") - ? bars === current_bar - : bars.some((b) => b == current_bar) - }; + public onbar = ( + bars: number[] | number, + n: number = this.app.clock.time_signature[0] + ): boolean => { + let current_bar = (this.bar() % n) + 1; + return typeof bars === "number" + ? bars === current_bar + : bars.some((b) => b == current_bar); + }; onbeat = (...beat: number[]): boolean => { /** @@ -949,12 +966,14 @@ export class UserAPI { */ let final_pulses: boolean[] = []; beat.forEach((b) => { - const beat = b % this.app.clock.time_signature[0] || this.app.clock.time_signature[0]; + const beat = + b % this.app.clock.time_signature[0] || + this.app.clock.time_signature[0]; const integral_part = Math.floor(beat); - const decimal_part = ((beat - integral_part) * this.app.clock.ppqn) + 1; + const decimal_part = (beat - integral_part) * this.app.clock.ppqn + 1; final_pulses.push( integral_part === this.app.clock.time_position.beat && - this.app.clock.time_position.pulse === decimal_part + this.app.clock.time_position.pulse === decimal_part ); }); return final_pulses.some((p) => p == true); @@ -1013,14 +1032,16 @@ export class UserAPI { }; ec: Function = this.euclid; - public rhythm = ( + public rhythm = ( div: number, pulses: number, length: number, rotate: number = 0 ): boolean => { - return this.mod(div) && this._euclidean_cycle(pulses, length, rotate).div(div); - } + return ( + this.mod(div) && this._euclidean_cycle(pulses, length, rotate).div(div) + ); + }; _euclidean_cycle( pulses: number, @@ -1274,9 +1295,9 @@ export class UserAPI { samples = samples; log = (message: any) => { - console.log(message); - this._logMessage(message); - } + console.log(message); + this._logMessage(message); + }; scale = scale; diff --git a/src/ArrayExtensions.ts b/src/ArrayExtensions.ts index b715fa9..a46b015 100644 --- a/src/ArrayExtensions.ts +++ b/src/ArrayExtensions.ts @@ -3,10 +3,10 @@ export {}; declare global { interface Array { - add(amount: number): number[]; - sub(amount: number): number[]; - mult(amount: number): number[]; - division(amount: number): number[]; + add(amount: number): number[]; + sub(amount: number): number[]; + mult(amount: number): number[]; + division(amount: number): number[]; palindrome(): T[]; random(index: number): T; rand(index: number): T; @@ -24,8 +24,8 @@ declare global { rotate(steps: number): this; unique(): this; in(value: T): boolean; - square(): number[]; - sqrt(): number[]; + square(): number[]; + sqrt(): number[]; } } @@ -33,54 +33,55 @@ export const makeArrayExtensions = (api: UserAPI) => { Array.prototype.in = function (this: T[], value: T): boolean { return this.includes(value); }; - - Array.prototype.square = function (): number[] { - /** - * @returns New array with squared values. - */ - return this.map((x: number) => x * x); - }; - Array.prototype.sqrt = function (): number[] { - /** - * @returns New array with square roots of values. Throws if any element is negative. - */ - if (this.some(x => x < 0)) throw new Error('Cannot take square root of negative number'); - return this.map((x: number) => Math.sqrt(x)); - }; - - Array.prototype.add = function (amount: number): number[] { - /** - * @param amount - The value to add to each element in the array. - * @returns New array with added values. - */ - return this.map((x: number) => x + amount); - }; - - Array.prototype.sub = function (amount: number): number[] { - /** - * @param amount - The value to subtract from each element in the array. - * @returns New array with subtracted values. - */ - return this.map((x: number) => x - amount); - }; - - Array.prototype.mult = function (amount: number): number[] { - /** - * @param amount - The value to multiply with each element in the array. - * @returns New array with multiplied values. - */ - return this.map((x: number) => x * amount); - }; - - Array.prototype.division = function (amount: number): number[] { - /** - * @param amount - The value to divide each element in the array by. - * @returns New array with divided values. Throws if division by zero. - */ - if (amount === 0) throw new Error('Division by zero'); - return this.map((x: number) => x / amount); - }; + Array.prototype.square = function (): number[] { + /** + * @returns New array with squared values. + */ + return this.map((x: number) => x * x); + }; + + Array.prototype.sqrt = function (): number[] { + /** + * @returns New array with square roots of values. Throws if any element is negative. + */ + if (this.some((x) => x < 0)) + throw new Error("Cannot take square root of negative number"); + return this.map((x: number) => Math.sqrt(x)); + }; + + Array.prototype.add = function (amount: number): number[] { + /** + * @param amount - The value to add to each element in the array. + * @returns New array with added values. + */ + return this.map((x: number) => x + amount); + }; + + Array.prototype.sub = function (amount: number): number[] { + /** + * @param amount - The value to subtract from each element in the array. + * @returns New array with subtracted values. + */ + return this.map((x: number) => x - amount); + }; + + Array.prototype.mult = function (amount: number): number[] { + /** + * @param amount - The value to multiply with each element in the array. + * @returns New array with multiplied values. + */ + return this.map((x: number) => x * amount); + }; + + Array.prototype.division = function (amount: number): number[] { + /** + * @param amount - The value to divide each element in the array by. + * @returns New array with divided values. Throws if division by zero. + */ + if (amount === 0) throw new Error("Division by zero"); + return this.map((x: number) => x / amount); + }; Array.prototype.pick = function () { /** diff --git a/src/Clock.ts b/src/Clock.ts index 1e1d164..aa93ee6 100644 --- a/src/Clock.ts +++ b/src/Clock.ts @@ -1,164 +1,163 @@ // @ts-ignore -import { TransportNode } from './TransportNode'; -import TransportProcessor from './TransportProcessor?worker&url'; -import { Editor } from './main'; +import { TransportNode } from "./TransportNode"; +import TransportProcessor from "./TransportProcessor?worker&url"; +import { Editor } from "./main"; export interface TimePosition { - /** - * A position in time. - * - * @param bar - The bar number - * @param beat - The beat number - * @param pulse - The pulse number - */ - bar: number - beat: number - pulse: number + /** + * A position in time. + * + * @param bar - The bar number + * @param beat - The beat number + * @param pulse - The pulse number + */ + bar: number; + beat: number; + pulse: number; } export class Clock { + /** + * The Clock Class is responsible for keeping track of the current time. + * It is also responsible for starting and stopping the Clock TransportNode. + * + * @param app - The main application instance + * @param ctx - The current AudioContext used by app + * @param transportNode - The TransportNode helper + * @param bpm - The current beats per minute value + * @param time_signature - The time signature + * @param time_position - The current time position + * @param ppqn - The pulses per quarter note + * @param tick - The current tick since origin + */ + ctx: AudioContext; + transportNode: TransportNode | null; + private _bpm: number; + time_signature: number[]; + time_position: TimePosition; + private _ppqn: number; + tick: number; + + constructor(public app: Editor, ctx: AudioContext) { + this.time_position = { bar: 0, beat: 0, pulse: 0 }; + this.time_signature = [4, 4]; + this.tick = 0; + this._bpm = 120; + this._ppqn = 48; + this.transportNode = null; + this.ctx = ctx; + ctx.audioWorklet + .addModule(TransportProcessor) + .then((e) => { + this.transportNode = new TransportNode(ctx, {}, this.app); + this.transportNode.connect(ctx.destination); + return e; + }) + .catch((e) => { + console.log("Error loading TransportProcessor.js:", e); + }); + } + + get ticks_before_new_bar(): number { /** - * The Clock Class is responsible for keeping track of the current time. - * It is also responsible for starting and stopping the Clock TransportNode. - * - * @param app - The main application instance - * @param ctx - The current AudioContext used by app - * @param transportNode - The TransportNode helper - * @param bpm - The current beats per minute value - * @param time_signature - The time signature - * @param time_position - The current time position - * @param ppqn - The pulses per quarter note - * @param tick - The current tick since origin + * This function returns the number of ticks separating the current moment + * from the beginning of the next bar. + * + * @returns number of ticks until next bar */ + const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; + const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; + return beatsMissingFromBar * this.ppqn + ticskMissingFromBeat; + } - ctx: AudioContext - transportNode: TransportNode | null - private _bpm: number - time_signature: number[] - time_position: TimePosition - private _ppqn: number - tick: number + get next_beat_in_ticks(): number { + /** + * This function returns the number of ticks separating the current moment + * from the beginning of the next beat. + * + * @returns number of ticks until next beat + */ + return this.app.clock.pulses_since_origin + this.time_position.pulse; + } - constructor(public app: Editor, ctx: AudioContext) { - this.time_position = { bar: 0, beat: 0, pulse: 0 } - this.time_signature = [4, 4]; - this.tick = 0; - this._bpm = 120; - this._ppqn = 48; - this.transportNode = null; - this.ctx = ctx; - ctx.audioWorklet.addModule(TransportProcessor).then((e) => { - this.transportNode = new TransportNode(ctx, {}, this.app); - this.transportNode.connect(ctx.destination); - return e - }) - .catch((e) => { - console.log('Error loading TransportProcessor.js:', e); - }) - } + get beats_per_bar(): number { + /** + * Returns the number of beats per bar. + */ + return this.time_signature[0]; + } - get ticks_before_new_bar(): number { - /** - * This function returns the number of ticks separating the current moment - * from the beginning of the next bar. - * - * @returns number of ticks until next bar - */ - const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; - const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; - return (beatsMissingFromBar * this.ppqn) + ticskMissingFromBeat; - } + get beats_since_origin(): number { + /** + * Returns the number of beats since the origin. + * + * @returns number of beats since origin + */ + return Math.floor(this.tick / this.ppqn); + } - get next_beat_in_ticks(): number { - /** - * This function returns the number of ticks separating the current moment - * from the beginning of the next beat. - * - * @returns number of ticks until next beat - */ - return this.app.clock.pulses_since_origin + this.time_position.pulse; - } + get pulses_since_origin(): number { + /** + * Returns the number of pulses since the origin. + * + * @returns number of pulses since origin + */ + return this.tick; + } - get beats_per_bar(): number { - /** - * Returns the number of beats per bar. - */ - return this.time_signature[0]; - } + get pulse_duration(): number { + /** + * Returns the duration of a pulse in seconds. + */ + return 60 / this.bpm / this.ppqn; + } - get beats_since_origin(): number { - /** - * Returns the number of beats since the origin. - * - * @returns number of beats since origin - */ - return Math.floor(this.tick / this.ppqn); - } + get bpm(): number { + return this._bpm; + } - get pulses_since_origin(): number { - /** - * Returns the number of pulses since the origin. - * - * @returns number of pulses since origin - */ - return this.tick; + set bpm(bpm: number) { + this._bpm = bpm; + this.transportNode?.setBPM(bpm); + } - } + get ppqn(): number { + return this._ppqn; + } - get pulse_duration(): number { - /** - * Returns the duration of a pulse in seconds. - */ - return 60 / this.bpm / this.ppqn; - } + set ppqn(ppqn: number) { + this._ppqn = ppqn; + this.transportNode?.setPPQN(ppqn); + } - get bpm(): number { - return this._bpm; - } - - set bpm(bpm: number) { - this._bpm = bpm; - this.transportNode?.setBPM(bpm); - } + public convertPulseToSecond(n: number): number { + /** + * Converts a pulse to a second. + */ + return n * this.pulse_duration; + } - get ppqn(): number { - return this._ppqn; - } + public start(): void { + /** + * Starts the TransportNode (starts the clock). + */ + this.app.audioContext.resume(); + this.transportNode?.start(); + } - set ppqn(ppqn: number) { - this._ppqn = ppqn; - this.transportNode?.setPPQN(ppqn); - } + public pause(): void { + /** + * Pauses the TransportNode (pauses the clock). + */ + this.transportNode?.pause(); + } - public convertPulseToSecond(n: number): number { - /** - * Converts a pulse to a second. - */ - return n * this.pulse_duration - } - - public start(): void { - /** - * Starts the TransportNode (starts the clock). - */ - this.app.audioContext.resume(); - this.transportNode?.start(); - - } - - public pause(): void { - /** - * Pauses the TransportNode (pauses the clock). - */ - this.transportNode?.pause(); - } - - public stop(): void { - /** - * Stops the TransportNode (stops the clock). - */ - this.app.clock.tick = 0; - this.transportNode?.stop(); - } -} \ No newline at end of file + public stop(): void { + /** + * Stops the TransportNode (stops the clock). + */ + this.app.clock.tick = 0; + this.transportNode?.stop(); + } +} diff --git a/src/Evaluator.ts b/src/Evaluator.ts index 6d5d46f..8e58f97 100644 --- a/src/Evaluator.ts +++ b/src/Evaluator.ts @@ -100,12 +100,12 @@ export const evaluateOnce = async ( application: Editor, code: string ): Promise => { - /** - * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. - * - * @param application - The application object that contains the Editor API. - * @param code - The code to be evaluated. - * @returns A promise that resolves when the code has been evaluated. - */ + /** + * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. + * + * @param application - The application object that contains the Editor API. + * @param code - The code to be evaluated. + * @returns A promise that resolves when the code has been evaluated. + */ await tryCatchWrapper(application, code); }; diff --git a/src/IO/MidiConnection.ts b/src/IO/MidiConnection.ts index 57e3eb8..acb81ef 100644 --- a/src/IO/MidiConnection.ts +++ b/src/IO/MidiConnection.ts @@ -1,264 +1,292 @@ -export class MidiConnection{ +export class MidiConnection { + /** + * Wrapper class for Web MIDI API. Provides methods for sending MIDI messages. + * + * + * @param midiAccess - Web MIDI API access object + * @param midiOutputs - Array of MIDI output objects + * @param currentOutputIndex - Index of the currently selected MIDI output + * @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs. + */ + private midiAccess: MIDIAccess | null = null; + public midiOutputs: MIDIOutput[] = []; + private currentOutputIndex: number = 0; + private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } + + constructor() { + this.initializeMidiAccess(); + } + + private async initializeMidiAccess(): Promise { /** - * Wrapper class for Web MIDI API. Provides methods for sending MIDI messages. - * - * - * @param midiAccess - Web MIDI API access object - * @param midiOutputs - Array of MIDI output objects - * @param currentOutputIndex - Index of the currently selected MIDI output - * @param scheduledNotes - Object containing scheduled notes. Keys are note numbers and values are timeout IDs. + * Initializes Web MIDI API access and populates the list of MIDI outputs. + * + * @returns Promise */ + try { + this.midiAccess = await navigator.requestMIDIAccess(); + this.midiOutputs = Array.from(this.midiAccess.outputs.values()); + if (this.midiOutputs.length === 0) { + console.warn("No MIDI outputs available."); + this.currentOutputIndex = -1; + } + } catch (error) { + console.error("Failed to initialize MIDI:", error); + } + } - private midiAccess: MIDIAccess | null = null; - public midiOutputs: MIDIOutput[] = []; - private currentOutputIndex: number = 0; - private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } - - constructor() { - this.initializeMidiAccess(); - } - - private async initializeMidiAccess(): Promise { - /** - * Initializes Web MIDI API access and populates the list of MIDI outputs. - * - * @returns Promise - */ - try { - this.midiAccess = await navigator.requestMIDIAccess(); - this.midiOutputs = Array.from(this.midiAccess.outputs.values()); - if (this.midiOutputs.length === 0) { - console.warn('No MIDI outputs available.'); - this.currentOutputIndex = -1; - } - } catch (error) { - console.error('Failed to initialize MIDI:', error); - } - } - - public getCurrentMidiPort(): string | null { - /** - * Returns the name of the currently selected MIDI output. - * - * @returns Name of the currently selected MIDI output or null if no MIDI output is selected or available. - */ - if (this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) { - return this.midiOutputs[this.currentOutputIndex].name; - } else { - console.error('No MIDI output selected or available.'); - return null; - } + public getCurrentMidiPort(): string | null { + /** + * Returns the name of the currently selected MIDI output. + * + * @returns Name of the currently selected MIDI output or null if no MIDI output is selected or available. + */ + if ( + this.midiOutputs.length > 0 && + this.currentOutputIndex >= 0 && + this.currentOutputIndex < this.midiOutputs.length + ) { + return this.midiOutputs[this.currentOutputIndex].name; + } else { + console.error("No MIDI output selected or available."); + return null; } + } - public getCurrentMidiPortIndex(): number { - /** - * Returns the index of the currently selected MIDI output. - * - * @returns Index of the currently selected MIDI output or -1 if no MIDI output is selected or available. - */ - if(this.midiOutputs.length > 0 && this.currentOutputIndex >= 0 && this.currentOutputIndex < this.midiOutputs.length) { - return this.currentOutputIndex; - } else { - console.error('No MIDI output selected or available.'); - return -1; - } + public getCurrentMidiPortIndex(): number { + /** + * Returns the index of the currently selected MIDI output. + * + * @returns Index of the currently selected MIDI output or -1 if no MIDI output is selected or available. + */ + if ( + this.midiOutputs.length > 0 && + this.currentOutputIndex >= 0 && + this.currentOutputIndex < this.midiOutputs.length + ) { + return this.currentOutputIndex; + } else { + console.error("No MIDI output selected or available."); + return -1; } + } - public sendMidiClock(): void { - /** - * Sends a single MIDI clock message to the currently selected MIDI output. - */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xF8]); // Send a single MIDI clock message - } else { - console.error('MIDI output not available.'); - } + public sendMidiClock(): void { + /** + * Sends a single MIDI clock message to the currently selected MIDI output. + */ + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xf8]); // Send a single MIDI clock message + } else { + console.error("MIDI output not available."); } - - public switchMidiOutput(outputName: string): boolean { - /** - * Switches the currently selected MIDI output. - * - * @param outputName Name of the MIDI output to switch to - * @returns True if the MIDI output was found and switched to, false otherwise - */ - const index = this.getMidiOutputIndex(outputName); + } + + public switchMidiOutput(outputName: string): boolean { + /** + * Switches the currently selected MIDI output. + * + * @param outputName Name of the MIDI output to switch to + * @returns True if the MIDI output was found and switched to, false otherwise + */ + const index = this.getMidiOutputIndex(outputName); + if (index !== -1) { + this.currentOutputIndex = index; + return true; + } else { + return false; + } + } + + public getMidiOutputIndex(output: string | number): number { + /** + * Returns the index of the MIDI output with the specified name. + * + * @param outputName Name of the MIDI output + * @returns Index of the new MIDI output or current output if new is not valid + * + */ + 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 + }.` + ); + return this.currentOutputIndex; + } else { + return output; + } + } else { + const index = this.midiOutputs.findIndex((o) => o.name === output); if (index !== -1) { - this.currentOutputIndex = index; - return true; + return index; } else { - return false; - } - } - - public getMidiOutputIndex(output: string|number): number { - /** - * Returns the index of the MIDI output with the specified name. - * - * @param outputName Name of the MIDI output - * @returns Index of the new MIDI output or current output if new is not valid - * - */ - 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}.`); - return this.currentOutputIndex; - } else { - return output; - } - } else { - const index = this.midiOutputs.findIndex((o) => o.name === output); - if (index !== -1) { - return index; - } else { - console.error(`MIDI output "${output}" not found.`); - return this.currentOutputIndex; - } - } - } - - public listMidiOutputs(): string { - /** - * Lists all available MIDI outputs to the console. - */ - let final_string = 'Available MIDI Outputs: '; - this.midiOutputs.forEach((output, index) => { - final_string += `(${index + 1}) ${output.name} `; - }); - return final_string; - } - - public sendMidiNote(noteNumber: number, channel: number, velocity: number, duration: number, port: number|string = this.currentOutputIndex, bend: number|undefined = undefined): void { - /** - * Sending a MIDI Note on/off message with the same note number and channel. Automatically manages - * the note off message after the specified duration. - * - * @param noteNumber MIDI note number (0-127) - * @param channel MIDI channel (0-15) - * @param velocity MIDI velocity (0-127) - * @param duration Duration in milliseconds - * - */ - - if(typeof port === 'string') port = this.getMidiOutputIndex(port); - const output = this.midiOutputs[port]; - noteNumber = Math.min(Math.max(noteNumber, 0), 127); - if (output) { - const noteOnMessage = [0x90 + channel, noteNumber, velocity]; - const noteOffMessage = [0x80 + channel, noteNumber, 0]; - - // Send Note On - output.send(noteOnMessage); - - if(bend) this.sendPitchBend(bend, channel, port); - - // Schedule Note Off - const timeoutId = setTimeout(() => { - output.send(noteOffMessage); - if(bend) this.sendPitchBend(8192, channel, port); - delete this.scheduledNotes[noteNumber]; - }, (duration - 0.02) * 1000); - - this.scheduledNotes[noteNumber] = timeoutId; - } else { - console.error('MIDI output not available.'); - } - } - - public sendSysExMessage(message: number[]): void { - /** - * Sends a SysEx message to the currently selected MIDI output. - * - * @param message Array of SysEx message bytes - * - * @example - * // Send a SysEx message to set the pitch bend range to 12 semitones - * sendSysExMessage([0xF0, 0x43, 0x10, 0x4C, 0x08, 0x00, 0x01, 0x00, 0x02, 0xF7]); - */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send(message); - } else { - console.error('MIDI output not available.'); - } - } - - public sendPitchBend(value: number, channel: number, port: number|string = this.currentOutputIndex): void { - /** - * Sends a MIDI Pitch Bend message to the currently selected MIDI output. - * - * @param value MIDI pitch bend value (0-16383) - * @param channel MIDI channel (0-15) - * - */ - if (value < 0 || value > 16383) { - console.error('Invalid pitch bend value. Value must be in the range 0-16383.'); - } - if (channel < 0 || channel > 15) { - console.error('Invalid MIDI channel. Channel must be in the range 0-15.'); - } - if(typeof port === 'string') port = this.getMidiOutputIndex(port); - const output = this.midiOutputs[port]; - if (output) { - const lsb = value & 0x7F; - const msb = (value >> 7) & 0x7F; - output.send([0xE0 | channel, lsb, msb]); - } else { - console.error('MIDI output not available.'); - } - } - - public sendProgramChange(programNumber: number, channel: number): void { - /** - * Sends a MIDI Program Change message to the currently selected MIDI output. - * - * @param programNumber MIDI program number (0-127) - * @param channel MIDI channel (0-15) - * - * @example - * // Send a Program Change message to select program 1 on channel 1 - * sendProgramChange(0, 0); - */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xC0 + channel, programNumber]); // Program Change - } else { - console.error('MIDI output not available.'); - } - } - - public sendMidiControlChange(controlNumber: number, value: number, channel: number): void { - /** - * Sends a MIDI Control Change message to the currently selected MIDI output. - * - * @param controlNumber MIDI control number (0-127) - * @param value MIDI control value (0-127) - * @param channel MIDI channel (0-15) - */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - output.send([0xB0 + channel, controlNumber, value]); // Control Change - } else { - console.error('MIDI output not available.'); - } - } - - public panic(): void { - /** - * Sends a Note Off message for all scheduled notes. - */ - const output = this.midiOutputs[this.currentOutputIndex]; - if (output) { - for (const noteNumber in this.scheduledNotes) { - const timeoutId = this.scheduledNotes[noteNumber]; - clearTimeout(timeoutId); - output.send([0x80, parseInt(noteNumber), 0]); // Note Off - } - this.scheduledNotes = {}; - } else { - console.error('MIDI output not available.'); + console.error(`MIDI output "${output}" not found.`); + return this.currentOutputIndex; } } } + + public listMidiOutputs(): string { + /** + * Lists all available MIDI outputs to the console. + */ + let final_string = "Available MIDI Outputs: "; + this.midiOutputs.forEach((output, index) => { + final_string += `(${index + 1}) ${output.name} `; + }); + return final_string; + } + + public sendMidiNote( + noteNumber: number, + channel: number, + velocity: number, + duration: number, + port: number | string = this.currentOutputIndex, + bend: number | undefined = undefined + ): void { + /** + * Sending a MIDI Note on/off message with the same note number and channel. Automatically manages + * the note off message after the specified duration. + * + * @param noteNumber MIDI note number (0-127) + * @param channel MIDI channel (0-15) + * @param velocity MIDI velocity (0-127) + * @param duration Duration in milliseconds + * + */ + + if (typeof port === "string") port = this.getMidiOutputIndex(port); + const output = this.midiOutputs[port]; + noteNumber = Math.min(Math.max(noteNumber, 0), 127); + if (output) { + const noteOnMessage = [0x90 + channel, noteNumber, velocity]; + const noteOffMessage = [0x80 + channel, noteNumber, 0]; + + // Send Note On + output.send(noteOnMessage); + + if (bend) this.sendPitchBend(bend, channel, port); + + // Schedule Note Off + const timeoutId = setTimeout(() => { + output.send(noteOffMessage); + if (bend) this.sendPitchBend(8192, channel, port); + delete this.scheduledNotes[noteNumber]; + }, (duration - 0.02) * 1000); + + this.scheduledNotes[noteNumber] = timeoutId; + } else { + console.error("MIDI output not available."); + } + } + + public sendSysExMessage(message: number[]): void { + /** + * Sends a SysEx message to the currently selected MIDI output. + * + * @param message Array of SysEx message bytes + * + * @example + * // Send a SysEx message to set the pitch bend range to 12 semitones + * sendSysExMessage([0xF0, 0x43, 0x10, 0x4C, 0x08, 0x00, 0x01, 0x00, 0x02, 0xF7]); + */ + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send(message); + } else { + console.error("MIDI output not available."); + } + } + + public sendPitchBend( + value: number, + channel: number, + port: number | string = this.currentOutputIndex + ): void { + /** + * Sends a MIDI Pitch Bend message to the currently selected MIDI output. + * + * @param value MIDI pitch bend value (0-16383) + * @param channel MIDI channel (0-15) + * + */ + if (value < 0 || value > 16383) { + console.error( + "Invalid pitch bend value. Value must be in the range 0-16383." + ); + } + if (channel < 0 || channel > 15) { + console.error("Invalid MIDI channel. Channel must be in the range 0-15."); + } + if (typeof port === "string") port = this.getMidiOutputIndex(port); + const output = this.midiOutputs[port]; + if (output) { + const lsb = value & 0x7f; + const msb = (value >> 7) & 0x7f; + output.send([0xe0 | channel, lsb, msb]); + } else { + console.error("MIDI output not available."); + } + } + + public sendProgramChange(programNumber: number, channel: number): void { + /** + * Sends a MIDI Program Change message to the currently selected MIDI output. + * + * @param programNumber MIDI program number (0-127) + * @param channel MIDI channel (0-15) + * + * @example + * // Send a Program Change message to select program 1 on channel 1 + * sendProgramChange(0, 0); + */ + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xc0 + channel, programNumber]); // Program Change + } else { + console.error("MIDI output not available."); + } + } + + public sendMidiControlChange( + controlNumber: number, + value: number, + channel: number + ): void { + /** + * Sends a MIDI Control Change message to the currently selected MIDI output. + * + * @param controlNumber MIDI control number (0-127) + * @param value MIDI control value (0-127) + * @param channel MIDI channel (0-15) + */ + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + output.send([0xb0 + channel, controlNumber, value]); // Control Change + } else { + console.error("MIDI output not available."); + } + } + + public panic(): void { + /** + * Sends a Note Off message for all scheduled notes. + */ + const output = this.midiOutputs[this.currentOutputIndex]; + if (output) { + for (const noteNumber in this.scheduledNotes) { + const timeoutId = this.scheduledNotes[noteNumber]; + clearTimeout(timeoutId); + output.send([0x80, parseInt(noteNumber), 0]); // Note Off + } + this.scheduledNotes = {}; + } else { + console.error("MIDI output not available."); + } + } +} diff --git a/src/Scales.ts b/src/Scales.ts index 9488ab2..de046f8 100644 --- a/src/Scales.ts +++ b/src/Scales.ts @@ -50,25 +50,29 @@ const SCALES: Record = { hindustan: [0, 2, 4, 5, 7, 8, 10], persian: [0, 1, 4, 5, 6, 8, 11], eastIndianPurvi: [0, 1, 4, 6, 7, 8, 11], - orientalA: [0, 1, 4, 5, 6, 9, 10] + orientalA: [0, 1, 4, 5, 6, 9, 10], }; -export function scale(n: number, scaleName: string = 'major', octave: number = 4): number { - /** - * Returns the MIDI note number for the given scale degree in the given scale. - * @param {number} n - The scale degree, where 0 is the tonic. - * @param {string} scaleName - The name of the scale. - * @param {number} octave - The octave number. - * @returns {number} The MIDI note number. - */ - const scale = SCALES[scaleName]; - - if (!scale) { - throw new Error(`Unknown scale ${scaleName}`); - } - - let index = n % scale.length; - if (index < 0) index += scale.length; // adjust for negative indexes - let additionalOctaves = Math.floor(n / scale.length); - return 60 + (octave + additionalOctaves) * 12 + scale[index]; +export function scale( + n: number, + scaleName: string = "major", + octave: number = 4 +): number { + /** + * Returns the MIDI note number for the given scale degree in the given scale. + * @param {number} n - The scale degree, where 0 is the tonic. + * @param {string} scaleName - The name of the scale. + * @param {number} octave - The octave number. + * @returns {number} The MIDI note number. + */ + const scale = SCALES[scaleName]; + + if (!scale) { + throw new Error(`Unknown scale ${scaleName}`); } + + let index = n % scale.length; + if (index < 0) index += scale.length; // adjust for negative indexes + let additionalOctaves = Math.floor(n / scale.length); + return 60 + (octave + additionalOctaves) * 12 + scale[index]; +} diff --git a/src/Utils/Drunk.ts b/src/Utils/Drunk.ts index 4d7babf..b71bb4c 100644 --- a/src/Utils/Drunk.ts +++ b/src/Utils/Drunk.ts @@ -1,65 +1,63 @@ export class DrunkWalk { + /** + * A class that implements a "drunk walk" algorithm. This is useful for generating random + * numbers in a constrained range. The "drunk" starts at a position, and then makes a step + * of +1, 0, or -1. The "drunk" can be constrained to a range, and can wrap around the range. + * + * @param min - The minimum value of the range + * @param max - The maximum value of the range + * @param wrap - Whether or not the "drunk" should wrap around the range + * @param position - The starting/current position of the "drunk" + */ + public min: number; + public max: number; + private wrap: boolean; + public position: number; + + constructor(min: number, max: number, wrap: boolean) { + this.min = min; + this.max = max; + this.wrap = wrap; + this.position = 0; + } + + step(): void { /** - * A class that implements a "drunk walk" algorithm. This is useful for generating random - * numbers in a constrained range. The "drunk" starts at a position, and then makes a step - * of +1, 0, or -1. The "drunk" can be constrained to a range, and can wrap around the range. - * - * @param min - The minimum value of the range - * @param max - The maximum value of the range - * @param wrap - Whether or not the "drunk" should wrap around the range - * @param position - The starting/current position of the "drunk" + * Makes a step in the "drunk walk" algorithm. This is a random step of +1, 0, or -1. */ - public min: number; - public max: number; - private wrap: boolean; - public position: number; + const stepSize: number = Math.floor(Math.random() * 3) - 1; + this.position += stepSize; - constructor(min: number, max: number, wrap: boolean) { - this.min = min; - this.max = max; - this.wrap = wrap; - this.position = 0; + if (this.wrap) { + if (this.position > this.max) { + this.position = this.min; + } else if (this.position < this.min) { + this.position = this.max; + } + } else { + if (this.position < this.min) { + this.position = this.min; + } else if (this.position > this.max) { + this.position = this.max; + } } + } - step(): void { + getPosition(): number { + /** + * @returns The current position of the "drunk" + */ + return this.position; + } - /** - * Makes a step in the "drunk walk" algorithm. This is a random step of +1, 0, or -1. - */ - - const stepSize: number = Math.floor(Math.random() * 3) - 1; - this.position += stepSize; - - if (this.wrap) { - if (this.position > this.max) { - this.position = this.min; - } else if (this.position < this.min) { - this.position = this.max; - } - } else { - if (this.position < this.min) { - this.position = this.min; - } else if (this.position > this.max) { - this.position = this.max; - } - } - } - - getPosition(): number { - /** - * @returns The current position of the "drunk" - */ - return this.position; - } - - toggleWrap(b: boolean): void { - /** - * Whether or not the "drunk" should wrap around the range - * - * @param b - Whether or not the "drunk" should wrap around the range - */ - this.wrap = b; - } -} \ No newline at end of file + toggleWrap(b: boolean): void { + /** + * Whether or not the "drunk" should wrap around the range + * + * @param b - Whether or not the "drunk" should wrap around the range + */ + this.wrap = b; + } +} diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 2ef0ac4..9c457d2 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -1,122 +1,126 @@ -import { type Editor } from '../main'; -import { freqToMidi, resolvePitchBend, getScale, isScale, parseScala } from 'zifferjs'; +import { type Editor } from "../main"; +import { + freqToMidi, + resolvePitchBend, + getScale, + isScale, + parseScala, +} from "zifferjs"; export abstract class Event { - seedValue: string|undefined = undefined; - randomGen: Function = Math.random; - app: Editor; - values: { [key: string]: any } = {}; + seedValue: string | undefined = undefined; + randomGen: Function = Math.random; + app: Editor; + values: { [key: string]: any } = {}; - constructor(app: Editor) { - this.app = app; - if(this.app.api.currentSeed) { - this.randomGen = this.app.api.randomGen; - } + constructor(app: Editor) { + this.app = app; + if (this.app.api.currentSeed) { + this.randomGen = this.app.api.randomGen; } + } - odds = (probability: number, func: Function): Event => { - if(this.randomGen() < probability) { - return this.modify(func); - } - return this; + odds = (probability: number, func: Function): Event => { + if (this.randomGen() < probability) { + return this.modify(func); } + return this; + }; - almostNever = (func: Function): Event => { - return this.odds(0.025, func); - } + almostNever = (func: Function): Event => { + return this.odds(0.025, func); + }; - rarely = (func: Function): Event => { - return this.odds(0.1, func); - } + rarely = (func: Function): Event => { + return this.odds(0.1, func); + }; - scarcely = (func: Function): Event => { - return this.odds(0.25, func); - } + scarcely = (func: Function): Event => { + return this.odds(0.25, func); + }; - sometimes = (func: Function): Event => { - return this.odds(0.5, func); - } + sometimes = (func: Function): Event => { + return this.odds(0.5, func); + }; - often = (func: Function): Event => { - return this.odds(0.75, func); - } + often = (func: Function): Event => { + return this.odds(0.75, func); + }; - frequently = (func: Function): Event => { - return this.odds(0.9, func); - } + frequently = (func: Function): Event => { + return this.odds(0.9, func); + }; - almostAlways = (func: Function): Event => { - return this.odds(0.985, func); - } + almostAlways = (func: Function): Event => { + return this.odds(0.985, func); + }; - modify = (func: Function): Event => { - return func(this); - } + modify = (func: Function): Event => { + return func(this); + }; - seed = (value: string|number): Event => { - this.seedValue = value.toString(); - this.randomGen = this.app.api.localSeededRandom(this.seedValue); - return this; - } + seed = (value: string | number): Event => { + this.seedValue = value.toString(); + this.randomGen = this.app.api.localSeededRandom(this.seedValue); + return this; + }; - clear = (): Event => { - this.app.api.clearLocalSeed(this.seedValue); - return this; - } + clear = (): Event => { + this.app.api.clearLocalSeed(this.seedValue); + return this; + }; - apply = (func: Function): Event => { - return this.modify(func); - } - - duration = (value: number): Event => { - this.values['duration'] = value; - return this; - } + apply = (func: Function): Event => { + return this.modify(func); + }; + duration = (value: number): Event => { + this.values["duration"] = value; + return this; + }; } export abstract class AudibleEvent extends Event { - constructor(app: Editor) { - super(app); - } + constructor(app: Editor) { + super(app); + } - octave = (value: number): this => { - this.values['octave'] = value; - this.update(); - return this; - } + octave = (value: number): this => { + this.values["octave"] = value; + this.update(); + return this; + }; - key = (value: string): this => { - this.values['key'] = value; - this.update(); - return this; - } + key = (value: string): this => { + this.values["key"] = value; + this.update(); + return this; + }; - scale = (value: string): this => { - if(!isScale(value)) { - this.values.parsedScale = parseScala(value) as number[]; - } else { - this.values.scaleName = value; - this.values.parsedScale = getScale(value) as number[]; - } - this.update(); - return this; + scale = (value: string): this => { + if (!isScale(value)) { + this.values.parsedScale = parseScala(value) as number[]; + } else { + this.values.scaleName = value; + this.values.parsedScale = getScale(value) as number[]; } + this.update(); + return this; + }; - freq = (value: number): this => { - this.values['freq'] = value; - const midiNote = freqToMidi(value); - if(midiNote % 1 !== 0) { - this.values['note'] = Math.floor(midiNote); - this.values['bend'] = resolvePitchBend(midiNote)[1]; - } else { - this.values['note'] = midiNote; - } - return this; + freq = (value: number): this => { + this.values["freq"] = value; + const midiNote = freqToMidi(value); + if (midiNote % 1 !== 0) { + this.values["note"] = Math.floor(midiNote); + this.values["bend"] = resolvePitchBend(midiNote)[1]; + } else { + this.values["note"] = midiNote; } + return this; + }; - update = (): void => { - // Overwrite in subclasses - } - -} \ No newline at end of file + update = (): void => { + // Overwrite in subclasses + }; +} diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index eed14af..76eef56 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -1,95 +1,102 @@ -import { AudibleEvent } from './AbstractEvents'; -import { type Editor } from '../main'; +import { AudibleEvent } from "./AbstractEvents"; +import { type Editor } from "../main"; import { MidiConnection } from "../IO/MidiConnection"; -import { midiToFreq, noteFromPc } from 'zifferjs'; +import { midiToFreq, noteFromPc } from "zifferjs"; export class NoteEvent extends AudibleEvent { - midiConnection: MidiConnection; + midiConnection: MidiConnection; - constructor(input: number|object, public app: Editor) { - super(app); - if(typeof input === 'number') this.values['note'] = input; - else this.values = input; - this.midiConnection = app.api.MidiConnection - } + constructor(input: number | object, public app: Editor) { + super(app); + if (typeof input === "number") this.values["note"] = input; + else this.values = input; + this.midiConnection = app.api.MidiConnection; + } - note = (value: number): this => { - this.values['note'] = value; - return this; - } + note = (value: number): this => { + this.values["note"] = value; + return this; + }; - sustain = (value: number): this => { - this.values['sustain'] = value; - return this; - } + sustain = (value: number): this => { + this.values["sustain"] = value; + return this; + }; - channel = (value: number): this => { - this.values['channel'] = value; - return this; - } + channel = (value: number): this => { + this.values["channel"] = value; + return this; + }; - port = (value: number|string): this => { - this.values['port'] = this.midiConnection.getMidiOutputIndex(value); - return this; - } + port = (value: number | string): this => { + this.values["port"] = this.midiConnection.getMidiOutputIndex(value); + return this; + }; - add = (value: number): this => { - this.values.note += value; - return this; - } + add = (value: number): this => { + this.values.note += value; + return this; + }; - modify = (func: Function): this => { - const funcResult = func(this); - if(funcResult instanceof Object) { - return funcResult; - } - else { - func(this.values); - this.update(); - return this; - } + modify = (func: Function): this => { + const funcResult = func(this); + if (funcResult instanceof Object) { + return funcResult; + } else { + func(this.values); + this.update(); + return this; } + }; - bend = (value: number): this => { - this.values['bend'] = value; - return this; - } - - random = (min: number = 0, max: number = 127): this => { - min = Math.min(Math.max(min, 0), 127); - max = Math.min(Math.max(max, 0), 127); - this.values['note'] = Math.floor(this.randomGen() * (max - min + 1)) + min; - return this; - } + bend = (value: number): this => { + this.values["bend"] = value; + return this; + }; - update = (): void => { - const [note, bend] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.note = note; - this.values.freq = midiToFreq(note); - if(bend) this.values.bend = bend; - } + random = (min: number = 0, max: number = 127): this => { + min = Math.min(Math.max(min, 0), 127); + max = Math.min(Math.max(max, 0), 127); + this.values["note"] = Math.floor(this.randomGen() * (max - min + 1)) + min; + return this; + }; - out = (): void => { - const note = this.values.note ? this.values.note : 60; - const channel = this.values.channel ? this.values.channel : 0; - const velocity = this.values.velocity ? this.values.velocity : 100; - - const sustain = this.values.sustain ? - this.values.sustain * this.app.clock.pulse_duration * this.app.api.ppqn() : - this.app.clock.pulse_duration * this.app.api.ppqn(); - - const bend = this.values.bend ? this.values.bend : undefined; - - const port = this.values.port ? - this.midiConnection.getMidiOutputIndex(this.values.port) : - this.midiConnection.getCurrentMidiPortIndex(); + update = (): void => { + const [note, bend] = noteFromPc( + this.values.key || "C4", + this.values.pitch || 0, + this.values.parsedScale || "MAJOR", + this.values.octave || 0 + ); + this.values.note = note; + this.values.freq = midiToFreq(note); + if (bend) this.values.bend = bend; + }; - this.midiConnection.sendMidiNote(note, channel, velocity, sustain, port, bend); - } - -} \ No newline at end of file + out = (): void => { + const note = this.values.note ? this.values.note : 60; + const channel = this.values.channel ? this.values.channel : 0; + const velocity = this.values.velocity ? this.values.velocity : 100; + + const sustain = this.values.sustain + ? this.values.sustain * + this.app.clock.pulse_duration * + this.app.api.ppqn() + : this.app.clock.pulse_duration * this.app.api.ppqn(); + + const bend = this.values.bend ? this.values.bend : undefined; + + const port = this.values.port + ? this.midiConnection.getMidiOutputIndex(this.values.port) + : this.midiConnection.getCurrentMidiPortIndex(); + + this.midiConnection.sendMidiNote( + note, + channel, + velocity, + sustain, + port, + bend + ); + }; +} diff --git a/src/classes/RestEvent.ts b/src/classes/RestEvent.ts index 5f3d0f9..9d52711 100644 --- a/src/classes/RestEvent.ts +++ b/src/classes/RestEvent.ts @@ -1,37 +1,39 @@ -import { type Editor } from '../main'; +import { type Editor } from "../main"; import { Event } from "./AbstractEvents"; export class RestEvent extends Event { - constructor(duration: number, app: Editor) { - super(app); - this.values["duration"] = duration; - } + constructor(duration: number, app: Editor) { + super(app); + this.values["duration"] = duration; + } - _fallbackMethod = (): Event => { - return RestEvent.createRestProxy(this.values["duration"], this.app); - } + _fallbackMethod = (): Event => { + return RestEvent.createRestProxy(this.values["duration"], this.app); + }; - public static createRestProxy = (duration: number, app: Editor): RestEvent => { - const instance = new RestEvent(duration, app); - return new Proxy(instance, { - // @ts-ignore - get(target, propKey, receiver) { - // @ts-ignore - if (typeof target[propKey] === 'undefined') { - return target._fallbackMethod; - } - // @ts-ignore - return target[propKey]; - }, - // @ts-ignore - set(target, propKey, value, receiver) { - return false; - } - }); - } + public static createRestProxy = ( + duration: number, + app: Editor + ): RestEvent => { + const instance = new RestEvent(duration, app); + return new Proxy(instance, { + // @ts-ignore + get(target, propKey, receiver) { + // @ts-ignore + if (typeof target[propKey] === "undefined") { + return target._fallbackMethod; + } + // @ts-ignore + return target[propKey]; + }, + // @ts-ignore + set(target, propKey, value, receiver) { + return false; + }, + }); + }; - out = (): void => { - // TODO? - } - -} \ No newline at end of file + out = (): void => { + // TODO? + }; +} diff --git a/src/classes/SkipEvent.ts b/src/classes/SkipEvent.ts index a9bc881..04f97c3 100644 --- a/src/classes/SkipEvent.ts +++ b/src/classes/SkipEvent.ts @@ -1,28 +1,26 @@ export class SkipEvent { + _fallbackMethod = (): SkipEvent => { + return SkipEvent.createSkipProxy(); + }; - _fallbackMethod = (): SkipEvent => { - return SkipEvent.createSkipProxy(); - } - - public static createSkipProxy = (): SkipEvent => { - const instance = new SkipEvent(); - return new Proxy(instance, { - // @ts-ignore - get(target, propKey, receiver) { - // @ts-ignore - if (typeof target[propKey] === 'undefined') { - return target._fallbackMethod; - } - // @ts-ignore - return target[propKey]; - }, - // @ts-ignore - set(target, propKey, value, receiver) { - return false; - } - }); - } + public static createSkipProxy = (): SkipEvent => { + const instance = new SkipEvent(); + return new Proxy(instance, { + // @ts-ignore + get(target, propKey, receiver) { + // @ts-ignore + if (typeof target[propKey] === "undefined") { + return target._fallbackMethod; + } + // @ts-ignore + return target[propKey]; + }, + // @ts-ignore + set(target, propKey, value, receiver) { + return false; + }, + }); + }; - out = (): void => {} - - } \ No newline at end of file + out = (): void => {}; +} diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 4daff76..2d33a16 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -238,13 +238,13 @@ export class SoundEvent extends AudibleEvent { sus = this.sustain; update = (): void => { - const [note, _] = noteFromPc( - this.values.key || "C4", - this.values.pitch || 0, - this.values.parsedScale || "MAJOR", - this.values.octave || 0 - ); - this.values.freq = midiToFreq(note); + const [note, _] = noteFromPc( + this.values.key || "C4", + this.values.pitch || 0, + this.values.parsedScale || "MAJOR", + this.values.octave || 0 + ); + this.values.freq = midiToFreq(note); }; out = (): object => { diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index b083372..7f8e093 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -9,194 +9,204 @@ import { RestEvent } from "./RestEvent"; export type InputOptions = { [key: string]: string | number }; export class Player extends Event { - input: string; - ziffers: Ziffers; - initCallTime: number = 1; - startCallTime: number = 1; - lastCallTime: number = 1; - waitTime = 0; - startBeat: number = 0; - played: boolean = false; - current!: Pitch|Chord|ZRest; - retro: boolean = false; - index: number = -1; - zid: string = ""; - options: InputOptions = {}; - skipIndex = 0; - endTime = 0; + input: string; + ziffers: Ziffers; + initCallTime: number = 1; + startCallTime: number = 1; + lastCallTime: number = 1; + waitTime = 0; + startBeat: number = 0; + played: boolean = false; + current!: Pitch | Chord | ZRest; + retro: boolean = false; + index: number = -1; + zid: string = ""; + options: InputOptions = {}; + skipIndex = 0; + endTime = 0; - constructor(input: string, options: InputOptions, public app: Editor) { - super(app); - this.input = input; - this.options = options; - this.ziffers = new Ziffers(input, options); + constructor(input: string, options: InputOptions, public app: Editor) { + super(app); + this.input = input; + this.options = options; + this.ziffers = new Ziffers(input, options); + } + + get ticks(): number { + const dur = this.ziffers.duration; + return dur * 4 * this.app.clock.ppqn; + } + + nextEndTime(): number { + return this.startCallTime + this.ticks; + } + + updateLastCallTime(): void { + if (this.notStarted() || this.played) { + this.lastCallTime = this.app.clock.pulses_since_origin; + this.played = false; + } + } + + notStarted(): boolean { + return this.ziffers.notStarted(); + } + + next = (): Pitch | Chord | ZRest => { + this.current = this.ziffers.next() as Pitch | Chord | ZRest; + this.played = true; + return this.current; + }; + + pulseToSecond = (pulse: number): number => { + return this.app.clock.convertPulseToSecond(pulse); + }; + + firstRun = (): boolean => { + return this.notStarted(); + }; + + atTheBeginning = (): boolean => { + return this.skipIndex === 0 && this.ziffers.index <= 0; + }; + + origin = (): number => { + return this.app.clock.pulses_since_origin; + }; + + pulse = (): number => { + return this.app.clock.time_position.pulse; + }; + + beat = (): number => { + return this.app.clock.time_position.beat; + }; + + nextBeat = (): number => { + return this.app.clock.next_beat_in_ticks; + }; + + // Check if it's time to play the event + areWeThereYet = (): boolean => { + // If clock has stopped + if (this.app.clock.pulses_since_origin < this.lastCallTime) { + this.lastCallTime = 1; + this.startCallTime = 1; + this.index = 0; + this.waitTime = 0; } - get ticks(): number { - const dur = this.ziffers.duration; - return dur * 4 * this.app.clock.ppqn; + // Main logic + const howAboutNow = + // If pattern is just starting + (this.notStarted() && + (this.app.clock.time_position.pulse === 1 || + this.app.clock.pulses_since_origin >= + this.app.clock.next_beat_in_ticks) && + this.app.clock.pulses_since_origin >= this.waitTime) || // If pattern is already playing + (this.current && + this.pulseToSecond(this.app.clock.pulses_since_origin) >= + this.pulseToSecond(this.lastCallTime) + + this.current.duration * + 4 * + this.pulseToSecond(this.app.api.ppqn()) && + this.app.clock.pulses_since_origin >= this.waitTime); + + // Increment index of how many times call is skipped + this.skipIndex = howAboutNow ? 0 : this.skipIndex + 1; + + // Increment index of how many times sound/midi have been called + this.index = howAboutNow ? this.index + 1 : this.index; + + if (howAboutNow && this.notStarted()) { + this.initCallTime = this.app.clock.pulses_since_origin; } - nextEndTime(): number { - return this.startCallTime + this.ticks; + if (this.atTheBeginning()) { + this.startCallTime = this.app.clock.pulses_since_origin; } - updateLastCallTime(): void { - if (this.notStarted() || this.played) { - this.lastCallTime = this.app.clock.pulses_since_origin; - this.played = false; - } - } + return howAboutNow; + }; - notStarted(): boolean { - return this.ziffers.notStarted(); - } - - next = (): Pitch|Chord|ZRest => { - this.current = this.ziffers.next() as Pitch|Chord|ZRest; - this.played = true; - return this.current; - } - - pulseToSecond = (pulse: number): number => { - return this.app.clock.convertPulseToSecond(pulse); - } - - firstRun = (): boolean => { - return this.notStarted(); - } - - atTheBeginning = (): boolean => { - return this.skipIndex === 0 && this.ziffers.index<=0; - } - - origin = (): number => { - return this.app.clock.pulses_since_origin; - } - - pulse = (): number => { - return this.app.clock.time_position.pulse; - } - - beat = (): number => { - return this.app.clock.time_position.beat; - } - - nextBeat = (): number => { - return this.app.clock.next_beat_in_ticks; - } - - // Check if it's time to play the event - areWeThereYet = (): boolean => { - - // If clock has stopped - if(this.app.clock.pulses_since_origin= this.app.clock.next_beat_in_ticks) && - (this.app.clock.pulses_since_origin >= this.waitTime) - ) - || - ( // If pattern is already playing - this.current && - (this.pulseToSecond(this.app.clock.pulses_since_origin) >= - this.pulseToSecond(this.lastCallTime) + - (this.current.duration*4) * this.pulseToSecond(this.app.api.ppqn())) && - (this.app.clock.pulses_since_origin >= this.waitTime) - ) + sound(name: string) { + if (this.areWeThereYet()) { + const event = this.next() as Pitch | Chord | ZRest; + if (event instanceof Pitch) { + const obj = event.getExisting( + "freq", + "pitch", + "key", + "scale", + "octave", + "parsedScale" ); - - // Increment index of how many times call is skipped - this.skipIndex = howAboutNow ? 0 : this.skipIndex+1; - - // Increment index of how many times sound/midi have been called - this.index = howAboutNow ? this.index+1 : this.index; - - - if(howAboutNow && this.notStarted()) { - this.initCallTime = this.app.clock.pulses_since_origin; - } - - if(this.atTheBeginning()) { - this.startCallTime = this.app.clock.pulses_since_origin; - } - - return howAboutNow; + return new SoundEvent(obj, this.app).sound(name); + } else if (event instanceof ZRest) { + return RestEvent.createRestProxy(event.duration, this.app); + } + } else { + return SkipEvent.createSkipProxy(); } + } - sound(name: string) { - if(this.areWeThereYet()) { - const event = this.next() as Pitch|Chord|ZRest; - if(event instanceof Pitch) { - const obj = event.getExisting("freq","pitch","key","scale","octave","parsedScale"); - return new SoundEvent(obj, this.app).sound(name); - } else if(event instanceof ZRest) { - return RestEvent.createRestProxy(event.duration, this.app); - } - } else { - return SkipEvent.createSkipProxy(); - } + midi(value: number | undefined = undefined) { + if (this.areWeThereYet()) { + const event = this.next() as Pitch | Chord | ZRest; + if (event instanceof Pitch) { + const obj = event.getExisting( + "note", + "pitch", + "bend", + "key", + "scale", + "octave", + "parsedScale" + ); + const note = new NoteEvent(obj, this.app); + return value ? note.note(value) : note; + } else if (event instanceof ZRest) { + return RestEvent.createRestProxy(event.duration, this.app); + } + } else { + return SkipEvent.createSkipProxy(); } + } - midi(value: number|undefined = undefined) { - if(this.areWeThereYet()) { - const event = this.next() as Pitch|Chord|ZRest; - if(event instanceof Pitch) { - const obj = event.getExisting("note","pitch","bend","key","scale","octave","parsedScale"); - const note = new NoteEvent(obj, this.app); - return value ? note.note(value) : note; - } else if(event instanceof ZRest) { - return RestEvent.createRestProxy(event.duration, this.app); - } - } else { - return SkipEvent.createSkipProxy(); - } - } + scale(name: string) { + if (this.atTheBeginning()) this.ziffers.scale(name); + return this; + } - scale(name: string) { - if(this.atTheBeginning()) this.ziffers.scale(name); + key(name: string) { + if (this.atTheBeginning()) this.ziffers.key(name); + return this; + } + + octave(value: number) { + if (this.atTheBeginning()) this.ziffers.octave(value); + return this; + } + + retrograde() { + if (this.atTheBeginning()) this.ziffers.retrograde(); + return this; + } + + wait(value: number | Function) { + if (this.atTheBeginning()) { + if (typeof value === "function") { + const refPat = this.app.api.patternCache.get(value.name) as Player; + if (refPat) this.waitTime = refPat.nextEndTime(); return this; + } + this.waitTime = + this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn); } + return this; + } - key(name: string) { - if(this.atTheBeginning()) this.ziffers.key(name); - return this; - } - - octave(value: number) { - if(this.atTheBeginning()) this.ziffers.octave(value); - return this; - } - - retrograde() { - if(this.atTheBeginning()) this.ziffers.retrograde(); - return this; - } - - wait(value: number|Function) { - if(this.atTheBeginning()) { - if(typeof value === "function") { - const refPat = this.app.api.patternCache.get(value.name) as Player; - if(refPat) this.waitTime = refPat.nextEndTime(); - return this; - } - this.waitTime = this.origin() + Math.ceil(value*4*this.app.clock.ppqn); - } - return this; - } - - out = (): void => { - // TODO? - } - - -} \ No newline at end of file + out = (): void => { + // TODO? + }; +} diff --git a/src/main.ts b/src/main.ts index 5bb79fa..c0d454a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,4 @@ -import { - uniqueNamesGenerator, - colors, - animals, -} from "unique-names-generator"; +import { uniqueNamesGenerator, colors, animals } from "unique-names-generator"; import { examples } from "./examples/excerpts"; import { EditorState, Compartment } from "@codemirror/state"; import { ViewUpdate, lineNumbers, keymap } from "@codemirror/view"; @@ -28,7 +24,7 @@ import { } from "./AppSettings"; import { tryEvaluate } from "./Evaluator"; // @ts-ignore -import { gzipSync, decompressSync, strFromU8 } from 'fflate'; +import { gzipSync, decompressSync, strFromU8 } from "fflate"; // Importing showdown and setting up the markdown converter import showdown from "showdown"; @@ -47,7 +43,7 @@ const classMap = { 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", + summary: "font-semibold text-xl", table: "justify-center lg:my-8 my-2 lg:mx-8 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse", thead: @@ -87,14 +83,16 @@ export class Editor { view: EditorView; clock: Clock; manualPlay: boolean = false; - isPlaying: boolean = false; + isPlaying: boolean = false; // Mouse position public _mouseX: number = 0; public _mouseY: number = 0; - // Topos Logo - topos_logo: HTMLElement = document.getElementById('topos-logo') as HTMLElement; + // Topos Logo + topos_logo: HTMLElement = document.getElementById( + "topos-logo" + ) as HTMLElement; // Transport elements play_buttons: HTMLButtonElement[] = [ @@ -108,7 +106,9 @@ export class Editor { document.getElementById("clear-button-1") as HTMLButtonElement, //document.getElementById("clear-button-2") as HTMLButtonElement, ]; - load_universe_button: HTMLButtonElement = document.getElementById("load-universe-button") as HTMLButtonElement; + load_universe_button: HTMLButtonElement = document.getElementById( + "load-universe-button" + ) as HTMLButtonElement; documentation_button: HTMLButtonElement = document.getElementById( "doc-button-1" @@ -201,10 +201,10 @@ export class Editor { this.selected_universe = "Welcome"; this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`; - // Picking a random example to populate the landing page - let random_example = examples[Math.floor(Math.random() * examples.length)]; - this.universes[this.selected_universe].global.committed = random_example; - this.universes[this.selected_universe].global.candidate = random_example; + // Picking a random example to populate the landing page + let random_example = examples[Math.floor(Math.random() * examples.length)]; + this.universes[this.selected_universe].global.committed = random_example; + this.universes[this.selected_universe].global.candidate = random_example; // ================================================================================ // Audio context and clock @@ -255,9 +255,11 @@ export class Editor { // ================================================================================ // Building the documentation - let pre_loading = async () => { await loadSamples(); }; - pre_loading(); - this.docs = documentation_factory(this); + let pre_loading = async () => { + await loadSamples(); + }; + pre_loading(); + this.docs = documentation_factory(this); // ================================================================================ // ================================================================================ @@ -265,7 +267,6 @@ export class Editor { // ================================================================================ window.addEventListener("keydown", (event: KeyboardEvent) => { - if (event.key === "Tab") { event.preventDefault(); } @@ -278,15 +279,15 @@ export class Editor { if (event.ctrlKey && event.key === "p") { event.preventDefault(); - if (this.isPlaying) { - this.isPlaying = false; - this.setButtonHighlighting("pause", true); - this.clock.pause(); - } else { - this.isPlaying = true; - this.setButtonHighlighting("play", true); - this.clock.start(); - } + if (this.isPlaying) { + this.isPlaying = false; + this.setButtonHighlighting("pause", true); + this.clock.pause(); + } else { + this.isPlaying = true; + this.setButtonHighlighting("play", true); + this.clock.start(); + } } // Ctrl + Shift + V: Vim Mode @@ -324,7 +325,7 @@ export class Editor { if (event.ctrlKey && event.key === "b") { event.preventDefault(); this.hideDocumentation(); - this.updateKnownUniversesView(); + this.updateKnownUniversesView(); this.openBuffersModal(); } @@ -372,10 +373,10 @@ export class Editor { if (event.keyCode === keycode) { event.preventDefault(); if (event.ctrlKey) { - event.preventDefault(); + event.preventDefault(); this.api.script(keycode - 111); } else { - event.preventDefault(); + event.preventDefault(); this.changeModeFromInterface("local"); this.changeToLocalBuffer(index); this.hideDocumentation(); @@ -385,12 +386,12 @@ export class Editor { ); if (event.keyCode == 121) { - event.preventDefault(); + event.preventDefault(); this.changeModeFromInterface("global"); this.hideDocumentation(); } if (event.keyCode == 122) { - event.preventDefault(); + event.preventDefault(); this.changeModeFromInterface("init"); this.hideDocumentation(); } @@ -418,24 +419,24 @@ export class Editor { }); } - this.topos_logo.addEventListener("click", () => { - this.hideDocumentation(); - this.updateKnownUniversesView(); - this.openBuffersModal(); - }) + this.topos_logo.addEventListener("click", () => { + this.hideDocumentation(); + this.updateKnownUniversesView(); + this.openBuffersModal(); + }); this.play_buttons.forEach((button) => { button.addEventListener("click", () => { - if (this.isPlaying) { - this.setButtonHighlighting("pause", true); - this.isPlaying = !this.isPlaying; - this.clock.pause(); - } else { - this.setButtonHighlighting("play", true); - this.isPlaying = !this.isPlaying; - this.clock.start(); - } - }); + if (this.isPlaying) { + this.setButtonHighlighting("pause", true); + this.isPlaying = !this.isPlaying; + this.clock.pause(); + } else { + this.setButtonHighlighting("play", true); + this.isPlaying = !this.isPlaying; + this.clock.start(); + } + }); }); this.clear_buttons.forEach((button) => { @@ -452,17 +453,16 @@ export class Editor { this.showDocumentation(); }); - this.load_universe_button.addEventListener("click", () => { - let query = this.buffer_search.value; - if (query.length > 2 && query.length < 20 && !query.includes(" ")) { - this.loadUniverse(query); - this.settings.selected_universe = query; - this.buffer_search.value = ""; - this.closeBuffersModal(); - this.view.focus(); - this.emptyUrl(); - } + let query = this.buffer_search.value; + if (query.length > 2 && query.length < 20 && !query.includes(" ")) { + this.loadUniverse(query); + this.settings.selected_universe = query; + this.buffer_search.value = ""; + this.closeBuffersModal(); + this.view.focus(); + this.emptyUrl(); + } }); this.eval_button.addEventListener("click", () => { @@ -470,7 +470,6 @@ export class Editor { this.flashBackground("#2d313d", 200); }); - this.stop_buttons.forEach((button) => { button.addEventListener("click", () => { this.setButtonHighlighting("stop", true); @@ -514,7 +513,7 @@ export class Editor { }); this.close_universes_button.addEventListener("click", () => { - this.openBuffersModal(); + this.openBuffersModal(); }); this.font_size_slider.addEventListener("input", () => { @@ -565,13 +564,12 @@ export class Editor { }); this.universe_creator.addEventListener("submit", (event) => { - event.preventDefault(); let data = new FormData(this.universe_creator); - let universeName = data.get("universe") as string|null; + let universeName = data.get("universe") as string | null; - if(universeName){ + if (universeName) { if (universeName.length > 2 && universeName.length < 20) { this.loadUniverse(universeName); this.settings.selected_universe = universeName; @@ -603,17 +601,17 @@ export class Editor { ].forEach((e) => { let name = `docs_` + e; document.getElementById(name)!.addEventListener("click", async () => { - if (name !== "docs_samples") { - this.currentDocumentationPane = e; - this.updateDocumentationContent(); - } else { - console.log('Loading samples!'); - await loadSamples().then(() => { - this.docs = documentation_factory(this) - this.currentDocumentationPane = e; - this.updateDocumentationContent(); - }); - } + if (name !== "docs_samples") { + this.currentDocumentationPane = e; + this.updateDocumentationContent(); + } else { + console.log("Loading samples!"); + await loadSamples().then(() => { + this.docs = documentation_factory(this); + this.currentDocumentationPane = e; + this.updateDocumentationContent(); + }); + } }); }); @@ -656,14 +654,18 @@ export class Editor { if (url !== null) { const universeParam = url.get("universe"); if (universeParam !== null) { - let data = Uint8Array.from(atob(universeParam), c => c.charCodeAt(0)) + 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], + length: 2, + separator: "_", + dictionaries: [colors, animals], }); this.loadUniverse(randomName, new_universe["universe"]); - this.emptyUrl(); this.emptyUrl(); + this.emptyUrl(); + this.emptyUrl(); } } } @@ -695,30 +697,30 @@ export class Editor { return JSON.parse(hash); }; - updateKnownUniversesView = () => { - let existing_universes = document.getElementById("existing-universes"); - let known_universes = Object.keys(this.universes); - let final_html = "
    "; - known_universes.forEach((name) => { - final_html += ` + updateKnownUniversesView = () => { + let existing_universes = document.getElementById("existing-universes"); + let known_universes = Object.keys(this.universes); + let final_html = + "
      "; + known_universes.forEach((name) => { + final_html += `
    • ${name}

    • `; - }); - final_html = final_html + "
    "; - existing_universes!.innerHTML = final_html; - } + }); + final_html = final_html + "
"; + existing_universes!.innerHTML = final_html; + }; async share() { - - async function bufferToBase64(buffer:Uint8Array) { - const base64url: string = await new Promise(r => { - const reader = new FileReader() - reader.onload = () => r(reader.result as string) - reader.readAsDataURL(new Blob([buffer])) + 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); + return base64url.slice(base64url.indexOf(",") + 1); } let data = JSON.stringify({ @@ -852,22 +854,22 @@ export class Editor { button: "play" | "pause" | "stop" | "clear", highlight: boolean ) { - document.getElementById('play-label')!.textContent = button !== "pause" ? "Pause" : "Play"; - if (button !== "pause") { - document.getElementById('pause-icon')!.classList.remove('hidden'); - document.getElementById('play-icon')!.classList.add('hidden'); - } else { - document.getElementById('pause-icon')!.classList.add('hidden'); - document.getElementById('play-icon')!.classList.remove('hidden'); - } - - if (button === "stop") { - this.isPlaying == false; - document.getElementById('play-label')!.textContent = "Play"; - document.getElementById('pause-icon')!.classList.add('hidden'); - document.getElementById('play-icon')!.classList.remove('hidden'); - } + document.getElementById("play-label")!.textContent = + button !== "pause" ? "Pause" : "Play"; + if (button !== "pause") { + document.getElementById("pause-icon")!.classList.remove("hidden"); + document.getElementById("play-icon")!.classList.add("hidden"); + } else { + document.getElementById("pause-icon")!.classList.add("hidden"); + document.getElementById("play-icon")!.classList.remove("hidden"); + } + if (button === "stop") { + this.isPlaying == false; + document.getElementById("play-label")!.textContent = "Play"; + document.getElementById("pause-icon")!.classList.add("hidden"); + document.getElementById("play-icon")!.classList.remove("hidden"); + } this.flashBackground("#2d313d", 200); const possible_selectors = [ diff --git a/src/themes/materialDark.ts b/src/themes/materialDark.ts index 4dd8beb..c84f33a 100644 --- a/src/themes/materialDark.ts +++ b/src/themes/materialDark.ts @@ -1,116 +1,125 @@ -import { EditorView } from '@codemirror/view' -import { Extension } from '@codemirror/state' -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' -import { tags as t } from '@lezer/highlight' +import { EditorView } from "@codemirror/view"; +import { Extension } from "@codemirror/state"; +import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"; +import { tags as t } from "@lezer/highlight"; -const base00 = '#171717', base01 = '#505d64', - base02 = 'white', base03 = '#707d8b', - base04 = '#a0a4ae', base05 = '#bdbdbd', - base06 = '#e0e0e0', base07 = '#fdf6e3', - base_red = '#ff5f52', base_deeporange = '#ff6e40', - base_pink = '#fa5788', base_yellow = '#facf4e', - base_orange = '#ffad42', base_cyan = '#1E6AE1', - base_indigo = '#7186f0', base_purple = '#D09EBF', - base_green = '#82d47c', base_lightgreen = '#82d47c', - base_teal = '#4ebaaa' +const base00 = "#171717", + base01 = "#505d64", + base02 = "white", + base03 = "#707d8b", + base04 = "#a0a4ae", + base05 = "#bdbdbd", + base06 = "#e0e0e0", + base07 = "#fdf6e3", + base_red = "#ff5f52", + base_deeporange = "#ff6e40", + base_pink = "#fa5788", + base_yellow = "#facf4e", + base_orange = "#ffad42", + base_cyan = "#1E6AE1", + base_indigo = "#7186f0", + base_purple = "#D09EBF", + base_green = "#82d47c", + base_lightgreen = "#82d47c", + base_teal = "#4ebaaa"; const invalid = base_red, - darkBackground = '#fdf6e3', - highlightBackground = '#545b61', - background = base00, - tooltipBackground = base01, - selection = base07, - cursor = base04 + darkBackground = "#fdf6e3", + highlightBackground = "#545b61", + background = base00, + tooltipBackground = base01, + selection = base07, + cursor = base04; /// The editor theme styles for Material Dark. export const materialDarkTheme = EditorView.theme( { - '&': { + "&": { color: base05, backgroundColor: background, - fontSize: '24px', + fontSize: "24px", }, - '.cm-content': { - caretColor: cursor + ".cm-content": { + caretColor: cursor, }, - '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, - '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': + ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor }, + "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: selection, border: `0.5px solid ${base_teal}` }, - '.cm-panels': { backgroundColor: darkBackground, color: base03 }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, + ".cm-panels": { backgroundColor: darkBackground, color: base03 }, + ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" }, + ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" }, - '.cm-searchMatch': { + ".cm-searchMatch": { outline: `1px solid ${base_yellow}`, - backgroundColor: 'transparent' + backgroundColor: "transparent", }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: highlightBackground + ".cm-searchMatch.cm-searchMatch-selected": { + backgroundColor: highlightBackground, }, - '.cm-activeLine': { backgroundColor: highlightBackground }, - '.cm-selectionMatch': { + ".cm-activeLine": { backgroundColor: highlightBackground }, + ".cm-selectionMatch": { backgroundColor: darkBackground, - outline: `1px solid ${base_teal}` + outline: `1px solid ${base_teal}`, }, - '&.cm-focused .cm-matchingBracket': { + "&.cm-focused .cm-matchingBracket": { color: base06, - outline: `1px solid ${base_teal}` + outline: `1px solid ${base_teal}`, }, - '&.cm-focused .cm-nonmatchingBracket': { - color: base_red + "&.cm-focused .cm-nonmatchingBracket": { + color: base_red, }, - '.cm-gutters': { + ".cm-gutters": { backgroundColor: base00, borderRight: `1px solid ${base07}`, - color: base02 + color: base02, }, - '.cm-activeLineGutter': { + ".cm-activeLineGutter": { backgroundColor: highlightBackground, - color: base07 + color: base07, }, - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', + ".cm-foldPlaceholder": { + backgroundColor: "transparent", + border: "none", color: `${base07}`, }, - '.cm-tooltip': { - border: 'none', - backgroundColor: tooltipBackground + ".cm-tooltip": { + border: "none", + backgroundColor: tooltipBackground, }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent' + ".cm-tooltip .cm-tooltip-arrow:before": { + borderTopColor: "transparent", + borderBottomColor: "transparent", }, - '.cm-tooltip .cm-tooltip-arrow:after': { + ".cm-tooltip .cm-tooltip-arrow:after": { borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground + borderBottomColor: tooltipBackground, }, - '.cm-tooltip-autocomplete': { - '& > ul > li[aria-selected]': { + ".cm-tooltip-autocomplete": { + "& > ul > li[aria-selected]": { backgroundColor: highlightBackground, - color: base03 - } - } + color: base03, + }, + }, }, { dark: true } -) +); /// The highlighting style for code in the Material Dark theme. export const materialDarkHighlightStyle = HighlightStyle.define([ { tag: t.keyword, color: base_purple }, { tag: [t.name, t.deleted, t.character, t.macroName], - color: base_cyan + color: base_cyan, }, { tag: [t.propertyName], color: base_yellow }, { tag: [t.variableName], color: base05 }, @@ -118,93 +127,93 @@ export const materialDarkHighlightStyle = HighlightStyle.define([ { tag: [t.labelName], color: base_purple }, { tag: [t.color, t.constant(t.name), t.standard(t.name)], - color: base_yellow + color: base_yellow, }, { tag: [t.definition(t.name), t.separator], color: base_pink }, { tag: [t.brace], color: base_purple }, { tag: [t.annotation], - color: invalid + color: invalid, }, { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], - color: base_orange + color: base_orange, }, { tag: [t.typeName, t.className], - color: base_orange + color: base_orange, }, { tag: [t.operator, t.operatorKeyword], - color: base_indigo + color: base_indigo, }, { tag: [t.tagName], - color: base_deeporange + color: base_deeporange, }, { tag: [t.squareBracket], - color: base_red + color: base_red, }, { tag: [t.angleBracket], - color: base02 + color: base02, }, { tag: [t.attributeName], - color: base05 + color: base05, }, { tag: [t.regexp], - color: invalid + color: invalid, }, { tag: [t.quote], - color: base_green + color: base_green, }, { tag: [t.string], color: base_lightgreen }, { tag: t.link, color: base_cyan, - textDecoration: 'underline', - textUnderlinePosition: 'under' + textDecoration: "underline", + textUnderlinePosition: "under", }, { tag: [t.url, t.escape, t.special(t.string)], - color: base_yellow + color: base_yellow, }, { tag: [t.meta], color: base03 }, - { tag: [t.comment], color: base02, fontStyle: 'italic' }, + { tag: [t.comment], color: base02, fontStyle: "italic" }, { tag: t.monospace, color: base05 }, - { tag: t.strong, fontWeight: 'bold', color: base_red }, - { tag: t.emphasis, fontStyle: 'italic', color: base_lightgreen }, - { tag: t.strikethrough, textDecoration: 'line-through' }, - { tag: t.heading, fontWeight: 'bold', color: base_yellow }, - { tag: t.heading1, fontWeight: 'bold', color: base_yellow }, + { tag: t.strong, fontWeight: "bold", color: base_red }, + { tag: t.emphasis, fontStyle: "italic", color: base_lightgreen }, + { tag: t.strikethrough, textDecoration: "line-through" }, + { tag: t.heading, fontWeight: "bold", color: base_yellow }, + { tag: t.heading1, fontWeight: "bold", color: base_yellow }, { tag: [t.heading2, t.heading3, t.heading4], - fontWeight: 'bold', - color: base_yellow + fontWeight: "bold", + color: base_yellow, }, { tag: [t.heading5, t.heading6], - color: base_yellow + color: base_yellow, }, { tag: [t.atom, t.bool, t.special(t.variableName)], color: base_cyan }, { tag: [t.processingInstruction, t.inserted], - color: base_red + color: base_red, }, { tag: [t.contentSeparator], - color: base_cyan + color: base_cyan, }, - { tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` } -]) + { tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` }, +]); /// Extension to enable the Material Dark theme (both the editor theme and /// the highlight style). export const materialDark: Extension = [ materialDarkTheme, - syntaxHighlighting(materialDarkHighlightStyle) -] \ No newline at end of file + syntaxHighlighting(materialDarkHighlightStyle), +];