Merge pull request #75 from Bubobubobubobubo/visualisation

Adding some visualisations
This commit is contained in:
Raphaël Forment
2023-10-22 23:26:58 +01:00
committed by GitHub
11 changed files with 393 additions and 41 deletions

View File

@ -22,7 +22,12 @@
padding: 0; padding: 0;
} }
#hydra-bg { .fluid-bg-transition {
transition: background-color 0.05s ease-in-out;
}
.fullscreencanvas {
position: fixed; /* ignore margins */ position: fixed; /* ignore margins */
top: 0px; top: 0px;
left: 0px; left: 0px;
@ -34,7 +39,6 @@
display: block; display: block;
} }
details br { details br {
display: none; display: none;
} }
@ -160,6 +164,7 @@
<div class="space-y-2"> <div class="space-y-2">
<h2 class="font-semibold lg:text-xl text-gray-400">More</h2> <h2 class="font-semibold lg:text-xl text-gray-400">More</h2>
<div class="flex flex-col"> <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_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> <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> </div>
@ -447,7 +452,9 @@
</ul> </ul>
<!-- Here comes the editor itself --> <!-- Here comes the editor itself -->
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden"> <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> </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> <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> </div>

View File

@ -26,6 +26,7 @@ import {
} from "superdough"; } from "superdough";
import { Speaker } from "./StringExtensions"; import { Speaker } from "./StringExtensions";
import { getScaleNotes } from "zifferjs"; import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
interface ControlChange { interface ControlChange {
channel: number; channel: number;
@ -269,10 +270,13 @@ export class UserAPI {
* @returns The result of the evaluation * @returns The result of the evaluation
*/ */
args.forEach((arg) => { args.forEach((arg) => {
tryEvaluate( if (arg >= 1 && arg <= 9) {
this.app, blinkScript(this.app, "local", arg);
this.app.universes[this.app.selected_universe].locals[arg] tryEvaluate(
); this.app,
this.app.universes[this.app.selected_universe].locals[arg]
);
}
}); });
}; };
s = this.script; s = this.script;
@ -1964,4 +1968,19 @@ export class UserAPI {
*/ */
return array.concat(array.slice(0, array.length - 1).reverse()); 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
View 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();
};

View File

@ -1,5 +1,6 @@
import { type Editor } from "./main"; import { type Editor } from "./main";
import { introduction } from "./documentation/introduction"; import { introduction } from "./documentation/introduction";
import { oscilloscope } from "./documentation/oscilloscope";
import { samples } from "./documentation/samples"; import { samples } from "./documentation/samples";
import { chaining } from "./documentation/chaining"; import { chaining } from "./documentation/chaining";
import { software_interface } from "./documentation/interface"; import { software_interface } from "./documentation/interface";
@ -85,6 +86,7 @@ export const documentation_factory = (application: Editor) => {
functions: functions(application), functions: functions(application),
reference: reference(), reference: reference(),
shortcuts: shortcuts(), shortcuts: shortcuts(),
oscilloscope: oscilloscope(application),
bonus: bonus(application), bonus: bonus(application),
about: about(), about: about(),
}; };

View File

@ -47,6 +47,8 @@ export const singleElements = {
dough_nudge_range: "dough_nudge", dough_nudge_range: "dough_nudge",
error_line: "error_line", error_line: "error_line",
hydra_canvas: "hydra-bg", hydra_canvas: "hydra-bg",
feedback: "feedback",
scope: "scope",
}; };
export const buttonGroups = { export const buttonGroups = {

View File

@ -451,6 +451,7 @@ export const installInterfaceLogic = (app: Editor) => {
"shortcuts", "shortcuts",
"about", "about",
"bonus", "bonus",
"oscilloscope",
].forEach((e) => { ].forEach((e) => {
let name = `docs_` + e; let name = `docs_` + e;
document.getElementById(name)!.addEventListener("click", async () => { document.getElementById(name)!.addEventListener("click", async () => {

View File

@ -13,14 +13,16 @@ export class TransportNode extends AudioWorkletNode {
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */ /** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => { handleMessage = (message) => {
if (message.data && message.data.type === "bang") { 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++; this.app.clock.tick++;
const futureTimeStamp = this.app.clock.convertTicksToTimeposition( const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick this.app.clock.tick
); );
this.app.clock.time_position = futureTimeStamp; this.app.clock.time_position = futureTimeStamp;
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1 this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`; futureTimeStamp.beat + 1
}:${zeroPad(futureTimeStamp.pulse, 2)} / ${this.app.clock.bpm}`;
if (this.app.exampleIsPlaying) { if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer); tryEvaluate(this.app, this.app.example_buffer);
} else { } else {

View File

@ -1,10 +1,32 @@
import { type Editor } from "./main"; 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 = ( export const installWindowBehaviors = (
app: Editor, app: Editor,
window: Window, window: Window,
preventMultipleTabs: boolean = false preventMultipleTabs: boolean = false
) => { ) => {
window.addEventListener("resize", () =>
handleResize(app.interface.scope as HTMLCanvasElement)
);
window.addEventListener("resize", () =>
handleResize(app.interface.feedback as HTMLCanvasElement)
);
window.addEventListener("beforeunload", () => { window.addEventListener("beforeunload", () => {
// @ts-ignore // @ts-ignore
event.preventDefault(); event.preventDefault();

View File

@ -1,6 +1,11 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { chord as parseChord, midiToFreq, noteFromPc, noteNameToMidi } from "zifferjs"; import {
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import { import {
superdough, superdough,
@ -10,10 +15,9 @@ import {
export type SoundParams = { export type SoundParams = {
dur: number; dur: number;
s?: string; s?: string;
} };
export class SoundEvent extends AudibleEvent { export class SoundEvent extends AudibleEvent {
nudge: number; nudge: number;
constructor(sound: string | object, public app: Editor) { constructor(sound: string | object, public app: Editor) {
@ -25,9 +29,10 @@ export class SoundEvent extends AudibleEvent {
s: sound.split(":")[0], s: sound.split(":")[0],
n: sound.split(":")[1], n: sound.split(":")[1],
dur: app.clock.convertPulseToSecond(app.clock.ppqn), dur: app.clock.convertPulseToSecond(app.clock.ppqn),
analyze: true,
}; };
} else { } else {
this.values = { s: sound, dur: 0.5 }; this.values = { s: sound, dur: 0.5, analyze: true };
} }
} else { } else {
this.values = sound; this.values = sound;
@ -116,7 +121,7 @@ export class SoundEvent extends AudibleEvent {
this.sustain(0.0); this.sustain(0.0);
this.release(0.0); this.release(0.0);
return this; return this;
} };
// Lowpass filter // Lowpass filter
public lpenv = (value: number) => this.updateValue("lpenv", value); public lpenv = (value: number) => this.updateValue("lpenv", value);
@ -247,30 +252,43 @@ export class SoundEvent extends AudibleEvent {
// Frequency management // Frequency management
public sound = (value: string) => this.updateValue("s", value); 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") { if (typeof value === "string") {
const chord = parseChord(value); 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") { } 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) { } 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); return this.updateValue("chord", value);
} };
public invert = (howMany: number = 0) => { public invert = (howMany: number = 0) => {
if (this.values.chord) { 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; notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) { for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12; 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); return this.updateValue("chord", chord);
} else { } else {
return this; return this;
} }
} };
public snd = this.sound; public snd = this.sound;
public cut = (value: number) => this.updateValue("cut", value); public cut = (value: number) => this.updateValue("cut", value);
public clip = (value: number) => this.updateValue("clip", value); public clip = (value: number) => this.updateValue("clip", value);
@ -309,7 +327,7 @@ export class SoundEvent extends AudibleEvent {
// Reverb management // Reverb management
public room = (value: number) => this.updateValue("room", value); 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 roomfade = (value: number) => this.updateValue("roomfade", value);
public rfade = this.roomfade; public rfade = this.roomfade;
public roomlp = (value: number) => this.updateValue("roomlp", value); public roomlp = (value: number) => this.updateValue("roomlp", value);
@ -320,25 +338,26 @@ export class SoundEvent extends AudibleEvent {
public sz = this.size; public sz = this.size;
// Compressor // Compressor
public comp = (value: number) => this.updateValue('compressor', value); public comp = (value: number) => this.updateValue("compressor", value);
public cmp = this.comp; 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 rt = this.ratio;
public knee = (value: number) => this.updateValue('compressorKnee', value); public knee = (value: number) => this.updateValue("compressorKnee", value);
public kn = this.knee; 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 cmpa = this.compAttack;
public compRelease = (value: number) => this.updateValue('compressorRelease', value); public compRelease = (value: number) =>
this.updateValue("compressorRelease", value);
public cmpr = this.compRelease; public cmpr = this.compRelease;
// Unit // Unit
public stretch = (beat: number) => { public stretch = (beat: number) => {
this.updateValue("unit", "c"); this.updateValue("unit", "c");
this.updateValue("speed", 1 / beat) this.updateValue("speed", 1 / beat);
this.updateValue("cut", beat) this.updateValue("cut", beat);
return this; return this;
} };
// ================================================================================ // ================================================================================
// AbstactEvent overrides // AbstactEvent overrides
@ -368,7 +387,7 @@ export class SoundEvent extends AudibleEvent {
if (this.values.chord) { if (this.values.chord) {
this.values.chord.forEach((obj: { [key: string]: number }) => { this.values.chord.forEach((obj: { [key: string]: number }) => {
const copy = { ...this.values }; const copy = { ...this.values };
copy.freq = obj.freq copy.freq = obj.freq;
superdough(copy, this.nudge, this.values.dur); superdough(copy, this.nudge, this.values.dur);
}); });
} else { } else {

View 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 :)
`;
};

View File

@ -1,3 +1,4 @@
import { OscilloscopeConfig, runOscilloscope } from "./AudioVisualisation";
import { EditorState, Compartment } from "@codemirror/state"; import { EditorState, Compartment } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown"; import { markdown } from "@codemirror/lang-markdown";
@ -24,6 +25,7 @@ import showdown from "showdown";
import { makeStringExtensions } from "./StringExtensions"; import { makeStringExtensions } from "./StringExtensions";
import { installInterfaceLogic } from "./InterfaceLogic"; import { installInterfaceLogic } from "./InterfaceLogic";
import { installWindowBehaviors } from "./WindowBehavior"; import { installWindowBehaviors } from "./WindowBehavior";
import { drawEmptyBlinkers } from "./AudioVisualisation";
export class Editor { export class Editor {
// Universes and settings // Universes and settings
@ -56,6 +58,16 @@ export class Editor {
show_error: boolean = false; show_error: boolean = false;
buttonElements: Record<string, HTMLButtonElement[]> = {}; buttonElements: Record<string, HTMLButtonElement[]> = {};
interface: ElementMap = {}; interface: ElementMap = {};
blinkTimeouts: Record<number, number> = {};
osc: OscilloscopeConfig = {
enabled: false,
color: "#fdba74",
thickness: 4,
fftSize: 256,
orientation: "horizontal",
is3D: false,
size: 1,
};
// UserAPI // UserAPI
api: UserAPI; api: UserAPI;
@ -79,6 +91,8 @@ export class Editor {
this.initializeElements(); this.initializeElements();
this.initializeButtonGroups(); this.initializeButtonGroups();
this.initializeHydra(); this.initializeHydra();
this.setCanvas(this.interface.feedback as HTMLCanvasElement);
this.setCanvas(this.interface.scope as HTMLCanvasElement);
// ================================================================================ // ================================================================================
// Loading the universe from local storage // Loading the universe from local storage
@ -127,12 +141,14 @@ export class Editor {
registerFillKeys(this); registerFillKeys(this);
registerOnKeyDown(this); registerOnKeyDown(this);
installInterfaceLogic(this); installInterfaceLogic(this);
drawEmptyBlinkers(this);
// ================================================================================ // ================================================================================
// Building CodeMirror Editor // Building CodeMirror Editor
// ================================================================================ // ================================================================================
installEditor(this); installEditor(this);
runOscilloscope(this.interface.scope as HTMLCanvasElement, this);
// First evaluation of the init file // First evaluation of the init file
tryEvaluate(this, this.universes[this.selected_universe.toString()].init); tryEvaluate(this, this.universes[this.selected_universe.toString()].init);
@ -381,25 +397,36 @@ export class Editor {
} }
/** /**
* @param color the color to flash the background * Flashes the background of the view and its gutters.
* @param duration the duration of the flash * @param {string} color - The color to set.
* @param {number} duration - Duration in milliseconds to maintain the color.
*/ */
flashBackground(color: string, duration: number): void { flashBackground(color: string, duration: number): void {
// Set the flashing color const domElement = this.view.dom;
this.view.dom.style.backgroundColor = color; const gutters = domElement.getElementsByClassName(
const gutters = this.view.dom.getElementsByClassName(
"cm-gutter" "cm-gutter"
) as HTMLCollectionOf<HTMLElement>; ) 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( Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = color) (gutter) => (gutter.style.backgroundColor = color)
); );
// Reset to original color after duration
setTimeout(() => { setTimeout(() => {
this.view.dom.style.backgroundColor = ""; domElement.style.backgroundColor = "";
Array.from(gutters).forEach( Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = "") (gutter) => (gutter.style.backgroundColor = "")
); );
domElement.classList.remove("fluid-bg-transition");
Array.from(gutters).forEach((gutter) =>
gutter.classList.remove("fluid-bg-transition")
);
}, duration); }, duration);
} }
@ -428,6 +455,21 @@ export class Editor {
}); });
this.hydra = this.hydra_backend.synth; 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(); let app = new Editor();