From 5fc7ce3c12791472fb2aaca148e03ccb2bc4b86b Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 20 Dec 2023 16:22:38 +0200 Subject: [PATCH] Move canvas methods under visuals --- src/API.ts | 407 ++---------------------- src/Utils/Generic.ts | 9 + src/Visuals/CanvasVisuals.ts | 593 +++++++++++++++++++++++++++++++++++ 3 files changed, 629 insertions(+), 380 deletions(-) create mode 100644 src/Visuals/CanvasVisuals.ts diff --git a/src/API.ts b/src/API.ts index aa36ef1..888752b 100644 --- a/src/API.ts +++ b/src/API.ts @@ -14,7 +14,7 @@ import { SoundEvent } from "./classes/SoundEvent"; import { MidiEvent, MidiParams } from "./classes/MidiEvent"; import { LRUCache } from "lru-cache"; import { InputOptions, Player } from "./classes/ZPlayer"; -import { isGenerator, isGeneratorFunction } from "./Utils/Generic"; +import { isGenerator, isGeneratorFunction, maybeToNumber } from "./Utils/Generic"; import { loadUniverse, openUniverseModal, @@ -35,6 +35,7 @@ import { blinkScript } from "./Visuals/Blinkers"; import { SkipEvent } from "./classes/SkipEvent"; import { AbstractEvent, EventOperation } from "./classes/AbstractEvents"; import drums from "./tidal-drum-machines.json"; +import { ShapeObject, createConicGradient, createLinearGradient, createRadialGradient, drawBackground, drawBall, drawBalloid, drawDonut, drawEquilateral, drawImage, drawPie, drawSmiley, drawStar, drawStroke, drawText, drawTriangular } from "./Visuals/CanvasVisuals"; interface ControlChange { channel: number; @@ -73,38 +74,6 @@ 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, - rotation: number, - points: number, - outerRadius: number, - eyeSize: number, - happiness: number, - slices: number, - gap: number, - font: string, - fontSize: number, - text: string, - filter: string, - url: string, - curve: number, - curves: number, - stroke: string, - eaten: number, - hole: number, -} - export class UserAPI { /** * The UserAPI class is the interface between the user's code and the backend. It provides @@ -758,15 +727,6 @@ export class UserAPI { this.patternCache.delete(id); }; - maybeToNumber = (something: any): number | any => { - // If something is BigInt - if (typeof something === "bigint") { - return Number(something); - } else { - return something; - } - } - cache = (key: string, value: any) => { /** * Gets or sets a value in the cache. @@ -782,35 +742,35 @@ export class UserAPI { if (cachedValue !== 0 && !cachedValue) { const generator = value as unknown as Generator this.patternCache.set(key, generator); - return this.maybeToNumber(generator.next().value); + return maybeToNumber(generator.next().value); } - return this.maybeToNumber(cachedValue); + return maybeToNumber(cachedValue); } else { const generator = value as unknown as Generator this.patternCache.set(key, generator); - return this.maybeToNumber(generator.next().value); + return maybeToNumber(generator.next().value); } } else if (isGeneratorFunction(value)) { if (this.patternCache.has(key)) { const cachedValue = (this.patternCache.get(key) as Generator).next().value; if (cachedValue || cachedValue === 0 || cachedValue === 0n) { - return this.maybeToNumber(cachedValue); + return maybeToNumber(cachedValue); } else { const generator = value(); this.patternCache.set(key, generator); - return this.maybeToNumber(generator.next().value); + return maybeToNumber(generator.next().value); } } else { const generator = value(); this.patternCache.set(key, generator); - return this.maybeToNumber(generator.next().value); + return maybeToNumber(generator.next().value); } } else { this.patternCache.set(key, value); - return this.maybeToNumber(value); + return maybeToNumber(value); } } else { - return this.maybeToNumber(this.patternCache.get(key)); + return maybeToNumber(this.patternCache.get(key)); } } @@ -2270,16 +2230,12 @@ export class UserAPI { * 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); + drawBackground(this.app.interface.drawings as HTMLCanvasElement, color, ...gb); return true; } bg = this.background; - public linearGradient = (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]) => { + public linearGradient = (x1: number, y1: number, x2: number, y2: number, ...stops: (number | string)[]): CanvasGradient => { /** * Set linear gradient on the canvas. * @param x1 - The x-coordinate of the start point @@ -2288,16 +2244,7 @@ export class UserAPI { * @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 < stops.length; i += 2) { - let color = stops[i + 1]; - if (typeof color === "number") color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; - gradient.addColorStop((stops[i] as number), color); - } - return gradient; + return createLinearGradient(this.app.interface.drawings as HTMLCanvasElement, x1, y1, x2, y2, ...stops); } public radialGradient = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number, ...stops: (number | string)[]) => { @@ -2311,15 +2258,7 @@ export class UserAPI { * @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 < stops.length; i += 2) { - let color = stops[i + 1]; - if (typeof color === "number") color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; - gradient.addColorStop((stops[i] as number), color); - } - return gradient; + return createRadialGradient(this.app.interface.drawings as HTMLCanvasElement, x1, y1, r1, x2, y2, r2, ...stops); } public conicGradient = (x: number, y: number, angle: number, ...stops: (number | string)[]) => { @@ -2330,15 +2269,7 @@ export class UserAPI { * @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 < stops.length; i += 2) { - let color = stops[i + 1]; - if (typeof color === "number") color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; - gradient.addColorStop((stops[i] as number), color); - } - return gradient; + return createConicGradient(this.app.interface.drawings as HTMLCanvasElement, x, y, angle, ...stops); } public draw = (func: Function): boolean => { @@ -2373,63 +2304,7 @@ export class UserAPI { 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.fillStyle = fillStyle; - - 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); - - // 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(); - } + drawBalloid(this.app.interface.drawings as HTMLCanvasElement, curves, radius, curve, fillStyle, secondary, x, y); return true; }; @@ -2448,18 +2323,7 @@ export class UserAPI { 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((rotation * 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(); + drawEquilateral(canvas, radius, fillStyle, rotation, x, y); return true; } @@ -2480,18 +2344,7 @@ export class UserAPI { 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((rotation * 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(); + drawTriangular(canvas, width, height, fillStyle, rotation, x, y); return true; } pointy = this.triangular; @@ -2509,12 +2362,7 @@ export class UserAPI { 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(); - ctx.closePath(); + drawBall(canvas, radius, fillStyle, x, y); return true; } circle = this.ball; @@ -2543,62 +2391,8 @@ export class UserAPI { stroke = slices.stroke || "black"; slices = slices.slices || 3; } - 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); - - if (slices < 2) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = slices < 1 ? secondary : fillStyle; - ctx.fill(); - - ctx.beginPath(); - ctx.arc(0, 0, hole, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = secondary; - ctx.fill(); - - ctx.restore(); - return true; - } - - // 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(); + drawDonut(canvas, slices, eaten, radius, hole, fillStyle, secondary, stroke, rotation, x, y); return true; }; @@ -2625,50 +2419,8 @@ export class UserAPI { eaten = slices.eaten || 0; slices = slices.slices || 3; } - 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); - - if (slices < 2) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = slices < 1 ? secondary : fillStyle; - ctx.fill(); - ctx.restore(); - return true; - } - - // 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.lineTo(0, 0); // Connect to center - 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(); + drawPie(canvas, slices, eaten, radius, fillStyle, secondary, stroke, rotation, x, y); return true; }; @@ -2693,24 +2445,7 @@ export class UserAPI { points = points.points || 5; } const canvas: HTMLCanvasElement = this.app.interface.drawings as HTMLCanvasElement; - 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); - ctx.rotate((rotation * 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(); + drawStar(canvas, points, radius, fillStyle, rotation, outerRadius, x, y); return true; }; @@ -2733,17 +2468,7 @@ export class UserAPI { 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((rotation * Math.PI) / 180); - ctx.beginPath(); - ctx.moveTo(0, 0); - ctx.lineTo(x2 - x1, y2 - y1); - ctx.lineWidth = width; - ctx.strokeStyle = strokeStyle; - ctx.stroke(); - ctx.restore(); + drawStroke(canvas, width, strokeStyle, rotation, x1, y1, x2, y2); return true; }; @@ -2764,13 +2489,7 @@ export class UserAPI { width = width.width || this.wc() / 4; } 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.fillStyle = fillStyle; - ctx.fillRect(0, 0, width, height); - ctx.restore(); + drawStroke(canvas, width, fillStyle, rotation, x, y, width, height); return true; } @@ -2793,59 +2512,7 @@ export class UserAPI { happiness = happiness.happiness || 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 - 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(); + drawSmiley(canvas, happiness, radius, eyeSize, fillStyle, rotation, x, y); return true; } @@ -2870,15 +2537,7 @@ export class UserAPI { 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(); + drawText(canvas, text, fontSize, rotation, font, x, y, fillStyle, filter); return true; } @@ -2902,21 +2561,12 @@ export class UserAPI { 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(); + drawImage(canvas, url, width, height, rotation, x, y, filter); 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(''); } @@ -2942,9 +2592,6 @@ export class UserAPI { return this.randomChar(n, 0x1f910, 0x1f92f); }; - - - // ============================================================= // OSC Functions // ============================================================= diff --git a/src/Utils/Generic.ts b/src/Utils/Generic.ts index 0043e7d..f5e4fc7 100644 --- a/src/Utils/Generic.ts +++ b/src/Utils/Generic.ts @@ -95,6 +95,15 @@ export function filterObject( ); } +export const maybeToNumber = (something: any): number | any => { + // If something is BigInt + if (typeof something === "bigint") { + return Number(something); + } else { + return something; + } +} + export const GeneratorType = (function*(){yield undefined;}).constructor; export const GeneratorIteratorType = (function*(){yield undefined;}).prototype.constructor; export const isGenerator = (v:any) => Object.prototype.toString.call(v) === '[object Generator]'; diff --git a/src/Visuals/CanvasVisuals.ts b/src/Visuals/CanvasVisuals.ts new file mode 100644 index 0000000..820f83d --- /dev/null +++ b/src/Visuals/CanvasVisuals.ts @@ -0,0 +1,593 @@ +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; + rotation: number; + points: number; + outerRadius: number; + eyeSize: number; + happiness: number; + slices: number; + gap: number; + font: string; + fontSize: number; + text: string; + filter: string; + url: string; + curve: number; + curves: number; + stroke: string; + eaten: number; + hole: number; +}; + +export const drawBackground = ( + canvas: HTMLCanvasElement, + 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 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); +}; + +export const createLinearGradient = ( + canvas: HTMLCanvasElement, + x1: number, + y1: number, + x2: number, + y2: number, + ...stops: (number | string)[] +): CanvasGradient => { + const ctx = canvas.getContext("2d")!; + const gradient = ctx.createLinearGradient(x1, y1, x2, y2); + // Parse pairs of values from stops + for (let i = 0; i < stops.length; i += 2) { + let color = stops[i + 1]; + if (typeof color === "number") + color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; + gradient.addColorStop(stops[i] as number, color); + } + return gradient; +}; + +export const createRadialGradient = ( + canvas: HTMLCanvasElement, + x1: number, + y1: number, + r1: number, + x2: number, + y2: number, + r2: number, + ...stops: (number | string)[] +) => { + /** + * 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 ctx = canvas.getContext("2d")!; + const gradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2); + for (let i = 0; i < stops.length; i += 2) { + let color = stops[i + 1]; + if (typeof color === "number") + color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; + gradient.addColorStop(stops[i] as number, color); + } + return gradient; +}; + +export const createConicGradient = ( + canvas: HTMLCanvasElement, + x: number, + y: number, + angle: number, + ...stops: (number | string)[] +) => { + /** + * 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 ctx = canvas.getContext("2d")!; + const gradient = ctx.createConicGradient(x, y, angle); + for (let i = 0; i < stops.length; i += 2) { + let color = stops[i + 1]; + if (typeof color === "number") + color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`; + gradient.addColorStop(stops[i] as number, color); + } + return gradient; +}; + +export const drawGradientImage = ( + canvas: HTMLCanvasElement, + time: number = 666 +) => { + /* TODO: This works but is really resource heavy. Should do method for requestAnimationFrame? */ + const context = canvas.getContext("2d")!; + const { width, height } = context.canvas; + const imageData = context.getImageData(0, 0, width, height); + + for (let p = 0; p < imageData.data.length; p += 4) { + const i = p / 4; + const x = i % width; + const y = (i / width) >>> 0; + + const red = 64 + (128 * x) / width + 64 * Math.sin(time / 1000); + const green = 64 + (128 * y) / height + 64 * Math.cos(time / 1000); + const blue = 128; + + imageData.data[p + 0] = red; + imageData.data[p + 1] = green; + imageData.data[p + 2] = blue; + imageData.data[p + 3] = 255; + } + + context.putImageData(imageData, 0, 0); + return true; +}; + +export const drawBalloid = ( + canvas: HTMLCanvasElement, + curves: number, + radius: number, + curve: number, + fillStyle: string, + secondary: string, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + + // Draw the shape using quadratic Bézier curves + ctx.beginPath(); + ctx.fillStyle = fillStyle; + + 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); + + // 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(); + } +}; + +export const drawEquilateral = ( + canvas: HTMLCanvasElement, + radius: number, + fillStyle: string, + rotation: number, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * 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(); +}; + +export const drawTriangular = ( + canvas: HTMLCanvasElement, + width: number, + height: number, + fillStyle: string, + rotation: number, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * 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(); +}; + +export const drawBall = ( + canvas: HTMLCanvasElement, + radius: number, + fillStyle: string, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.fillStyle = fillStyle; + ctx.fill(); + ctx.closePath(); +}; + +export const drawDonut = ( + canvas: HTMLCanvasElement, + slices: number, + eaten: number, + radius: number, + hole: number, + fillStyle: string, + secondary: string, + stroke: string, + rotation: number, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + + if (slices < 2) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fillStyle = slices < 1 ? secondary : fillStyle; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(0, 0, hole, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fillStyle = secondary; + ctx.fill(); + + ctx.restore(); + } + + // 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(); +}; + +export const drawPie = ( + canvas: HTMLCanvasElement, + slices: number, + eaten: number, + radius: number, + fillStyle: string, + secondary: string, + stroke: string, + rotation: number, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + + if (slices < 2) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fillStyle = slices < 1 ? secondary : fillStyle; + ctx.fill(); + ctx.restore(); + } + + // 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.lineTo(0, 0); // Connect to center + 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(); +}; + +export const drawStar = ( + canvas: HTMLCanvasElement, + points: number, + radius: number, + fillStyle: string, + rotation: number, + outerRadius: number, + x: number, + y: number +): void => { + if (points < 1) return drawBall(canvas, radius, fillStyle, x, y); + if (points == 1) return drawEquilateral(canvas, radius, fillStyle, 0, x, y); + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * 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(); +}; + +export const drawStroke = ( + canvas: HTMLCanvasElement, + width: number, + strokeStyle: string, + rotation: number = 0, + x1: number, + y1: number, + x2: number, + y2: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x1, y1); + ctx.rotate((rotation * Math.PI) / 180); + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(x2 - x1, y2 - y1); + ctx.lineWidth = width; + ctx.strokeStyle = strokeStyle; + ctx.stroke(); + ctx.restore(); +}; + +export const drawBox = ( + canvas: HTMLCanvasElement, + width: number, + height: number, + fillStyle: string, + rotation: number, + x: number, + y: number +): void => { + const ctx = canvas.getContext("2d")!; + ctx.save(); + ctx.translate(x, y); + ctx.rotate((rotation * Math.PI) / 180); + ctx.fillStyle = fillStyle; + ctx.fillRect(0, 0, width, height); + ctx.restore(); +}; + +export const drawSmiley = ( + canvas: HTMLCanvasElement, + happiness: number, + radius: number, + eyeSize: number, + fillStyle: string, + rotation: number, + x: number, + y: number +): void => { + 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(); +}; + +export const drawText = ( + canvas: HTMLCanvasElement, + text: string, + fontSize: number, + rotation: number, + font: string, + x: number, + y: number, + fillStyle: string, + filter: string +): void => { + 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(); +}; + +export const drawImage = ( + canvas: HTMLCanvasElement, + url: string, + width: number, + height: number, + rotation: number, + x: number, + y: number, + filter: string = "none" +): void => { + 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(); +};