e-virt-table
Version:
A powerful data table based on canvas. You can use it as data grid、Microsoft Excel or Google sheets. It supports virtual scroll、cell edit etc.
408 lines • 15.4 kB
JavaScript
export class Paint {
constructor(target) {
Object.defineProperty(this, "ctx", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "textCacheMap", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
const ctx = target.getContext('2d');
if (!ctx)
throw new Error('canvas context not found');
this.ctx = ctx;
}
clearTextCache() {
this.textCacheMap.clear();
}
scale(dpr) {
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.scale(dpr, dpr);
}
save() {
this.ctx.save();
}
restore() {
this.ctx.restore();
}
translate(x, y) {
this.ctx.translate(x, y);
}
setCursor(cursor = 'default') {
this.ctx.canvas.style.cursor = cursor;
}
clear(x = 0, y = 0, width, height) {
this.ctx.clearRect(x, y, width || this.ctx.canvas.width, height || this.ctx.canvas.height);
}
/**
* 在 Canvas 上绘制单侧阴影
* @param {number} x - 矩形的 x 坐标
* @param {number} y - 矩形的 y 坐标
* @param {number} width - 矩形的宽度
* @param {number} height - 矩形的高度
* @param {string} side - 阴影的位置('left', 'right', 'top', 'bottom')
* @param {number} shadowWidth - 阴影的宽度
* @param {string} color - 阴影的颜色
*/
drawShadow(x, y, width, height, options) {
const { fillColor, side, shadowWidth, colorStart, colorEnd } = options;
this.ctx.save();
if (fillColor) {
this.ctx.fillStyle = fillColor;
this.ctx.fillRect(x, y, width, height);
}
let gradient;
switch (side) {
case 'left':
gradient = this.ctx.createLinearGradient(x - shadowWidth, y, x, y);
gradient.addColorStop(0, colorStart);
gradient.addColorStop(1, colorEnd);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x - shadowWidth, y, shadowWidth, height);
break;
case 'right':
gradient = this.ctx.createLinearGradient(x + width, y, x + width + shadowWidth, y);
gradient.addColorStop(0, colorStart);
gradient.addColorStop(1, colorEnd);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x + width, y, shadowWidth, height);
break;
case 'top':
gradient = this.ctx.createLinearGradient(x, y - shadowWidth, x, y);
gradient.addColorStop(0, colorStart);
gradient.addColorStop(1, colorEnd);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x, y - shadowWidth, width, shadowWidth);
break;
case 'bottom':
gradient = this.ctx.createLinearGradient(x, y + height, x, y + height + shadowWidth);
gradient.addColorStop(0, colorStart);
gradient.addColorStop(1, colorEnd);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x, y + height, width, shadowWidth);
break;
default:
console.error('Invalid side specified for shadow');
break;
}
this.ctx.restore();
}
// 绘制线条
drawLine(points, options) {
if (points.length < 4 || points.length % 2 !== 0) {
throw new Error('A valid array of points is required to draw a line');
}
this.ctx.save();
const { borderColor = 'black', borderWidth = 1 } = options;
this.ctx.beginPath();
this.ctx.moveTo(points[0] - 0.5, points[1] - 0.5);
for (let i = 2; i < points.length; i += 2) {
this.ctx.lineTo(points[i] - 0.5, points[i + 1] - 0.5);
}
this.ctx.strokeStyle = borderColor;
this.ctx.lineWidth = borderWidth;
if (options.lineDash) {
this.ctx.lineDashOffset = options.lineDashOffset ?? 0;
this.ctx.setLineDash(options.lineDash);
}
if (options.fillColor) {
this.ctx.fillStyle = options.fillColor;
this.ctx.fill();
}
if (options.borderColor) {
this.ctx.strokeStyle = options.borderColor;
}
this.ctx.stroke();
this.ctx.closePath();
this.ctx.restore();
}
drawImage(img, x, y, width, height) {
this.ctx.save();
this.ctx.drawImage(img, x, y, width, height);
this.ctx.restore();
}
drawRect(x, y, width, height, { borderWidth = 1, borderColor, fillColor, radius = 0 } = {}) {
this.ctx.save();
this.ctx.beginPath();
// 填充颜色
if (fillColor !== undefined) {
this.ctx.fillStyle = fillColor;
}
// 线条宽度及绘制颜色
if (borderColor !== undefined) {
this.ctx.lineWidth = borderWidth;
this.ctx.strokeStyle = borderColor;
}
if (radius === 0) {
// 绘制矩形路径,- 0.5解决1px模糊的问题
this.ctx.rect(x - 0.5, y - 0.5, width, height);
}
else {
// 确保 radius 是一个包含四个元素的数组
const [tl, tr, br, bl] = typeof radius === 'number' ? [radius, radius, radius, radius] : radius;
// 绘制圆角矩形路径
this.ctx.moveTo(x + tl, y);
this.ctx.arcTo(x + width, y, x + width, y + tr, tr); // draw right side and top-right corner
this.ctx.arcTo(x + width, y + height, x + width - br, y + height, br); // draw bottom side and bottom-right corner
this.ctx.arcTo(x, y + height, x, y + height - bl, bl); // draw left side and bottom-left corner
this.ctx.arcTo(x, y, x + tl, y, tl); // draw top side and top-left corner
}
// 如果有填充色,则填充
if (fillColor !== undefined) {
this.ctx.fill();
}
// 如果有绘制色,则绘制
if (borderColor !== undefined) {
this.ctx.stroke();
}
this.ctx.restore();
}
/**
* 画文本
* @param text
* @param x
* @param y
* @param width
* @param height
* @param options
* @returns 是否溢出
*/
drawText(text = '', x, y, width, height, options = {}) {
this.ctx.save();
const { font = '12px Arial', align = 'center', color = '#495060', padding = 0, verticalAlign = 'middle', maxLineClamp = 1, autoRowHeight = false, offsetLeft = 0, offsetRight = 0, } = options;
this.ctx.font = font;
this.ctx.fillStyle = color;
this.ctx.textAlign = align;
if (['', null, undefined].includes(text)) {
this.ctx.restore();
return false;
}
const fontSize = parseInt(font.match(/\d+/)?.[0] || '12');
const lineHeight = fontSize * (options.lineHeight || 1.2); // 默认行高为字体大小的1.2倍
const availableWidth = width - padding * 2 - offsetLeft - offsetRight;
let textEllipsis = false;
// 计算总行数,向上取整round
const maxTextLine = Math.round((height - 2 * padding) / lineHeight);
// 将文本按可用宽度分割成行,如果为1直接就不计算了,直接绘制
let lines = this.wrapText(text, availableWidth, options.cacheTextKey);
let totalTextLine = Math.min(lines.length, Math.max(maxTextLine, 1));
if (maxLineClamp === 'auto' && autoRowHeight) {
totalTextLine = lines.length;
}
else if (typeof maxLineClamp === 'number' && maxLineClamp < totalTextLine && maxLineClamp !== 1) {
totalTextLine = maxLineClamp;
}
else {
// 处理边界问题
if (maxLineClamp === 1) {
lines = [text];
totalTextLine = 1;
}
if (maxLineClamp === 'auto' && maxTextLine === 1) {
lines = [text];
totalTextLine = 1;
}
}
// 计算起始Y位置
let startY = y + padding;
const totalTextHeight = Math.round(totalTextLine * lineHeight);
if (verticalAlign === 'middle') {
startY = y + (height - totalTextHeight) / 2;
}
else if (verticalAlign === 'bottom') {
startY = y + height - totalTextHeight - padding;
}
// 计算起始X位置
let startX = x + padding + offsetLeft;
if (align === 'center') {
startX = x + width / 2;
}
else if (align === 'right') {
startX = x + width - padding - offsetRight;
}
// 绘制每一行用for循环
for (let i = 0; i < lines.length; i++) {
const lineText = lines[i];
const lineY = startY + i * lineHeight;
this.ctx.textBaseline = 'top';
// 如果设置了lineClamp,则只绘制lineClamp行
if (i === totalTextLine - 1) {
//截取剩余lines,组合成字符串处理省略号
const remainingLines = lines.slice(i);
const remainingText = remainingLines.join('');
const { _text, ellipsis } = this.handleEllipsis(remainingText, width, padding, font);
this.ctx.fillText(_text, startX, lineY);
textEllipsis = ellipsis;
break;
}
this.ctx.fillText(lineText, startX, lineY);
}
// 文字信息回调,用于画跟随图标的
if (options.textCallback && lines.length) {
// 取最长行
const maxLineWidth = lines.reduce((max, line) => {
return Math.max(max, this.ctx.measureText(line).width);
}, 0);
const textMaxWidth = Math.round(maxLineWidth);
let left = startX;
let right = startX + textMaxWidth;
if (align === 'center') {
left = startX - textMaxWidth / 2;
right = startX + textMaxWidth / 2;
}
else if (align === 'right') {
left = startX - textMaxWidth;
right = startX;
}
const textInfo = {
x: startX,
y: startY,
width: textMaxWidth,
height: totalTextHeight,
left,
right,
top: startY,
bottom: startY + totalTextHeight,
};
options.textCallback(textInfo);
}
this.ctx.restore();
return textEllipsis;
}
/**
* 将文本按宽度换行
* @param text
* @param maxWidth
* @returns
*/
wrapText(text, maxWidth, cacheTextKey = '') {
if (!text)
return [''];
// 缓存文本
if (cacheTextKey && this.textCacheMap.has(cacheTextKey)) {
return this.textCacheMap.get(cacheTextKey) || [''];
}
const lines = [];
const paragraphs = text.split('\n');
for (const paragraph of paragraphs) {
if (paragraph === '') {
lines.push('');
continue;
}
const words = paragraph.split('');
let currentLine = '';
for (const word of words) {
const testLine = currentLine + word;
const testWidth = this.ctx.measureText(testLine).width;
if (testWidth <= maxWidth) {
currentLine = testLine;
}
else {
if (currentLine) {
lines.push(currentLine);
currentLine = word;
}
else {
// 单个字符也超出宽度,强制换行
lines.push(word);
currentLine = '';
}
}
}
if (currentLine) {
lines.push(currentLine);
}
}
const result = lines.length > 0 ? lines : [''];
if (cacheTextKey) {
this.textCacheMap.set(cacheTextKey, result);
}
return result;
}
/**
* 计算文本自适应高度
* @param text
* @param width
* @param options
* @returns 计算出的高度
*/
calculateTextHeight(text = '', width, options = {}) {
const { font = '12px Arial', padding = 0, align = 'center', color = '#495060', maxLineClamp = 1, cacheTextKey = '', } = options;
this.ctx.save();
this.ctx.font = font;
this.ctx.fillStyle = color;
this.ctx.textAlign = align;
// // 获取字体高度
const fontSize = parseInt(font.match(/\d+/)?.[0] || '12');
const lineHeight = fontSize * (options.lineHeight || 1.2); // 默认行高为字体大小的1.2倍
// 将文本按可用宽度分割成行
const availableWidth = width - padding * 2;
const lines = this.wrapText(text, availableWidth, cacheTextKey);
// 计算总行数
let totalLines = 1;
if (maxLineClamp === 'auto') {
totalLines = lines.length;
}
else {
if (lines.length > maxLineClamp) {
totalLines = maxLineClamp;
}
else {
totalLines = lines.length;
}
}
this.ctx.restore();
// 计算总高度:行数 * 行高 + 上下padding
return Math.max(Math.floor(totalLines * lineHeight + padding * 2), Math.floor(fontSize + padding * 2));
}
handleEllipsis(text, width, padding = 0, font = '12px Arial') {
this.ctx.save();
let ellipsis = false;
let _text = text;
this.ctx.font = font;
if (text === null || text === undefined || text === '') {
this.ctx.restore();
return {
_text: '',
ellipsis,
};
}
const ellipsesWidth = this.ctx.measureText('...').width;
// 如果宽度小于省略号宽度,则不进行省略,直接返回空字符串
if (width <= ellipsesWidth + padding * 2) {
this.ctx.restore();
return {
_text: '',
ellipsis: true,
};
}
const textWidth = this.ctx.measureText(text).width;
const lineWidth = width - padding * 2;
// 单行文本省略
if (textWidth && textWidth >= lineWidth) {
ellipsis = true;
// text字符截取并添加省略号
let textLength = 0;
for (let i = 0; i < text.length; i++) {
textLength += this.ctx.measureText(text[i]).width;
if (textLength >= lineWidth - ellipsesWidth) {
_text = text.slice(0, i) + '...';
ellipsis = true;
break;
}
}
}
this.ctx.restore();
return {
_text,
ellipsis,
};
}
}
export default Paint;
//# sourceMappingURL=Paint.js.map