diff --git a/src/lang/data/en.ts b/src/lang/data/en.ts index 9b12882..744f6b2 100644 --- a/src/lang/data/en.ts +++ b/src/lang/data/en.ts @@ -149,5 +149,12 @@ const en = { "日漫3D": "Japanese 3D", "迪斯尼皮克斯": "Disney Pixar", "潮玩IP": "IP Toy", + "浮雕挂饰": "Relief Pendant", + "人物浮雕挂饰": "Character Relief Pendant", + "宠物浮雕挂饰": "Pet Relief Pendant", + "浮雕挂饰-正面": "Relief Pendant-Front", + "浮雕挂饰-背面": "Relief Pendant-Back", + "请输入正面文字": "Please enter front text", + "请输入背面文字": "Please enter back text", } export default en \ No newline at end of file diff --git a/src/lang/data/ko.ts b/src/lang/data/ko.ts index 38126d6..f899fa3 100644 --- a/src/lang/data/ko.ts +++ b/src/lang/data/ko.ts @@ -149,5 +149,12 @@ const ko = { "日漫3D": "일본 3D", "迪斯尼皮克斯": "디즈니 픽사", "潮玩IP": "IP 토이", + "浮雕挂饰": "부조 걸이", + "人物浮雕挂饰": "캐릭터 부조 걸이", + "宠物浮雕挂饰": "애완동물 부조 걸이", + "浮雕挂饰-正面": "부조 걸이-정면", + "浮雕挂饰-背面": "부조 걸이-뒷면", + "请输入正面文字": "정면 텍스트를 입력하세요", + "请输入背面文字": "뒷면 텍스트를 입력하세요", } export default ko \ No newline at end of file diff --git a/src/lang/data/zh-CN.ts b/src/lang/data/zh-CN.ts index 2046f75..edf4edb 100644 --- a/src/lang/data/zh-CN.ts +++ b/src/lang/data/zh-CN.ts @@ -149,5 +149,12 @@ const zhCN = { "日漫3D": "日漫3D", "迪斯尼皮克斯": "迪斯尼皮克斯", "潮玩IP": "潮玩IP", + "浮雕挂饰": "浮雕挂饰", + "人物浮雕挂饰": "人物浮雕挂饰", + "宠物浮雕挂饰": "宠物浮雕挂饰", + "浮雕挂饰-正面": "浮雕挂饰-正面", + "浮雕挂饰-背面": "浮雕挂饰-背面", + "请输入正面文字": "请输入正面文字", + "请输入背面文字": "请输入背面文字", } export default zhCN \ No newline at end of file diff --git a/src/lang/data/zh-TW.ts b/src/lang/data/zh-TW.ts index dff3ee8..e8b8f04 100644 --- a/src/lang/data/zh-TW.ts +++ b/src/lang/data/zh-TW.ts @@ -149,5 +149,12 @@ const zhCT = { "日漫3D": "日漫3D", "迪斯尼皮克斯": "迪士尼皮克斯", "潮玩IP": "潮玩IP", + "浮雕挂饰": "浮雕掛飾", + "人物浮雕挂饰": "人物浮雕掛飾", + "宠物浮雕挂饰": "寵物浮雕掛飾", + "浮雕挂饰-正面": "浮雕掛飾-正面", + "浮雕挂饰-背面": "浮雕掛飾-背面", + "请输入正面文字": "請輸入正面文字", + "请输入背面文字": "請輸入背面文字", } export default zhCT \ No newline at end of file diff --git a/src/utils/textHelper.ts b/src/utils/textHelper.ts new file mode 100644 index 0000000..ed8e09a --- /dev/null +++ b/src/utils/textHelper.ts @@ -0,0 +1,586 @@ +// 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.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 = 2; + 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 + }; +} \ No newline at end of file diff --git a/src/views/badge/index.vue b/src/views/badge/index.vue index f921188..8292c02 100644 --- a/src/views/badge/index.vue +++ b/src/views/badge/index.vue @@ -27,6 +27,9 @@
{{ toValueWithout("注:需要先输入手机号才可体验徽章设计") }}