画布增加的新功能

This commit is contained in:
李志鹏
2026-01-02 11:24:11 +08:00
parent 1ae365b1f3
commit f8e4ab8cdb
59 changed files with 4401 additions and 1213 deletions

View File

@@ -79,6 +79,9 @@ export class LayerSort {
} else if (layer.isFixed && layer.fabricObject) {
// 固定图层对象
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (layer.isFixedOther && layer.fabricObject) {
// 其他固定图层对象
zIndexMap.set(layer.fabricObject.id, currentZIndex++);
} else if (!layer.isBackground && !layer.isFixed) {
// 普通图层
currentZIndex = this.processLayerObjects(

View File

@@ -0,0 +1,37 @@
class EventManager {
constructor() {
this.eventMap = {};
}
/**
* 注册事件
* @param {string} eventName - 事件名称
* @param {function} callback - 事件回调函数
*/
on(eventName, callback) {
if (!this.eventMap[eventName]) {
this.eventMap[eventName] = [];
}
this.eventMap[eventName].push(callback);
}
/**
* 触发事件
* @param {string} eventName - 事件名称
* @param {...any} args - 事件参数
*/
emit(eventName, ...args) {
if (this.eventMap[eventName]) {
this.eventMap[eventName].forEach(callback => callback(...args));
}
}
/**
* 移除事件
* @param {string} eventName - 事件名称
* @param {function} callback - 事件回调函数
*/
off(eventName, callback) {
if (this.eventMap[eventName]) {
this.eventMap[eventName] = this.eventMap[eventName].filter(cb => cb !== callback);
}
}
}
export default new EventManager();

View File

@@ -429,7 +429,8 @@ export function objectIsInCanvas(canvas, targetObj) {
}
const targetId = targetObj.id;
if (!targetId) {
const targetLayerId = targetObj.layerId;
if (!targetId && !targetLayerId) {
return { flag: false, object: null, parent: null };
}
@@ -437,7 +438,11 @@ export function objectIsInCanvas(canvas, targetObj) {
const topLevelObjects = canvas.getObjects();
// 直接在顶层查找
const directMatch = topLevelObjects.find((obj) => obj.id === targetId);
const directMatch = topLevelObjects.find((obj) => {
const isId = !targetId ? true : obj.id === targetId;
const isLayerId = !targetLayerId ? true : obj.layerId === targetLayerId;
return isId && isLayerId;
});
if (directMatch) {
return { flag: true, object: directMatch, parent: null };
}
@@ -500,6 +505,22 @@ export function findObjectById(canvas, objectId) {
return { object: result.object, parent: result.parent };
}
/**
* 通过layerID查找对象增强版
* @param {fabric.Canvas} canvas 画布实例
* @param {string} layerId 图层ID
* @returns {Object} { object: fabric.Object|null, parent: fabric.Group|null }
*/
export function findObjectByLayerId(canvas, layerId) {
if (!canvas || !layerId) {
return { object: null, parent: null };
}
const result = objectIsInCanvas(canvas, { layerId: layerId });
return { object: result.object, parent: result.parent };
}
/**
* 安全移除画布对象(包括组内对象)
* @param {fabric.Canvas} canvas 画布实例
@@ -738,3 +759,203 @@ export function getLayerObjectsZIndex(canvas, layerId) {
const allInfo = getAllObjectsZIndex(canvas);
return allInfo.filter((info) => info.layerId === layerId);
}
/**
* 计算两点之间的角度
* @param {number} x1 第一个点的x坐标
* @param {number} y1 第一个点的y坐标
* @param {number} x2 第二个点的x坐标
* @param {number} y2 第二个点的y坐标
* @returns {number} 角度值(-90 - 270度
*/
export function calculateAngle(x1, y1, x2, y2, int = false) {
// 计算两点之间的差值
const deltaX = x2 - x1;
const deltaY = y2 - y1;
// 使用Math.atan2计算弧度然后转换为角度
let angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90;
return int ? Math.round(angle) : angle;
}
/**
* 通过角度计算直线上的两点坐标返回0-1范围的坐标
* @param {number} angle 角度值0-360度
* @returns {{x1: number, y1: number, x2: number, y2: number}} 包含两个点坐标
*/
export function calculateLinePoints(angle) {
// 将角度转换为弧度
const radian = (angle - 90) * Math.PI / 180;
// 计算直线上的两点坐标
const x1 = 0.5 - 0.5 * Math.cos(radian);
const y1 = 0.5 - 0.5 * Math.sin(radian);
const x2 = 0.5 + 0.5 * Math.cos(radian);
const y2 = 0.5 + 0.5 * Math.sin(radian);
return {x1, y1, x2, y2};
}
export function rgbaToHex(rgba){
const r = rgba.r.toString(16).padStart(2, "0");
const g = rgba.g.toString(16).padStart(2, "0");
const b = rgba.b.toString(16).padStart(2, "0");
return `#${r}${g}${b}`;
}
export function fillToPallet(fill) {
if(!fill.coords || !fill.colorStops) return {};
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
const colors = new Set();
// console.log("==========fill", fill);
const gradientList = fill.colorStops.map((stop) => {
colors.add(stop.color);
const rgbas = stop.color.replace("rgb(", "").replace("rgba(", "").replace(")", "").split(", ");
const rgba = {
r: parseInt(rgbas[0]),
g: parseInt(rgbas[1]),
b: parseInt(rgbas[2]),
a: parseFloat(rgbas[3]),
};
if(isNaN(rgba.r)) rgba.r = 0;
if(isNaN(rgba.g)) rgba.g = 0;
if(isNaN(rgba.b)) rgba.b = 0;
if(isNaN(rgba.a)) rgba.a = 1;
return {
rgba: rgba,
left: parseInt(stop.offset * 100) + "%",
};
});
const isGradient = colors.size > 1;
if(isGradient) {
return {
// hex: rgbaToHex(gradientList[0].rgba),
rgba: gradientList[0].rgba,
gradient:{ angle, selectIndex: 0, gradientShow: true, gradientList },
};
} else {
return {
hex: rgbaToHex(gradientList[0].rgba),
rgba: gradientList[0].rgba,
};
}
}
export function palletToFill(pallet) {
const fill = {
coords: calculateLinePoints(0),
colorStops: [
{ offset: 0, color: "rgba(0, 0, 0, 0)" },
{ offset: 1, color: "rgba(0, 0, 0, 0)" }
]
}
if(pallet?.gradient){
let obj = pallet.gradient;
fill.coords = calculateLinePoints(obj.angle);
if(obj.gradientList.length >= 2){
fill.colorStops = obj.gradientList.map(item => ({
offset: parseInt(item.left) / 100,
color: `rgba(${item.rgba.r}, ${item.rgba.g}, ${item.rgba.b}, ${item.rgba.a})`,
}));
}
}else if(pallet?.rgba?.hasOwnProperty("r") && pallet?.rgba?.hasOwnProperty("g") && pallet?.rgba?.hasOwnProperty("b")){
let rgba = pallet.rgba;
fill.colorStops = [
{ offset: 0, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` },
{ offset: 1, color: `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` }
]
}
return fill;
}
export function fillToCssStyle(fill) {
if(!fill.coords || !fill.colorStops) return "";
const angle = calculateAngle(fill.coords.x1, fill.coords.y1, fill.coords.x2, fill.coords.y2);
if(fill.colorStops.every(v => v.color === fill.colorStops[0].color)){
return fill.colorStops[0].color;
}else{
var str = "linear-gradient(" + angle + "deg, ";
fill.colorStops.forEach((v) => {
str += `${v.color} ${v.offset * 100}%, `
})
return str.slice(0, -2) + ")";
}
}
/**
* 根据左上角坐标计算旋转后的新坐标
* @param {number} W - 宽度
* @param {number} H - 高度
* @param {number} currentX - 当前左上角x坐标
* @param {number} currentY - 当前左上角y坐标
* @param {number} currentAngleDeg - 当前角度(度)
* @param {number} newAngleDeg - 新角度(度)
* @returns {Object} 旋转后的左上角坐标 {x, y}
*/
export function calculateRotatedTopLeftDeg(
W,
H,
currentX,
currentY,
currentAngleDeg,
newAngleDeg
) {
const currentAngle = (currentAngleDeg * Math.PI) / 180;
const newAngle = (newAngleDeg * Math.PI) / 180;
// 1. 用当前角度计算中心点位置
const cosCurrent = Math.cos(currentAngle);
const sinCurrent = Math.sin(currentAngle);
const Cx = currentX + (W / 2) * cosCurrent - (H / 2) * sinCurrent;
const Cy = currentY + (W / 2) * sinCurrent + (H / 2) * cosCurrent;
// 2. 用新角度计算旋转后的左上角位置
const cosNew = Math.cos(newAngle);
const sinNew = Math.sin(newAngle);
const newX = Cx + (-W / 2) * cosNew - (-H / 2) * sinNew;
const newY = Cy + (-W / 2) * sinNew + (-H / 2) * cosNew;
return { x: newX, y: newY };
}
/**
* 创建缩放+旋转的变换矩阵
* @param {number} scale - 缩放比例
* @param {number} angle - 旋转角度(度)
* @returns {Array} 变换矩阵 [a, b, c, d, e, f]
*/
export function createPatternTransform(scale, angle) {
// return fabric.util.composeMatrix({
// scaleX: scale,
// scaleY: scale,
// angle: angle,
// });
const angle_ = angle * Math.PI / 180;
const cos = Math.cos(angle_);
const sin = Math.sin(angle_);
// 先缩放,后旋转
return [
scale * cos, // a
scale * sin, // b
-scale * sin, // c
scale * cos, // d
0, // e (水平位移)
0 // f (垂直位移)
];
}
/**
* 获取变换矩阵的缩放、旋转
* @param {Array} Transform - 变换矩阵、
* @returns {Object} 缩放、旋转角度 {scale, angle}
*/
export function getTransformScaleAngle(Transform) {
const a = Transform[0];
const b = Transform[1];
const c = Transform[2];
const d = Transform[3];
const scale = Math.sqrt(a * a + b * b);
const angle = Math.round(Math.atan2(b, a) * 180 / Math.PI);
return { scale, angle };
}

View File

@@ -5,8 +5,8 @@
*/
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineOffset = 1,
aligningLineMargin = 1,
aligningLineWidth = 1,
aligningLineColor = "rgb(0,255,0)",
viewportTransform,
@@ -14,9 +14,9 @@ function initAligningGuidelines(canvas) {
function drawVerticalLine(coords) {
drawLine(
coords.x + 0.5,
coords.x,
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
coords.x + 0.5,
coords.x,
coords.y2 > coords.y1 ? coords.y2 : coords.y1
);
}
@@ -24,9 +24,9 @@ function initAligningGuidelines(canvas) {
function drawHorizontalLine(coords) {
drawLine(
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
coords.y + 0.5,
coords.y,
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
coords.y + 0.5
coords.y
);
}
@@ -351,7 +351,7 @@ export function initCenteringGuidelines(canvas) {
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = {},
canvasHeightCenterMap = {},
centerLineMargin = 4,
centerLineMargin = 1,
centerLineColor = "rgba(255,0,241,0.5)",
centerLineWidth = 1,
ctx = canvas.getSelectionContext(),

View File

@@ -18,6 +18,16 @@ export const LayerType = {
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
};
/**
* 特殊图层ID
*/
export const SpecialLayerId = {
SPECIAL_GROUP: "group_special", // 特殊组
COLOR: "special_color", // 颜色图层
}
/**
* 画布操作模式枚举draw(绘画)、select(选择)、pan(拖拽)....
*/
@@ -178,12 +188,17 @@ export function createLayer(options = {}) {
locked: options.locked !== undefined ? options.locked : false,
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
isHidenDragHandle: options.isHidenDragHandle || false,
isDisableUnlock: options.isDisableUnlock || false,
isFixedOther: options.isFixedOther || false,
isFixedClipMask: options.isFixedClipMask || false,
// 确保不是背景图层
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
fabricObject: options.fabricObject || null,
// 嵌套结构 - 适用于组图层
children: options.children || [],

View File

@@ -172,6 +172,10 @@ export function simplifyLayers(layers) {
opacity: layer.opacity,
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
isFixedOther: layer.isFixedOther || false,
isFixedClipMask: layer.isFixedClipMask || false,
isHidenDragHandle: layer.isHidenDragHandle || false,
isDisableUnlock: layer.isDisableUnlock || false,
clippingMask:
layer.clippingMask?.toObject?.(["id", "layerId"]) ||
layer.clippingMask ||

View File

@@ -7,55 +7,92 @@ import { fabric } from "fabric-with-all";
* @returns {Promise<fabric.Object>} 恢复的 fabric 对象
*/
export async function restoreFabricObject(serializedObject, canvas) {
return new Promise((resolve, reject) => {
const objectType = serializedObject.type;
// 定义恢复后的处理函数
const handleRestoredObject = (fabricObject) => {
if (!fabricObject) {
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
return;
}
return new Promise((resolve, reject) => {
const objectType = serializedObject.type;
// 定义恢复后的处理函数
const handleRestoredObject = (fabricObject) => {
if (!fabricObject) {
reject(new Error(`无法恢复 ${objectType} 类型的对象`));
return;
}
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
// 恢复自定义属性
if (serializedObject.id) fabricObject.id = serializedObject.id;
if (serializedObject.layerId) fabricObject.layerId = serializedObject.layerId;
if (serializedObject.layerName) fabricObject.layerName = serializedObject.layerName;
// 更新坐标
fabricObject.setCoords();
// 更新坐标
fabricObject.setCoords();
// 添加到画布
// canvas.add(fabricObject);
// 添加到画布
// canvas.add(fabricObject);
resolve(fabricObject);
};
resolve(fabricObject);
};
// 根据类型选择恢复方法
switch (objectType) {
case "rect":
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
break;
case "circle":
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
break;
case "path":
fabric.Path.fromObject(serializedObject, handleRestoredObject);
break;
case "image":
fabric.Image.fromObject(serializedObject, handleRestoredObject);
break;
case "group":
fabric.Group.fromObject(serializedObject, handleRestoredObject);
break;
default:
// 使用通用方法
fabric.util.enlivenObjects([serializedObject], (objects) => {
if (objects && objects[0]) {
handleRestoredObject(objects[0]);
} else {
reject(new Error("对象恢复失败"));
}
});
}
});
// 根据类型选择恢复方法
switch (objectType) {
case "rect":
fabric.Rect.fromObject(serializedObject, handleRestoredObject);
break;
case "circle":
fabric.Circle.fromObject(serializedObject, handleRestoredObject);
break;
case "path":
fabric.Path.fromObject(serializedObject, handleRestoredObject);
break;
case "image":
fabric.Image.fromObject(serializedObject, handleRestoredObject);
break;
case "group":
fabric.Group.fromObject(serializedObject, handleRestoredObject);
break;
default:
// 使用通用方法
fabric.util.enlivenObjects([serializedObject], (objects) => {
if (objects && objects[0]) {
handleRestoredObject(objects[0]);
} else {
reject(new Error("对象恢复失败"));
}
});
}
});
}
/**
* 获取对象黑白通道画布
*/
export function getObjectAlphaToCanvas(object) {
const image = object.getElement();
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");
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];
if (r || g || b || a) {
data.data[i + 0] = 255;
data.data[i + 1] = 255;
data.data[i + 2] = 255;
data.data[i + 3] = 255;
}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;
}

View File

@@ -68,7 +68,7 @@ export const createRasterizedImage = async ({
isReturenDataURL,
});
} catch (error) {
console.error("创建栅格化图像失败:", error);
console.warn("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
@@ -163,7 +163,7 @@ const createClippedObjects = async ({
console.log("✅ 返回裁剪后的fabric对象已恢复到优化后的原始大小和位置");
return fabricImage;
} catch (error) {
console.error("创建裁剪对象失败:", error);
console.warn("创建裁剪对象失败:", error);
throw error;
}
};
@@ -1239,7 +1239,7 @@ const calculateOptimizedBounds = (clippingObject, fabricObjects) => {
return optimizedBounds;
} catch (error) {
console.error("计算优化边界框失败:", error);
console.warn("计算优化边界框失败:", error);
// 返回原始计算方式作为备选
return clippingObject.getBoundingRect(true, true);
}