Clean up UI a bit, need to fix transport again

This commit is contained in:
2024-04-21 01:41:24 +02:00
parent bb6a52bd4b
commit fc0c7cc34c
11 changed files with 167 additions and 198 deletions

View File

@ -109,7 +109,7 @@
</a>
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
<!-- Play Button -->
<a title="Play button (Ctrl+P)" id="play-button-1" class="bar_button">
<a title="Play button (Ctrl+P)" id="play-button" class="bar_button">
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
</svg>
@ -120,7 +120,7 @@
</a>
<!-- Stop button -->
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="bar_button">
<a title="Stop button (Ctrl+R)" id="stop-button" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
@ -136,13 +136,6 @@
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
</a>
<a title="Clear button" id="clear-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
<path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Clear</p>
</a>
<a title="Share button" id="share-button" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
@ -157,6 +150,7 @@
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
</a>
<div id="transport_viewer" class="pr-2 text-selection_background"></div>
</nav>
</nav>
</div>

View File

@ -404,7 +404,7 @@ export const gif = (app: Editor) => (options: any): void => {
duration = 10
} = options;
let real_duration = duration * app.clock.pulse_duration * app.clock.ppqn;
let real_duration = duration * app.clock.time_position.tick_duration * app.clock.ppqn;
let fadeOutDuration = real_duration * 0.1;
let visibilityDuration = real_duration - fadeOutDuration;
const gifElement = document.createElement("img");

View File

@ -1,7 +1,8 @@
import { sendToServer, type OSCMessage } from "../../IO/OSC";
import { oscMessages } from "../../IO/OSC";
import { type Editor } from "../../main";
export const osc = () => (address: string, port: number, ...args: any[]): void => {
export const osc = (app: Editor) => (address: string, port: number, ...args: any[]): void => {
/**
* Sends an OSC message to the server.
*/
@ -9,7 +10,7 @@ export const osc = () => (address: string, port: number, ...args: any[]): void =
address: address,
port: port,
args: args,
timetag: Math.round(Date.now()),
timetag: Math.round(Date.now() - app.clock.getTimeDeviation()),
} as OSCMessage);
};

View File

@ -29,6 +29,7 @@ export const singleElements = {
line_numbers_checkbox: "show-line-numbers",
time_position_checkbox: "show-time-position",
tips_checkbox: "show-tips",
transport_viewer: "transport_viewer",
completion_checkbox: "show-completions",
midi_clock_checkbox: "send-midi-clock",
midi_channels_scripts: "midi-channels-scripts",
@ -44,6 +45,11 @@ export const singleElements = {
hydra_canvas: "hydra-bg",
feedback: "feedback",
scope: "scope",
play_button: "play-button",
play_label: "play-label",
stop_button: "stop-button",
play_icon: "play-icon",
pause_icon: "pause-icon",
} as const;
export type SingleElementsKeys = keyof typeof singleElements;
@ -60,12 +66,6 @@ export type ElementMap = {
| HTMLInputElement;
};
export const buttonGroups = {
play_buttons: ["play-button-1"],
stop_buttons: ["stop-button-1"],
clear_buttons: ["clear-button-1"],
};
//@ts-ignore
export const createDocumentationStyle = (app: Editor) => {
/**

View File

@ -3,6 +3,7 @@ import { vim } from "@replit/codemirror-vim";
import { tryEvaluate } from "../Evaluator";
import { hideDocumentation, showDocumentation } from "../Docs/Documentation";
import { openSettingsModal, openUniverseModal } from "../Editor/FileManagement";
import { resetTransportView, updatePlayButton } from "./UILogic";
export const registerFillKeys = (app: Editor) => {
document.addEventListener("keydown", (event) => {
@ -53,21 +54,21 @@ export const registerOnKeyDown = (app: Editor) => {
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
app.setButtonHighlighting("stop", true);
app.clock.stop();
app.flashBackground("#404040", 200);
requestAnimationFrame (() => {
updatePlayButton(app);
resetTransportView(app);
});
app.clock.stop()
}
if (event.ctrlKey && event.key === "p") {
event.preventDefault();
if (app.isPlaying) {
app.isPlaying = false;
app.setButtonHighlighting("pause", true);
app.clock.pause();
} else {
app.isPlaying = true;
app.setButtonHighlighting("play", true);
app.clock.start();
}
app.flashBackground("#404040", 200);
requestAnimationFrame(() => {
updatePlayButton(app);
});
app.clock.resume()
}
// Ctrl + Shift + V: Vim Mode

View File

@ -73,33 +73,22 @@ export const installInterfaceLogic = (app: Editor) => {
openUniverseModal();
});
app.buttonElements['play_buttons']!.forEach((button) => {
button.addEventListener("click", () => {
if (app.isPlaying) {
app.setButtonHighlighting("pause", true);
app.isPlaying = !app.isPlaying;
app.clock.pause();
app.api.MidiConnection.sendStopMessage();
} else {
app.setButtonHighlighting("play", true);
app.isPlaying = !app.isPlaying;
app.clock.start();
app.api.MidiConnection.sendStartMessage();
}
});
app.interface['play_button'].addEventListener("click", () => {
if (app.isPlaying) {
app.clock.pause();
} else {
app.clock.resume()
}
updatePlayButton(app);
});
app.buttonElements['clear_buttons']!.forEach((button) => {
button.addEventListener("click", () => {
app.setButtonHighlighting("clear", true);
if (confirm("Do you want to reset the current universe?")) {
app.universes[app.selected_universe] =
structuredClone(template_universe);
app.updateEditorView();
}
});
app.interface['stop_button'].addEventListener("click", () => {
app.isPlaying = false;
app.clock.stop();
updatePlayButton(app);
});
app.interface.documentation_button.addEventListener("click", () => {
showDocumentation(app);
});
@ -140,13 +129,6 @@ export const installInterfaceLogic = (app: Editor) => {
}
});
app.interface.audio_nudge_range.addEventListener("input", () => {
// TODO: rebuild this
// app.clock.nudge = parseInt(
// (app.interface.audio_nudge_range as HTMLInputElement).value,
// );
});
app.interface.dough_nudge_range.addEventListener("input", () => {
app.dough_nudge = parseInt(
(app.interface.dough_nudge_range as HTMLInputElement).value,
@ -239,14 +221,6 @@ export const installInterfaceLogic = (app: Editor) => {
app.flashBackground("#404040", 200);
});
app.buttonElements['stop_buttons']!.forEach((button) => {
button.addEventListener("click", () => {
app.setButtonHighlighting("stop", true);
app.isPlaying = false;
app.clock.stop();
});
});
app.interface.local_button.addEventListener("click", () =>
app.changeModeFromInterface("local"),
);
@ -537,3 +511,43 @@ export const installInterfaceLogic = (app: Editor) => {
}
});
};
export const updatePlayButton = (app: Editor) => {
switch (app.clock.state) {
case 'stopped':
app.interface.play_label.innerText = "Play";
updatePlayPauseIcon(app, "play");
break;
case 'paused':
app.interface.play_label.innerText = "Resume";
updatePlayPauseIcon(app, "play");
break;
case 'running':
app.interface.play_label.innerText = "Pause";
updatePlayPauseIcon(app, "pause");
break;
}
}
export const updatePlayPauseIcon = (app: Editor, state: "play" | "pause"): void => {
const { play_icon, pause_icon } = app.interface;
const isPlayIconHidden = play_icon.classList.contains("hidden");
const isPauseIconHidden = pause_icon.classList.contains("hidden");
if (state === "play" && isPlayIconHidden) {
play_icon.classList.remove("hidden");
pause_icon.classList.add("hidden");
} else if (state === "pause" && isPauseIconHidden) {
play_icon.classList.add("hidden");
pause_icon.classList.remove("hidden");
}
}
export const resetTransportView = (app: Editor) => {
requestAnimationFrame(() => {
app.interface.transport_viewer.innerHTML = `<span class="text-xl text-neutral">00:00:00</span>`;
});
}

View File

@ -122,8 +122,8 @@ export class MidiEvent extends AudibleEvent {
const note = params.note ? params.note : 60;
const sustain = params.sustain
? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn()
: event.app.clock.pulse_duration * event.app.api.ppqn();
? params.sustain * event.app.clock.time_position.tick_duration * event.app.api.ppqn()
: event.app.clock.time_position.tick_duration * event.app.api.ppqn();
const bend = params.bend ? params.bend : undefined;

View File

@ -7,12 +7,16 @@ export interface TimePosition {
bpm: number; ppqn: number; time: number;
tick: number; beat: number; bar: number;
num: number; den: number; grain: number;
tick_duration: number;
}
export class Clock {
ctx: AudioContext;
transportNode: ClockNode | null;
time_position: TimePosition;
startTime: number | null = null;
elapsedTime: number = 0;
state: 'running' | 'paused' | 'stopped' = 'stopped';
constructor(
public app: Editor,
@ -28,6 +32,7 @@ export class Clock {
num: 0,
den: 0,
grain: 0,
tick_duration: 0,
};
this.transportNode = null;
this.ctx = ctx;
@ -43,6 +48,53 @@ export class Clock {
});
}
public play(): void {
if (this.state !== 'running') {
this.elapsedTime = 0;
this.state = 'running';
}
this.startTime = performance.now();
this.app.api.MidiConnection.sendStartMessage();
this.transportNode?.start();
}
public pause(): void {
this.state = 'paused';
if (this.startTime !== null) {
this.elapsedTime += performance.now() - this.startTime;
this.startTime = null;
}
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.pause();
}
public resume(): void {
if (this.state === 'stopped' || this.state === 'paused') {
this.startTime = performance.now();
this.state = 'running';
this.app.api.MidiConnection.sendStartMessage();
this.transportNode?.start();
} else if (this.state === 'running') {
this.state = 'paused';
if (this.startTime !== null) {
this.elapsedTime += performance.now() - this.startTime;
this.startTime = null;
}
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.pause();
}
}
public stop(): void {
if (this.startTime !== null) {
this.elapsedTime += performance.now() - this.startTime;
this.startTime = null;
}
this.state = 'stopped';
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop();
}
get grain(): number {
return this.time_position.grain;
}
@ -85,20 +137,6 @@ export class Clock {
return Math.floor(this.time_position.tick / this.ppqn)
}
get pulse_duration(): number {
/**
* Returns the duration of a pulse in seconds.
*/
return 60 / this.time_position.bpm / this.time_position.ppqn;
}
public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/**
* Returns the duration of a pulse in seconds at a specific bpm.
*/
return 60 / bpm / this.time_position.ppqn;
}
get bpm(): number {
return this.time_position.bpm;
}
@ -126,7 +164,7 @@ export class Clock {
* @param nudge - nudge in the future (in seconds)
* @returns remainingTime
*/
const pulseDuration = this.pulse_duration;
const pulseDuration = this.time_position.tick_duration;
const nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime;
@ -146,39 +184,35 @@ export class Clock {
const grain = n;
const beat = Math.floor(n / ppqn) % num;
const bar = Math.floor(n / ppqn / num);
const time = n * this.pulse_duration;
return { bpm, ppqn, time, tick, beat, bar, num, den, grain };
const time = n * this.time_position.tick_duration;
const tick_duration = this.time_position.tick_duration;
return { bpm, ppqn, time, tick, beat, bar, num, den, grain, tick_duration };
}
public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.
*/
return n * this.pulse_duration;
return n * this.time_position.tick_duration;
}
public start(): void {
/**
* Starts the TransportNode (starts the clock).
*
* @remark also sends a MIDI message if a port is declared
*/
this.app.api.MidiConnection.sendStartMessage();
this.transportNode?.start();
}
public pause(): void {
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.pause()
}
public setSignature(num: number, den: number): void {
this.transportNode?.setSignature(num, den);
}
public stop(): void {
this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop()
public getElapsed(): number {
if (this.startTime === null) {
return this.elapsedTime;
} else {
return this.elapsedTime + (performance.now() - this.startTime);
}
}
public getTimeDeviation(grain: number, tick_duration: number): number {
const idealTime = grain * tick_duration;
const elapsedTime = this.getElapsed();
const timeDeviation = elapsedTime - idealTime;
return timeDeviation;
}
}

View File

@ -24,9 +24,11 @@ export class ClockNode extends AudioWorkletNode {
num: message.data.num,
den: message.data.den,
grain: message.data.grain,
tick_duration: message.data.tick_duration,
}
this.app.settings.send_clock ?? this.app.api.MidiConnection.sendMidiClock();
tryEvaluate(
this.updateTransportViewer();
tryEvaluate(
this.app,
this.app.exampleIsPlaying
? this.app.example_buffer
@ -34,6 +36,15 @@ export class ClockNode extends AudioWorkletNode {
);
}
};
updateTransportViewer() {
const { bar, beat, tick } = this.app.clock.time_position;
const paddedBar = String(bar).padStart(2, '0');
const paddedBeat = String(beat).padStart(2, '0');
const paddedTick = String(tick).padStart(2, '0');
requestAnimationFrame(() => {
this.app.interface.transport_viewer.innerHTML = `<span class="text-xl text-neutral">${paddedBar}:${paddedBeat}:${paddedTick}</span>`;
});
}
start() {
this.port.postMessage({ type: "start" });

View File

@ -35,6 +35,7 @@ class TransportProcessor extends AudioWorkletProcessor {
this.pauseTime = 0;
this.totalPauseTime = 0;
this.currentPulsePosition = 0;
this.grain = 0;
} else if (message.data.type === "bpm") {
this.bpm = message.data.value;
this.startTime = currentTime;
@ -78,7 +79,8 @@ class TransportProcessor extends AudioWorkletProcessor {
bpm: this.bpm,
ppqn: this.ppqn,
type: 'time',
time: currentTime,
//time: currentTime,
time: adjustedCurrentTime,
tick: currentTick,
beat: currentBeat,
bar: currentBar,
@ -86,6 +88,7 @@ class TransportProcessor extends AudioWorkletProcessor {
num: this.timeSignature[0],
den: this.timeSignature[1],
grain: this.grain,
tick_duration: 60 / this.bpm / this.ppqn,
});
}
}

View File

@ -12,7 +12,7 @@ import {
Universe,
loadUniverserFromUrl,
} from "./Editor/FileManagement";
import { singleElements, buttonGroups, ElementMap, createDocumentationStyle } from "./DOM/DomElements";
import { singleElements, ElementMap, createDocumentationStyle } from "./DOM/DomElements";
import { registerFillKeys, registerOnKeyDown } from "./DOM/Keyboard";
import { installEditor } from "./Editor/EditorSetup";
import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Docs/Documentation";
@ -125,7 +125,6 @@ export class Editor {
// ================================================================================
this.initializeElements();
this.initializeButtonGroups();
this.setCanvas(this.interface["feedback"] as HTMLCanvasElement);
// ================================================================================
@ -393,86 +392,6 @@ export class Editor {
this.updateEditorView();
}
setButtonHighlighting(
button: "play" | "pause" | "stop" | "clear",
highlight: boolean,
) {
/**
* Sets the highlighting for a specific button.
*
* @param button - The button to highlight ("play", "pause", "stop", or "clear").
* @param highlight - A boolean indicating whether to highlight the button or not.
*/
document.getElementById("play-label")!.textContent =
button !== "pause" ? "Pause" : "Play";
if (button !== "pause") {
document.getElementById("pause-icon")!.classList.remove("hidden");
document.getElementById("play-icon")!.classList.add("hidden");
} else {
document.getElementById("pause-icon")!.classList.add("hidden");
document.getElementById("play-icon")!.classList.remove("hidden");
}
if (button === "stop") {
this.isPlaying == false;
document.getElementById("play-label")!.textContent = "Play";
document.getElementById("pause-icon")!.classList.add("hidden");
document.getElementById("play-icon")!.classList.remove("hidden");
}
this.flashBackground("#404040", 200);
const possible_selectors = [
'[id^="play-button-"]',
'[id^="clear-button-"]',
'[id^="stop-button-"]',
];
let selector: number;
switch (button) {
case "play":
selector = 0;
break;
case "pause":
selector = 1;
break;
case "clear":
selector = 2;
break;
case "stop":
selector = 3;
break;
}
const selectorValue = possible_selectors[selector];
if (selectorValue) {
document
.querySelectorAll(selectorValue)
.forEach((button) => {
if (highlight && button.children[0]) button.children[0].classList.add("animate-pulse");
});
}
// All other buttons must lose the highlighting
document
.querySelectorAll(
possible_selectors.filter((_, index) => index != selector).join(","),
)
.forEach((button) => {
if (button.children[0]) {
button.children[0].classList.remove("animate-pulse");
}
if (button.children[1]) {
button.children[1].classList.remove("animate-pulse");
}
});
}
unfocusPlayButtons() {
document.querySelectorAll('[id^="play-button-"]').forEach((button) => {
if (button.children[0]) {
button.children[0].classList.remove("fill-foreground_selection");
button.children[0].classList.remove("animate-pulse");
}
});
}
updateEditorView(): void {
this.view.dispatch({
changes: {
@ -538,14 +457,6 @@ export class Editor {
}
}
private initializeButtonGroups(): void {
for (const [key, ids] of Object.entries(buttonGroups)) {
this.buttonElements[key] = ids.map(
(id) => document.getElementById(id) as HTMLButtonElement,
);
}
}
public ensureHydraLoaded(): Promise<void> {
if (this.hydra_loaded) {
return Promise.resolve();