diff --git a/index.html b/index.html index 4f1c05e..173eb1f 100644 --- a/index.html +++ b/index.html @@ -237,6 +237,7 @@
diff --git a/src/API.ts b/src/API.ts index f5bba41..20f1207 100644 --- a/src/API.ts +++ b/src/API.ts @@ -73,6 +73,36 @@ export async function loadSamples() { ]); } +export type ShapeObject = { + x: number, + y: number, + x1: number, + y1: number, + x2: number, + y2: number, + radius: number, + width: number, + height: number, + fillStyle: string, + secondary: string, + strokeStyle: string, + rotate: number, + points: number, + outerRadius: number, + rotation: number, + eyeSize: number, + happiness: number, + slices: number, + gap: number, + font: string, + fontSize: number, + text: string, + filter: string, + url: string, + curve: number, + curves: number, +} + export class UserAPI { /** * The UserAPI class is the interface between the user's code and the backend. It provides @@ -2191,7 +2221,7 @@ export class UserAPI { // Canvas Functions // ============================================================= - public clear = (): void => { + public clear = (): boolean => { /** * Clears the canvas after a given timeout. * @param timeout - The timeout in seconds @@ -2199,27 +2229,44 @@ export class UserAPI { const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; const ctx = canvas.getContext("2d")!; ctx.clearRect(0, 0, canvas.width, canvas.height); + return true; } - public width = (): number => { + public w = (): 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; + return canvas.clientWidth; } - public height = (): number => { + public h = (): 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; + return canvas.clientHeight; } - public background = (color: string|number, ...gb:number[]): void => { + public hc = (): number => { + /** + * Returns the center y-coordinate of the canvas. + * @returns The center y-coordinate of the canvas + */ + return this.h() / 2; + } + + public wc = (): number => { + /** + * Returns the center x-coordinate of the canvas. + * @returns The center x-coordinate of the canvas + */ + return this.w() / 2; + } + + public background = (color: string|number, ...gb:number[]): boolean => { /** * Set background color of the canvas. * @param color - The color to set. String or 3 numbers representing RGB values. @@ -2229,7 +2276,9 @@ export class UserAPI { if(typeof color === "number") color = `rgb(${color},${gb[0]},${gb[1]})`; ctx.fillStyle = color; ctx.fillRect(0, 0, canvas.width, canvas.height); + return true; } + bg = this.background; public linearGradient = (x1: number, y1: number, x2: number, y2: number, ...stops: (number|string)[]) => { /** @@ -2293,37 +2342,112 @@ export class UserAPI { return gradient; } - public draw = (func: Function): void => { + public draw = (func: Function): boolean => { /** * 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); + if(typeof func === "string") { + this.drawText (func); + } else { + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + func(ctx); + } + return true; } - public circle = ( - x: number, - y: number, - radius: number, - fillStyle: string, - ): void => { + public balloid = ( + curves: number|ShapeObject = 6, + radius: number = this.hc()/2, + curve: number = 1.5, + fillStyle: string = "white", + secondary: string = "black", + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof curves === "object") { + fillStyle = curves.fillStyle || "white"; + x = curves.x || this.wc(); + y = curves.y || this.hc(); + curve = curves.curve || 1.5; + radius = curves.radius || this.hc()/2; + curves = curves.curves || 6; + } const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; const ctx = canvas.getContext("2d")!; + + // Draw the shape using quadratic Bézier curves ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI); ctx.fillStyle = fillStyle; - ctx.fill(); - }; + + if (curves === 0) { + // Draw a circle if curves = 0 + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fill(); + } else if (curves === 1) { + // Draw a single curve (ellipse) if curves = 1 + ctx.ellipse(x, y, radius*0.8, (radius* curve)*0.7, 0, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fill(); + } else if (curves === 2) { + // Draw a shape with two symmetric curves starting from the top and meeting at the bottom + ctx.moveTo(x, y - radius); - public triangular = ( - x: number, - y: number, - radius: number, - fillStyle: string, - rotate: number - ): void => { + // First curve + ctx.quadraticCurveTo(x + radius * curve, y, x, y + radius); + + // Second symmetric curve + ctx.quadraticCurveTo(x - radius * curve, y, x, y - radius); + + ctx.closePath(); + ctx.fill(); + } else { + // Draw the curved shape with the specified number of curves + ctx.moveTo(x, y - radius); + let points = []; + for (let i = 0; i < curves; i++) { + const startAngle = (i / curves) * 2 * Math.PI; + const endAngle = startAngle + (2 * Math.PI) / curves; + + const controlX = x + radius * curve * Math.cos(startAngle + Math.PI / curves); + const controlY = y + radius * curve * Math.sin(startAngle + Math.PI / curves); + points.push([x + radius * Math.cos(startAngle), y + radius * Math.sin(startAngle)]); + ctx.moveTo(x + radius * Math.cos(startAngle), y + radius * Math.sin(startAngle)); + ctx.quadraticCurveTo(controlX, controlY, x + radius * Math.cos(endAngle), y + radius * Math.sin(endAngle)); + } + ctx.closePath(); + ctx.closePath(); + ctx.fill(); + + ctx.beginPath(); + ctx.fillStyle = secondary; + // Form the shape from points with straight lines and fill it + ctx.moveTo(points[0][0], points[0][1]); + for(let point of points) ctx.lineTo(point[0], point[1]); + // Close and fill + + ctx.closePath(); + ctx.fill(); + } + return true; + }; + + public equilateral = ( + radius: number|ShapeObject = this.hc()/3, + fillStyle: string = "white", + rotate: number = 0, + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || this.wc(); + y = radius.y || this.hc(); + rotate = radius.rotate || 0; + radius = radius.radius || this.hc()/3; + } const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; const ctx = canvas.getContext("2d")!; ctx.save(); @@ -2337,20 +2461,205 @@ export class UserAPI { ctx.fillStyle = fillStyle; ctx.fill(); ctx.restore(); + return true; } - public star = ( - x: number, - y: number, - radius: number, - points: number = 5, + public triangular = ( + width: number|ShapeObject = this.hc()/3, + height: number = this.hc()/3, fillStyle: string = "white", - outerRadius: number = 1.0, rotate: number = 0, - ): void => { + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof width === "object") { + fillStyle = width.fillStyle || "white"; + x = width.x || this.wc(); + y = width.y || this.hc(); + rotate = width.rotate || 0; + height = width.height || this.hc()/3; + width = width.width || this.hc()/3; + } + 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, -height); + ctx.lineTo(width, height); + ctx.lineTo(-width, height); + ctx.closePath(); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.restore(); + return true; + } + pointy = this.triangular; + + public ball = ( + radius: number|ShapeObject = this.hc()/3, + fillStyle: string = "white", + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || this.wc(); + y = radius.y || this.hc(); + radius = radius.radius || this.hc()/3; + } + 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(); + return true; + } + circle = this.ball; + + public donut = ( + slices: number = 3, + eaten: number = 0, + radius: number | ShapeObject = this.hc() / 3, + hole: number = this.hc() / 12, + fillStyle: string = "white", + secondary: string = "black", + stroke: string = "black", + rotate: number = 0, + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if (typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || this.wc(); + y = radius.y || this.hc(); + rotate = radius.rotate || 0; + slices = radius.slices || 3; + radius = radius.radius || this.hc() / 3; + } + + 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); + + // Draw slices as arcs + const totalSlices = slices; + const sliceAngle = (2 * Math.PI) / totalSlices; + for (let i = 0; i < totalSlices; i++) { + const startAngle = i * sliceAngle; + const endAngle = (i + 1) * sliceAngle; + + // Calculate the position of the outer arc + const outerStartX = hole * Math.cos(startAngle); + const outerStartY = hole * Math.sin(startAngle); + + ctx.beginPath(); + ctx.moveTo(outerStartX, outerStartY); + ctx.arc(0, 0, radius, startAngle, endAngle); + ctx.arc(0, 0, hole, endAngle, startAngle, true); + ctx.closePath(); + + // Fill and stroke the slices with the specified fill style + if (i < slices - eaten) { + // Regular slices are white + ctx.fillStyle = fillStyle; + } else { + // Missing slices are black + ctx.fillStyle = secondary; + } + ctx.lineWidth = 2; + ctx.fill(); + ctx.strokeStyle = stroke; + ctx.stroke(); + } + + ctx.restore(); + return true; + }; + + public pie = ( + slices: number = 3, + eaten: number = 0, + radius: number | ShapeObject = this.hc() / 3, + fillStyle: string = "white", + secondary: string = "black", + stroke: string = "black", + rotate: number = 0, + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if (typeof radius === "object") { + fillStyle = radius.fillStyle || "white"; + x = radius.x || this.wc(); + y = radius.y || this.hc(); + rotate = radius.rotate || 0; + slices = radius.slices || 3; + radius = radius.radius || this.hc() / 3; + } + + 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); + + // Draw slices as arcs + const totalSlices = slices; + const sliceAngle = ((2 * Math.PI) / totalSlices); + for (let i = 0; i < totalSlices; i++) { + const startAngle = i * sliceAngle; + const endAngle = (i + 1) * sliceAngle; + + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.arc(0, 0, radius, startAngle, endAngle); + ctx.closePath(); + + // Fill and stroke the slices with the specified fill style + if (i < slices - eaten) { + // Regular slices are white + ctx.fillStyle = fillStyle; + } else { + // Missing slices are black + ctx.fillStyle = secondary; + } + ctx.lineWidth = 2; + ctx.strokeStyle = stroke; + ctx.fill(); + ctx.stroke(); + + } + + ctx.restore(); + return true; + }; + + + public star = ( + points: number|ShapeObject = 5, + radius: number = this.hc()/3, + fillStyle: string = "white", + rotate: number = 0, + outerRadius: number = radius/100, + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof points === "object") { + radius = points.radius || this.hc()/3; + fillStyle = points.fillStyle || "white"; + x = points.x || this.wc(); + y = points.y || this.hc(); + rotate = points.rotate || 0; + outerRadius = points.outerRadius || radius/100; + points = points.points || 5; + } 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); + if(points<1) return this.ball(radius, fillStyle, x, y); + if(points==1) return this.equilateral(radius, fillStyle, 0, x, y); const ctx = canvas.getContext("2d")!; ctx.save(); ctx.translate(x, y); @@ -2367,34 +2676,59 @@ export class UserAPI { ctx.fillStyle = fillStyle; ctx.fill(); ctx.restore(); + return true; }; public stroke = ( - x1: number, - y1: number, - x2: number, - y2: number, - fillStyle: string, - width: number = 1, - ): void => { + width: number|ShapeObject = 1, + strokeStyle: string = "white", + rotate: number = 0, + x1: number = this.wc()-this.wc()/10, + y1: number = this.hc(), + x2: number = this.wc()+this.wc()/5, + y2: number = this.hc(), + ): boolean => { + if(typeof width === "object") { + strokeStyle = width.strokeStyle || "white"; + x1 = width.x1 || this.wc()-this.wc()/10; + y1 = width.y1 || this.hc(); + x2 = width.x2 || this.wc()+this.wc()/5; + y2 = width.y2 || this.hc(); + rotate = width.rotate || 0; + width = width.width || 1; + } const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x1, y1); + ctx.rotate((rotate * Math.PI) / 180); ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - ctx.strokeStyle = fillStyle; + ctx.moveTo(0, 0); + ctx.lineTo(x2-x1, y2-y1); ctx.lineWidth = width; + ctx.strokeStyle = strokeStyle; ctx.stroke(); + ctx.restore(); + return true; }; - public rectangle = ( - x: number, - y: number, - width: number, - height: number, - fillStyle: string, + public box = ( + width: number|ShapeObject = this.wc()/4, + height: number = this.wc()/4, + fillStyle: string = "white", rotate: number = 0, - ): void => { + x: number = this.wc()-this.wc()/8, + y: number = this.hc()-this.hc()/8, + ): boolean => { + if(typeof width === "object") { + fillStyle = width.fillStyle || "white"; + x = width.x || this.wc()-this.wc()/4; + y = width.y || this.hc()-this.hc()/2; + rotate = width.rotate || 0; + height = width.height || this.wc()/4; + width = width.width || this.wc()/4; + + } const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; const ctx = canvas.getContext("2d")!; ctx.save(); @@ -2403,17 +2737,27 @@ export class UserAPI { ctx.fillStyle = fillStyle; ctx.fillRect(0, 0, width, height); ctx.restore(); + return true; } public smiley = ( - x: number, - y: number, - radius: number, - fillStyle: string, - eyeSize: number = 1.0, - happiness: number = 0.0, - rotation: number = 0 - ): void => { + happiness: number|ShapeObject = 2.0, + radius: number = this.hc()/3, + eyeSize: number = 3.0, + fillStyle: string = "yellow", + rotation: number = 0, + x: number = this.wc(), + y: number = this.hc(), + ): boolean => { + if(typeof happiness === "object") { + fillStyle = happiness.fillStyle || "yellow"; + x = happiness.x || this.wc(); + y = happiness.y || this.hc(); + rotation = happiness.rotation || 0; + eyeSize = happiness.eyeSize || 3.0; + radius = happiness.radius || this.hc()/3; + happiness = happiness.happiness || 2.0; + } 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 @@ -2468,9 +2812,104 @@ export class UserAPI { ctx.strokeStyle = "black"; ctx.stroke(); ctx.restore(); - + return true; } + drawText = ( + text: string|ShapeObject, + fontSize: number = 24, + rotation: number = 0, + font: string = "Arial", + x: number = this.wc(), + y: number = this.hc(), + fillStyle: string = "white", + filter: string = "none", + ): boolean => { + if(typeof text === "object") { + fillStyle = text.fillStyle || "white"; + x = text.x || this.wc(); + y = text.y || this.hc(); + rotation = text.rotation || 0; + font = text.font || "Arial"; + fontSize = text.fontSize || 24; + filter = text.filter || "none"; + text = text.text || ""; + } + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + ctx.filter = filter; + ctx.font = `${fontSize}px ${font}`; + ctx.fillStyle = fillStyle; + ctx.fillText(text, 0, 0); + ctx.restore(); + return true; + } + + image = ( + url: string|ShapeObject, + width: number = this.wc()/2, + height: number = this.hc()/2, + rotation: number = 0, + x: number = this.wc(), + y: number = this.hc(), + filter: string = "none", + ): boolean => { + if(typeof url === "object") { + if(!url.url) return true; + x = url.x || this.wc(); + y = url.y || this.hc(); + rotation = url.rotation || 0; + width = url.width || 100; + height = url.height || 100; + filter = url.filter || "none"; + url = url.url || ""; + } + const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + ctx.filter = filter; + const image = new Image(); + image.src = url; + ctx.drawImage(image, -width/2, -height/2, width, height); + ctx.restore(); + return true; + } + + randomChar = (length: number= 1, min: number = 0, max: number = 65536): string => { + return Array.from( + + { length }, () => String.fromCodePoint(Math.floor(Math.random() * (max - min) + min)) + ).join(''); + } + + randomFromRange = (min: number, max: number): string => { + const codePoint = Math.floor(Math.random() * (max - min) + min); + return String.fromCodePoint(codePoint); + }; + + emoji = (n: number = 1): string => { + return this.randomChar(n, 0x1f600, 0x1f64f); + }; + + food = (n: number = 1): string => { + return this.randomChar(n, 0x1f32d, 0x1f37f); + }; + + animals = (n: number = 1): string => { + return this.randomChar(n, 0x1f400, 0x1f4d3); + }; + + expressions = (n: number = 1): string => { + return this.randomChar(n, 0x1f910, 0x1f92f); + }; + + + // ============================================================= // OSC Functions diff --git a/src/Documentation.ts b/src/Documentation.ts index d2ea9d7..cef7251 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -17,6 +17,7 @@ import { oscilloscope } from "./documentation/more/oscilloscope"; import { synchronisation } from "./documentation/more/synchronisation"; import { about } from "./documentation/more/about"; import { bonus } from "./documentation/more/bonus"; +import { visualization } from "./documentation/more/visualization"; import { chaining } from "./documentation/patterns/chaining"; import { interaction } from "./documentation/basics/interaction"; import { time } from "./documentation/learning/time/time"; @@ -117,6 +118,7 @@ export const documentation_factory = (application: Editor) => { audio_basics: audio_basics(application), synchronisation: synchronisation(application), bonus: bonus(application), + visualization: visualization(application), sample_list: sample_list(application), sample_banks: sample_banks(application), loading_samples: loading_samples(application), diff --git a/src/InterfaceLogic.ts b/src/InterfaceLogic.ts index bb5eaa6..736eab6 100644 --- a/src/InterfaceLogic.ts +++ b/src/InterfaceLogic.ts @@ -559,6 +559,7 @@ export const installInterfaceLogic = (app: Editor) => { "oscilloscope", "sample_list", "loading_samples", + "visualization", ].forEach((e) => { let name = `docs_` + e; diff --git a/src/documentation/more/visualization.ts b/src/documentation/more/visualization.ts new file mode 100644 index 0000000..7fc1324 --- /dev/null +++ b/src/documentation/more/visualization.ts @@ -0,0 +1,197 @@ +import { type Editor } from "../../main"; +import { key_shortcut, makeExampleFactory } from "../../Documentation"; + +export const visualization = (application: Editor): string => { + const makeExample = makeExampleFactory(application); + + return ` +# Vizualisation + +While Topos is mainly being developed as a live coding environment for algorithmic music composition, it also includes some features for live code visualizatoins. This section will introduce you to these features. + +## Hydra Visual Live Coding + +