222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
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;
|
|
}
|
|
}
|