2026-03-11 15:34:56 +08:00
|
|
|
|
/** 克隆对象 */
|
|
|
|
|
|
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,
|
2026-03-13 14:08:40 +08:00
|
|
|
|
angle: obj.angle,
|
2026-03-11 15:34:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
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 => {
|
2026-03-13 14:08:40 +08:00
|
|
|
|
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)
|
2026-03-11 15:34:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
|
|
|
left: box1.x,
|
|
|
|
|
|
top: box1.y,
|
|
|
|
|
|
width: box2.x - box1.x,
|
|
|
|
|
|
height: box2.y - box1.y,
|
|
|
|
|
|
}
|
2026-03-17 17:17:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 获取五角星数组 */
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-23 16:43:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取对象黑白通道画布
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|