/** 克隆对象 */ export async function cloneObjects(objects = []) { const arrs = [] for (const obj of objects) { const clonedObj = await new Promise((resolve, reject) => { obj.clone((v) => { v.set({ left: obj.left, top: obj.top, width: obj.width, height: obj.height, scaleX: obj.scaleX, scaleY: obj.scaleY, angle: obj.angle, }) resolve(v) }) }) arrs.push(clonedObj) } return arrs } /** 获取组合对象边界 */ export async function getObjectsBoundingBox(objects = []) { const box1 = { x: Infinity, y: Infinity } const box2 = { x: -Infinity, y: -Infinity } objects.forEach(obj => { const rect = obj.getBoundingRect() box1.x = Math.min(box1.x, rect.left) box1.y = Math.min(box1.y, rect.top) box2.x = Math.max(box2.x, rect.left + rect.width) box2.y = Math.max(box2.y, rect.top + rect.height) }) return { left: box1.x, top: box1.y, width: box2.x - box1.x, height: box2.y - box1.y, } } /** 获取五角星数组 */ export function getStarArr(width = 0, height = 0) { const arr = [ { x: 0, y: -0.5 }, // 顶点0 (上) { x: 0.15, y: -0.15 }, // 顶点1 (内) { x: 0.50, y: -0.15 }, // 顶点2 (右上外) { x: 0.20, y: 0.10 }, // 顶点3 (内) { x: 0.30, y: 0.50 }, // 顶点4 (右下外) { x: 0.0, y: 0.25 }, // 顶点5 (内) { x: -0.30, y: 0.50 }, // 顶点6 (左下外) { x: -0.20, y: 0.10 }, // 顶点7 (内) { x: -0.50, y: -0.15 }, // 顶点8 (左上外) { x: -0.15, y: -0.15 } // 顶点9 (内) ] return arr.map(item => ({ x: item.x * width, y: item.y * height, })) } /** 获取箭头路径 */ export function getArrowPath(width = 0, height = 0) { const arr = [ ["M", 0, height / 2], ["L", width, height / 2], ["M", width - 8, 0], ["L", width, height / 2], ["L", width - 8, height], ] var path = "" arr.forEach(item => { path += item.join(" ") + " " }) return path } /** 计算两点之间的距离 */ export function distance(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; return Math.sqrt(dx * dx + dy * dy); } /** 计算两点之间的角度(角度) */ export function angleBetweenPointsDegrees(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; // 计算弧度并转换为角度 const rad = Math.atan2(dy, dx); const deg = rad * 180 / Math.PI; return deg; } /** * 获取对象黑白通道画布 * @param {fabric.Object} object - 要处理的 fabric 对象 * @param {ImageData} revData - 相反的ImageData,白通道的相同位置是否为透明,revData为白色为透明,黑色为不透明 * @param {number} diff - 差值,默认 25 * @param {Object} rgba - 自定义 rgba 值,默认 { r: 255, g: 255, b: 255, a: 255 } * @param {boolean} isMerge - 是否合并,true=合并revData,false=反转revData * @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败 */ export function getObjectAlphaToCanvas(object, revData, diff = 30, rgba = { r: 255, g: 255, b: 255, a: 255 }, isMerge = false) { const image = object.getElement(); if (image.nodeName !== "IMG" && image.nodeName !== "CANVAS") { console.warn("对象不是图片"); return null; } const { width, height } = image; if (!width || !height) { console.warn("对象没有元素"); return null; } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d", { willReadFrequently: true }); ctx.drawImage(image, 0, 0, width, height); const data = ctx.getImageData(0, 0, width, height); for (let i = 0; i < data.data.length; i += 4) { const r = data.data[i + 0]; const g = data.data[i + 1]; const b = data.data[i + 2]; const a = data.data[i + 3]; const revR = revData?.data[i + 0] || 0; const revG = revData?.data[i + 1] || 0; const revB = revData?.data[i + 2] || 0; const revA = revData?.data[i + 3] || 0; let isHave = false; if (r || g || b || a) { if (revR > diff || revG > diff || revB > diff || revA > diff) { isHave = false; } else { isHave = true; } } if (isMerge && (revR || revG || revB || revA)) isHave = true; if (isHave) { data.data[i + 0] = rgba.r; data.data[i + 1] = rgba.g; data.data[i + 2] = rgba.b; data.data[i + 3] = rgba.a; } else { data.data[i + 0] = 0; data.data[i + 1] = 0; data.data[i + 2] = 0; data.data[i + 3] = 0; } } ctx.clearRect(0, 0, width, height); ctx.putImageData(data, 0, 0); return canvas; } /** * 图片边界跟踪算法(透明底) * @param {HTMLCanvasElement} canvas - canvas元素 * @param {Number} scale - 缩放比例 * @returns {Array} 边界点数组 [{x, y}, ...] */ export function traceImageContour(canvas) { const ctx = canvas.getContext("2d", { willReadFrequently: true }); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const width = canvas.width; const height = canvas.height; // 查找起始点(第一个不透明像素) let startX = -1; let startY = -1; outer: for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const index = (y * width + x) * 4; if (data[index + 3] > 0) { startX = x; startY = y; break outer; } } } if (startX === -1) return []; // 没有不透明像素 // Moore-Neighbor边界跟踪算法 const contour = []; const visited = new Set(); const directions = [ [-1, 0], [-1, -1], [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], ]; let currentX = startX; let currentY = startY; let backtrackDir = 4; // 起始方向:右 do { const pointKey = `${currentX},${currentY}`; if (!visited.has(pointKey)) { contour.push({ x: currentX, y: currentY }); visited.add(pointKey); } // 从右方向开始顺时针查找 let found = false; for (let i = 0; i < 8; i++) { const dir = (backtrackDir + i) % 8; const dx = directions[dir][0]; const dy = directions[dir][1]; const checkX = currentX + dx; const checkY = currentY + dy; if ( checkX >= 0 && checkX < width && checkY >= 0 && checkY < height ) { const index = (checkY * width + checkX) * 4; if (data[index + 3] > 0) { currentX = checkX; currentY = checkY; backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向 found = true; break; } } } if (!found) break; } while ( !(currentX === startX && currentY === startY) && visited.size < width * height ); return contour; }