diff --git a/index.html b/index.html index e88f57b..4f1c05e 100644 --- a/index.html +++ b/index.html @@ -552,6 +552,7 @@ + diff --git a/src/API.ts b/src/API.ts index dba0115..f5bba41 100644 --- a/src/API.ts +++ b/src/API.ts @@ -2187,6 +2187,291 @@ export class UserAPI { }, real_duration * 1000); }; + // ============================================================= + // Canvas Functions + // ============================================================= + + public clear = (): void => { + /** + * Clears the canvas after a given timeout. + * @param timeout - The timeout in seconds + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + + public width = (): number => { + /** + * Returns the width of the canvas. + * @returns The width of the canvas + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + return canvas.width; + } + + public height = (): number => { + /** + * Returns the height of the canvas. + * @returns The height of the canvas + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + return canvas.height; + } + + public background = (color: string|number, ...gb:number[]): void => { + /** + * Set background color of the canvas. + * @param color - The color to set. String or 3 numbers representing RGB values. + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + if(typeof color === "number") color = `rgb(${color},${gb[0]},${gb[1]})`; + ctx.fillStyle = color; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + public linearGradient = (x1: number, y1: number, x2: number, y2: number, ...stops: (number|string)[]) => { + /** + * Set linear gradient on the canvas. + * @param x1 - The x-coordinate of the start point + * @param y1 - The y-coordinate of the start point + * @param x2 - The x-coordinate of the end point + * @param y2 - The y-coordinate of the end point + * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createLinearGradient(x1, y1, x2, y2); + // Parse pairs of values from stops + for(let i=0; i { + /** + * Set radial gradient on the canvas. + * @param x1 - The x-coordinate of the start circle + * @param y1 - The y-coordinate of the start circle + * @param r1 - The radius of the start circle + * @param x2 - The x-coordinate of the end circle + * @param y2 - The y-coordinate of the end circle + * @param r2 - The radius of the end circle + * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2); + for(let i=0; i { + /** + * Set conic gradient on the canvas. + * @param x - The x-coordinate of the center of the gradient + * @param y - The y-coordinate of the center of the gradient + * @param angle - The angle of the gradient, in radians + * @param stops - The stops to set. Pairs of numbers representing the position and color of the stop. + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createConicGradient(x, y, angle); + for(let i=0; i { + /** + * Draws on the canvas. + * @param func - The function to execute + */ + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + func(ctx); + } + + public circle = ( + x: number, + y: number, + radius: number, + fillStyle: string, + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.fillStyle = fillStyle; + ctx.fill(); + }; + + public triangular = ( + x: number, + y: number, + radius: number, + fillStyle: string, + rotate: number + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotate * Math.PI) / 180); + ctx.beginPath(); + ctx.moveTo(0, -radius); + ctx.lineTo(radius, radius); + ctx.lineTo(-radius, radius); + ctx.closePath(); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.restore(); + } + + public star = ( + x: number, + y: number, + radius: number, + points: number = 5, + fillStyle: string = "white", + outerRadius: number = 1.0, + rotate: number = 0, + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + if(points<1) return this.circle(x, y, radius+outerRadius, fillStyle); + if(points==1) return this.triangular(x, y, radius, fillStyle, 0); + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotate * Math.PI) / 180); + ctx.beginPath(); + ctx.moveTo(0, -radius); + for (let i = 0; i < points; i++) { + ctx.rotate(Math.PI / points); + ctx.lineTo(0, -(radius * outerRadius)); + ctx.rotate(Math.PI / points); + ctx.lineTo(0, -radius); + } + ctx.closePath(); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.restore(); + }; + + public stroke = ( + x1: number, + y1: number, + x2: number, + y2: number, + fillStyle: string, + width: number = 1, + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.strokeStyle = fillStyle; + ctx.lineWidth = width; + ctx.stroke(); + }; + + public rectangle = ( + x: number, + y: number, + width: number, + height: number, + fillStyle: string, + rotate: number = 0, + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotate * Math.PI) / 180); + ctx.fillStyle = fillStyle; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + } + + public smiley = ( + x: number, + y: number, + radius: number, + fillStyle: string, + eyeSize: number = 1.0, + happiness: number = 0.0, + rotation: number = 0 + ): void => { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + // Map the rotation value to an angle within the range of -PI to PI + const rotationAngle = rotation/100 * Math.PI; + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rotationAngle); + + // Draw face + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, 2 * Math.PI); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.lineWidth = radius / 20; + ctx.strokeStyle = "black"; + ctx.stroke(); + + // Draw eyes + const eyeY = -radius / 5; + const eyeXOffset = radius / 2.5; + const eyeRadiusX = radius / 8; + const eyeRadiusY = eyeSize * radius / 10; + + ctx.beginPath(); + ctx.ellipse(-eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI); + ctx.fillStyle = "black"; + ctx.fill(); + + ctx.beginPath(); + ctx.ellipse(eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI); + ctx.fillStyle = "black"; + ctx.fill(); + + // Draw mouth with happiness number -1.0 to 1.0. 0.0 Should be a straight line. + const mouthY = radius / 2; + const mouthLength = radius * 0.9; + const smileFactor = 0.25; // Adjust for the smile curvature + + let controlPointX = 0; + let controlPointY = 0; + + if (happiness >= 0) { + controlPointY = mouthY + happiness * smileFactor * radius / 2; + } else { + controlPointY = mouthY + happiness * smileFactor * radius / 2; + } + + ctx.beginPath(); + ctx.moveTo(-mouthLength / 2, mouthY); + ctx.quadraticCurveTo(controlPointX, controlPointY, mouthLength / 2, mouthY); + ctx.lineWidth = 10; + ctx.strokeStyle = "black"; + ctx.stroke(); + ctx.restore(); + + } + + // ============================================================= // OSC Functions // ============================================================= diff --git a/src/DomElements.ts b/src/DomElements.ts index 2784767..c2f8278 100644 --- a/src/DomElements.ts +++ b/src/DomElements.ts @@ -54,6 +54,7 @@ export const singleElements = { error_line: "error_line", hydra_canvas: "hydra-bg", feedback: "feedback", + drawings: "drawings", scope: "scope", }; diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts index ee69e4c..0043e7d 100644 --- a/src/Utils/Generic.ts +++ b/src/Utils/Generic.ts @@ -69,6 +69,15 @@ export function arrayOfObjectsToObjectWithArrays>( ); } +export function maybeAtomic(value: T): T | T[] { + /* + * Returns first value of array if array of length 1, otherwise returns value + * @param {any} value - Value to check + * @returns {any} Value or array + */ + return Array.isArray(value) && value.length === 1 ? value[0] : value; +} + export function filterObject( obj: Record, filter: string[], diff --git a/src/WindowBehavior.ts b/src/WindowBehavior.ts index 89462f3..e42ef35 100644 --- a/src/WindowBehavior.ts +++ b/src/WindowBehavior.ts @@ -44,6 +44,9 @@ export const installWindowBehaviors = ( window.addEventListener("resize", () => handleResize(app.interface.feedback as HTMLCanvasElement), ); + window.addEventListener("resize", () => + handleResize(app.interface.drawings as HTMLCanvasElement), +); window.addEventListener("beforeunload", (event) => { event.preventDefault(); saveBeforeExit(app); diff --git a/src/classes/AbstractEvents.ts b/src/classes/AbstractEvents.ts index 2fa67c7..256d060 100644 --- a/src/classes/AbstractEvents.ts +++ b/src/classes/AbstractEvents.ts @@ -467,6 +467,16 @@ export abstract class AudibleEvent extends AbstractEvent { return this; } + public draw = (lambda: Function) => { + lambda(this.values, (this.app.interface.drawings as HTMLCanvasElement).getContext("2d")); + return this; + } + + public clear = () => { + this.app.api.clear(); + return this; + } + freq = (value: number | number[], ...kwargs: number[]): this => { /* * This function is used to set the frequency of the Event. diff --git a/src/classes/MidiEvent.ts b/src/classes/MidiEvent.ts index 5335eff..1515b23 100644 --- a/src/classes/MidiEvent.ts +++ b/src/classes/MidiEvent.ts @@ -6,6 +6,7 @@ import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects, + maybeAtomic, } from "../Utils/Generic"; export type MidiParams = { @@ -109,8 +110,8 @@ export class MidiEvent extends AudibleEvent { const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams; - this.values.note = newArrays.note; - if (newArrays.bend) this.values.bend = newArrays.bend; + this.values.note = maybeAtomic(newArrays.note); + if (newArrays.bend) this.values.bend = maybeAtomic(newArrays.bend); return this; }; diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 51aadad..bba55f5 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -5,6 +5,7 @@ import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects, + maybeAtomic, } from "../Utils/Generic"; import { midiToFreq, resolvePitchClass } from "zifferjs"; @@ -413,11 +414,11 @@ export class SoundEvent extends AudibleEvent { const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams; - this.values.note = newArrays.note; - this.values.freq = newArrays.freq; - this.values.pitch = newArrays.pitch; - this.values.octave = newArrays.octave; - this.values.pitchOctave = newArrays.pitchOctave; + this.values.note = maybeAtomic(newArrays.note); + this.values.freq = maybeAtomic(newArrays.freq); + this.values.pitch = maybeAtomic(newArrays.pitch); + this.values.octave = maybeAtomic(newArrays.octave); + this.values.pitchOctave = maybeAtomic(newArrays.pitchOctave); return this; }; diff --git a/src/documentation/patterns/ziffers/ziffers_basics.ts b/src/documentation/patterns/ziffers/ziffers_basics.ts index ecb2d96..21bd6ab 100644 --- a/src/documentation/patterns/ziffers/ziffers_basics.ts +++ b/src/documentation/patterns/ziffers/ziffers_basics.ts @@ -190,7 +190,7 @@ ${makeExample( )} ${makeExample( - "Chord transposition with roman numerals", + "Chord inversions with roman numerals", ` z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1') .sound('triangle').adsr(1/16, 1/5, 0.1, 0) @@ -201,7 +201,7 @@ ${makeExample( )} ${makeExample( - "Chord transposition with named chords", + "Chord inversion with named chords", ` z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1') .sound("sine").bpf(500 + usine(1/4) * 2000) diff --git a/src/main.ts b/src/main.ts index 20360c5..a8efb33 100644 --- a/src/main.ts +++ b/src/main.ts @@ -127,6 +127,7 @@ export class Editor { this.initializeButtonGroups(); this.setCanvas(this.interface.feedback as HTMLCanvasElement); this.setCanvas(this.interface.scope as HTMLCanvasElement); + this.setCanvas(this.interface.drawings as HTMLCanvasElement); try { this.loadHydraSynthAsync(); } catch (error) {