接入画布

This commit is contained in:
X1627315083
2025-06-09 10:25:54 +08:00
parent 87a08f5f8f
commit c266967f16
157 changed files with 43833 additions and 1571 deletions

View File

@@ -0,0 +1,31 @@
//import { fabric } from "fabric-with-all";
import { canvasConfig } from "../config/canvasConfig";
/**
* Factory for creating optimized fabric canvas instances
*/
export const createCanvas = (elementId, options = {}) => {
// Create the canvas instance
const canvas = new fabric.Canvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
renderOnAddRemove: false,
enableRetinaScaling: true,
preserveObjectStacking: true, // 保持对象堆叠顺序
// skipOffscreen: true, // 跳过离屏渲染
...options,
});
return canvas;
};
/**
* Utility to create a static canvas (for improved performance when interaction is not needed)
*/
export const createStaticCanvas = (elementId, options = {}) => {
const canvas = new fabric.StaticCanvas(elementId, {
enableRetinaScaling: canvasConfig.enableRetinaScaling,
...options,
});
return canvas;
};

View File

@@ -0,0 +1,458 @@
export function deepCompare(obj1, obj2) {
const diff = {};
// 处理基础类型
if (obj1 === obj2) {
return null;
}
if (
obj1 === null ||
obj2 === null ||
typeof obj1 !== "object" ||
typeof obj2 !== "object"
) {
return { _value: obj2, _oldValue: obj1 };
}
// 处理数组
if (Array.isArray(obj1) && Array.isArray(obj2)) {
if (obj1.length !== obj2.length) {
return { _value: obj2, _oldValue: obj1 };
}
for (let i = 0; i < obj1.length; i++) {
const itemDiff = deepCompare(obj1[i], obj2[i]);
if (itemDiff !== null) {
diff[i] = itemDiff;
}
}
return Object.keys(diff).length > 0 ? diff : null;
}
// 处理对象
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of allKeys) {
const val1 = obj1[key];
const val2 = obj2[key];
if (!(key in obj1)) {
diff[key] = { _value: val2, _type: "added" };
} else if (!(key in obj2)) {
diff[key] = { _value: undefined, _oldValue: val1, _type: "removed" };
} else {
const itemDiff = deepCompare(val1, val2);
if (itemDiff !== null) {
diff[key] = itemDiff;
}
}
}
return Object.keys(diff).length > 0 ? diff : null;
}
/**
* 深度克隆对象
* @param {any} obj 要克隆的对象
* @returns {any} 克隆后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}
if (typeof obj === "object") {
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
return obj;
}
/**
* 应用差异到基础对象
* @param {Object} baseObj 基础对象
* @param {Object} diff 差异对象
* @returns {Object} 应用差异后的对象
*/
export function applyDiff(baseObj, diff) {
if (!diff) {
return deepClone(baseObj);
}
// 如果是直接值替换
if (diff._value !== undefined) {
return diff._value;
}
const result = deepClone(baseObj) || {};
for (const key in diff) {
const change = diff[key];
if (change._type === "added" || change._type === "removed") {
if (change._type === "removed") {
delete result[key];
} else {
result[key] = change._value;
}
} else if (change._value !== undefined) {
result[key] = change._value;
} else {
// 递归应用嵌套差异
result[key] = applyDiff(result[key], change);
}
}
return result;
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} wait 等待时间
* @returns {Function} 节流后的函数
*/
export function throttle(func, wait) {
let timeout;
let previous = 0;
return function executedFunction(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
return func.apply(this, args);
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now();
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 等待时间
* @param {boolean} immediate 是否立即执行
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait, immediate = false) {
let timeout;
return function executedFunction(...args) {
const later = () => {
timeout = null;
if (!immediate) func.apply(this, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(this, args);
};
}
/**
* 生成唯一ID
* @param {string} prefix 前缀
* @returns {string} 唯一ID
*/
export function generateId(prefix = "id") {
return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 格式化文件大小
* @param {number} bytes 字节数
* @returns {string} 格式化后的大小
*/
export function formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* 格式化时间差
* @param {number} milliseconds 毫秒数
* @returns {string} 格式化后的时间
*/
export function formatDuration(milliseconds) {
if (milliseconds < 1000) {
return `${milliseconds.toFixed(2)}ms`;
} else if (milliseconds < 60000) {
return `${(milliseconds / 1000).toFixed(2)}s`;
} else {
const minutes = Math.floor(milliseconds / 60000);
const seconds = ((milliseconds % 60000) / 1000).toFixed(2);
return `${minutes}m ${seconds}s`;
}
}
/**
* 检查是否是有效的命令对象
* @param {*} command 命令对象
* @returns {boolean} 是否有效
*/
export function isValidCommand(command) {
return (
command &&
typeof command === "object" &&
typeof command.execute === "function"
);
}
/**
* 检查是否是Promise
* @param {*} obj 对象
* @returns {boolean} 是否是Promise
*/
export function isPromise(obj) {
return obj && typeof obj.then === "function";
}
/**
* 安全的JSON解析
* @param {string} jsonString JSON字符串
* @param {*} defaultValue 默认值
* @returns {*} 解析结果
*/
export function safeJSONParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn("JSON解析失败:", error);
return defaultValue;
}
}
/**
* 安全的JSON序列化
* @param {*} obj 要序列化的对象
* @param {*} defaultValue 默认值
* @returns {string} JSON字符串
*/
export function safeJSONStringify(obj, defaultValue = "{}") {
try {
return JSON.stringify(obj);
} catch (error) {
console.warn("JSON序列化失败:", error);
return defaultValue;
}
}
/**
* 计算对象深度
* @param {*} obj 对象
* @returns {number} 深度
*/
export function getObjectDepth(obj) {
if (obj === null || typeof obj !== "object") {
return 0;
}
let maxDepth = 0;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const depth = getObjectDepth(obj[key]);
maxDepth = Math.max(maxDepth, depth);
}
}
return maxDepth + 1;
}
/**
* 计算对象大小(字节)
* @param {*} obj 对象
* @returns {number} 大小(字节)
*/
export function getObjectSize(obj) {
const jsonString = safeJSONStringify(obj, "{}");
return new Blob([jsonString]).size;
}
/**
* 检查浏览器支持
* @returns {Object} 支持信息
*/
export function checkBrowserSupport() {
return {
WeakRef: typeof WeakRef !== "undefined",
FinalizationRegistry: typeof FinalizationRegistry !== "undefined",
PerformanceMemory:
typeof performance !== "undefined" && !!performance.memory,
RequestIdleCallback: typeof requestIdleCallback !== "undefined",
IntersectionObserver: typeof IntersectionObserver !== "undefined",
ResizeObserver: typeof ResizeObserver !== "undefined",
};
}
/**
* 延迟执行
* @param {number} ms 延迟毫秒数
* @returns {Promise} Promise对象
*/
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 重试执行函数
* @param {Function} fn 要执行的函数
* @param {Object} options 重试选项
* @returns {Promise} 执行结果
*/
export async function retry(fn, options = {}) {
const {
retries = 3,
delay: delayMs = 1000,
backoff = 1.5,
shouldRetry = () => true,
} = options;
let attempt = 0;
let currentDelay = delayMs;
while (attempt <= retries) {
try {
return await fn();
} catch (error) {
attempt++;
if (attempt > retries || !shouldRetry(error)) {
throw error;
}
await delay(currentDelay);
currentDelay *= backoff;
}
}
}
/**
* 批处理执行
* @param {Array} items 要处理的项目
* @param {Function} processor 处理函数
* @param {Object} options 批处理选项
* @returns {Promise<Array>} 处理结果
*/
export async function batchProcess(items, processor, options = {}) {
const { batchSize = 10, delay: delayMs = 0, onProgress = () => {} } = options;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map((item) => processor(item))
);
results.push(...batchResults);
onProgress({
completed: Math.min(i + batchSize, items.length),
total: items.length,
percentage: Math.min(((i + batchSize) / items.length) * 100, 100),
});
if (delayMs > 0 && i + batchSize < items.length) {
await delay(delayMs);
}
}
return results;
}
/**
* 创建可取消的Promise
* @param {Function} executor Promise执行器
* @returns {Object} 包含promise和cancel方法的对象
*/
export function createCancellablePromise(executor) {
let isCancelled = false;
let cancelCallback = null;
const promise = new Promise((resolve, reject) => {
const wrappedResolve = (value) => {
if (!isCancelled) resolve(value);
};
const wrappedReject = (error) => {
if (!isCancelled) reject(error);
};
cancelCallback = () => {
isCancelled = true;
reject(new Error("Promise was cancelled"));
};
executor(wrappedResolve, wrappedReject);
});
return {
promise,
cancel: () => {
if (cancelCallback) {
cancelCallback();
}
},
isCancelled: () => isCancelled,
};
}
// 导出所有工具函数
export default {
deepCompare,
deepClone,
applyDiff,
throttle,
debounce,
generateId,
formatFileSize,
formatDuration,
isValidCommand,
isPromise,
safeJSONParse,
safeJSONStringify,
getObjectDepth,
getObjectSize,
checkBrowserSupport,
delay,
retry,
batchProcess,
createCancellablePromise,
};

View File

@@ -0,0 +1,532 @@
/**
* Should objects be aligned by a bounding box?
* [Bug] Scaled objects sometimes can not be aligned by edges
*
*/
function initAligningGuidelines(canvas) {
var ctx = canvas.getSelectionContext(),
aligningLineOffset = 5,
aligningLineMargin = 4,
aligningLineWidth = 1,
aligningLineColor = "rgb(0,255,0)",
viewportTransform,
zoom = 1;
function drawVerticalLine(coords) {
drawLine(
coords.x + 0.5,
coords.y1 > coords.y2 ? coords.y2 : coords.y1,
coords.x + 0.5,
coords.y2 > coords.y1 ? coords.y2 : coords.y1
);
}
function drawHorizontalLine(coords) {
drawLine(
coords.x1 > coords.x2 ? coords.x2 : coords.x1,
coords.y + 0.5,
coords.x2 > coords.x1 ? coords.x2 : coords.x1,
coords.y + 0.5
);
}
function drawLine(x1, y1, x2, y2) {
ctx.save();
ctx.lineWidth = aligningLineWidth;
ctx.strokeStyle = aligningLineColor;
ctx.beginPath();
ctx.moveTo(
x1 * zoom + viewportTransform[4],
y1 * zoom + viewportTransform[5]
);
ctx.lineTo(
x2 * zoom + viewportTransform[4],
y2 * zoom + viewportTransform[5]
);
ctx.stroke();
ctx.restore();
}
function isInRange(value1, value2) {
value1 = Math.round(value1);
value2 = Math.round(value2);
for (
var i = value1 - aligningLineMargin, len = value1 + aligningLineMargin;
i <= len;
i++
) {
if (i === value2) {
return true;
}
}
return false;
}
var verticalLines = [],
horizontalLines = [];
canvas.on("mouse:down", function () {
viewportTransform = canvas.viewportTransform;
zoom = canvas.getZoom();
});
canvas.on("object:moving", function (e) {
var activeObject = e.target,
canvasObjects = canvas.getObjects(),
activeObjectCenter = activeObject.getCenterPoint(),
activeObjectLeft = activeObjectCenter.x,
activeObjectTop = activeObjectCenter.y,
activeObjectBoundingRect = activeObject.getBoundingRect(),
activeObjectHeight =
activeObjectBoundingRect.height / viewportTransform[3],
activeObjectWidth = activeObjectBoundingRect.width / viewportTransform[0],
horizontalInTheRange = false,
verticalInTheRange = false,
transform = canvas._currentTransform;
if (!transform) return;
// It should be trivial to DRY this up by encapsulating (repeating) creation of x1, x2, y1, and y2 into functions,
// but we're not doing it here for perf. reasons -- as this a function that's invoked on every mouse move
for (var i = canvasObjects.length; i--; ) {
if (canvasObjects[i] === activeObject) continue;
var objectCenter = canvasObjects[i].getCenterPoint(),
objectLeft = objectCenter.x,
objectTop = objectCenter.y,
objectBoundingRect = canvasObjects[i].getBoundingRect(),
objectHeight = objectBoundingRect.height / viewportTransform[3],
objectWidth = objectBoundingRect.width / viewportTransform[0];
// snaps if the right side of the active object touches the left side of the object
if (
isInRange(
activeObjectLeft + activeObjectWidth / 2,
objectLeft - objectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snaps if the left side of the active object touches the right side of the object
if (
isInRange(
activeObjectLeft - activeObjectWidth / 2,
objectLeft + objectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snaps if the bottom of the object touches the top of the active object
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 + activeObjectHeight / 2
),
"center",
"center"
);
}
// snaps if the top of the object touches the bottom of the active object
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 - activeObjectHeight / 2
),
"center",
"center"
);
}
// snap by the horizontal center line
if (isInRange(objectLeft, activeObjectLeft)) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(objectLeft, activeObjectTop),
"center",
"center"
);
}
// snap by the left edge
if (
isInRange(
objectLeft - objectWidth / 2,
activeObjectLeft - activeObjectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft - objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft - objectWidth / 2 + activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snap by the right edge
if (
isInRange(
objectLeft + objectWidth / 2,
activeObjectLeft + activeObjectWidth / 2
)
) {
verticalInTheRange = true;
verticalLines.push({
x: objectLeft + objectWidth / 2,
y1:
objectTop < activeObjectTop
? objectTop - objectHeight / 2 - aligningLineOffset
: objectTop + objectHeight / 2 + aligningLineOffset,
y2:
activeObjectTop > objectTop
? activeObjectTop + activeObjectHeight / 2 + aligningLineOffset
: activeObjectTop - activeObjectHeight / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
objectLeft + objectWidth / 2 - activeObjectWidth / 2,
activeObjectTop
),
"center",
"center"
);
}
// snap by the vertical center line
if (isInRange(objectTop, activeObjectTop)) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(activeObjectLeft, objectTop),
"center",
"center"
);
}
// snap by the top edge
if (
isInRange(
objectTop - objectHeight / 2,
activeObjectTop - activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop - objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop - objectHeight / 2 + activeObjectHeight / 2
),
"center",
"center"
);
}
// snap by the bottom edge
if (
isInRange(
objectTop + objectHeight / 2,
activeObjectTop + activeObjectHeight / 2
)
) {
horizontalInTheRange = true;
horizontalLines.push({
y: objectTop + objectHeight / 2,
x1:
objectLeft < activeObjectLeft
? objectLeft - objectWidth / 2 - aligningLineOffset
: objectLeft + objectWidth / 2 + aligningLineOffset,
x2:
activeObjectLeft > objectLeft
? activeObjectLeft + activeObjectWidth / 2 + aligningLineOffset
: activeObjectLeft - activeObjectWidth / 2 - aligningLineOffset,
});
activeObject.setPositionByOrigin(
new fabric.Point(
activeObjectLeft,
objectTop + objectHeight / 2 - activeObjectHeight / 2
),
"center",
"center"
);
}
}
if (!horizontalInTheRange) {
horizontalLines.length = 0;
}
if (!verticalInTheRange) {
verticalLines.length = 0;
}
});
canvas.on("before:render", function () {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on("after:render", function () {
for (var i = verticalLines.length; i--; ) {
drawVerticalLine(verticalLines[i]);
}
for (var i = horizontalLines.length; i--; ) {
drawHorizontalLine(horizontalLines[i]);
}
verticalLines.length = horizontalLines.length = 0;
});
canvas.on("mouse:up", function () {
verticalLines.length = horizontalLines.length = 0;
canvas.renderAll();
});
}
export default initAligningGuidelines;
/**
* Augments canvas by assigning to `onObjectMove` and `onAfterRender`.
* This kind of sucks because other code using those methods will stop functioning.
* Need to fix it by replacing callbacks with pub/sub kind of subscription model.
* (or maybe use existing fabric.util.fire/observe (if it won't be too slow))
*/
export function initCenteringGuidelines(canvas) {
var canvasWidth = canvas.getWidth(),
canvasHeight = canvas.getHeight(),
canvasWidthCenter = canvasWidth / 2,
canvasHeightCenter = canvasHeight / 2,
canvasWidthCenterMap = {},
canvasHeightCenterMap = {},
centerLineMargin = 4,
centerLineColor = "rgba(255,0,241,0.5)",
centerLineWidth = 1,
ctx = canvas.getSelectionContext(),
viewportTransform;
for (
var i = canvasWidthCenter - centerLineMargin,
len = canvasWidthCenter + centerLineMargin;
i <= len;
i++
) {
canvasWidthCenterMap[Math.round(i)] = true;
}
for (
var i = canvasHeightCenter - centerLineMargin,
len = canvasHeightCenter + centerLineMargin;
i <= len;
i++
) {
canvasHeightCenterMap[Math.round(i)] = true;
}
function showVerticalCenterLine() {
showCenterLine(
canvasWidthCenter + 0.5,
0,
canvasWidthCenter + 0.5,
canvasHeight
);
}
function showHorizontalCenterLine() {
showCenterLine(
0,
canvasHeightCenter + 0.5,
canvasWidth,
canvasHeightCenter + 0.5
);
}
function showCenterLine(x1, y1, x2, y2) {
ctx.save();
ctx.strokeStyle = centerLineColor;
ctx.lineWidth = centerLineWidth;
ctx.beginPath();
ctx.moveTo(x1 * viewportTransform[0], y1 * viewportTransform[3]);
ctx.lineTo(x2 * viewportTransform[0], y2 * viewportTransform[3]);
ctx.stroke();
ctx.restore();
}
var afterRenderActions = [],
isInVerticalCenter,
isInHorizontalCenter;
canvas.on("mouse:down", function () {
viewportTransform = canvas.viewportTransform;
});
canvas.on("object:moving", function (e) {
var object = e.target,
objectCenter = object.getCenterPoint(),
transform = canvas._currentTransform;
if (!transform) return;
(isInVerticalCenter = Math.round(objectCenter.x) in canvasWidthCenterMap),
(isInHorizontalCenter =
Math.round(objectCenter.y) in canvasHeightCenterMap);
if (isInHorizontalCenter || isInVerticalCenter) {
object.setPositionByOrigin(
new fabric.Point(
isInVerticalCenter ? canvasWidthCenter : objectCenter.x,
isInHorizontalCenter ? canvasHeightCenter : objectCenter.y
),
"center",
"center"
);
}
});
canvas.on("before:render", function () {
if (canvas.contextTop) {
canvas.clearContext(canvas.contextTop);
}
});
canvas.on("after:render", function () {
if (isInVerticalCenter) {
showVerticalCenterLine();
}
if (isInHorizontalCenter) {
showHorizontalCenterLine();
}
});
canvas.on("mouse:up", function () {
// clear these values, to stop drawing guidelines once mouse is up
isInVerticalCenter = isInHorizontalCenter = null;
canvas.renderAll();
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,463 @@
/**
* 图层类型枚举
*/
export const LayerType = {
EMPTY: "empty", // 空图层
BITMAP: "bitmap", // 位图图层
VECTOR: "vector", // 矢量图层
TEXT: "text", // 文字图层
GROUP: "group", // 组图层
ADJUSTMENT: "adjustment", // 调整图层
SMART_OBJECT: "smartObject", // 智能对象
SHAPE: "shape", // 形状图层
VIDEO: "video", // 视频图层 (预留)
AUDIO: "audio", // 音频图层 (预留)
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
};
/**
* 画布操作模式枚举draw(绘画)、select(选择)、pan(拖拽)....
*/
export const OperationType = {
// 编辑器模式
DRAW: "draw", // 绘画模式
ERASER: "eraser", // 橡皮擦模式
SELECT: "select", // 选择模式
PAN: "pan", // 拖拽模式
EYEDROPPER: "eyedropper", // 吸色器模式
// 套索工具
LASSO: "lasso", // 套索工具模式 - 自由套索模式
LASSO_RECTANGLE: "lasso_rectangle", // 套索工具模式 - 矩形模式
LASSO_ELLIPSE: "lasso_ellipse", // 套索工具模式 - 椭圆
// 创建临时选区工具模式 - 类似于临时图层 在这个区域的操作不会影响其他图层
AREA_RECTANGLE: "area_rectangle", // 矩形选区模式
// 材质笔刷工具模式
TEXTURE: "texture", // 选择材质笔刷工具模式 - // 选择材质笔刷后会切换到绘画模式 笔刷固定到材质笔刷
// 液化工具
LIQUIFY: "liquify", // 液化工具模式
// 矢量工具
// VECTOR: "vector", // 矢量工具模式
// 矢量工具模式 - 自由绘制
// VECTOR_FREE: "vector_free",
TEXT: "text", // 文字工具模式
// 红绿图模式
RED_GREEN: "red_green", // 红绿图模式 - 只有红色和绿色笔刷还有橡皮擦 不支持添加其他图片 特殊模式
RED_BRUSH: "red_brush", // 红色笔刷
GREEN_BRUSH: "green_brush", // 绿色笔刷
// SHAPE: "shape", // 形状模式
// 可以根据需要添加更多工具
};
// 所有操作模式类型列表
export const OperationTypes = Object.values(OperationType);
/**
* 混合模式枚举
* 与 fabricjs 和 CSS3 的 globalCompositeOperation 对应
*/
export const BlendMode = {
NORMAL: "source-over", // 正常模式
MULTIPLY: "multiply", // 正片叠底
SCREEN: "screen", // 滤色
OVERLAY: "overlay", // 叠加
DARKEN: "darken", // 变暗
LIGHTEN: "lighten", // 变亮
COLOR_DODGE: "color-dodge", // 颜色减淡
COLOR_BURN: "color-burn", // 颜色加深
HARD_LIGHT: "hard-light", // 强光
SOFT_LIGHT: "soft-light", // 柔光
DIFFERENCE: "difference", // 差值
EXCLUSION: "exclusion", // 排除
HUE: "hue", // 色相
SATURATION: "saturation", // 饱和度
COLOR: "color", // 颜色
LUMINOSITY: "luminosity", // 明度
DESTINATION_IN: "destination-in", // 目标内
DESTINATION_OUT: "destination-out", // 目标外
};
/**
* 判断图层是否为组图层
* @param {Object} layer 要检查的图层
* @returns {boolean} 是否为组图层
*/
export function isGroupLayer(layer) {
if (!layer) return false;
return (
layer.type === LayerType.GROUP ||
(Array.isArray(layer.children) && layer.children.length > 0)
);
}
/**
* 从fabric对象创建图层
* @param {Object} fabricObject fabric对象
* @param {String} layerType 图层类型
* @param {Object} options 其他选项
* @returns {Object} 创建的图层对象
*/
export function createLayerFromFabricObject(
fabricObject,
layerType = "bitmap",
options = {}
) {
if (!fabricObject) return null;
// 确定图层类型
let type = layerType;
if (fabricObject.type === "textbox" || fabricObject.type === "text") {
type = LayerType.TEXT;
} else if (
fabricObject.type === "rect" ||
fabricObject.type === "circle" ||
fabricObject.type === "polygon" ||
fabricObject.type === "polyline"
) {
type = LayerType.SHAPE;
} else if (fabricObject.type === "path" || fabricObject.type === "line") {
type = LayerType.VECTOR;
} else if (fabricObject.type === "image") {
type = LayerType.BITMAP;
}
// 创建基础图层
let layer = createLayer({
...options,
type: type,
name:
options.name ||
`${
fabricObject.type.charAt(0).toUpperCase() + fabricObject.type.slice(1)
} 图层`,
parentId: options.parentId || null,
});
// 添加对象到图层
if (Array.isArray(layer.fabricObjects)) {
layer.fabricObjects.push(fabricObject);
} else {
layer.fabricObjects = [fabricObject];
}
// 如果对象有自己的ID将其与图层关联
if (fabricObject.id) {
fabricObject.layerId = layer.id;
fabricObject.layerName = layer.name;
}
return layer;
}
/**
* 创建标准图层对象
* @param {Object} options 图层选项
* @returns {Object} 图层对象
*/
export function createLayer(options = {}) {
const id =
options.id || `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || `图层 ${id.substring(id.lastIndexOf("_") + 1)}`,
type: options.type || LayerType.EMPTY,
visible: options.visible !== undefined ? options.visible : true,
locked: options.locked !== undefined ? options.locked : false,
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
// 确保不是背景图层
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
// 嵌套结构 - 适用于组图层
children: options.children || [],
// 剪切蒙版
clippingMask: options.clippingMask || null,
// 位置和大小信息(可选)
bounds: options.bounds || null,
// 图层特定属性
layerProperties: options.layerProperties || {},
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 创建背景图层
* @param {Object} options 背景图层选项
* @returns {Object} 背景图层对象
*/
export function createBackgroundLayer(options = {}) {
const id =
options.id || `bg_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || "背景",
type: LayerType.BITMAP,
visible: true,
locked: true, // 背景图层默认锁定
opacity: 1.0, // 背景图层始终不透明
blendMode: BlendMode.NORMAL, // 背景图层始终使用正常混合模式
// 标记为背景图层
isBackground: true,
// 画布尺寸
canvasWidth: options.canvasWidth || 800,
canvasHeight: options.canvasHeight || 600,
backgroundColor: options.backgroundColor || "#ffffff",
// Fabric.js 背景对象 (单个矩形对象)
fabricObject: null, // 创建后设置
// Fabric.js 对象列表
fabricObjects: [], // 创建后设置
// 无子图层
children: [],
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 创建位图图层
* @param {Object} options 图层选项
* @returns {Object} 位图图层对象
*/
export function createBitmapLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.BITMAP,
});
// 添加位图特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
filters: options.filters || [], // 滤镜数组
imageUrl: options.imageUrl || null, // 图片URL
imageElement: options.imageElement || null, // 图片元素
};
return baseLayer;
}
/**
* 创建文本图层
* @param {Object} options 图层选项
* @returns {Object} 文本图层对象
*/
export function createTextLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.TEXT,
});
// 添加文字特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
text: options.text || "新文本",
fontFamily: options.fontFamily || "Arial",
fontSize: options.fontSize || 24,
fontWeight: options.fontWeight || "normal",
fontStyle: options.fontStyle || "normal",
textAlign: options.textAlign || "left",
underline: options.underline || false,
overline: options.overline || false,
linethrough: options.linethrough || false,
textBackgroundColor: options.textBackgroundColor || "transparent",
lineHeight: options.lineHeight || 1.16,
charSpacing: options.charSpacing || 0,
};
return baseLayer;
}
/**
* 创建矢量图层
* @param {Object} options 图层选项
* @returns {Object} 矢量图层对象
*/
export function createVectorLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.VECTOR,
});
// 添加矢量特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
vectorType: options.vectorType || "path", // path, polygon, polyline等
strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1,
strokeColor: options.strokeColor || "#000000",
fillColor: options.fillColor || "transparent",
fillRule: options.fillRule || "nonzero",
strokeLineCap: options.strokeLineCap || "butt",
strokeLineJoin: options.strokeLineJoin || "miter",
strokeDashArray: options.strokeDashArray || null,
strokeDashOffset: options.strokeDashOffset || 0,
};
return baseLayer;
}
/**
* 创建形状图层
* @param {Object} options 图层选项
* @returns {Object} 形状图层对象
*/
export function createShapeLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.SHAPE,
});
// 添加形状特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
shapeType: options.shapeType || "rect", // rect, circle, ellipse等
strokeWidth: options.strokeWidth !== undefined ? options.strokeWidth : 1,
strokeColor: options.strokeColor || "#000000",
fillColor: options.fillColor || "#ffffff",
rx: options.rx || 0, // 矩形圆角
ry: options.ry || 0, // 矩形圆角
};
return baseLayer;
}
/**
* 创建调整图层
* @param {Object} options 图层选项
* @returns {Object} 调整图层对象
*/
export function createAdjustmentLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.ADJUSTMENT,
});
// 添加调整图层特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
adjustmentType: options.adjustmentType || "brightness", // brightness, contrast, hue, saturation等
value: options.value !== undefined ? options.value : 0,
affectedLayerIds: options.affectedLayerIds || [], // 受影响的图层ID列表
};
return baseLayer;
}
/**
* 创建智能对象图层
* @param {Object} options 图层选项
* @returns {Object} 智能对象图层
*/
export function createSmartObjectLayer(options = {}) {
const baseLayer = createLayer({
...options,
type: LayerType.SMART_OBJECT,
});
// 添加智能对象特定属性
baseLayer.layerProperties = {
...baseLayer.layerProperties,
sourceType: options.sourceType || "image", // image, vector, embedded等
sourceUrl: options.sourceUrl || null,
sourceData: options.sourceData || null,
originalWidth: options.originalWidth || 0,
originalHeight: options.originalHeight || 0,
embedded: options.embedded !== undefined ? options.embedded : true,
};
return baseLayer;
}
/**
* 创建固定图层 - 位于背景图层之上,普通图层之下
* @param {Object} options 固定图层选项
* @returns {Object} 固定图层对象
*/
export function createFixedLayer(options = {}) {
const id =
options.id ||
`fixed_layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
id: id,
// 图层基本属性
name: options.name || "固定图层",
type: LayerType.FIXED,
visible: true, // 固定图层始终可见
locked: true, // 固定图层默认锁定
opacity: options.opacity !== undefined ? options.opacity : 1.0,
blendMode: options.blendMode || BlendMode.NORMAL,
// 标记为固定图层
isFixed: true,
isBackground: false,
// Fabric.js 对象列表
fabricObjects: options.fabricObjects || [],
// 无子图层
children: [],
// 元数据 - 可用于存储任意数据
metadata: options.metadata || {},
};
}
/**
* 深拷贝图层对象
* @param {Object} layer 要拷贝的图层
* @returns {Object} 拷贝后的图层
*/
export function cloneLayer(layer) {
if (!layer) return null;
// 基本属性深拷贝
const clonedLayer = {
...JSON.parse(JSON.stringify(layer)), // 深拷贝基本属性
fabricObjects: [], // 重置,后面处理
};
// 复制 fabric 对象 (如果存在)
if (Array.isArray(layer.fabricObjects)) {
clonedLayer.fabricObjects = layer.fabricObjects.map((obj) => {
return obj && typeof obj.clone === "function"
? obj.clone()
: JSON.parse(JSON.stringify(obj));
});
}
// 复制背景对象 (如果存在)
if (layer.isBackground && layer.fabricObject) {
clonedLayer.fabricObject =
typeof layer.fabricObject.clone === "function"
? layer.fabricObject.clone()
: JSON.parse(JSON.stringify(layer.fabricObject));
}
// 递归复制子图层
if (Array.isArray(layer.children) && layer.children.length > 0) {
clonedLayer.children = layer.children.map((child) => cloneLayer(child));
}
return clonedLayer;
}