import { CanvasTexture, LinearFilter, ClampToEdgeWrapping } from 'three'; import { rgbToHex, to1D } from '../../common/convert'; /** * Draws a rounded rectangle using the current state of the canvas. * If you omit the last three params, it will draw a rectangle * outline with a 5 pixel border radius * * https://stackoverflow.com/a/3368118 * * @param {CanvasRenderingContext2D} ctx * @param {Number} x The top left x coordinate * @param {Number} y The top left y coordinate * @param {Number} width The width of the rectangle * @param {Number} height The height of the rectangle * @param {Number} [radius = 5] The corner radius; It can also be an object * to specify different radii for corners * @param {Number} [radius.tl = 0] Top left * @param {Number} [radius.tr = 0] Top right * @param {Number} [radius.br = 0] Bottom right * @param {Number} [radius.bl = 0] Bottom left * @param {Boolean} [fill = false] Whether to fill the rectangle. * @param {Boolean} [stroke = true] Whether to stroke the rectangle. */ export function roundRect( ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius?: number | { tl: number; tr: number; br: number; bl: number }, fill?: boolean, stroke?: boolean, ) { if (typeof stroke === 'undefined') { stroke = true; } if (typeof radius === 'undefined') { radius = 5; } if (typeof radius === 'number') { radius = { tl: radius, tr: radius, br: radius, bl: radius }; } else { var defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 }; for (var side in defaultRadius) { radius[side] = radius[side] || defaultRadius[side]; } } ctx.beginPath(); ctx.moveTo(x + radius.tl, y); ctx.lineTo(x + width - radius.tr, y); ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr); ctx.lineTo(x + width, y + height - radius.br); ctx.quadraticCurveTo( x + width, y + height, x + width - radius.br, y + height, ); ctx.lineTo(x + radius.bl, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl); ctx.lineTo(x, y + radius.tl); ctx.quadraticCurveTo(x, y, x + radius.tl, y); ctx.closePath(); if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } export interface CanvasUtilsOptions { fill?: boolean; backgroundColor?: string; foregroundColor?: string; rounded?: boolean; roundedRadius?: number; textBorderColor?: string; textBorderSize?: number; textShadowBlur?: number; } export class CanvasUtils { constructor(private options?: CanvasUtilsOptions) {} public createTextCanvas( text: string | string[], bold = true, fontSize = 16, padding = 4, ): { texture: CanvasTexture; width: number; height: number } { const ctx = document.createElement('canvas').getContext('2d')!; const font = `${fontSize}px${bold ? ' bold' : ''} sans`; const lines = Array.isArray(text) ? text : [text]; const lineWidths: number[] = []; let longestLine = 0; // Measure the text bounds ctx.font = font; lines.forEach((line) => { const lineWidth = ctx.measureText(line).width; if (longestLine < lineWidth) { longestLine = lineWidth; } lineWidths.push(lineWidth); }); const width = longestLine + padding * 2; const textHeight = fontSize * lines.length; const height = textHeight + padding * 2; // Resize canvas ctx.canvas.width = width; ctx.canvas.height = height; // Set text parameters ctx.font = font; ctx.textAlign = 'center'; // Draw background if (this.options?.fill ?? true) { ctx.fillStyle = this.options?.backgroundColor || '#fff'; if (this.options?.rounded) { roundRect( ctx, 0, 0, width, height, this.options?.roundedRadius || 4, true, false, ); } else { ctx.fillRect(0, 0, width, height); } } // Scale the text to fit within the canvas const scaleFactor = Math.min(1, width / longestLine); ctx.translate( Math.floor(width / 2 - padding) + 0.5, Math.floor(padding + fontSize / 2) + 0.5, ); ctx.scale(scaleFactor, 1); // Draw the text if (this.options?.textShadowBlur !== undefined) { ctx.shadowColor = this.options?.textBorderColor || '#000'; ctx.shadowBlur = this.options?.textShadowBlur; if (this.options?.textBorderSize !== undefined) { ctx.lineWidth = this.options?.textBorderSize; lines.forEach((line, i) => { ctx.strokeText(line, padding, i * fontSize + padding); }); } ctx.shadowBlur = 0; } ctx.fillStyle = this.options?.foregroundColor || '#000'; lines.forEach((line, i) => { ctx.fillText(line, padding, i * fontSize + padding); }); // Create texture with appropriate flags const texture = new CanvasTexture(ctx.canvas); texture.minFilter = LinearFilter; texture.wrapS = ClampToEdgeWrapping; texture.wrapT = ClampToEdgeWrapping; return { texture, width, height }; } public readPixelDataRGB(image: HTMLImageElement): number[] { const array = new Array(image.width * image.height); const ctx = document.createElement('canvas').getContext('2d')!; ctx.canvas.width = image.width; ctx.canvas.height = image.height; ctx.drawImage(image, 0, 0, image.width, image.height); // pixel data const data = ctx.getImageData(0, 0, image.width, image.height); for (let x = 0; x < image.width; x++) { for (let y = 0; y < image.height; y++) { const index = to1D(x, y, image.width); array[index] = rgbToHex( data.data[index * 4], data.data[index * 4 + 1], data.data[index * 4 + 2], ); } } return array; } public readPixelDataRScaled( image: HTMLImageElement, scale: number, ): number[] { const array = new Array(image.width * image.height); const ctx = document.createElement('canvas').getContext('2d')!; ctx.canvas.width = image.width; ctx.canvas.height = image.height; ctx.drawImage(image, 0, 0, image.width, image.height); // pixel data const data = ctx.getImageData(0, 0, image.width, image.height); for (let x = 0; x < image.width; x++) { for (let y = 0; y < image.height; y++) { const index = to1D(x, y, image.width); array[index] = (data.data[index * 4] * scale) / 255; } } return array; } }