Running Prettier on most files

This commit is contained in:
2023-08-30 12:04:49 +02:00
parent dca176cb04
commit fe0e46ec90
15 changed files with 1281 additions and 1198 deletions

View File

@ -51,25 +51,24 @@ export class UserAPI {
private errorTimeoutID: number = 0; private errorTimeoutID: number = 0;
private printTimeoutID: number = 0; private printTimeoutID: number = 0;
MidiConnection: MidiConnection = new MidiConnection(); MidiConnection: MidiConnection = new MidiConnection();
load: samples; load: samples;
constructor(public app: Editor) {} constructor(public app: Editor) {}
_loadUniverseFromInterface = (universe: string) => { _loadUniverseFromInterface = (universe: string) => {
this.app.loadUniverse(universe as string); this.app.loadUniverse(universe as string);
this.app.openBuffersModal(); this.app.openBuffersModal();
} };
_deleteUniverseFromInterface = (universe: string) => { _deleteUniverseFromInterface = (universe: string) => {
delete this.app.universes[universe]; delete this.app.universes[universe];
this.app.settings.saveApplicationToLocalStorage( this.app.settings.saveApplicationToLocalStorage(
this.app.universes, this.app.universes,
this.app.settings this.app.settings
); );
this.app.updateKnownUniversesView(); this.app.updateKnownUniversesView();
} };
_playDocExample = (code?: string) => { _playDocExample = (code?: string) => {
this.play(); this.play();
@ -82,7 +81,6 @@ export class UserAPI {
); );
}; };
_playDocExampleOnce = (code?: string) => { _playDocExampleOnce = (code?: string) => {
this.play(); this.play();
console.log("Executing documentation example: " + this.app.selectedExample); console.log("Executing documentation example: " + this.app.selectedExample);
@ -98,7 +96,7 @@ export class UserAPI {
clearTimeout(this.errorTimeoutID); clearTimeout(this.errorTimeoutID);
clearTimeout(this.printTimeoutID); clearTimeout(this.printTimeoutID);
this.app.error_line.innerHTML = error as string; 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.app.error_line.classList.remove("hidden");
this.errorTimeoutID = setTimeout( this.errorTimeoutID = setTimeout(
() => this.app.error_line.classList.add("hidden"), () => this.app.error_line.classList.add("hidden"),
@ -111,7 +109,7 @@ export class UserAPI {
clearTimeout(this.printTimeoutID); clearTimeout(this.printTimeoutID);
clearTimeout(this.errorTimeoutID); clearTimeout(this.errorTimeoutID);
this.app.error_line.innerHTML = message as string; 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.app.error_line.classList.remove("hidden");
this.printTimeoutID = setTimeout( this.printTimeoutID = setTimeout(
() => this.app.error_line.classList.add("hidden"), () => this.app.error_line.classList.add("hidden"),
@ -151,10 +149,10 @@ export class UserAPI {
// Mouse functions // Mouse functions
// ============================================================= // =============================================================
onmousemove = (e: MouseEvent) => { onmousemove = (e: MouseEvent) => {
this.app._mouseX = e.clientX; this.app._mouseX = e.clientX;
this.app._mouseY = e.clientY; this.app._mouseY = e.clientY;
} };
public mouseX = (): number => { public mouseX = (): number => {
/** /**
@ -359,17 +357,16 @@ export class UserAPI {
public z = ( public z = (
input: string, input: string,
options: InputOptions = {}, options: InputOptions = {},
id: number|string = "" id: number | string = ""
) => { ) => {
const zid = "z" + id.toString();
const zid = "z"+id.toString(); const key = id === "" ? this.generateCacheKey(input, options) : zid;
const key = id==="" ? this.generateCacheKey(input, options) : zid;
let player; let player;
if (this.app.api.patternCache.has(key)) { if (this.app.api.patternCache.has(key)) {
player = this.app.api.patternCache.get(key) as Player; player = this.app.api.patternCache.get(key) as Player;
if(player.input!==input) { if (player.input !== input) {
player = undefined; player = undefined;
} }
} }
@ -379,30 +376,47 @@ export class UserAPI {
this.app.api.patternCache.set(key, player); this.app.api.patternCache.set(key, player);
} }
if(typeof id === "number") player.zid = zid; if (typeof id === "number") player.zid = zid;
player.updateLastCallTime(); player.updateLastCallTime();
return player; return player;
}; };
public z0 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 0); public z0 = (input: string, opts: InputOptions = {}) =>
public z1 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 1); this.z(input, opts, 0);
public z2 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 2); public z1 = (input: string, opts: InputOptions = {}) =>
public z3 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 3); this.z(input, opts, 1);
public z4 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 4); public z2 = (input: string, opts: InputOptions = {}) =>
public z5 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 5); this.z(input, opts, 2);
public z6 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 6); public z3 = (input: string, opts: InputOptions = {}) =>
public z7 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 7); this.z(input, opts, 3);
public z8 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 8); public z4 = (input: string, opts: InputOptions = {}) =>
public z9 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 9); this.z(input, opts, 4);
public z10 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 10); public z5 = (input: string, opts: InputOptions = {}) =>
public z11 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 11); this.z(input, opts, 5);
public z12 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 12); public z6 = (input: string, opts: InputOptions = {}) =>
public z13 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 13); this.z(input, opts, 6);
public z14 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 14); public z7 = (input: string, opts: InputOptions = {}) =>
public z15 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 15); this.z(input, opts, 7);
public z16 = (input: string, opts: InputOptions = {}) => this.z(input, opts, 16); 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 // Counter and iteration
@ -930,12 +944,15 @@ export class UserAPI {
return current_chunk % 2 === 0; return current_chunk % 2 === 0;
}; };
public onbar = (bars: number[] | number, n: number = this.app.clock.time_signature[0]): boolean => { public onbar = (
let current_bar = (this.bar() % n) + 1; bars: number[] | number,
return (typeof bars === "number") n: number = this.app.clock.time_signature[0]
? bars === current_bar ): boolean => {
: bars.some((b) => b == current_bar) let current_bar = (this.bar() % n) + 1;
}; return typeof bars === "number"
? bars === current_bar
: bars.some((b) => b == current_bar);
};
onbeat = (...beat: number[]): boolean => { onbeat = (...beat: number[]): boolean => {
/** /**
@ -949,12 +966,14 @@ export class UserAPI {
*/ */
let final_pulses: boolean[] = []; let final_pulses: boolean[] = [];
beat.forEach((b) => { 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 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( final_pulses.push(
integral_part === this.app.clock.time_position.beat && 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); return final_pulses.some((p) => p == true);
@ -1013,14 +1032,16 @@ export class UserAPI {
}; };
ec: Function = this.euclid; ec: Function = this.euclid;
public rhythm = ( public rhythm = (
div: number, div: number,
pulses: number, pulses: number,
length: number, length: number,
rotate: number = 0 rotate: number = 0
): boolean => { ): 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( _euclidean_cycle(
pulses: number, pulses: number,
@ -1274,9 +1295,9 @@ export class UserAPI {
samples = samples; samples = samples;
log = (message: any) => { log = (message: any) => {
console.log(message); console.log(message);
this._logMessage(message); this._logMessage(message);
} };
scale = scale; scale = scale;

View File

@ -3,10 +3,10 @@ export {};
declare global { declare global {
interface Array<T> { interface Array<T> {
add(amount: number): number[]; add(amount: number): number[];
sub(amount: number): number[]; sub(amount: number): number[];
mult(amount: number): number[]; mult(amount: number): number[];
division(amount: number): number[]; division(amount: number): number[];
palindrome(): T[]; palindrome(): T[];
random(index: number): T; random(index: number): T;
rand(index: number): T; rand(index: number): T;
@ -24,8 +24,8 @@ declare global {
rotate(steps: number): this; rotate(steps: number): this;
unique(): this; unique(): this;
in(value: T): boolean; in(value: T): boolean;
square(): number[]; square(): number[];
sqrt(): number[]; sqrt(): number[];
} }
} }
@ -33,54 +33,55 @@ export const makeArrayExtensions = (api: UserAPI) => {
Array.prototype.in = function <T>(this: T[], value: T): boolean { Array.prototype.in = function <T>(this: T[], value: T): boolean {
return this.includes(value); 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[] { Array.prototype.square = function (): number[] {
/** /**
* @returns New array with square roots of values. Throws if any element is negative. * @returns New array with squared values.
*/ */
if (this.some(x => x < 0)) throw new Error('Cannot take square root of negative number'); return this.map((x: number) => x * x);
return this.map((x: number) => Math.sqrt(x)); };
};
Array.prototype.sqrt = function (): number[] {
Array.prototype.add = function (amount: number): number[] { /**
/** * @returns New array with square roots of values. Throws if any element is negative.
* @param amount - The value to add to each element in the array. */
* @returns New array with added values. if (this.some((x) => x < 0))
*/ throw new Error("Cannot take square root of negative number");
return this.map((x: number) => x + amount); return this.map((x: number) => Math.sqrt(x));
}; };
Array.prototype.sub = function (amount: number): number[] { Array.prototype.add = function (amount: number): number[] {
/** /**
* @param amount - The value to subtract from each element in the array. * @param amount - The value to add to each element in the array.
* @returns New array with subtracted values. * @returns New array with added values.
*/ */
return this.map((x: number) => x - amount); return this.map((x: number) => x + amount);
}; };
Array.prototype.mult = function (amount: number): number[] { Array.prototype.sub = function (amount: number): number[] {
/** /**
* @param amount - The value to multiply with each element in the array. * @param amount - The value to subtract from each element in the array.
* @returns New array with multiplied values. * @returns New array with subtracted values.
*/ */
return this.map((x: number) => x * amount); return this.map((x: number) => x - amount);
}; };
Array.prototype.division = function (amount: number): number[] { Array.prototype.mult = function (amount: number): number[] {
/** /**
* @param amount - The value to divide each element in the array by. * @param amount - The value to multiply with each element in the array.
* @returns New array with divided values. Throws if division by zero. * @returns New array with multiplied values.
*/ */
if (amount === 0) throw new Error('Division by zero'); return this.map((x: number) => x * amount);
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 () { Array.prototype.pick = function () {
/** /**

View File

@ -1,164 +1,163 @@
// @ts-ignore // @ts-ignore
import { TransportNode } from './TransportNode'; import { TransportNode } from "./TransportNode";
import TransportProcessor from './TransportProcessor?worker&url'; import TransportProcessor from "./TransportProcessor?worker&url";
import { Editor } from './main'; import { Editor } from "./main";
export interface TimePosition { export interface TimePosition {
/** /**
* A position in time. * A position in time.
* *
* @param bar - The bar number * @param bar - The bar number
* @param beat - The beat number * @param beat - The beat number
* @param pulse - The pulse number * @param pulse - The pulse number
*/ */
bar: number bar: number;
beat: number beat: number;
pulse: number pulse: number;
} }
export class Clock { 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. * This function returns the number of ticks separating the current moment
* It is also responsible for starting and stopping the Clock TransportNode. * from the beginning of the next bar.
* *
* @param app - The main application instance * @returns number of ticks until next bar
* @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
*/ */
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 get next_beat_in_ticks(): number {
transportNode: TransportNode | null /**
private _bpm: number * This function returns the number of ticks separating the current moment
time_signature: number[] * from the beginning of the next beat.
time_position: TimePosition *
private _ppqn: number * @returns number of ticks until next beat
tick: number */
return this.app.clock.pulses_since_origin + this.time_position.pulse;
}
constructor(public app: Editor, ctx: AudioContext) { get beats_per_bar(): number {
this.time_position = { bar: 0, beat: 0, pulse: 0 } /**
this.time_signature = [4, 4]; * Returns the number of beats per bar.
this.tick = 0; */
this._bpm = 120; return this.time_signature[0];
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 { get beats_since_origin(): number {
/** /**
* This function returns the number of ticks separating the current moment * Returns the number of beats since the origin.
* from the beginning of the next bar. *
* * @returns number of beats since origin
* @returns number of ticks until next bar */
*/ return Math.floor(this.tick / this.ppqn);
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; }
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
return (beatsMissingFromBar * this.ppqn) + ticskMissingFromBeat;
}
get next_beat_in_ticks(): number { get pulses_since_origin(): number {
/** /**
* This function returns the number of ticks separating the current moment * Returns the number of pulses since the origin.
* from the beginning of the next beat. *
* * @returns number of pulses since origin
* @returns number of ticks until next beat */
*/ return this.tick;
return this.app.clock.pulses_since_origin + this.time_position.pulse; }
}
get beats_per_bar(): number { get pulse_duration(): number {
/** /**
* Returns the number of beats per bar. * Returns the duration of a pulse in seconds.
*/ */
return this.time_signature[0]; return 60 / this.bpm / this.ppqn;
} }
get beats_since_origin(): number { get bpm(): number {
/** return this._bpm;
* Returns the number of beats since the origin. }
*
* @returns number of beats since origin
*/
return Math.floor(this.tick / this.ppqn);
}
get pulses_since_origin(): number { set bpm(bpm: number) {
/** this._bpm = bpm;
* Returns the number of pulses since the origin. this.transportNode?.setBPM(bpm);
* }
* @returns number of pulses since origin
*/
return this.tick;
} get ppqn(): number {
return this._ppqn;
}
get pulse_duration(): number { set ppqn(ppqn: number) {
/** this._ppqn = ppqn;
* Returns the duration of a pulse in seconds. this.transportNode?.setPPQN(ppqn);
*/ }
return 60 / this.bpm / this.ppqn;
}
get bpm(): number { public convertPulseToSecond(n: number): number {
return this._bpm; /**
} * Converts a pulse to a second.
*/
set bpm(bpm: number) { return n * this.pulse_duration;
this._bpm = bpm; }
this.transportNode?.setBPM(bpm);
}
get ppqn(): number { public start(): void {
return this._ppqn; /**
} * Starts the TransportNode (starts the clock).
*/
this.app.audioContext.resume();
this.transportNode?.start();
}
set ppqn(ppqn: number) { public pause(): void {
this._ppqn = ppqn; /**
this.transportNode?.setPPQN(ppqn); * Pauses the TransportNode (pauses the clock).
} */
this.transportNode?.pause();
}
public convertPulseToSecond(n: number): number { public stop(): void {
/** /**
* Converts a pulse to a second. * Stops the TransportNode (stops the clock).
*/ */
return n * this.pulse_duration this.app.clock.tick = 0;
} this.transportNode?.stop();
}
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();
}
}

View File

@ -100,12 +100,12 @@ export const evaluateOnce = async (
application: Editor, application: Editor,
code: string code: string
): Promise<void> => { ): Promise<void> => {
/** /**
* Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. * 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 application - The application object that contains the Editor API.
* @param code - The code to be evaluated. * @param code - The code to be evaluated.
* @returns A promise that resolves when the code has been evaluated. * @returns A promise that resolves when the code has been evaluated.
*/ */
await tryCatchWrapper(application, code); await tryCatchWrapper(application, code);
}; };

View File

@ -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<void> {
/** /**
* Wrapper class for Web MIDI API. Provides methods for sending MIDI messages. * Initializes Web MIDI API access and populates the list of MIDI outputs.
* *
* * @returns Promise
* @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.
*/ */
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 getCurrentMidiPort(): string | null {
public midiOutputs: MIDIOutput[] = []; /**
private currentOutputIndex: number = 0; * Returns the name of the currently selected MIDI output.
private scheduledNotes: { [noteNumber: number]: number } = {}; // { noteNumber: timeoutId } *
* @returns Name of the currently selected MIDI output or null if no MIDI output is selected or available.
constructor() { */
this.initializeMidiAccess(); if (
} this.midiOutputs.length > 0 &&
this.currentOutputIndex >= 0 &&
private async initializeMidiAccess(): Promise<void> { this.currentOutputIndex < this.midiOutputs.length
/** ) {
* Initializes Web MIDI API access and populates the list of MIDI outputs. return this.midiOutputs[this.currentOutputIndex].name;
* } else {
* @returns Promise console.error("No MIDI output selected or available.");
*/ return null;
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 getCurrentMidiPortIndex(): number { public getCurrentMidiPortIndex(): number {
/** /**
* Returns the index of the currently selected MIDI output. * 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. * @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) { if (
return this.currentOutputIndex; this.midiOutputs.length > 0 &&
} else { this.currentOutputIndex >= 0 &&
console.error('No MIDI output selected or available.'); this.currentOutputIndex < this.midiOutputs.length
return -1; ) {
} return this.currentOutputIndex;
} else {
console.error("No MIDI output selected or available.");
return -1;
} }
}
public sendMidiClock(): void { public sendMidiClock(): void {
/** /**
* Sends a single MIDI clock message to the currently selected MIDI output. * Sends a single MIDI clock message to the currently selected MIDI output.
*/ */
const output = this.midiOutputs[this.currentOutputIndex]; const output = this.midiOutputs[this.currentOutputIndex];
if (output) { if (output) {
output.send([0xF8]); // Send a single MIDI clock message output.send([0xf8]); // Send a single MIDI clock message
} else { } else {
console.error('MIDI output not available.'); console.error("MIDI output not available.");
}
} }
}
public switchMidiOutput(outputName: string): boolean {
/** public switchMidiOutput(outputName: string): boolean {
* Switches the currently selected MIDI output. /**
* * 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 * @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); */
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) { if (index !== -1) {
this.currentOutputIndex = index; return index;
return true;
} else { } else {
return false; console.error(`MIDI output "${output}" not found.`);
} return this.currentOutputIndex;
}
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.');
} }
} }
} }
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.");
}
}
}

View File

@ -50,25 +50,29 @@ const SCALES: Record<string, number[]> = {
hindustan: [0, 2, 4, 5, 7, 8, 10], hindustan: [0, 2, 4, 5, 7, 8, 10],
persian: [0, 1, 4, 5, 6, 8, 11], persian: [0, 1, 4, 5, 6, 8, 11],
eastIndianPurvi: [0, 1, 4, 6, 7, 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 { export function scale(
/** n: number,
* Returns the MIDI note number for the given scale degree in the given scale. scaleName: string = "major",
* @param {number} n - The scale degree, where 0 is the tonic. octave: number = 4
* @param {string} scaleName - The name of the scale. ): number {
* @param {number} octave - The octave number. /**
* @returns {number} The MIDI note 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.
const scale = SCALES[scaleName]; * @param {string} scaleName - The name of the scale.
* @param {number} octave - The octave number.
if (!scale) { * @returns {number} The MIDI note number.
throw new Error(`Unknown scale ${scaleName}`); */
} const scale = SCALES[scaleName];
let index = n % scale.length; if (!scale) {
if (index < 0) index += scale.length; // adjust for negative indexes throw new Error(`Unknown scale ${scaleName}`);
let additionalOctaves = Math.floor(n / scale.length);
return 60 + (octave + additionalOctaves) * 12 + scale[index];
} }
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];
}

View File

@ -1,65 +1,63 @@
export class DrunkWalk { 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 * Makes a step in the "drunk walk" algorithm. This is a random step of +1, 0, or -1.
* 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; const stepSize: number = Math.floor(Math.random() * 3) - 1;
public max: number; this.position += stepSize;
private wrap: boolean;
public position: number;
constructor(min: number, max: number, wrap: boolean) { if (this.wrap) {
this.min = min; if (this.position > this.max) {
this.max = max; this.position = this.min;
this.wrap = wrap; } else if (this.position < this.min) {
this.position = 0; 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;
}
/** toggleWrap(b: boolean): void {
* Makes a step in the "drunk walk" algorithm. This is a random step of +1, 0, or -1. /**
*/ * Whether or not the "drunk" should wrap around the range
*
const stepSize: number = Math.floor(Math.random() * 3) - 1; * @param b - Whether or not the "drunk" should wrap around the range
this.position += stepSize; */
this.wrap = b;
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;
}
}

View File

@ -1,122 +1,126 @@
import { type Editor } from '../main'; import { type Editor } from "../main";
import { freqToMidi, resolvePitchBend, getScale, isScale, parseScala } from 'zifferjs'; import {
freqToMidi,
resolvePitchBend,
getScale,
isScale,
parseScala,
} from "zifferjs";
export abstract class Event { export abstract class Event {
seedValue: string|undefined = undefined; seedValue: string | undefined = undefined;
randomGen: Function = Math.random; randomGen: Function = Math.random;
app: Editor; app: Editor;
values: { [key: string]: any } = {}; values: { [key: string]: any } = {};
constructor(app: Editor) { constructor(app: Editor) {
this.app = app; this.app = app;
if(this.app.api.currentSeed) { if (this.app.api.currentSeed) {
this.randomGen = this.app.api.randomGen; this.randomGen = this.app.api.randomGen;
}
} }
}
odds = (probability: number, func: Function): Event => { odds = (probability: number, func: Function): Event => {
if(this.randomGen() < probability) { if (this.randomGen() < probability) {
return this.modify(func); return this.modify(func);
}
return this;
} }
return this;
};
almostNever = (func: Function): Event => { almostNever = (func: Function): Event => {
return this.odds(0.025, func); return this.odds(0.025, func);
} };
rarely = (func: Function): Event => { rarely = (func: Function): Event => {
return this.odds(0.1, func); return this.odds(0.1, func);
} };
scarcely = (func: Function): Event => { scarcely = (func: Function): Event => {
return this.odds(0.25, func); return this.odds(0.25, func);
} };
sometimes = (func: Function): Event => { sometimes = (func: Function): Event => {
return this.odds(0.5, func); return this.odds(0.5, func);
} };
often = (func: Function): Event => { often = (func: Function): Event => {
return this.odds(0.75, func); return this.odds(0.75, func);
} };
frequently = (func: Function): Event => { frequently = (func: Function): Event => {
return this.odds(0.9, func); return this.odds(0.9, func);
} };
almostAlways = (func: Function): Event => { almostAlways = (func: Function): Event => {
return this.odds(0.985, func); return this.odds(0.985, func);
} };
modify = (func: Function): Event => { modify = (func: Function): Event => {
return func(this); return func(this);
} };
seed = (value: string|number): Event => { seed = (value: string | number): Event => {
this.seedValue = value.toString(); this.seedValue = value.toString();
this.randomGen = this.app.api.localSeededRandom(this.seedValue); this.randomGen = this.app.api.localSeededRandom(this.seedValue);
return this; return this;
} };
clear = (): Event => { clear = (): Event => {
this.app.api.clearLocalSeed(this.seedValue); this.app.api.clearLocalSeed(this.seedValue);
return this; return this;
} };
apply = (func: Function): Event => { apply = (func: Function): Event => {
return this.modify(func); return this.modify(func);
} };
duration = (value: number): Event => {
this.values['duration'] = value;
return this;
}
duration = (value: number): Event => {
this.values["duration"] = value;
return this;
};
} }
export abstract class AudibleEvent extends Event { export abstract class AudibleEvent extends Event {
constructor(app: Editor) { constructor(app: Editor) {
super(app); super(app);
} }
octave = (value: number): this => { octave = (value: number): this => {
this.values['octave'] = value; this.values["octave"] = value;
this.update(); this.update();
return this; return this;
} };
key = (value: string): this => { key = (value: string): this => {
this.values['key'] = value; this.values["key"] = value;
this.update(); this.update();
return this; return this;
} };
scale = (value: string): this => { scale = (value: string): this => {
if(!isScale(value)) { if (!isScale(value)) {
this.values.parsedScale = parseScala(value) as number[]; this.values.parsedScale = parseScala(value) as number[];
} else { } else {
this.values.scaleName = value; this.values.scaleName = value;
this.values.parsedScale = getScale(value) as number[]; this.values.parsedScale = getScale(value) as number[];
}
this.update();
return this;
} }
this.update();
return this;
};
freq = (value: number): this => { freq = (value: number): this => {
this.values['freq'] = value; this.values["freq"] = value;
const midiNote = freqToMidi(value); const midiNote = freqToMidi(value);
if(midiNote % 1 !== 0) { if (midiNote % 1 !== 0) {
this.values['note'] = Math.floor(midiNote); this.values["note"] = Math.floor(midiNote);
this.values['bend'] = resolvePitchBend(midiNote)[1]; this.values["bend"] = resolvePitchBend(midiNote)[1];
} else { } else {
this.values['note'] = midiNote; this.values["note"] = midiNote;
}
return this;
} }
return this;
};
update = (): void => { update = (): void => {
// Overwrite in subclasses // Overwrite in subclasses
} };
}
}

View File

@ -1,95 +1,102 @@
import { AudibleEvent } from './AbstractEvents'; import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from '../main'; import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection"; import { MidiConnection } from "../IO/MidiConnection";
import { midiToFreq, noteFromPc } from 'zifferjs'; import { midiToFreq, noteFromPc } from "zifferjs";
export class NoteEvent extends AudibleEvent { export class NoteEvent extends AudibleEvent {
midiConnection: MidiConnection; midiConnection: MidiConnection;
constructor(input: number|object, public app: Editor) { constructor(input: number | object, public app: Editor) {
super(app); super(app);
if(typeof input === 'number') this.values['note'] = input; if (typeof input === "number") this.values["note"] = input;
else this.values = input; else this.values = input;
this.midiConnection = app.api.MidiConnection this.midiConnection = app.api.MidiConnection;
} }
note = (value: number): this => { note = (value: number): this => {
this.values['note'] = value; this.values["note"] = value;
return this; return this;
} };
sustain = (value: number): this => { sustain = (value: number): this => {
this.values['sustain'] = value; this.values["sustain"] = value;
return this; return this;
} };
channel = (value: number): this => { channel = (value: number): this => {
this.values['channel'] = value; this.values["channel"] = value;
return this; return this;
} };
port = (value: number|string): this => { port = (value: number | string): this => {
this.values['port'] = this.midiConnection.getMidiOutputIndex(value); this.values["port"] = this.midiConnection.getMidiOutputIndex(value);
return this; return this;
} };
add = (value: number): this => { add = (value: number): this => {
this.values.note += value; this.values.note += value;
return this; return this;
} };
modify = (func: Function): this => { modify = (func: Function): this => {
const funcResult = func(this); const funcResult = func(this);
if(funcResult instanceof Object) { if (funcResult instanceof Object) {
return funcResult; return funcResult;
} } else {
else { func(this.values);
func(this.values); this.update();
this.update(); return this;
return this;
}
} }
};
bend = (value: number): this => { bend = (value: number): this => {
this.values['bend'] = value; this.values["bend"] = value;
return this; 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;
}
update = (): void => { random = (min: number = 0, max: number = 127): this => {
const [note, bend] = noteFromPc( min = Math.min(Math.max(min, 0), 127);
this.values.key || "C4", max = Math.min(Math.max(max, 0), 127);
this.values.pitch || 0, this.values["note"] = Math.floor(this.randomGen() * (max - min + 1)) + min;
this.values.parsedScale || "MAJOR", return this;
this.values.octave || 0 };
);
this.values.note = note;
this.values.freq = midiToFreq(note);
if(bend) this.values.bend = bend;
}
out = (): void => { update = (): void => {
const note = this.values.note ? this.values.note : 60; const [note, bend] = noteFromPc(
const channel = this.values.channel ? this.values.channel : 0; this.values.key || "C4",
const velocity = this.values.velocity ? this.values.velocity : 100; this.values.pitch || 0,
this.values.parsedScale || "MAJOR",
const sustain = this.values.sustain ? this.values.octave || 0
this.values.sustain * this.app.clock.pulse_duration * this.app.api.ppqn() : );
this.app.clock.pulse_duration * this.app.api.ppqn(); this.values.note = note;
this.values.freq = midiToFreq(note);
const bend = this.values.bend ? this.values.bend : undefined; if (bend) this.values.bend = bend;
};
const port = this.values.port ?
this.midiConnection.getMidiOutputIndex(this.values.port) :
this.midiConnection.getCurrentMidiPortIndex();
this.midiConnection.sendMidiNote(note, channel, velocity, sustain, port, bend); 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
);
};
}

View File

@ -1,37 +1,39 @@
import { type Editor } from '../main'; import { type Editor } from "../main";
import { Event } from "./AbstractEvents"; import { Event } from "./AbstractEvents";
export class RestEvent extends Event { export class RestEvent extends Event {
constructor(duration: number, app: Editor) { constructor(duration: number, app: Editor) {
super(app); super(app);
this.values["duration"] = duration; this.values["duration"] = duration;
} }
_fallbackMethod = (): Event => { _fallbackMethod = (): Event => {
return RestEvent.createRestProxy(this.values["duration"], this.app); return RestEvent.createRestProxy(this.values["duration"], this.app);
} };
public static createRestProxy = (duration: number, app: Editor): RestEvent => { public static createRestProxy = (
const instance = new RestEvent(duration, app); duration: number,
return new Proxy(instance, { app: Editor
// @ts-ignore ): RestEvent => {
get(target, propKey, receiver) { const instance = new RestEvent(duration, app);
// @ts-ignore return new Proxy(instance, {
if (typeof target[propKey] === 'undefined') { // @ts-ignore
return target._fallbackMethod; get(target, propKey, receiver) {
} // @ts-ignore
// @ts-ignore if (typeof target[propKey] === "undefined") {
return target[propKey]; return target._fallbackMethod;
}, }
// @ts-ignore // @ts-ignore
set(target, propKey, value, receiver) { return target[propKey];
return false; },
} // @ts-ignore
}); set(target, propKey, value, receiver) {
} return false;
},
});
};
out = (): void => { out = (): void => {
// TODO? // TODO?
} };
}
}

View File

@ -1,28 +1,26 @@
export class SkipEvent { export class SkipEvent {
_fallbackMethod = (): SkipEvent => {
return SkipEvent.createSkipProxy();
};
_fallbackMethod = (): SkipEvent => { public static createSkipProxy = (): SkipEvent => {
return SkipEvent.createSkipProxy(); const instance = new SkipEvent();
} return new Proxy(instance, {
// @ts-ignore
public static createSkipProxy = (): SkipEvent => { get(target, propKey, receiver) {
const instance = new SkipEvent(); // @ts-ignore
return new Proxy(instance, { if (typeof target[propKey] === "undefined") {
// @ts-ignore return target._fallbackMethod;
get(target, propKey, receiver) { }
// @ts-ignore // @ts-ignore
if (typeof target[propKey] === 'undefined') { return target[propKey];
return target._fallbackMethod; },
} // @ts-ignore
// @ts-ignore set(target, propKey, value, receiver) {
return target[propKey]; return false;
}, },
// @ts-ignore });
set(target, propKey, value, receiver) { };
return false;
}
});
}
out = (): void => {} out = (): void => {};
}
}

View File

@ -238,13 +238,13 @@ export class SoundEvent extends AudibleEvent {
sus = this.sustain; sus = this.sustain;
update = (): void => { update = (): void => {
const [note, _] = noteFromPc( const [note, _] = noteFromPc(
this.values.key || "C4", this.values.key || "C4",
this.values.pitch || 0, this.values.pitch || 0,
this.values.parsedScale || "MAJOR", this.values.parsedScale || "MAJOR",
this.values.octave || 0 this.values.octave || 0
); );
this.values.freq = midiToFreq(note); this.values.freq = midiToFreq(note);
}; };
out = (): object => { out = (): object => {

View File

@ -9,194 +9,204 @@ import { RestEvent } from "./RestEvent";
export type InputOptions = { [key: string]: string | number }; export type InputOptions = { [key: string]: string | number };
export class Player extends Event { export class Player extends Event {
input: string; input: string;
ziffers: Ziffers; ziffers: Ziffers;
initCallTime: number = 1; initCallTime: number = 1;
startCallTime: number = 1; startCallTime: number = 1;
lastCallTime: number = 1; lastCallTime: number = 1;
waitTime = 0; waitTime = 0;
startBeat: number = 0; startBeat: number = 0;
played: boolean = false; played: boolean = false;
current!: Pitch|Chord|ZRest; current!: Pitch | Chord | ZRest;
retro: boolean = false; retro: boolean = false;
index: number = -1; index: number = -1;
zid: string = ""; zid: string = "";
options: InputOptions = {}; options: InputOptions = {};
skipIndex = 0; skipIndex = 0;
endTime = 0; endTime = 0;
constructor(input: string, options: InputOptions, public app: Editor) { constructor(input: string, options: InputOptions, public app: Editor) {
super(app); super(app);
this.input = input; this.input = input;
this.options = options; this.options = options;
this.ziffers = new Ziffers(input, 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 { // Main logic
const dur = this.ziffers.duration; const howAboutNow =
return dur * 4 * this.app.clock.ppqn; // 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 { if (this.atTheBeginning()) {
return this.startCallTime + this.ticks; this.startCallTime = this.app.clock.pulses_since_origin;
} }
updateLastCallTime(): void { return howAboutNow;
if (this.notStarted() || this.played) { };
this.lastCallTime = this.app.clock.pulses_since_origin;
this.played = false;
}
}
notStarted(): boolean { sound(name: string) {
return this.ziffers.notStarted(); if (this.areWeThereYet()) {
} const event = this.next() as Pitch | Chord | ZRest;
if (event instanceof Pitch) {
next = (): Pitch|Chord|ZRest => { const obj = event.getExisting(
this.current = this.ziffers.next() as Pitch|Chord|ZRest; "freq",
this.played = true; "pitch",
return this.current; "key",
} "scale",
"octave",
pulseToSecond = (pulse: number): number => { "parsedScale"
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;
}
// 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)
)
); );
return new SoundEvent(obj, this.app).sound(name);
// Increment index of how many times call is skipped } else if (event instanceof ZRest) {
this.skipIndex = howAboutNow ? 0 : this.skipIndex+1; return RestEvent.createRestProxy(event.duration, this.app);
}
// Increment index of how many times sound/midi have been called } else {
this.index = howAboutNow ? this.index+1 : this.index; return SkipEvent.createSkipProxy();
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;
} }
}
sound(name: string) { midi(value: number | undefined = undefined) {
if(this.areWeThereYet()) { if (this.areWeThereYet()) {
const event = this.next() as Pitch|Chord|ZRest; const event = this.next() as Pitch | Chord | ZRest;
if(event instanceof Pitch) { if (event instanceof Pitch) {
const obj = event.getExisting("freq","pitch","key","scale","octave","parsedScale"); const obj = event.getExisting(
return new SoundEvent(obj, this.app).sound(name); "note",
} else if(event instanceof ZRest) { "pitch",
return RestEvent.createRestProxy(event.duration, this.app); "bend",
} "key",
} else { "scale",
return SkipEvent.createSkipProxy(); "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) { scale(name: string) {
if(this.areWeThereYet()) { if (this.atTheBeginning()) this.ziffers.scale(name);
const event = this.next() as Pitch|Chord|ZRest; return this;
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) { key(name: string) {
if(this.atTheBeginning()) this.ziffers.scale(name); 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; return this;
}
this.waitTime =
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
} }
return this;
}
key(name: string) { out = (): void => {
if(this.atTheBeginning()) this.ziffers.key(name); // TODO?
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?
}
}

View File

@ -1,8 +1,4 @@
import { import { uniqueNamesGenerator, colors, animals } from "unique-names-generator";
uniqueNamesGenerator,
colors,
animals,
} from "unique-names-generator";
import { examples } from "./examples/excerpts"; import { examples } from "./examples/excerpts";
import { EditorState, Compartment } from "@codemirror/state"; import { EditorState, Compartment } from "@codemirror/state";
import { ViewUpdate, lineNumbers, keymap } from "@codemirror/view"; import { ViewUpdate, lineNumbers, keymap } from "@codemirror/view";
@ -28,7 +24,7 @@ import {
} from "./AppSettings"; } from "./AppSettings";
import { tryEvaluate } from "./Evaluator"; import { tryEvaluate } from "./Evaluator";
// @ts-ignore // @ts-ignore
import { gzipSync, decompressSync, strFromU8 } from 'fflate'; import { gzipSync, decompressSync, strFromU8 } from "fflate";
// Importing showdown and setting up the markdown converter // Importing showdown and setting up the markdown converter
import showdown from "showdown"; 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", blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
details: details:
"lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600", "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: 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", "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: thead:
@ -87,14 +83,16 @@ export class Editor {
view: EditorView; view: EditorView;
clock: Clock; clock: Clock;
manualPlay: boolean = false; manualPlay: boolean = false;
isPlaying: boolean = false; isPlaying: boolean = false;
// Mouse position // Mouse position
public _mouseX: number = 0; public _mouseX: number = 0;
public _mouseY: number = 0; public _mouseY: number = 0;
// Topos Logo // Topos Logo
topos_logo: HTMLElement = document.getElementById('topos-logo') as HTMLElement; topos_logo: HTMLElement = document.getElementById(
"topos-logo"
) as HTMLElement;
// Transport elements // Transport elements
play_buttons: HTMLButtonElement[] = [ play_buttons: HTMLButtonElement[] = [
@ -108,7 +106,9 @@ export class Editor {
document.getElementById("clear-button-1") as HTMLButtonElement, document.getElementById("clear-button-1") as HTMLButtonElement,
//document.getElementById("clear-button-2") 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( documentation_button: HTMLButtonElement = document.getElementById(
"doc-button-1" "doc-button-1"
@ -201,10 +201,10 @@ export class Editor {
this.selected_universe = "Welcome"; this.selected_universe = "Welcome";
this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`; this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`;
// Picking a random example to populate the landing page // Picking a random example to populate the landing page
let random_example = examples[Math.floor(Math.random() * examples.length)]; 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.committed = random_example;
this.universes[this.selected_universe].global.candidate = random_example; this.universes[this.selected_universe].global.candidate = random_example;
// ================================================================================ // ================================================================================
// Audio context and clock // Audio context and clock
@ -255,9 +255,11 @@ export class Editor {
// ================================================================================ // ================================================================================
// Building the documentation // Building the documentation
let pre_loading = async () => { await loadSamples(); }; let pre_loading = async () => {
pre_loading(); await loadSamples();
this.docs = documentation_factory(this); };
pre_loading();
this.docs = documentation_factory(this);
// ================================================================================ // ================================================================================
// ================================================================================ // ================================================================================
@ -265,7 +267,6 @@ export class Editor {
// ================================================================================ // ================================================================================
window.addEventListener("keydown", (event: KeyboardEvent) => { window.addEventListener("keydown", (event: KeyboardEvent) => {
if (event.key === "Tab") { if (event.key === "Tab") {
event.preventDefault(); event.preventDefault();
} }
@ -278,15 +279,15 @@ export class Editor {
if (event.ctrlKey && event.key === "p") { if (event.ctrlKey && event.key === "p") {
event.preventDefault(); event.preventDefault();
if (this.isPlaying) { if (this.isPlaying) {
this.isPlaying = false; this.isPlaying = false;
this.setButtonHighlighting("pause", true); this.setButtonHighlighting("pause", true);
this.clock.pause(); this.clock.pause();
} else { } else {
this.isPlaying = true; this.isPlaying = true;
this.setButtonHighlighting("play", true); this.setButtonHighlighting("play", true);
this.clock.start(); this.clock.start();
} }
} }
// Ctrl + Shift + V: Vim Mode // Ctrl + Shift + V: Vim Mode
@ -324,7 +325,7 @@ export class Editor {
if (event.ctrlKey && event.key === "b") { if (event.ctrlKey && event.key === "b") {
event.preventDefault(); event.preventDefault();
this.hideDocumentation(); this.hideDocumentation();
this.updateKnownUniversesView(); this.updateKnownUniversesView();
this.openBuffersModal(); this.openBuffersModal();
} }
@ -372,10 +373,10 @@ export class Editor {
if (event.keyCode === keycode) { if (event.keyCode === keycode) {
event.preventDefault(); event.preventDefault();
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault(); event.preventDefault();
this.api.script(keycode - 111); this.api.script(keycode - 111);
} else { } else {
event.preventDefault(); event.preventDefault();
this.changeModeFromInterface("local"); this.changeModeFromInterface("local");
this.changeToLocalBuffer(index); this.changeToLocalBuffer(index);
this.hideDocumentation(); this.hideDocumentation();
@ -385,12 +386,12 @@ export class Editor {
); );
if (event.keyCode == 121) { if (event.keyCode == 121) {
event.preventDefault(); event.preventDefault();
this.changeModeFromInterface("global"); this.changeModeFromInterface("global");
this.hideDocumentation(); this.hideDocumentation();
} }
if (event.keyCode == 122) { if (event.keyCode == 122) {
event.preventDefault(); event.preventDefault();
this.changeModeFromInterface("init"); this.changeModeFromInterface("init");
this.hideDocumentation(); this.hideDocumentation();
} }
@ -418,24 +419,24 @@ export class Editor {
}); });
} }
this.topos_logo.addEventListener("click", () => { this.topos_logo.addEventListener("click", () => {
this.hideDocumentation(); this.hideDocumentation();
this.updateKnownUniversesView(); this.updateKnownUniversesView();
this.openBuffersModal(); this.openBuffersModal();
}) });
this.play_buttons.forEach((button) => { this.play_buttons.forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
if (this.isPlaying) { if (this.isPlaying) {
this.setButtonHighlighting("pause", true); this.setButtonHighlighting("pause", true);
this.isPlaying = !this.isPlaying; this.isPlaying = !this.isPlaying;
this.clock.pause(); this.clock.pause();
} else { } else {
this.setButtonHighlighting("play", true); this.setButtonHighlighting("play", true);
this.isPlaying = !this.isPlaying; this.isPlaying = !this.isPlaying;
this.clock.start(); this.clock.start();
} }
}); });
}); });
this.clear_buttons.forEach((button) => { this.clear_buttons.forEach((button) => {
@ -452,17 +453,16 @@ export class Editor {
this.showDocumentation(); this.showDocumentation();
}); });
this.load_universe_button.addEventListener("click", () => { this.load_universe_button.addEventListener("click", () => {
let query = this.buffer_search.value; let query = this.buffer_search.value;
if (query.length > 2 && query.length < 20 && !query.includes(" ")) { if (query.length > 2 && query.length < 20 && !query.includes(" ")) {
this.loadUniverse(query); this.loadUniverse(query);
this.settings.selected_universe = query; this.settings.selected_universe = query;
this.buffer_search.value = ""; this.buffer_search.value = "";
this.closeBuffersModal(); this.closeBuffersModal();
this.view.focus(); this.view.focus();
this.emptyUrl(); this.emptyUrl();
} }
}); });
this.eval_button.addEventListener("click", () => { this.eval_button.addEventListener("click", () => {
@ -470,7 +470,6 @@ export class Editor {
this.flashBackground("#2d313d", 200); this.flashBackground("#2d313d", 200);
}); });
this.stop_buttons.forEach((button) => { this.stop_buttons.forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
this.setButtonHighlighting("stop", true); this.setButtonHighlighting("stop", true);
@ -514,7 +513,7 @@ export class Editor {
}); });
this.close_universes_button.addEventListener("click", () => { this.close_universes_button.addEventListener("click", () => {
this.openBuffersModal(); this.openBuffersModal();
}); });
this.font_size_slider.addEventListener("input", () => { this.font_size_slider.addEventListener("input", () => {
@ -565,13 +564,12 @@ export class Editor {
}); });
this.universe_creator.addEventListener("submit", (event) => { this.universe_creator.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
let data = new FormData(this.universe_creator); 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) { if (universeName.length > 2 && universeName.length < 20) {
this.loadUniverse(universeName); this.loadUniverse(universeName);
this.settings.selected_universe = universeName; this.settings.selected_universe = universeName;
@ -603,17 +601,17 @@ export class Editor {
].forEach((e) => { ].forEach((e) => {
let name = `docs_` + e; let name = `docs_` + e;
document.getElementById(name)!.addEventListener("click", async () => { document.getElementById(name)!.addEventListener("click", async () => {
if (name !== "docs_samples") { if (name !== "docs_samples") {
this.currentDocumentationPane = e; this.currentDocumentationPane = e;
this.updateDocumentationContent(); this.updateDocumentationContent();
} else { } else {
console.log('Loading samples!'); console.log("Loading samples!");
await loadSamples().then(() => { await loadSamples().then(() => {
this.docs = documentation_factory(this) this.docs = documentation_factory(this);
this.currentDocumentationPane = e; this.currentDocumentationPane = e;
this.updateDocumentationContent(); this.updateDocumentationContent();
}); });
} }
}); });
}); });
@ -656,14 +654,18 @@ export class Editor {
if (url !== null) { if (url !== null) {
const universeParam = url.get("universe"); const universeParam = url.get("universe");
if (universeParam !== null) { 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))); new_universe = JSON.parse(strFromU8(decompressSync(data)));
const randomName: string = uniqueNamesGenerator({ const randomName: string = uniqueNamesGenerator({
length: 2, separator: '_', length: 2,
dictionaries: [colors, animals], separator: "_",
dictionaries: [colors, animals],
}); });
this.loadUniverse(randomName, new_universe["universe"]); 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); return JSON.parse(hash);
}; };
updateKnownUniversesView = () => { updateKnownUniversesView = () => {
let existing_universes = document.getElementById("existing-universes"); let existing_universes = document.getElementById("existing-universes");
let known_universes = Object.keys(this.universes); let known_universes = Object.keys(this.universes);
let final_html = "<ul class='lg:h-80 lg:w-80 lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-gray-800'>"; let final_html =
known_universes.forEach((name) => { "<ul class='lg:h-80 lg:w-80 lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-gray-800'>";
final_html += ` known_universes.forEach((name) => {
final_html += `
<li onclick="_loadUniverseFromInterface('${name}')" class="hover:fill-black hover:bg-white py-2 hover:text-black flex justify-between px-4"> <li onclick="_loadUniverseFromInterface('${name}')" class="hover:fill-black hover:bg-white py-2 hover:text-black flex justify-between px-4">
<p >${name}</p> <p >${name}</p>
<button onclick=_deleteUniverseFromInterface('${name}')>🗑</button> <button onclick=_deleteUniverseFromInterface('${name}')>🗑</button>
</li>`; </li>`;
}); });
final_html = final_html + "</ul>"; final_html = final_html + "</ul>";
existing_universes!.innerHTML = final_html; existing_universes!.innerHTML = final_html;
} };
async share() { async share() {
async function bufferToBase64(buffer: Uint8Array) {
async function bufferToBase64(buffer:Uint8Array) { const base64url: string = await new Promise((r) => {
const base64url: string = await new Promise(r => { const reader = new FileReader();
const reader = new FileReader() reader.onload = () => r(reader.result as string);
reader.onload = () => r(reader.result as string) reader.readAsDataURL(new Blob([buffer]));
reader.readAsDataURL(new Blob([buffer]))
}); });
return base64url.slice(base64url.indexOf(',') + 1); return base64url.slice(base64url.indexOf(",") + 1);
} }
let data = JSON.stringify({ let data = JSON.stringify({
@ -852,22 +854,22 @@ export class Editor {
button: "play" | "pause" | "stop" | "clear", button: "play" | "pause" | "stop" | "clear",
highlight: boolean highlight: boolean
) { ) {
document.getElementById('play-label')!.textContent = button !== "pause" ? "Pause" : "Play"; document.getElementById("play-label")!.textContent =
if (button !== "pause") { button !== "pause" ? "Pause" : "Play";
document.getElementById('pause-icon')!.classList.remove('hidden'); if (button !== "pause") {
document.getElementById('play-icon')!.classList.add('hidden'); document.getElementById("pause-icon")!.classList.remove("hidden");
} else { document.getElementById("play-icon")!.classList.add("hidden");
document.getElementById('pause-icon')!.classList.add('hidden'); } else {
document.getElementById('play-icon')!.classList.remove('hidden'); 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');
}
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); this.flashBackground("#2d313d", 200);
const possible_selectors = [ const possible_selectors = [

View File

@ -1,116 +1,125 @@
import { EditorView } from '@codemirror/view' import { EditorView } from "@codemirror/view";
import { Extension } from '@codemirror/state' import { Extension } from "@codemirror/state";
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from '@lezer/highlight' import { tags as t } from "@lezer/highlight";
const base00 = '#171717', base01 = '#505d64', const base00 = "#171717",
base02 = 'white', base03 = '#707d8b', base01 = "#505d64",
base04 = '#a0a4ae', base05 = '#bdbdbd', base02 = "white",
base06 = '#e0e0e0', base07 = '#fdf6e3', base03 = "#707d8b",
base_red = '#ff5f52', base_deeporange = '#ff6e40', base04 = "#a0a4ae",
base_pink = '#fa5788', base_yellow = '#facf4e', base05 = "#bdbdbd",
base_orange = '#ffad42', base_cyan = '#1E6AE1', base06 = "#e0e0e0",
base_indigo = '#7186f0', base_purple = '#D09EBF', base07 = "#fdf6e3",
base_green = '#82d47c', base_lightgreen = '#82d47c', base_red = "#ff5f52",
base_teal = '#4ebaaa' 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, const invalid = base_red,
darkBackground = '#fdf6e3', darkBackground = "#fdf6e3",
highlightBackground = '#545b61', highlightBackground = "#545b61",
background = base00, background = base00,
tooltipBackground = base01, tooltipBackground = base01,
selection = base07, selection = base07,
cursor = base04 cursor = base04;
/// The editor theme styles for Material Dark. /// The editor theme styles for Material Dark.
export const materialDarkTheme = EditorView.theme( export const materialDarkTheme = EditorView.theme(
{ {
'&': { "&": {
color: base05, color: base05,
backgroundColor: background, backgroundColor: background,
fontSize: '24px', fontSize: "24px",
}, },
'.cm-content': { ".cm-content": {
caretColor: cursor caretColor: cursor,
}, },
'.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{ backgroundColor: selection, border: `0.5px solid ${base_teal}` }, { backgroundColor: selection, border: `0.5px solid ${base_teal}` },
'.cm-panels': { backgroundColor: darkBackground, color: base03 }, ".cm-panels": { backgroundColor: darkBackground, color: base03 },
'.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
'.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
'.cm-searchMatch': { ".cm-searchMatch": {
outline: `1px solid ${base_yellow}`, outline: `1px solid ${base_yellow}`,
backgroundColor: 'transparent' backgroundColor: "transparent",
}, },
'.cm-searchMatch.cm-searchMatch-selected': { ".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: highlightBackground backgroundColor: highlightBackground,
}, },
'.cm-activeLine': { backgroundColor: highlightBackground }, ".cm-activeLine": { backgroundColor: highlightBackground },
'.cm-selectionMatch': { ".cm-selectionMatch": {
backgroundColor: darkBackground, backgroundColor: darkBackground,
outline: `1px solid ${base_teal}` outline: `1px solid ${base_teal}`,
}, },
'&.cm-focused .cm-matchingBracket': { "&.cm-focused .cm-matchingBracket": {
color: base06, color: base06,
outline: `1px solid ${base_teal}` outline: `1px solid ${base_teal}`,
}, },
'&.cm-focused .cm-nonmatchingBracket': { "&.cm-focused .cm-nonmatchingBracket": {
color: base_red color: base_red,
}, },
'.cm-gutters': { ".cm-gutters": {
backgroundColor: base00, backgroundColor: base00,
borderRight: `1px solid ${base07}`, borderRight: `1px solid ${base07}`,
color: base02 color: base02,
}, },
'.cm-activeLineGutter': { ".cm-activeLineGutter": {
backgroundColor: highlightBackground, backgroundColor: highlightBackground,
color: base07 color: base07,
}, },
'.cm-foldPlaceholder': { ".cm-foldPlaceholder": {
backgroundColor: 'transparent', backgroundColor: "transparent",
border: 'none', border: "none",
color: `${base07}`, color: `${base07}`,
}, },
'.cm-tooltip': { ".cm-tooltip": {
border: 'none', border: "none",
backgroundColor: tooltipBackground backgroundColor: tooltipBackground,
}, },
'.cm-tooltip .cm-tooltip-arrow:before': { ".cm-tooltip .cm-tooltip-arrow:before": {
borderTopColor: 'transparent', borderTopColor: "transparent",
borderBottomColor: 'transparent' borderBottomColor: "transparent",
}, },
'.cm-tooltip .cm-tooltip-arrow:after': { ".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: tooltipBackground, borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground borderBottomColor: tooltipBackground,
}, },
'.cm-tooltip-autocomplete': { ".cm-tooltip-autocomplete": {
'& > ul > li[aria-selected]': { "& > ul > li[aria-selected]": {
backgroundColor: highlightBackground, backgroundColor: highlightBackground,
color: base03 color: base03,
} },
} },
}, },
{ dark: true } { dark: true }
) );
/// The highlighting style for code in the Material Dark theme. /// The highlighting style for code in the Material Dark theme.
export const materialDarkHighlightStyle = HighlightStyle.define([ export const materialDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: base_purple }, { tag: t.keyword, color: base_purple },
{ {
tag: [t.name, t.deleted, t.character, t.macroName], tag: [t.name, t.deleted, t.character, t.macroName],
color: base_cyan color: base_cyan,
}, },
{ tag: [t.propertyName], color: base_yellow }, { tag: [t.propertyName], color: base_yellow },
{ tag: [t.variableName], color: base05 }, { tag: [t.variableName], color: base05 },
@ -118,93 +127,93 @@ export const materialDarkHighlightStyle = HighlightStyle.define([
{ tag: [t.labelName], color: base_purple }, { tag: [t.labelName], color: base_purple },
{ {
tag: [t.color, t.constant(t.name), t.standard(t.name)], 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.definition(t.name), t.separator], color: base_pink },
{ tag: [t.brace], color: base_purple }, { tag: [t.brace], color: base_purple },
{ {
tag: [t.annotation], tag: [t.annotation],
color: invalid color: invalid,
}, },
{ {
tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
color: base_orange color: base_orange,
}, },
{ {
tag: [t.typeName, t.className], tag: [t.typeName, t.className],
color: base_orange color: base_orange,
}, },
{ {
tag: [t.operator, t.operatorKeyword], tag: [t.operator, t.operatorKeyword],
color: base_indigo color: base_indigo,
}, },
{ {
tag: [t.tagName], tag: [t.tagName],
color: base_deeporange color: base_deeporange,
}, },
{ {
tag: [t.squareBracket], tag: [t.squareBracket],
color: base_red color: base_red,
}, },
{ {
tag: [t.angleBracket], tag: [t.angleBracket],
color: base02 color: base02,
}, },
{ {
tag: [t.attributeName], tag: [t.attributeName],
color: base05 color: base05,
}, },
{ {
tag: [t.regexp], tag: [t.regexp],
color: invalid color: invalid,
}, },
{ {
tag: [t.quote], tag: [t.quote],
color: base_green color: base_green,
}, },
{ tag: [t.string], color: base_lightgreen }, { tag: [t.string], color: base_lightgreen },
{ {
tag: t.link, tag: t.link,
color: base_cyan, color: base_cyan,
textDecoration: 'underline', textDecoration: "underline",
textUnderlinePosition: 'under' textUnderlinePosition: "under",
}, },
{ {
tag: [t.url, t.escape, t.special(t.string)], tag: [t.url, t.escape, t.special(t.string)],
color: base_yellow color: base_yellow,
}, },
{ tag: [t.meta], color: base03 }, { 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.monospace, color: base05 },
{ tag: t.strong, fontWeight: 'bold', color: base_red }, { tag: t.strong, fontWeight: "bold", color: base_red },
{ tag: t.emphasis, fontStyle: 'italic', color: base_lightgreen }, { tag: t.emphasis, fontStyle: "italic", color: base_lightgreen },
{ tag: t.strikethrough, textDecoration: 'line-through' }, { tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.heading, fontWeight: 'bold', color: base_yellow }, { tag: t.heading, fontWeight: "bold", color: base_yellow },
{ tag: t.heading1, fontWeight: 'bold', color: base_yellow }, { tag: t.heading1, fontWeight: "bold", color: base_yellow },
{ {
tag: [t.heading2, t.heading3, t.heading4], tag: [t.heading2, t.heading3, t.heading4],
fontWeight: 'bold', fontWeight: "bold",
color: base_yellow color: base_yellow,
}, },
{ {
tag: [t.heading5, t.heading6], 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.atom, t.bool, t.special(t.variableName)], color: base_cyan },
{ {
tag: [t.processingInstruction, t.inserted], tag: [t.processingInstruction, t.inserted],
color: base_red color: base_red,
}, },
{ {
tag: [t.contentSeparator], 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 /// Extension to enable the Material Dark theme (both the editor theme and
/// the highlight style). /// the highlight style).
export const materialDark: Extension = [ export const materialDark: Extension = [
materialDarkTheme, materialDarkTheme,
syntaxHighlighting(materialDarkHighlightStyle) syntaxHighlighting(materialDarkHighlightStyle),
] ];