Merge pull request #75 from Bubobubobubobubo/visualisation
Adding some visualisations
This commit is contained in:
13
index.html
13
index.html
@ -22,7 +22,12 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#hydra-bg {
|
||||
.fluid-bg-transition {
|
||||
transition: background-color 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.fullscreencanvas {
|
||||
position: fixed; /* ignore margins */
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
@ -34,7 +39,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
details br {
|
||||
display: none;
|
||||
}
|
||||
@ -160,6 +164,7 @@
|
||||
<div class="space-y-2">
|
||||
<h2 class="font-semibold lg:text-xl text-gray-400">More</h2>
|
||||
<div class="flex flex-col">
|
||||
<a rel="noopener noreferrer" id="docs_oscilloscope" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Oscilloscope</a>
|
||||
<a rel="noopener noreferrer" id="docs_bonus" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Bonus/Trivia</a>
|
||||
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
|
||||
</div>
|
||||
@ -447,7 +452,9 @@
|
||||
</ul>
|
||||
<!-- Here comes the editor itself -->
|
||||
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden">
|
||||
<canvas id="hydra-bg"></canvas>
|
||||
<canvas id="hydra-bg" class="fullscreencanvas"></canvas>
|
||||
<canvas id="scope" class="fullscreencanvas"></canvas>
|
||||
<canvas id="feedback" class="fullscreencanvas"></canvas>
|
||||
</div>
|
||||
<p id="error_line" class="hidden text-red-400 w-screen bg-neutral-900 font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
|
||||
</div>
|
||||
|
||||
27
src/API.ts
27
src/API.ts
@ -26,6 +26,7 @@ import {
|
||||
} from "superdough";
|
||||
import { Speaker } from "./StringExtensions";
|
||||
import { getScaleNotes } from "zifferjs";
|
||||
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
|
||||
|
||||
interface ControlChange {
|
||||
channel: number;
|
||||
@ -269,10 +270,13 @@ export class UserAPI {
|
||||
* @returns The result of the evaluation
|
||||
*/
|
||||
args.forEach((arg) => {
|
||||
tryEvaluate(
|
||||
this.app,
|
||||
this.app.universes[this.app.selected_universe].locals[arg]
|
||||
);
|
||||
if (arg >= 1 && arg <= 9) {
|
||||
blinkScript(this.app, "local", arg);
|
||||
tryEvaluate(
|
||||
this.app,
|
||||
this.app.universes[this.app.selected_universe].locals[arg]
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
s = this.script;
|
||||
@ -1964,4 +1968,19 @@ export class UserAPI {
|
||||
*/
|
||||
return array.concat(array.slice(0, array.length - 1).reverse());
|
||||
};
|
||||
|
||||
// =============================================================
|
||||
// Oscilloscope Configuration
|
||||
// =============================================================
|
||||
|
||||
public scope = (config: OscilloscopeConfig): void => {
|
||||
/**
|
||||
* Configures the oscilloscope.
|
||||
* @param config - The configuration object
|
||||
*/
|
||||
this.app.osc = {
|
||||
...this.app.osc,
|
||||
...config,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
207
src/AudioVisualisation.ts
Normal file
207
src/AudioVisualisation.ts
Normal file
@ -0,0 +1,207 @@
|
||||
// @ts-ignore
|
||||
import { getAnalyser } from "superdough";
|
||||
import { type Editor } from "./main";
|
||||
|
||||
/**
|
||||
* Draw a circle at a specific position on the canvas.
|
||||
* @param {number} x - The x-coordinate of the circle's center.
|
||||
* @param {number} y - The y-coordinate of the circle's center.
|
||||
* @param {number} radius - The radius of the circle.
|
||||
* @param {string} color - The fill color of the circle.
|
||||
*/
|
||||
export const drawCircle = (
|
||||
app: Editor,
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
color: string
|
||||
): void => {
|
||||
// @ts-ignore
|
||||
const canvas: HTMLCanvasElement = app.interface.feedback;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
};
|
||||
|
||||
/**
|
||||
* Blinks a script indicator circle.
|
||||
* @param script - The type of script.
|
||||
* @param no - The shift amount multiplier.
|
||||
*/
|
||||
export const blinkScript = (
|
||||
app: Editor,
|
||||
script: "local" | "global" | "init",
|
||||
no?: number
|
||||
) => {
|
||||
if (no !== undefined && no < 1 && no > 9) return;
|
||||
const blinkDuration =
|
||||
(app.clock.bpm / 60 / app.clock.time_signature[1]) * 200;
|
||||
// @ts-ignore
|
||||
const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context
|
||||
|
||||
/**
|
||||
* Draws a circle at a given shift.
|
||||
* @param shift - The pixel distance from the origin.
|
||||
*/
|
||||
const _drawBlinker = (shift: number) => {
|
||||
const horizontalOffset = 50;
|
||||
drawCircle(
|
||||
app,
|
||||
horizontalOffset + shift,
|
||||
app.interface.feedback.clientHeight - 15,
|
||||
8,
|
||||
"#fdba74"
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the circle at a given shift.
|
||||
* @param shift - The pixel distance from the origin.
|
||||
*/
|
||||
const _clearBlinker = (shift: number) => {
|
||||
const x = 50 + shift;
|
||||
const y = app.interface.feedback.clientHeight - 15;
|
||||
const radius = 8;
|
||||
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2);
|
||||
};
|
||||
|
||||
if (script === "local" && no !== undefined) {
|
||||
const shiftAmount = no * 25;
|
||||
|
||||
// Clear existing timeout if any
|
||||
if (app.blinkTimeouts[shiftAmount]) {
|
||||
clearTimeout(app.blinkTimeouts[shiftAmount]);
|
||||
}
|
||||
|
||||
_drawBlinker(shiftAmount);
|
||||
|
||||
// Save timeout ID for later clearing
|
||||
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
|
||||
_clearBlinker(shiftAmount);
|
||||
// Clear the canvas before drawing new blinkers
|
||||
(app.interface.feedback as HTMLCanvasElement)
|
||||
.getContext("2d")!
|
||||
.clearRect(
|
||||
0,
|
||||
0,
|
||||
(app.interface.feedback as HTMLCanvasElement).width,
|
||||
(app.interface.feedback as HTMLCanvasElement).height
|
||||
);
|
||||
drawEmptyBlinkers(app);
|
||||
}, blinkDuration);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a series of 9 white circles.
|
||||
* @param app - The Editor application context.
|
||||
*/
|
||||
export const drawEmptyBlinkers = (app: Editor) => {
|
||||
for (let no = 1; no <= 9; no++) {
|
||||
const shiftAmount = no * 25;
|
||||
drawCircle(
|
||||
app,
|
||||
50 + shiftAmount,
|
||||
app.interface.feedback.clientHeight - 15,
|
||||
8,
|
||||
"white"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export interface OscilloscopeConfig {
|
||||
enabled: boolean;
|
||||
color: string;
|
||||
thickness: number;
|
||||
fftSize: number; // multiples of 256
|
||||
orientation: "horizontal" | "vertical";
|
||||
is3D: boolean;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and runs an oscilloscope using an AnalyzerNode.
|
||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope.
|
||||
* @param {OscilloscopeConfig} config - Configuration for the oscilloscope's appearance and behavior.
|
||||
*/
|
||||
export const runOscilloscope = (
|
||||
canvas: HTMLCanvasElement,
|
||||
app: Editor
|
||||
): void => {
|
||||
let config = app.osc;
|
||||
let analyzer = getAnalyser(config.fftSize);
|
||||
let dataArray = new Float32Array(analyzer.frequencyBinCount);
|
||||
const canvasCtx = canvas.getContext("2d")!;
|
||||
const WIDTH = canvas.width;
|
||||
const HEIGHT = canvas.height;
|
||||
|
||||
function draw() {
|
||||
requestAnimationFrame(draw);
|
||||
if (!app.osc.enabled) {
|
||||
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (analyzer.fftSize !== app.osc.fftSize) {
|
||||
analyzer = getAnalyser(app.osc.fftSize);
|
||||
dataArray = new Float32Array(analyzer.frequencyBinCount);
|
||||
}
|
||||
|
||||
analyzer.getFloatTimeDomainData(dataArray);
|
||||
|
||||
canvasCtx.fillStyle = "rgba(0, 0, 0, 0)";
|
||||
canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
canvasCtx.lineWidth = app.osc.thickness;
|
||||
|
||||
if (app.osc.color === "random") {
|
||||
if (app.clock.time_position.pulse % 16 === 0) {
|
||||
canvasCtx.strokeStyle = `hsl(${Math.random() * 360}, 100%, 50%)`;
|
||||
}
|
||||
} else {
|
||||
canvasCtx.strokeStyle = app.osc.color;
|
||||
}
|
||||
canvasCtx.beginPath();
|
||||
|
||||
// Drawing logic varies based on orientation and 3D setting
|
||||
if (app.osc.is3D) {
|
||||
for (let i = 0; i < dataArray.length; i += 2) {
|
||||
const x = (dataArray[i] * WIDTH * app.osc.size) / 2 + WIDTH / 4;
|
||||
const y = (dataArray[i + 1] * HEIGHT * app.osc.size) / 2 + HEIGHT / 4;
|
||||
i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
|
||||
}
|
||||
} else if (app.osc.orientation === "horizontal") {
|
||||
let x = 0;
|
||||
const sliceWidth = (WIDTH * 1.0) / dataArray.length;
|
||||
const yOffset = HEIGHT / 4;
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
const v = dataArray[i] * 0.5 * HEIGHT * app.osc.size;
|
||||
const y = v + yOffset;
|
||||
i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
|
||||
x += sliceWidth;
|
||||
}
|
||||
canvasCtx.lineTo(WIDTH, yOffset);
|
||||
} else {
|
||||
let y = 0;
|
||||
const sliceHeight = (HEIGHT * 1.0) / dataArray.length;
|
||||
const xOffset = WIDTH / 4;
|
||||
for (let i = 0; i < dataArray.length; i++) {
|
||||
const v = dataArray[i] * 0.5 * WIDTH * app.osc.size;
|
||||
const x = v + xOffset;
|
||||
i === 0 ? canvasCtx.moveTo(x, y) : canvasCtx.lineTo(x, y);
|
||||
y += sliceHeight;
|
||||
}
|
||||
canvasCtx.lineTo(xOffset, HEIGHT);
|
||||
}
|
||||
|
||||
canvasCtx.stroke();
|
||||
}
|
||||
|
||||
draw();
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { type Editor } from "./main";
|
||||
import { introduction } from "./documentation/introduction";
|
||||
import { oscilloscope } from "./documentation/oscilloscope";
|
||||
import { samples } from "./documentation/samples";
|
||||
import { chaining } from "./documentation/chaining";
|
||||
import { software_interface } from "./documentation/interface";
|
||||
@ -85,6 +86,7 @@ export const documentation_factory = (application: Editor) => {
|
||||
functions: functions(application),
|
||||
reference: reference(),
|
||||
shortcuts: shortcuts(),
|
||||
oscilloscope: oscilloscope(application),
|
||||
bonus: bonus(application),
|
||||
about: about(),
|
||||
};
|
||||
|
||||
@ -47,6 +47,8 @@ export const singleElements = {
|
||||
dough_nudge_range: "dough_nudge",
|
||||
error_line: "error_line",
|
||||
hydra_canvas: "hydra-bg",
|
||||
feedback: "feedback",
|
||||
scope: "scope",
|
||||
};
|
||||
|
||||
export const buttonGroups = {
|
||||
|
||||
@ -451,6 +451,7 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
"shortcuts",
|
||||
"about",
|
||||
"bonus",
|
||||
"oscilloscope",
|
||||
].forEach((e) => {
|
||||
let name = `docs_` + e;
|
||||
document.getElementById(name)!.addEventListener("click", async () => {
|
||||
|
||||
@ -13,14 +13,16 @@ export class TransportNode extends AudioWorkletNode {
|
||||
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
|
||||
handleMessage = (message) => {
|
||||
if (message.data && message.data.type === "bang") {
|
||||
if (this.app.settings.send_clock) this.app.api.MidiConnection.sendMidiClock();
|
||||
if (this.app.settings.send_clock)
|
||||
this.app.api.MidiConnection.sendMidiClock();
|
||||
this.app.clock.tick++;
|
||||
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
|
||||
this.app.clock.tick
|
||||
);
|
||||
this.app.clock.time_position = futureTimeStamp;
|
||||
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1
|
||||
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`;
|
||||
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
|
||||
futureTimeStamp.beat + 1
|
||||
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`;
|
||||
if (this.app.exampleIsPlaying) {
|
||||
tryEvaluate(this.app, this.app.example_buffer);
|
||||
} else {
|
||||
|
||||
@ -1,10 +1,32 @@
|
||||
import { type Editor } from "./main";
|
||||
|
||||
const handleResize = (canvas: HTMLCanvasElement) => {
|
||||
if (!canvas) return;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Assuming the canvas takes up the whole window
|
||||
canvas.width = window.innerWidth * dpr;
|
||||
canvas.height = window.innerHeight * dpr;
|
||||
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
};
|
||||
|
||||
export const installWindowBehaviors = (
|
||||
app: Editor,
|
||||
window: Window,
|
||||
preventMultipleTabs: boolean = false
|
||||
) => {
|
||||
window.addEventListener("resize", () =>
|
||||
handleResize(app.interface.scope as HTMLCanvasElement)
|
||||
);
|
||||
window.addEventListener("resize", () =>
|
||||
handleResize(app.interface.feedback as HTMLCanvasElement)
|
||||
);
|
||||
window.addEventListener("beforeunload", () => {
|
||||
// @ts-ignore
|
||||
event.preventDefault();
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { type Editor } from "../main";
|
||||
import { AudibleEvent } from "./AbstractEvents";
|
||||
import { chord as parseChord, midiToFreq, noteFromPc, noteNameToMidi } from "zifferjs";
|
||||
import {
|
||||
chord as parseChord,
|
||||
midiToFreq,
|
||||
noteFromPc,
|
||||
noteNameToMidi,
|
||||
} from "zifferjs";
|
||||
|
||||
import {
|
||||
superdough,
|
||||
@ -10,10 +15,9 @@ import {
|
||||
export type SoundParams = {
|
||||
dur: number;
|
||||
s?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export class SoundEvent extends AudibleEvent {
|
||||
|
||||
nudge: number;
|
||||
|
||||
constructor(sound: string | object, public app: Editor) {
|
||||
@ -25,9 +29,10 @@ export class SoundEvent extends AudibleEvent {
|
||||
s: sound.split(":")[0],
|
||||
n: sound.split(":")[1],
|
||||
dur: app.clock.convertPulseToSecond(app.clock.ppqn),
|
||||
analyze: true,
|
||||
};
|
||||
} else {
|
||||
this.values = { s: sound, dur: 0.5 };
|
||||
this.values = { s: sound, dur: 0.5, analyze: true };
|
||||
}
|
||||
} else {
|
||||
this.values = sound;
|
||||
@ -116,7 +121,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
this.sustain(0.0);
|
||||
this.release(0.0);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// Lowpass filter
|
||||
public lpenv = (value: number) => this.updateValue("lpenv", value);
|
||||
@ -247,30 +252,43 @@ export class SoundEvent extends AudibleEvent {
|
||||
// Frequency management
|
||||
|
||||
public sound = (value: string) => this.updateValue("s", value);
|
||||
public chord = (value: string | object[] | number[] | number, ...kwargs: number[]) => {
|
||||
public chord = (
|
||||
value: string | object[] | number[] | number,
|
||||
...kwargs: number[]
|
||||
) => {
|
||||
if (typeof value === "string") {
|
||||
const chord = parseChord(value);
|
||||
value = chord.map((note: number) => { return { note: note, freq: midiToFreq(note) } });
|
||||
value = chord.map((note: number) => {
|
||||
return { note: note, freq: midiToFreq(note) };
|
||||
});
|
||||
} else if (value instanceof Array && typeof value[0] === "number") {
|
||||
value = (value as number[]).map((note: number) => { return { note: note, freq: midiToFreq(note) } });
|
||||
value = (value as number[]).map((note: number) => {
|
||||
return { note: note, freq: midiToFreq(note) };
|
||||
});
|
||||
} else if (typeof value === "number" && kwargs.length > 0) {
|
||||
value = [value, ...kwargs].map((note: number) => { return { note: note, freq: midiToFreq(note) } });
|
||||
value = [value, ...kwargs].map((note: number) => {
|
||||
return { note: note, freq: midiToFreq(note) };
|
||||
});
|
||||
}
|
||||
return this.updateValue("chord", value);
|
||||
}
|
||||
};
|
||||
public invert = (howMany: number = 0) => {
|
||||
if (this.values.chord) {
|
||||
let notes = this.values.chord.map((obj: { [key: string]: number }) => obj.note);
|
||||
let notes = this.values.chord.map(
|
||||
(obj: { [key: string]: number }) => obj.note
|
||||
);
|
||||
notes = howMany < 0 ? [...notes].reverse() : notes;
|
||||
for (let i = 0; i < Math.abs(howMany); i++) {
|
||||
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
|
||||
}
|
||||
const chord = notes.map((note: number) => { return { note: note, freq: midiToFreq(note) } });
|
||||
const chord = notes.map((note: number) => {
|
||||
return { note: note, freq: midiToFreq(note) };
|
||||
});
|
||||
return this.updateValue("chord", chord);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
};
|
||||
public snd = this.sound;
|
||||
public cut = (value: number) => this.updateValue("cut", value);
|
||||
public clip = (value: number) => this.updateValue("clip", value);
|
||||
@ -309,7 +327,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
|
||||
// Reverb management
|
||||
public room = (value: number) => this.updateValue("room", value);
|
||||
public rm = this.room
|
||||
public rm = this.room;
|
||||
public roomfade = (value: number) => this.updateValue("roomfade", value);
|
||||
public rfade = this.roomfade;
|
||||
public roomlp = (value: number) => this.updateValue("roomlp", value);
|
||||
@ -320,25 +338,26 @@ export class SoundEvent extends AudibleEvent {
|
||||
public sz = this.size;
|
||||
|
||||
// Compressor
|
||||
public comp = (value: number) => this.updateValue('compressor', value);
|
||||
public comp = (value: number) => this.updateValue("compressor", value);
|
||||
public cmp = this.comp;
|
||||
public ratio = (value: number) => this.updateValue('compressorRatio', value);
|
||||
public ratio = (value: number) => this.updateValue("compressorRatio", value);
|
||||
public rt = this.ratio;
|
||||
public knee = (value: number) => this.updateValue('compressorKnee', value);
|
||||
public knee = (value: number) => this.updateValue("compressorKnee", value);
|
||||
public kn = this.knee;
|
||||
public compAttack = (value: number) => this.updateValue('compressorAttack', value);
|
||||
public compAttack = (value: number) =>
|
||||
this.updateValue("compressorAttack", value);
|
||||
public cmpa = this.compAttack;
|
||||
public compRelease = (value: number) => this.updateValue('compressorRelease', value);
|
||||
public compRelease = (value: number) =>
|
||||
this.updateValue("compressorRelease", value);
|
||||
public cmpr = this.compRelease;
|
||||
|
||||
|
||||
// Unit
|
||||
public stretch = (beat: number) => {
|
||||
this.updateValue("unit", "c");
|
||||
this.updateValue("speed", 1 / beat)
|
||||
this.updateValue("cut", beat)
|
||||
this.updateValue("speed", 1 / beat);
|
||||
this.updateValue("cut", beat);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// ================================================================================
|
||||
// AbstactEvent overrides
|
||||
@ -368,7 +387,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
if (this.values.chord) {
|
||||
this.values.chord.forEach((obj: { [key: string]: number }) => {
|
||||
const copy = { ...this.values };
|
||||
copy.freq = obj.freq
|
||||
copy.freq = obj.freq;
|
||||
superdough(copy, this.nudge, this.values.dur);
|
||||
});
|
||||
} else {
|
||||
|
||||
29
src/documentation/oscilloscope.ts
Normal file
29
src/documentation/oscilloscope.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const oscilloscope = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `# Oscilloscope
|
||||
|
||||
You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the <ic>scope()</ic> function to turn it on and off. The oscilloscope is off by default.
|
||||
|
||||
${makeExample(
|
||||
"Oscilloscope configuration",
|
||||
`
|
||||
scope({
|
||||
enabled: true, // off by default
|
||||
color: "#fdba74", // any valid CSS color or "random"
|
||||
thickness: 4, // stroke thickness
|
||||
fftSize: 256, // multiples of 128
|
||||
orientation: "horizontal", // "vertical" or "horizontal"
|
||||
is3D: false, // 3D oscilloscope
|
||||
size: 1, // size of the oscilloscope
|
||||
})
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :)
|
||||
|
||||
`;
|
||||
};
|
||||
56
src/main.ts
56
src/main.ts
@ -1,3 +1,4 @@
|
||||
import { OscilloscopeConfig, runOscilloscope } from "./AudioVisualisation";
|
||||
import { EditorState, Compartment } from "@codemirror/state";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
@ -24,6 +25,7 @@ import showdown from "showdown";
|
||||
import { makeStringExtensions } from "./StringExtensions";
|
||||
import { installInterfaceLogic } from "./InterfaceLogic";
|
||||
import { installWindowBehaviors } from "./WindowBehavior";
|
||||
import { drawEmptyBlinkers } from "./AudioVisualisation";
|
||||
|
||||
export class Editor {
|
||||
// Universes and settings
|
||||
@ -56,6 +58,16 @@ export class Editor {
|
||||
show_error: boolean = false;
|
||||
buttonElements: Record<string, HTMLButtonElement[]> = {};
|
||||
interface: ElementMap = {};
|
||||
blinkTimeouts: Record<number, number> = {};
|
||||
osc: OscilloscopeConfig = {
|
||||
enabled: false,
|
||||
color: "#fdba74",
|
||||
thickness: 4,
|
||||
fftSize: 256,
|
||||
orientation: "horizontal",
|
||||
is3D: false,
|
||||
size: 1,
|
||||
};
|
||||
|
||||
// UserAPI
|
||||
api: UserAPI;
|
||||
@ -79,6 +91,8 @@ export class Editor {
|
||||
this.initializeElements();
|
||||
this.initializeButtonGroups();
|
||||
this.initializeHydra();
|
||||
this.setCanvas(this.interface.feedback as HTMLCanvasElement);
|
||||
this.setCanvas(this.interface.scope as HTMLCanvasElement);
|
||||
|
||||
// ================================================================================
|
||||
// Loading the universe from local storage
|
||||
@ -127,12 +141,14 @@ export class Editor {
|
||||
registerFillKeys(this);
|
||||
registerOnKeyDown(this);
|
||||
installInterfaceLogic(this);
|
||||
drawEmptyBlinkers(this);
|
||||
|
||||
// ================================================================================
|
||||
// Building CodeMirror Editor
|
||||
// ================================================================================
|
||||
|
||||
installEditor(this);
|
||||
runOscilloscope(this.interface.scope as HTMLCanvasElement, this);
|
||||
|
||||
// First evaluation of the init file
|
||||
tryEvaluate(this, this.universes[this.selected_universe.toString()].init);
|
||||
@ -381,25 +397,36 @@ export class Editor {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param color the color to flash the background
|
||||
* @param duration the duration of the flash
|
||||
* Flashes the background of the view and its gutters.
|
||||
* @param {string} color - The color to set.
|
||||
* @param {number} duration - Duration in milliseconds to maintain the color.
|
||||
*/
|
||||
flashBackground(color: string, duration: number): void {
|
||||
// Set the flashing color
|
||||
this.view.dom.style.backgroundColor = color;
|
||||
const gutters = this.view.dom.getElementsByClassName(
|
||||
const domElement = this.view.dom;
|
||||
const gutters = domElement.getElementsByClassName(
|
||||
"cm-gutter"
|
||||
) as HTMLCollectionOf<HTMLElement>;
|
||||
|
||||
domElement.classList.add("fluid-bg-transition");
|
||||
Array.from(gutters).forEach((gutter) =>
|
||||
gutter.classList.add("fluid-bg-transition")
|
||||
);
|
||||
|
||||
domElement.style.backgroundColor = color;
|
||||
Array.from(gutters).forEach(
|
||||
(gutter) => (gutter.style.backgroundColor = color)
|
||||
);
|
||||
|
||||
// Reset to original color after duration
|
||||
setTimeout(() => {
|
||||
this.view.dom.style.backgroundColor = "";
|
||||
domElement.style.backgroundColor = "";
|
||||
Array.from(gutters).forEach(
|
||||
(gutter) => (gutter.style.backgroundColor = "")
|
||||
);
|
||||
|
||||
domElement.classList.remove("fluid-bg-transition");
|
||||
Array.from(gutters).forEach((gutter) =>
|
||||
gutter.classList.remove("fluid-bg-transition")
|
||||
);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
@ -428,6 +455,21 @@ export class Editor {
|
||||
});
|
||||
this.hydra = this.hydra_backend.synth;
|
||||
}
|
||||
|
||||
private setCanvas(canvas: HTMLCanvasElement): void {
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Assuming the canvas takes up the whole window
|
||||
canvas.width = window.innerWidth * dpr;
|
||||
canvas.height = window.innerHeight * dpr;
|
||||
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app = new Editor();
|
||||
|
||||
Reference in New Issue
Block a user