diff --git a/index.html b/index.html index 2b115d8..11a7f62 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,11 @@ padding: 0; } + .fluid-bg-transition { + transition: background-color 0.05s ease-in-out; + } + + #hydra-bg { position: fixed; /* ignore margins */ top: 0px; diff --git a/src/API.ts b/src/API.ts index 1c604b6..9367af5 100644 --- a/src/API.ts +++ b/src/API.ts @@ -26,6 +26,7 @@ import { } from "superdough"; import { Speaker } from "./StringExtensions"; import { getScaleNotes } from "zifferjs"; +import { 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; diff --git a/src/AudioVisualisation.ts b/src/AudioVisualisation.ts index 423fb51..307d760 100644 --- a/src/AudioVisualisation.ts +++ b/src/AudioVisualisation.ts @@ -1,2 +1,115 @@ // @ts-ignore import { analyser, getAnalyzerData } 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" + ); + } +}; diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts index fe3a6bd..84a966f 100644 --- a/src/WindowBehavior.ts +++ b/src/WindowBehavior.ts @@ -1,10 +1,28 @@ import { type Editor } from "./main"; +const handleResize = (app: Editor) => { + const canvas = app.interface.feedback as HTMLCanvasElement | null; // add type guard + 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)); window.addEventListener("beforeunload", () => { // @ts-ignore event.preventDefault(); diff --git a/src/main.ts b/src/main.ts index a9064b1..52f612e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,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 +57,7 @@ export class Editor { show_error: boolean = false; buttonElements: Record = {}; interface: ElementMap = {}; + blinkTimeouts: Record = {}; // UserAPI api: UserAPI; @@ -79,6 +81,7 @@ export class Editor { this.initializeElements(); this.initializeButtonGroups(); this.initializeHydra(); + this.setCanvas(); // ================================================================================ // Loading the universe from local storage @@ -127,6 +130,7 @@ export class Editor { registerFillKeys(this); registerOnKeyDown(this); installInterfaceLogic(this); + drawEmptyBlinkers(this); // ================================================================================ // Building CodeMirror Editor @@ -381,25 +385,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; + + 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 +443,23 @@ export class Editor { }); this.hydra = this.hydra_backend.synth; } + + private setCanvas(): void { + const canvas = this.interface.feedback as HTMLCanvasElement | null; // add type guard + + 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();