Files
ece.suwa3d.com/src/utils/textHelper.ts
T

586 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// textHelper.ts
/**
* 文字定位助手模块
* @module TextPosition
*/
/**
* 创建文字定位助手实例
* @param {Object} options - 配置选项
* @param {Function} options.setData - 设置数据的方法(用于更新响应式数据)
* @returns {Object} 包含renderText方法的对象
*/
export function createTextPosition({ setData }) {
if (!setData) {
throw new Error('setData is required');
}
// 默认样式配置
const defaultStyle = {
fontSize: 16, // 字体大小(px)
fontFamily: 'Arial', // 字体
color: '#000000', // 文字颜色
textAlign: 'left', // 文本对齐方式: left, center, right
fontWeight: 'normal', // 字体粗细: normal, bold
lineHeight: 1.5, // 行高
opacity: 1, // 透明度
backgroundColor: 'transparent', // 背景颜色
padding: 0, // 内边距(px)
borderRadius: 0, // 边框圆角(px)
};
/**
* 渲染文字到指定位置
* @param {Object} options - 文字配置
* @param {string} options.text - 要显示的文字
* @param {number} options.x - 文字左上角X坐标(基于原始容器尺寸)
* @param {number} options.y - 文字左上角Y坐标(基于原始容器尺寸)
* @param {number} [options.width] - 文字容器宽度(基于原始容器尺寸)
* @param {number} [options.height] - 文字容器高度(基于原始容器尺寸)
* @param {Object} [options.style] - 文字样式配置
* @param {string} [options.bindKey] - 绑定到页面数据的键名
*/
function roundText({ shapeImage, x, y, width, height, radius, style = {}, bindKey = 'roundTextConfig', text = '', isFront = true, cachedImageInfo = null, onImageInfoCached = null }) {
// 合并默认样式和用户自定义样式
const mergedStyle = { ...defaultStyle, ...style };
// 获取设备信息,计算缩放比例
const screenWidth = 320;
// 计算宽度和高度的缩放比例
const scale = 0.6;
// 计算字符位置的函数
const calculateTextPosition = (imageInfo) => {
// 计算图片宽高比
const imageRatio = imageInfo.width / imageInfo.height;
const containerRatio = 1; // 容器是正方形,宽高比为1
let ratio;
// 根据图片和容器的比例关系决定如何缩放
if (imageRatio > containerRatio) {
// 图片更宽,以宽度为基准缩放
ratio = screenWidth / imageInfo.width;
} else {
// 图片更高或等比,以高度为基准缩放
ratio = screenWidth / imageInfo.height;
}
// 计算缩放后的参数
const scaledX = x * ratio * scale;
const scaledY = y * ratio * scale;
const scaledWidth = width * ratio * scale;
const scaledHeight = height * ratio * scale;
const scaledRadius = radius * ratio * scale;
const scaledFontSize = mergedStyle.fontSize * ratio * scale;
// 计算圆心位置(基于容器的中心点)
const centerX = scaledX + scaledWidth / 2;
const centerY = scaledY + scaledHeight / 2;
// 计算每个字符的位置信息
// 根据正面/背面使用不同的计算方法,互不影响
const charList = isFront
? calculateFrontRoundTextChars({
text: text,
centerX: scaledWidth / 2, // 相对于容器的中心
centerY: scaledHeight / 2,
radius: scaledRadius,
fontSize: scaledFontSize,
fontWeight: mergedStyle.fontWeight || 'normal',
color: mergedStyle.color || '#000000'
})
: calculateBackRoundTextChars({
text: text,
centerX: scaledWidth / 2, // 相对于容器的中心
centerY: scaledHeight / 2,
radius: scaledRadius,
fontSize: scaledFontSize,
fontWeight: mergedStyle.fontWeight || 'normal',
color: mergedStyle.color || '#000000'
});
// 构建圆形文字配置对象
const roundTextConfig = {
x: scaledX,
y: scaledY,
width: scaledWidth,
height: scaledHeight,
centerX: centerX,
centerY: centerY,
radius: scaledRadius,
text: text,
charList: charList, // 字符位置列表
style: {
...mergedStyle,
fontSize: scaledFontSize,
}
};
console.log('roundTextConfig===>', roundTextConfig);
// 更新页面数据
setData({
[bindKey]: roundTextConfig
});
// 返回配置对象,以便后续使用
return roundTextConfig;
};
// 如果已有缓存的图片信息,直接使用
if (cachedImageInfo && cachedImageInfo.src === shapeImage) {
calculateTextPosition(cachedImageInfo);
return;
}
// 否则获取图片信息
const img = new Image();
img.onload = () => {
const res = {
width: img.width,
height: img.height,
path: shapeImage,
src: shapeImage
};
// 如果提供了缓存回调,调用它
if (typeof onImageInfoCached === 'function') {
onImageInfoCached(res);
}
calculateTextPosition(res);
};
img.onerror = (err) => {
console.error('Failed to get image info:', err);
};
img.src = shapeImage;
}
/**
* 计算正面圆形文字中每个字符的位置和样式
* @param {Object} options - 配置选项
* @param {string} options.text - 文字内容
* @param {number} options.centerX - 圆心X坐标(相对于容器)
* @param {number} options.centerY - 圆心Y坐标(相对于容器)
* @param {number} options.radius - 半径
* @param {number} options.fontSize - 字体大小
* @param {string} options.fontWeight - 字体粗细
* @param {string} options.color - 文字颜色
* @returns {Array} 字符位置信息数组
*/
function calculateFrontRoundTextChars({ text, centerX, centerY, radius, fontSize, fontWeight, color }) {
if (!text) {
return [];
}
// 计算字符宽度
// 使用更精确的估算方法
const charWidths = [];
let totalWidth = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
let charWidth;
// 判断字符类型,使用不同的宽度估算
if (/[\u4e00-\u9fa5]/.test(char)) {
// 中文字符:宽度约为 fontSize
charWidth = fontSize;
} else if (/[0-9]/.test(char)) {
// 数字:宽度约为 fontSize * 0.6
charWidth = fontSize * 0.6;
} else if (/[a-zA-Z]/.test(char)) {
// 英文字母:宽度约为 fontSize * 0.6(大写)或 0.5(小写)
charWidth = /[A-Z]/.test(char) ? fontSize * 0.6 : fontSize * 0.45;
} else if (/[\u3000-\u303f\uff00-\uffef]/.test(char)) {
// 全角符号:宽度约为 fontSize
charWidth = fontSize;
} else {
// 其他字符(半角符号等):宽度约为 fontSize * 0.4
charWidth = fontSize * 0.4;
}
// 如果字体是粗体,稍微增加宽度
if (fontWeight === 'bold' || fontWeight === '700') {
charWidth *= 1.1;
}
charWidths.push(charWidth);
totalWidth += charWidth;
}
// 为文字之间添加2px间距(除了最后一个字符)
const spacing = 0;
const totalSpacing = (text.length > 1) ? (text.length - 1) * spacing : 0;
const totalWidthWithSpacing = totalWidth + totalSpacing;
// 计算文字总弧长和每个字符的角度(正面使用原始半径)
const circumference = 2 * Math.PI * radius;
const arcLength = totalWidthWithSpacing;
const totalAngle = (arcLength / circumference) * 2 * Math.PI;
// 计算起始角度(使文字居中于底部)
// 从底部中心开始,顺时针排列,起始角度 = 底部中心 + 总角度的一半
const startAngle = Math.PI / 2 + totalAngle / 2;
// 创建并定位每个字符(逆时针排列,所以角度递减)
let currentAngle = startAngle;
const charList = [];
for (let i = 0; i < text.length; i++) {
const char = text[i];
const charWidth = charWidths[i];
// 计算字符角度增量(包括字符宽度和间距)
// 最后一个字符不需要添加后面的间距
const spacingAfter = (i < text.length - 1) ? spacing : 0;
const charAngleIncrement = ((charWidth + spacingAfter) / circumference) * 2 * Math.PI;
// 计算字符角度位置(逆时针排列,所以角度递减)
// 字符中心位置 = 当前角度 - 字符宽度的一半对应的角度
const charWidthAngle = (charWidth / circumference) * 2 * Math.PI;
const charAngle = currentAngle - charWidthAngle / 2;
// 计算字符位置(正面使用原始半径)
const x = centerX + radius * Math.cos(charAngle);
const y = centerY + radius * Math.sin(charAngle) - fontSize / 3;
// 计算旋转角度(正面:文字底部向外,顶部朝向圆心)
const rotateAngle = charAngle + Math.PI * 1.5;
// 转换为度数(用于CSS transform)
const rotateDeg = (rotateAngle * 180) / Math.PI;
charList.push({
char: char,
x: x,
y: y,
rotate: rotateAngle,
rotateDeg: rotateDeg, // 度数版本
fontSize: fontSize,
fontWeight: fontWeight,
color: color
});
// 更新当前角度(逆时针排列,所以角度递减)
currentAngle -= charAngleIncrement;
}
return charList;
}
/**
* 计算背面圆形文字中每个字符的位置和样式
* @param {Object} options - 配置选项
* @param {string} options.text - 文字内容
* @param {number} options.centerX - 圆心X坐标(相对于容器)
* @param {number} options.centerY - 圆心Y坐标(相对于容器)
* @param {number} options.radius - 半径
* @param {number} options.fontSize - 字体大小
* @param {string} options.fontWeight - 字体粗细
* @param {string} options.color - 文字颜色
* @returns {Array} 字符位置信息数组
*/
function calculateBackRoundTextChars({ text, centerX, centerY, radius, fontSize, fontWeight, color }) {
if (!text) {
return [];
}
// 计算字符宽度
// 使用更精确的估算方法
const charWidths = [];
let totalWidth = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
let charWidth;
// 判断字符类型,使用不同的宽度估算
if (/[\u4e00-\u9fa5]/.test(char)) {
// 中文字符:宽度约为 fontSize
charWidth = fontSize;
} else if (/[0-9]/.test(char)) {
// 数字:宽度约为 fontSize * 0.6
charWidth = fontSize * 0.6;
} else if (/[a-zA-Z]/.test(char)) {
// 英文字母:宽度约为 fontSize * 0.6(大写)或 0.5(小写)
charWidth = /[A-Z]/.test(char) ? fontSize * 0.65 : fontSize * 0.5;
} else if (/[\u3000-\u303f\uff00-\uffef]/.test(char)) {
// 全角符号:宽度约为 fontSize
charWidth = fontSize;
} else {
// 其他字符(半角符号等):宽度约为 fontSize * 0.4
charWidth = fontSize * 0.4;
}
// 如果字体是粗体,稍微增加宽度
if (fontWeight === 'bold' || fontWeight === '700') {
charWidth *= 1.1;
}
charWidths.push(charWidth);
totalWidth += charWidth;
}
// 为文字之间添加2px间距(除了最后一个字符)
const spacing = 1;
const totalSpacing = (text.length > 1) ? (text.length - 1) * spacing : 0;
const totalWidthWithSpacing = totalWidth + totalSpacing;
// 计算文字总弧长和每个字符的角度
// 背面:文字到圆心的距离 = 半径 + 文字高度的一半
const adjustedRadius = radius + fontSize / 2;
const circumference = 2 * Math.PI * adjustedRadius;
const arcLength = totalWidthWithSpacing;
const totalAngle = (arcLength / circumference) * 2 * Math.PI;
// 计算起始角度(使文字居中于底部)
// 从底部中心开始,顺时针排列,起始角度 = 底部中心 - 总角度的一半
const startAngle = Math.PI / 2 - totalAngle / 2;
// 创建并定位每个字符(顺时针排列,所以角度递增)
let currentAngle = startAngle;
const charList = [];
for (let i = 0; i < text.length; i++) {
const char = text[i];
const charWidth = charWidths[i];
// 计算字符角度增量(包括字符宽度和间距)
// 最后一个字符不需要添加后面的间距
const spacingAfter = (i < text.length - 1) ? spacing : 0;
const charAngleIncrement = ((charWidth + spacingAfter) / circumference) * 2 * Math.PI;
// 计算字符角度位置(顺时针排列,所以角度递增)
// 字符中心位置 = 当前角度 + 字符宽度的一半对应的角度
const charWidthAngle = (charWidth / circumference) * 2 * Math.PI;
const charAngle = currentAngle + charWidthAngle / 2;
// 计算字符位置(背面:文字到圆心的距离 = 半径 + 文字高度的一半)
const x = centerX + adjustedRadius * Math.cos(charAngle);
const y = centerY + adjustedRadius * Math.sin(charAngle);
// 计算旋转角度(背面:文字底部朝向圆心,顶部向外)
const rotateAngle = charAngle + Math.PI * 0.5;
// 转换为度数(用于CSS transform)
const rotateDeg = (rotateAngle * 180) / Math.PI;
charList.push({
char: char,
x: x,
y: y,
rotate: rotateAngle,
rotateDeg: rotateDeg, // 度数版本
fontSize: fontSize,
fontWeight: fontWeight,
color: color
});
// 更新当前角度(顺时针排列,所以角度递增)
currentAngle += charAngleIncrement;
}
return charList;
}
/**
* 初始化圆形文字 Canvas
* @param {string} canvasId - Canvas 的 ID 或选择器
* @param {Object} config - 配置对象,包含 width 和 height
* @returns {Promise} 返回包含 canvas 和 ctx 的对象
*/
function initRoundTextCanvas(canvasId, config) {
return new Promise((resolve) => {
const tryInit = () => {
// 如果是选择器字符串,去掉 # 或 . 前缀
const selector = canvasId.startsWith('#') ? canvasId.slice(1) :
canvasId.startsWith('.') ? canvasId.slice(1) : canvasId;
// 尝试通过 ID 查找
let canvasElement: HTMLCanvasElement | null = document.getElementById(selector) as HTMLCanvasElement | null;
// 如果找不到,尝试通过类名查找(取第一个)
if (!canvasElement && canvasId.startsWith('.')) {
const elements = document.getElementsByClassName(selector);
if (elements.length > 0) {
canvasElement = elements[0] as HTMLCanvasElement;
}
}
// 如果还是找不到,尝试直接使用 canvasId 作为选择器
if (!canvasElement) {
canvasElement = document.querySelector(canvasId) as HTMLCanvasElement | null;
}
if (canvasElement && canvasElement instanceof HTMLCanvasElement) {
const canvas = canvasElement;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('无法获取 Canvas 2D 上下文');
}
const dpr = window.devicePixelRatio || 1;
// 使用配置中的尺寸,如果没有则使用 Canvas 的实际尺寸或默认值
const width = (config && config.width) || canvas.offsetWidth || 700;
const height = (config && config.height) || canvas.offsetHeight || 700;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
resolve({ canvas, ctx });
} else {
// 如果 Canvas 还未渲染,延迟重试
setTimeout(() => {
tryInit();
}, 100);
}
};
tryInit();
});
}
/**
* 在 Canvas 上绘制圆形文字
* @param {Object} options - 绘制选项
* @param {Object} options.ctx - Canvas 2D 上下文
* @param {Object} options.config - 圆形文字配置对象
* @param {Function} options.setData - 设置数据的方法
* @param {HTMLCanvasElement} options.canvas - Canvas 元素
* @param {string} options.imageKey - 图片数据键名,用于存储生成的图片路径
* @returns {Promise} 返回生成的图片路径(Data URL)
*/
function drawRoundTextOnCanvas({ ctx, config, setData, canvas, imageKey = 'roundTextImage' }) {
return new Promise((resolve, reject) => {
if (!ctx || !config) {
console.log('drawRoundTextOnCanvas: ctx 或 config 不存在', { ctx: !!ctx, config: !!config });
if (setData) {
setData({
[imageKey]: ''
});
}
resolve('');
return;
}
const text = config.text;
if (!text) {
console.log('drawRoundTextOnCanvas: 文字为空');
// 如果没有文字,清空图片
if (setData) {
setData({
[imageKey]: ''
});
}
resolve('');
return;
}
console.log('drawRoundTextOnCanvas: 开始绘制文字', { text, config });
// 使用配置中的宽高
const canvasWidth = config.width || 700;
const canvasHeight = config.height || 700;
// 圆心位置:根据需求,圆心应该在容器的中心
// 由于容器就是 Canvas,所以圆心就是 Canvas 的中心
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
const radius = config.radius;
const fontSize = config.style.fontSize;
const color = config.style.color || '#000000';
const fontWeight = config.style.fontWeight || 'normal';
// 清空画布(注意:Canvas 的实际尺寸已经乘以了 dpr,但绘制时使用的是逻辑尺寸)
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
// 设置字体样式
ctx.font = `${fontWeight} ${fontSize}px sans-serif`;
ctx.fillStyle = color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; // 使用 middle 基线,文字中心对齐,更适合圆形排列
// 计算每个字符的角度
const circumference = 2 * Math.PI * radius;
const charWidths = [];
let totalWidth = 0;
// 测量每个字符的宽度
for (let i = 0; i < text.length; i++) {
const char = text[i];
const metrics = ctx.measureText(char);
const width = metrics.width;
charWidths.push(width);
totalWidth += width;
}
// 计算文字总弧长和每个字符的角度
const arcLength = totalWidth;
const totalAngle = (arcLength / circumference) * 2 * Math.PI;
// 计算起始角度(使文字居中于底部)
// 从底部中心开始,顺时针排列,起始角度 = 底部中心 + 总角度的一半
const startAngle = Math.PI / 2 + totalAngle / 2;
// 创建并定位每个字符(逆时针排列,所以角度递减)
let currentAngle = startAngle;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const charWidth = charWidths[i];
// 计算字符角度增量
const charAngleIncrement = (charWidth / circumference) * 2 * Math.PI;
// 计算字符角度位置(逆时针排列,所以角度递减)
const charAngle = currentAngle - charAngleIncrement / 2;
// 计算字符位置
const x = centerX + radius * Math.cos(charAngle);
const y = centerY + radius * Math.sin(charAngle);
// 保存上下文
ctx.save();
// 移动到字符位置
ctx.translate(x, y);
// 旋转文字,使其沿着圆形路径排列
// 文字底部向外,顶部朝向圆心,所以旋转角度为字符角度 + π * 1.5
ctx.rotate(charAngle + Math.PI * 1.5);
// 绘制文字(使用 0, 0 作为中心点,因为已经 translate 到字符位置)
ctx.fillText(char, 0, 0);
// 恢复上下文
ctx.restore();
// 更新当前角度(逆时针排列,所以角度递减)
currentAngle -= charAngleIncrement;
}
// 将 Canvas 转换为图片(Data URL)
try {
const dataURL = canvas.toDataURL('image/png');
if (setData) {
setData({
[imageKey]: dataURL
});
}
resolve(dataURL);
} catch (err) {
console.error('Canvas 转图片失败:', err);
reject(err);
}
});
}
return {
roundText,
initRoundTextCanvas,
drawRoundTextOnCanvas,
calculateFrontRoundTextChars,
calculateBackRoundTextChars
};
}