feat(CanvasManager): enhance image layer management and event handling

This commit is contained in:
bighuixiang
2025-06-26 00:37:07 +08:00
parent afa3b69f71
commit 2fcba962d1
16 changed files with 901 additions and 448 deletions

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089605497" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22868" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M185.396221 1024a49.219161 49.219161 0 0 1 0-98.462134h630.08622a49.219161 49.219161 0 0 1 0 98.462134z m273.146103-175.898375L141.772852 518.402301a49.147725 49.147725 0 1 1 70.84035-68.149608l232.689715 242.000161a0.142871 0.142871 0 0 0 0.142872-0.142871V50.332128a49.83827 49.83827 0 0 1 52.409953-50.243072 49.195349 49.195349 0 0 1 46.242675 49.100102v641.111122c0 0.142871 0 0.309554 0.142872 0.142871l232.713527-241.976349a49.147725 49.147725 0 1 1 70.84035 68.149608L529.382674 848.101625a48.981042 48.981042 0 0 1-70.84035 0z" fill="#040000" p-id="22869"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750089605497" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22868" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M185.396221 1024a49.219161 49.219161 0 0 1 0-98.462134h630.08622a49.219161 49.219161 0 0 1 0 98.462134z m273.146103-175.898375L141.772852 518.402301a49.147725 49.147725 0 1 1 70.84035-68.149608l232.689715 242.000161a0.142871 0.142871 0 0 0 0.142872-0.142871V50.332128a49.83827 49.83827 0 0 1 52.409953-50.243072 49.195349 49.195349 0 0 1 46.242675 49.100102v641.111122c0 0.142871 0 0.309554 0.142872 0.142871l232.713527-241.976349a49.147725 49.147725 0 1 1 70.84035 68.149608L529.382674 848.101625a48.981042 48.981042 0 0 1-70.84035 0z" p-id="22869"></path></svg>

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 897 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750841444538" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2373" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M413.416 284.528H456v34.832h-42.584zM299.744 284.528h63v34.832h-63zM186.992 319.36h62.088v-34.832h-70.168a25.168 25.168 0 0 0-25.168 25.176v46.328h33.248V319.36zM153.744 406.696h33.248v78.368h-33.248zM153.744 664.784h33.248v78.368h-33.248zM312.776 828.696h78.36v34.832h-78.36zM441.808 828.696h78.376v34.832h-78.376zM570.856 828.696h78.36v34.832h-78.36zM775 687.864h33.248v63h-33.248zM775 600.696h33.248v36.504h-33.248zM775 828.696h-75.112v34.832h83.184a25.144 25.144 0 0 0 25.168-25.168v-36.824h-33.248v27.16zM186.992 828.696v-34.872h-33.248v44.528a25.152 25.152 0 0 0 25.168 25.168h83.192v-34.832h-75.112zM153.744 535.744h33.248v78.368h-33.248zM376.08 515.528a44.24 44.24 0 1 0-0.008-88.48 44.24 44.24 0 0 0 0.008 88.48zM444.048 672.728L376.08 578.352 265.504 736.696h442.328l-72.536-136H495.128z" p-id="2374"></path><path d="M830.008 451.256c16.872-14.616 38.24-22.496 61.864-22.496a95.544 95.544 0 0 1 95.624 95.624c0 52.864-42.752 95.6-95.624 95.6a95.528 95.528 0 0 1-95.616-95.6c0-11.248 2.248-21.384 5.624-31.504l-75.376-66.376-172.12 127.12H520.64l175.496-154.12L520.64 245.392h33.752l172.12 127.12 75.376-66.368c-9.008-24.752-6.76-52.872 6.744-78.752 23.624-42.744 76.504-60.744 121.504-41.616 5.616 2.248 7.872 8.992 5.616 14.624-2.248 5.624-9 7.872-14.624 5.624-33.752-14.624-74.232-1.12-92.232 31.504-19.136 34.88-6.76 79.872 28.12 100.12 34.88 20.256 79.864 6.76 99-29.248 11.24-20.24 12.368-43.864 3.376-64.12-2.248-5.624 0-12.376 5.624-14.624s12.368 0 14.616 5.624c11.256 27 10.128 57.368-4.496 83.248-25.88 46.128-84.376 63-130.496 37.128-5.624-3.376-10.136-6.752-14.624-10.128l-68.632 50.624 68.624 55.104z m134.992 73.128a72.984 72.984 0 0 0-73.128-73.128c-40.504 0-73.128 32.624-73.128 73.128s32.624 73.12 73.128 73.12a72.984 72.984 0 0 0 73.128-73.12zM740.008 400.64c0-6.752-4.504-11.248-11.24-11.248-6.768 0-11.264 4.496-11.264 11.248s4.504 11.248 11.264 11.248c6.736 0 11.24-4.504 11.24-11.248z" p-id="2375"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,4 +1,4 @@
import { createRasterizedImage } from "../utils/SelectionToImage.js";
import { createRasterizedImage } from "../utils/selectionToImage.js";
import { CompositeCommand } from "./Command.js";
import { CreateImageLayerCommand } from "./LayerCommands.js";
import { ClearSelectionCommand } from "./SelectionCommands.js";
@@ -63,21 +63,27 @@ export class LassoCutoutCommand extends CompositeCommand {
const selectionBounds = selectionObject.getBoundingRect(true, true);
// 使用createRasterizedImage执行抠图操作
this.cutoutImageUrl = await this._performCutoutWithRasterized(
// this.cutoutImageUrl = await this._performCutoutWithRasterized(
// sourceObjects,
// selectionObject,
// selectionBounds
// );
// if (!this.cutoutImageUrl) {
// console.error("抠图失败");
// return false;
// }
this.fabricImage = await this._performCutoutWithRasterized(
sourceObjects,
selectionObject,
selectionBounds
);
if (!this.cutoutImageUrl) {
console.error("抠图失败");
return false;
}
// 创建fabric图像对象传递选区边界信息
this.fabricImage = await this._createFabricImage(
this.cutoutImageUrl,
selectionBounds
);
// // 创建fabric图像对象传递选区边界信息
// this.fabricImage = await this._createFabricImage(
// this.cutoutImageUrl,
// selectionBounds
// );
if (!this.fabricImage) {
console.error("创建图像对象失败");
return false;
@@ -224,7 +230,7 @@ export class LassoCutoutCommand extends CompositeCommand {
console.log(`源对象数量: ${sourceObjects.length}`);
console.log(`选区边界:`, selectionBounds);
// 确定缩放因子
// 确定缩放因子,确保高质量输出
let scaleFactor = this.baseResolutionScale;
if (this.highResolutionEnabled) {
const devicePixelRatio = window.devicePixelRatio || 1;
@@ -235,26 +241,27 @@ export class LassoCutoutCommand extends CompositeCommand {
const rasterizedDataURL = await createRasterizedImage({
canvas: this.canvas,
fabricObjects: sourceObjects,
clipPath: selectionObject, // 使用选区作为裁剪路径而不是遮罩
clipPath: selectionObject, // 使用选区作为裁剪路径
trimWhitespace: true,
trimPadding: 2,
quality: 1.0,
format: "png",
scaleFactor: scaleFactor,
isReturenDataURL: true, // 返回DataURL
// isReturenDataURL: true, // 返回DataURL
preserveOriginalQuality: true, // 启用高质量模式
});
if (!rasterizedDataURL) {
throw new Error("栅格化生成图像失败");
}
console.log(`抠图完成,缩放因子: ${scaleFactor}x`);
console.log(`✅ 高质量抠图完成,缩放因子: ${scaleFactor}x`);
return rasterizedDataURL;
} catch (error) {
console.error("使用createRasterizedImage执行抠图失败:", error);
// 如果createRasterizedImage失败回退到原始方法
console.log("回退到原始抠图方法...");
console.log("⚠️ 回退到原始抠图方法...");
return await this._performCutout(
{ fabricObjects: sourceObjects },
selectionObject

View File

@@ -71,11 +71,7 @@ export class BatchInitializeRedGreenModeCommand extends Command {
// 3. 保存原始状态
this.originalBackgroundObject = backgroundLayer.fabricObject
? {
...backgroundLayer.fabricObject.toObject([
"id",
"type",
"layerId",
]),
...backgroundLayer.fabricObject,
ref: backgroundLayer.fabricObject,
}
: null;

View File

@@ -385,7 +385,7 @@ function findParentLayerId() {
<SvgIcon
v-if="!isHidenDragHandle"
:name="isChild ? 'CSort' : 'CSort'"
:size="16"
:size="32"
></SvgIcon>
</div>

View File

@@ -166,7 +166,7 @@ const props = defineProps({
// 响应式数据
const visible = ref(false);
const debug = ref(true); // 设为true可以显示调试信息
const debug = ref(false); // 设为true可以显示调试信息
const currImage = ref("");
// 当前状态
@@ -229,11 +229,11 @@ const availableModes = ref([
{ id: "push", name: "推", iconText: "↔" },
{ id: "clockwise", name: "顺时针转动", iconText: "↻" },
{ id: "counterclockwise", name: "逆时针转动", iconText: "↺" },
{ id: "pinch", name: "捏合", iconText: "⤢" },
{ id: "expand", name: "展开", iconText: "⤡" },
{ id: "crystal", name: "水晶", iconText: "✧" },
{ id: "edge", name: "边缘", iconText: "◈" },
{ id: "reconstruct", name: "重建", iconText: "↩" },
// { id: "pinch", name: "捏合", iconText: "⤢" },
// { id: "expand", name: "展开", iconText: "⤡" },
// { id: "crystal", name: "水晶", iconText: "✧" },
// { id: "edge", name: "边缘", iconText: "◈" },
// { id: "reconstruct", name: "重建", iconText: "↩" },
]);
// 事件监听器引用
@@ -1535,6 +1535,7 @@ function stopPressTimer() {
z-index: 1000;
color: #333;
border: 1px solid rgba(0, 0, 0, 0.05);
padding-bottom: 12px;
}
.fade-enter-active,
@@ -1712,7 +1713,7 @@ function stopPressTimer() {
}
.mode-name {
font-size: 9px;
font-size: 14px;
color: #333;
text-align: center;
line-height: 1.1;
@@ -1774,8 +1775,9 @@ function stopPressTimer() {
}
.param-label {
font-size: 11px;
margin-bottom: 6px;
font-size: 12px;
margin-top: 12px;
margin-bottom: 12px;
color: #666;
font-weight: 500;
}

View File

@@ -47,9 +47,17 @@
<!-- 底部选区操作工具栏 -->
<div class="tool-actions">
<div class="action-btn" @click="copySelectionToNewLayer">
<svg-icon name="CPaste" />
<svg-icon name="CPaste" size="20" />
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
</div>
<div class="action-btn" @click="cutSelectionToNewLayer">
<svg-icon name="CCut" size="30" />
<span class="btn-text">{{ $t("剪切并粘贴") }}</span>
</div>
<div class="action-btn" @click="clearSelectionContent">
<svg-icon name="CClear" size="22" />
<span class="btn-text">{{ $t("清除选择内容") }}</span>
</div>
<!-- <button
class="action-btn"
@click="addSelection"
@@ -399,6 +407,22 @@ function copySelectionToNewLayer() {
checkSelectionStatus();
}
/**
* 剪切选区到新图层
*/
function cutSelectionToNewLayer() {
if (!hasSelection.value) return;
props.commandManager.execute(
new CopySelectionToNewLayerCommand({
canvas: props.canvas,
layerManager: props.layerManager,
selectionManager: props.selectionManager,
toolManager: props.toolManager,
})
);
checkSelectionStatus();
}
/**
* 清除选区内容
*/
@@ -479,6 +503,7 @@ function confirmColorPicker() {
z-index: 1000;
color: #333;
border: 1px solid rgba(0, 0, 0, 0.05);
user-select: none;
}
/* 平板和手机适配 */
@@ -597,7 +622,7 @@ function confirmColorPicker() {
.tool-btn span {
margin-top: 6px;
font-size: 11px;
font-size: 14px;
}
.tool-btn svg {
@@ -669,7 +694,7 @@ function confirmColorPicker() {
}
.btn-text {
font-size: 10px;
font-size: 14px;
text-align: center;
}

View File

@@ -35,13 +35,19 @@ import { fabric } from "fabric-with-all";
import {
uploadImageAndCreateLayer,
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
import { next } from "lodash-es";
// import MinimapPanel from "./components/MinimapPanel.vue";
const KeyboardShortcutHelp = defineAsyncComponent(() =>
import("./components/KeyboardShortcutHelp.vue")
);
const emit = defineEmits(["trigger-red-green-mouseup", "changeCanvas"]);
const emit = defineEmits([
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
"changeCanvas", // 画布变更事件
"canvasInit", // 画布初始化事件
]);
const props = defineProps({
canvasJSON: {
@@ -151,6 +157,24 @@ function handleToolSelect(tool) {
}); // 命令模式 可撤回操作
}
// 触发组件初始化事件
function handleCanvasInit(isLoadJson = false) {
emit("canvasInit", {
isLoadJson: isLoadJson ?? !!props.canvasJSON, // 是否加载了JSON数据
isRedGreenMode: isRedGreenMode.value,
layers,
activeLayerId,
canvasManager,
layerManager,
commandManager,
toolManager,
keyboardManager,
liquifyManager,
selectionManager,
redGreenModeManager,
});
}
function toggleMinimap(enabled) {
// minimapEnabled.value = enabled;
// if (minimapManager.value) {
@@ -177,6 +201,7 @@ onMounted(async () => {
canvasHeight,
canvasColor,
enabledRedGreenMode: props.enabledRedGreenMode,
isFixedErasable: props.isFixedErasable,
});
canvasManager.canvas.activeLayerId = activeLayerId;
canvasManager.canvas.activeElementId = activeElementId;
@@ -205,24 +230,6 @@ onMounted(async () => {
// 设置缩略图管理器需要访问的图层数据
// canvasManager.layers = layers;
if (props.canvasJSON) {
// 如果传入了初始JSON数据加载到画布上
if (typeof props.canvasJSON === "string") {
try {
canvasManager.loadJSON(props.canvasJSON);
} catch (error) {
console.error("加载画布JSON失败:", error);
// 初始化图层 - 确保创建背景层
layerManager.initializeLayers();
}
} else if (typeof props.canvasJSON === "object") {
canvasManager.loadJSON(JSON.stringify(props.canvasJSON));
}
} else {
// 初始化图层 - 确保创建背景层
layerManager.initializeLayers();
}
// 创建工具管理器实例
toolManager = new ToolManager({
canvas: canvasManager.canvas, // fabric.js 画布实例
@@ -253,6 +260,9 @@ onMounted(async () => {
// 绑定快捷键事件
keyboardManager.init();
// 绑定画布操作事件
canvasManager.setupCanvasEvents(activeElementId, layerManager);
canvasManager.setupCanvasInitEvent(handleCanvasInit); // 绑定画布初始化事件
provide("canvasManager", canvasManager); // 提供给子组件使用
provide("layerManager", layerManager); // 提供给子组件使用
@@ -262,27 +272,12 @@ onMounted(async () => {
provide("activeTool", activeTool); // 提供给子组件使用
provide("liquifyManager", () => liquifyManager); // 提供液化管理器
provide("layers", layers); // 提供图层数据
// 绑定画布操作事件
canvasManager.setupCanvasEvents(activeElementId, layerManager);
canvasManagerLoaded.value = true;
// 初始化网格设置
// toggleGridVisibility(gridEnabled.value);
// 初始化小地图
// minimapManager.value = new MinimapManager(canvasManager.canvas);
setTimeout(() => {
// 初始状态下生成所有预览图
canvasManager.updateAllThumbnails();
}, 300);
// 使用window的resize事件代替ResizeObserver
// 只有当窗口大小变化时才更新画布尺寸
window.addEventListener("resize", handleWindowResize);
// 初始化液化管理器
liquifyManager = new LiquifyManager({
canvas: canvasManager.canvas,
@@ -298,6 +293,24 @@ onMounted(async () => {
});
canvasManager.setSelectionManager(selectionManager);
if (props.canvasJSON) {
// 如果传入了初始JSON数据加载到画布上
if (typeof props.canvasJSON === "string") {
try {
await canvasManager.loadJSON(props.canvasJSON);
} catch (error) {
console.error("加载画布JSON失败:", error);
// 初始化图层 - 确保创建背景层
await layerManager.initializeLayers();
}
} else if (typeof props.canvasJSON === "object") {
await canvasManager.loadJSON(JSON.stringify(props.canvasJSON));
}
} else {
// 初始化图层 - 确保创建背景层
await layerManager.initializeLayers();
}
if (
props.enabledRedGreenMode &&
props.clothingImageUrl &&
@@ -340,18 +353,14 @@ onMounted(async () => {
// 初始设置
handleWindowResize(); // 设置画布大小
} else if (!isRedGreenMode.value && props.clothingImageUrl) {
nextTick(() => {
setTimeout(() => {
try {
canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
undoable: false, // 不可撤销操作
...(props?.clothingImageOpts || {}),
});
} catch (error) {
console.error("更换底图失败:", error);
}
}, 92); // 延迟 确保更新底图完成
});
try {
await canvasManager?.changeFixedImage?.(props.clothingImageUrl, {
undoable: false, // 不可撤销操作
...(props?.clothingImageOpts || {}),
});
} catch (error) {
console.error("更换底图失败:", error);
}
canvasManager?.centerBackgroundLayer?.(
canvasManager.canvas.width,
@@ -369,6 +378,24 @@ onMounted(async () => {
// type: "isBackground",
// flag: !props.isBackgroundErasable, // 设置操作类型为可擦除
// });
canvasManagerLoaded.value = true;
// 触发组件初始化事件
nextTick(() => {
// 确保所有依赖都已加载完成
handleCanvasInit();
requestAnimationFrame(() => {
setTimeout(() => {
// 初始状态下生成所有预览图
canvasManager.updateAllThumbnails();
}, 300);
});
});
// 使用window的resize事件代替ResizeObserver
// 只有当窗口大小变化时才更新画布尺寸
window.addEventListener("resize", handleWindowResize);
});
watchEffect(() => {
@@ -707,18 +734,38 @@ defineExpose({
},
// (更换底图,不可撤销,不可操作)
changeFixedImage: (url, opts) => {
return canvasManager?.changeFixedImage?.(url, opts);
return canvasManager?.changeFixedImage?.(url, {
...(props?.clothingImageOpts || {}),
...opts,
});
},
//图片url或者base64
addImageToLayer: async (url) => {
addImageToLayer: async (
url,
{ layerId, undoable } = { layerId: null, undoable: true } // 可选参数 layerId 指定图层 将内容添加到指定图层 undoable 是否可撤销 false不可撤销 默认可撤销
) => {
if (!url) return Promise.reject(new Error("图片URL不能为空"));
return await loadImageUrlToLayer({
imageUrl: url,
layerManager,
canvas: canvasManager.canvas,
toolManager,
});
if (layerId) {
const fabricImage = await loadImage(url);
// 如果指定了图层ID确保图层存在
return await canvasManager?.addImageToLayer?.(url, {
targetLayerId: layerId,
fabricImage,
undoable, // 是否可撤销操作
});
}
// 未指定图层ID默认添加到新的图层
return await loadImageUrlToLayer(
{
imageUrl: url,
layerManager,
canvas: canvasManager.canvas,
toolManager,
},
{ undoable }
);
},
// 导出图片
exportImage: ({

View File

@@ -3,6 +3,7 @@ import { fabric } from "fabric-with-all";
/**
* 笔刷指示器
* 在画笔模式下显示当前笔刷大小的圆圈指示器
* 使用独立的 fabric.StaticCanvas完全不干扰主画布的绘制流程
*/
export class BrushIndicator {
/**
@@ -19,18 +20,129 @@ export class BrushIndicator {
...options,
};
// 指示器圆圈对象
this.indicator = null;
// 创建独立的静态画布
this.indicatorCanvas = null;
this.staticCanvas = null;
this.indicatorCircle = null;
this._createStaticCanvas();
// 事件处理器
this._mouseEnterHandler = null;
this._mouseLeaveHandler = null;
this._mouseMoveHandler = null;
this._zoomHandler = null;
this._viewportHandler = null;
// 当前状态
this.isVisible = false;
this.currentSize = 10;
this.isEnabled = false;
this.currentPosition = { x: 0, y: 0 };
}
/**
* 创建独立的 fabric.StaticCanvas 画布层
* @private
*/
_createStaticCanvas() {
if (!this.canvas.wrapperEl) return;
// 创建独立的 canvas 元素
this.indicatorCanvas = document.createElement("canvas");
this.indicatorCanvas.className = "brush-indicator-canvas";
// 设置样式,使其覆盖在主画布上
Object.assign(this.indicatorCanvas.style, {
position: "absolute",
top: "0",
left: "0",
pointerEvents: "none", // 不阻挡鼠标事件
zIndex: "10", // 确保在最上层
width: "100%",
height: "100%",
});
// 将指示器画布添加到 fabric.js 画布容器中
this.canvas.wrapperEl.appendChild(this.indicatorCanvas);
// 创建 fabric.StaticCanvas 实例
this.staticCanvas = new fabric.StaticCanvas(this.indicatorCanvas, {
enableRetinaScaling: this.canvas.enableRetinaScaling,
allowTouchScrolling: false,
selection: false,
skipTargetFind: true,
preserveObjectStacking: true,
});
// 同步画布尺寸和变换
this._syncCanvasProperties();
// 监听主画布变化
this._observeCanvasChanges();
}
/**
* 同步画布属性(尺寸、缩放、视口变换等)
* @private
*/
_syncCanvasProperties() {
if (!this.staticCanvas || !this.canvas) return;
// 同步画布尺寸
this.staticCanvas.setWidth(this.canvas.width);
this.staticCanvas.setHeight(this.canvas.height);
// 同步缩放
this.staticCanvas.setZoom(this.canvas.getZoom());
// 同步视口变换
const vpt = this.canvas.viewportTransform;
if (vpt) {
this.staticCanvas.setViewportTransform([...vpt]);
}
// 同步背景色(可选,通常保持透明)
// this.staticCanvas.backgroundColor = 'transparent';
// 重新渲染
this.staticCanvas.renderAll();
}
/**
* 监听主画布变化
* @private
*/
_observeCanvasChanges() {
if (!this.canvas) return;
// 监听缩放变化
this._zoomHandler = () => {
this._syncCanvasProperties();
};
// 监听视口变化
this._viewportHandler = () => {
this._syncCanvasProperties();
};
// 监听画布尺寸变化
this._resizeHandler = () => {
this._syncCanvasProperties();
};
// 绑定事件
this.canvas.on("after:render", this._zoomHandler);
this.canvas.on("canvas:zoomed", this._zoomHandler);
this.canvas.on("viewport:changed", this._viewportHandler);
this.canvas.on("canvas:resized", this._resizeHandler);
// 使用 ResizeObserver 监听 DOM 尺寸变化
if (window.ResizeObserver && this.canvas.upperCanvasEl) {
this.resizeObserver = new ResizeObserver(() => {
this._syncCanvasProperties();
});
this.resizeObserver.observe(this.canvas.upperCanvasEl);
}
}
/**
@@ -74,8 +186,11 @@ export class BrushIndicator {
this.currentSize = size;
// 如果指示器正在显示,更新其大小
if (this.isVisible && this.indicator) {
this._updateIndicatorSize();
if (this.isVisible && this.indicatorCircle) {
this.indicatorCircle.set({
radius: size / 2,
});
this.staticCanvas.renderAll();
}
}
@@ -87,19 +202,15 @@ export class BrushIndicator {
// 更新配置选项中的颜色
if (color) {
this.options.strokeColor = color;
// 将颜色转换为半透明填充色
// this.options.fillColor = this._convertToTransparentFill(color);
}
// 如果指示器正在显示,更新其颜色
if (this.isVisible && this.indicator) {
this.indicator.set({
if (this.isVisible && this.indicatorCircle) {
this.indicatorCircle.set({
stroke: this.options.strokeColor,
fill: this.options.fillColor,
});
// 重新渲染画布
this.canvas.requestRenderAll();
this.staticCanvas.renderAll();
}
}
@@ -113,23 +224,26 @@ export class BrushIndicator {
this.isVisible = true;
// 创建指示器圆圈
this._createIndicator(pointer);
this._createIndicatorCircle();
// 更新位置
this.updatePosition(pointer);
}
/**
* 隐藏指示器
*/
hide() {
if (!this.isVisible || !this.indicator) return;
if (!this.isVisible) return;
this.isVisible = false;
// 从画布移除指示器
this.canvas.remove(this.indicator);
this.indicator = null;
// 重新渲染画布
this.canvas.renderAll();
// 移除指示器圆圈
if (this.indicatorCircle && this.staticCanvas) {
this.staticCanvas.remove(this.indicatorCircle);
this.indicatorCircle = null;
this.staticCanvas.renderAll();
}
}
/**
@@ -137,22 +251,53 @@ export class BrushIndicator {
* @param {Object} pointer 鼠标位置
*/
updatePosition(pointer) {
if (!this.isVisible || !this.indicator) return;
if (!this.isVisible || !this.indicatorCircle) return;
// 转换坐标考虑画布变
// 使用主画布的坐标转
const canvasPointer = this.canvas.getPointer(pointer);
// 批量更新位置属性
this.indicator.set({
// 更新当前位置
this.currentPosition = {
x: canvasPointer.x,
y: canvasPointer.y,
};
// 更新指示器位置
this.indicatorCircle.set({
left: canvasPointer.x,
top: canvasPointer.y,
});
// 更新坐标系
this.indicator.setCoords();
// 重新渲染静态画布
// this.staticCanvas.renderAll();
// 优化渲染
this.staticCanvas.requestRenderAll();
}
// 使用requestRenderAll优化渲染性能
this.canvas.requestRenderAll();
/**
* 创建指示器圆圈对象
* @private
*/
_createIndicatorCircle() {
if (this.indicatorCircle || !this.staticCanvas) return;
// 创建圆圈对象
this.indicatorCircle = new fabric.Circle({
radius: this.currentSize / 2,
fill: this.options.fillColor,
stroke: this.options.strokeColor,
strokeWidth: this.options.strokeWidth,
originX: "center",
originY: "center",
selectable: false,
evented: false,
excludeFromExport: true,
isTemp: true,
pointer: true,
});
// 添加到静态画布
this.staticCanvas.add(this.indicatorCircle);
}
/**
@@ -164,7 +309,6 @@ export class BrushIndicator {
// 鼠标进入画布
this._mouseEnterHandler = (e) => {
// 只在画笔相关模式下显示
if (this._shouldShowIndicator()) {
this.show(e.e);
}
@@ -175,75 +319,29 @@ export class BrushIndicator {
this.hide();
};
// 鼠标在画布上移动 - 修改事件处理逻辑
// 鼠标移动
this._mouseMoveHandler = (e) => {
// 如果正在绘图,不处理指示器更新,避免干扰绘图
if (this.canvas._isCurrentlyDrawing) {
return;
}
if (this._shouldShowIndicator()) {
if (!this.isVisible) {
this.show(e.e);
} else {
this.updatePosition(e.e);
// requestIdleCallback(() => {
// this.updatePosition(e.e);
// });
requestAnimationFrame(() => {
this.updatePosition(e.e);
});
}
} else {
this.hide();
}
};
// 监听绘图开始事件,隐藏指示器并模拟微小移动
this._drawingStartHandler = (e) => {
if (this.isVisible) {
this.hide();
}
// 模拟1px的微小移动来确保笔刷能正常启动绘画
this._simulateTinyMovement(e);
};
// 监听绘图结束事件,重新显示指示器
this._drawingEndHandler = () => {
// 延迟一点重新显示,确保绘图完全结束
setTimeout(() => {
if (this._shouldShowIndicator() && !this.isVisible) {
// 重新检查鼠标位置并显示指示器
this._checkAndShowIndicator();
}
}, 50);
};
// 画布缩放变化处理器
this._zoomHandler = () => {
if (this.isVisible && this.indicator) {
// 立即更新指示器大小
this._updateIndicatorSize();
}
};
// 画布视口变化处理器
this._viewportHandler = () => {
if (this.isVisible && this.indicator) {
// 视口变化时也需要更新指示器
this._updateIndicatorSize();
}
};
// 绑定事件
this.canvas.on("mouse:over", this._mouseEnterHandler);
this.canvas.on("mouse:out", this._mouseLeaveHandler);
this.canvas.on("mouse:move", this._mouseMoveHandler);
// 监听绘图状态变化
this.canvas.on("path:created", this._drawingEndHandler);
this.canvas.on("mouse:down", this._drawingStartHandler);
this.canvas.on("mouse:up", this._drawingEndHandler);
// 监听画布缩放和视口变化
this.canvas.on("after:render", this._zoomHandler);
this.canvas.on("canvas:zoomed", this._zoomHandler);
this.canvas.on("viewport:changed", this._viewportHandler);
}
/**
@@ -253,6 +351,7 @@ export class BrushIndicator {
_unbindEvents() {
if (!this.canvas) return;
// 解绑鼠标事件
if (this._mouseEnterHandler) {
this.canvas.off("mouse:over", this._mouseEnterHandler);
this._mouseEnterHandler = null;
@@ -268,16 +367,7 @@ export class BrushIndicator {
this._mouseMoveHandler = null;
}
// 清理绘图相关事件
if (this._drawingStartHandler) {
this.canvas.off("mouse:down", this._drawingStartHandler);
this.canvas.off("path:created", this._drawingEndHandler);
this.canvas.off("mouse:up", this._drawingEndHandler);
this._drawingStartHandler = null;
this._drawingEndHandler = null;
}
// 清理缩放相关事件
// 解绑画布变化事件
if (this._zoomHandler) {
this.canvas.off("after:render", this._zoomHandler);
this.canvas.off("canvas:zoomed", this._zoomHandler);
@@ -288,81 +378,11 @@ export class BrushIndicator {
this.canvas.off("viewport:changed", this._viewportHandler);
this._viewportHandler = null;
}
}
/**
* 创建指示器圆圈
* @private
* @param {Object} pointer 鼠标位置
*/
_createIndicator(pointer) {
// 转换坐标考虑画布变换
const canvasPointer = this.canvas.getPointer(pointer);
// 计算考虑画布缩放的半径
const radius = this._getIndicatorRadius();
// 创建圆圈
this.indicator = new fabric.Circle({
left: canvasPointer.x,
top: canvasPointer.y,
radius: radius,
fill: this.options.fillColor,
stroke: this.options.strokeColor,
strokeWidth: this.options.strokeWidth / this.canvas.getZoom(), // 线宽不受缩放影响
originX: "center",
originY: "center",
selectable: false,
evented: false,
excludeFromExport: true, // 导出时排除
isTemp: true, // 标记为临时对象
pointer: true, // 标记为鼠标指示器
});
// 添加到画布
this.canvas.add(this.indicator);
// 确保指示器在最顶层
this.canvas.bringToFront(this.indicator);
// 重新渲染画布
this.canvas.renderAll();
}
/**
* 更新指示器大小
* @private
*/
_updateIndicatorSize() {
if (!this.indicator) return;
// 获取当前画布缩放比例
const zoom = this.canvas.getZoom();
const radius = this._getIndicatorRadius();
const strokeWidth = this.options.strokeWidth / zoom;
// 批量更新属性,减少重绘次数
this.indicator.set({
radius: radius,
strokeWidth: strokeWidth,
});
// 标记指示器需要重绘
this.indicator.setCoords();
// 重新渲染画布
this.canvas.requestRenderAll();
}
/**
* 获取指示器半径(与笔刷实际绘制大小一致)
* @private
* @returns {Number} 半径值
*/
_getIndicatorRadius() {
// 指示器大小应该与笔刷实际绘制大小一致
// 笔刷的实际绘制大小不受画布缩放影响,所以指示器也不应该受缩放影响
return this.currentSize / 2;
if (this._resizeHandler) {
this.canvas.off("canvas:resized", this._resizeHandler);
this._resizeHandler = null;
}
}
/**
@@ -380,104 +400,35 @@ export class BrushIndicator {
return true;
}
/**
* 检查并显示指示器(如果鼠标在画布内)
* @private
*/
_checkAndShowIndicator() {
// 这个方法用于在绘图结束后重新显示指示器
// 由于无法直接获取当前鼠标位置,我们依赖下一次鼠标移动事件
}
/**
* 模拟微小移动
* @private
* @param {Object} e 鼠标事件对象
*/
_simulateTinyMovement(e) {
// 获取当前鼠标位置
const pointer = this.canvas.getPointer(e.e);
// 模拟移动1px
this.canvas.fire("mouse:move", {
...e,
e: {
...e.e,
clientX: pointer.x + 1,
clientY: pointer.y + 1,
pointer: {
x: pointer.x + 1,
y: pointer.y + 1,
},
},
});
// 再模拟移动回原位置
this.canvas.fire("mouse:move", {
...e,
e: {
...e.e,
clientX: pointer.x,
clientY: pointer.y,
pointer: {
x: pointer.x,
y: pointer.y,
},
},
});
}
/**
* 销毁指示器
*/
dispose() {
this.disable();
// 解绑画布变化事件
this._unbindEvents();
// 停止监听尺寸变化
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
// 销毁静态画布
if (this.staticCanvas) {
this.staticCanvas.dispose();
this.staticCanvas = null;
}
// 移除指示器画布
if (this.indicatorCanvas && this.indicatorCanvas.parentNode) {
this.indicatorCanvas.parentNode.removeChild(this.indicatorCanvas);
}
this.canvas = null;
this.indicator = null;
this.indicatorCanvas = null;
this.indicatorCircle = null;
this.options = null;
}
/**
* 将颜色转换为半透明填充色
* @private
* @param {String} color 原始颜色值
* @returns {String} 半透明填充色
*/
_convertToTransparentFill(color) {
// 如果已经是 rgba 格式,直接使用但降低透明度
if (color.startsWith("rgba")) {
return color.replace(/[\d\.]+\)$/, "0.1)");
}
// 如果是 rgb 格式,转换为 rgba
if (color.startsWith("rgb")) {
return color.replace("rgb", "rgba").replace(")", ", 0.1)");
}
// 如果是十六进制格式,转换为 rgba
if (color.startsWith("#")) {
const hex = color.slice(1);
let r, g, b;
if (hex.length === 3) {
// 处理 #RGB 格式
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
} else if (hex.length === 6) {
// 处理 #RRGGBB 格式
r = parseInt(hex.slice(0, 2), 16);
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
} else {
// 无法解析的格式,返回默认半透明黑色
return "rgba(0, 0, 0, 0.1)";
}
return `rgba(${r}, ${g}, ${b}, 0.1)`;
}
// 处理命名颜色或其他格式,使用默认半透明效果
return "rgba(0, 0, 0, 0.1)";
}
}

View File

@@ -5,6 +5,7 @@ import initAligningGuidelines, {
import { ThumbnailManager } from "./ThumbnailManager";
import { ExportManager } from "./ExportManager";
import {
findLayerRecursively,
isGroupLayer,
OperationType,
OperationTypes,
@@ -15,7 +16,11 @@ import { CanvasEventManager } from "./events/CanvasEventManager";
import CanvasConfig from "../config/canvasConfig";
import { RedGreenModeManager } from "./RedGreenModeManager";
import { EraserStateManager } from "./EraserStateManager";
import { deepClone, optimizeCanvasRendering } from "../utils/helper";
import {
deepClone,
generateId,
optimizeCanvasRendering,
} from "../utils/helper";
import { ChangeFixedImageCommand } from "../commands/ObjectLayerCommands";
import { isFunction } from "lodash-es";
import {
@@ -39,7 +44,9 @@ export class CanvasManager {
this.canvasHeight = options.canvasHeight || this.height; // 画布高度
this.canvasColor = options.canvasColor || "#ffffff"; // 画布背景颜色
this.enabledRedGreenMode = options.enabledRedGreenMode || false; // 是否启用红绿图模式
this.isFixedErasable = options.isFixedErasable || false; // 是否允许擦除固定图层
this.eraserStateManager = null; // 橡皮擦状态管理器引用
this.handleCanvasInit = null; // 画布初始化回调函数
// 初始化画布
this.initializeCanvas();
}
@@ -73,16 +80,50 @@ export class CanvasManager {
this.canvas.thumbnailManager = this.thumbnailManager; // 将缩略图管理器绑定到画布
// 设置画布辅助线
initAligningGuidelines(this.canvas);
// // 设置画布辅助线
// initAligningGuidelines(this.canvas);
// 设置画布中心线
initCenteringGuidelines(this.canvas);
// // 设置画布中心线
// initCenteringGuidelines(this.canvas);
// 初始化画布事件监听器
this._initCanvasEvents();
}
// 添加图像到指定图层
async addImageToLayer({ targetLayerId, fabricImage, ...options }) {
// 如果图层管理器存在,将图像合并到当前活动图层
if (this.layerManager) {
// 获取当前活动图层
let activeLayer = targetLayerId
? findLayerRecursively(this.layers.value, targetLayerId)?.layer
: this.layerManager.getActiveLayer();
if (activeLayer) {
// 确保新图像具有正确的图层信息
fabricImage.set({
layerId: activeLayer.id,
layerName: activeLayer.name,
id: fabricImage.id || generateId("brush_img_"),
});
// 执行高保真合并操作
await this.eventManager?.mergeLayerObjectsForPerformance?.({
fabricImage,
activeLayer,
options,
});
this.thumbnailManager?.generateLayerThumbnail(activeLayer.id);
// 返回true表示不要自动添加到画布因为我们已经通过图层管理器处理了
return true;
} else {
console.warn("没有活动图层,无法添加图像");
}
}
}
/**
* 初始化画布事件监听器
* 设置鼠标事件、键盘事件等
@@ -91,36 +132,7 @@ export class CanvasManager {
_initCanvasEvents() {
// 添加笔刷图像转换处理回调
this.canvas.onBrushImageConverted = async (fabricImage) => {
// 如果图层管理器存在,将图像合并到当前活动图层
if (this.layerManager) {
// 获取当前活动图层
const activeLayer = this.layerManager.getActiveLayer();
if (activeLayer) {
// 确保新图像具有正确的图层信息
fabricImage.set({
layerId: activeLayer.id,
layerName: activeLayer.name,
id:
fabricImage.id ||
`brush_img_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
});
// 执行高保真合并操作
await this.eventManager?.mergeLayerObjectsForPerformance?.({
fabricImage,
activeLayer,
});
this.thumbnailManager?.generateLayerThumbnail(activeLayer.id);
// 返回true表示不要自动添加到画布因为我们已经通过图层管理器处理了
return true;
} else {
console.warn("没有活动图层,使用默认行为添加图像");
}
}
await this.addImageToLayer({ fabricImage, targetLayerId: null });
// 返回false表示使用默认行为直接添加到画布
return false;
};
@@ -155,6 +167,15 @@ export class CanvasManager {
this.thumbnailManager?.generateLayerThumbnail(
this.layerManager?.activeLayerId?.value
);
// 固定图层 的擦除也需要重新生成缩略图 要判断 当前固定图层是否锁定
const fixedLayer = this.layers?.value?.find(
(layer) => layer.isFixed && !layer.locked
);
// 如果有固定图层且未锁定,则生成缩略图
fixedLayer &&
this.isFixedErasable &&
this.thumbnailManager?.generateLayerThumbnail(fixedLayer?.id);
});
}
@@ -263,6 +284,10 @@ export class CanvasManager {
this.animationManager.setupInteractionAnimations();
}
setupCanvasInitEvent(handleCanvasInit) {
this.handleCanvasInit = handleCanvasInit;
}
setupLongPress(callback) {
if (this.eventManager) {
this.eventManager.setupLongPress(callback);
@@ -697,13 +722,26 @@ export class CanvasManager {
command.undoable =
options.undoable !== undefined ? options.undoable : false; // 默认不可撤销 undoable = true 为可撤销
return (
(await command?.execute?.()) || {
success: false,
layerId: null,
imageUrl: null,
const result = (await command?.execute?.()) || {
success: false,
layerId: null,
imageUrl: null,
};
const findLayer = this.layers.value.find((fItem) => {
if (options.targetLayerType === "fixed") {
return fItem.isFixed;
}
);
if (options.targetLayerType === "background") {
return fItem.isBackground;
}
return false;
});
// 如果找到了图层,则生成缩略图
findLayer && this.thumbnailManager?.generateLayerThumbnail(findLayer.id);
return result;
}
/**
@@ -934,7 +972,7 @@ export class CanvasManager {
}
// 清除当前画布内容
this.canvas.clear();
// this.canvas.clear(); // 清除画布内容 可以先去掉 这样加载闪动的情况就比较少 如果有问题 可以再打开
console.log("清除当前画布内容", canvasData);
delete canvasData.clipPath; // 删除当前裁剪路径
// 加载画布数据
@@ -1002,6 +1040,8 @@ export class CanvasManager {
}, 500);
console.log("画布JSON数据加载完成");
// 画布初始化事件
this.handleCanvasInit?.(ture);
resolve();
} catch (error) {
console.error("恢复图层数据失败:", error);

View File

@@ -569,7 +569,7 @@ export class LayerManager {
* @param {string} layerId 目标图层ID如果不提供则使用当前活动图层
* @returns {Object} 添加的对象
*/
addObjectToLayer(fabricObject, layerId = null) {
addObjectToLayer(fabricObject, layerId = null, options = {}) {
const targetLayerId = layerId || this.activeLayerId.value;
// 如果没有指定图层ID也没有活动图层则返回错误
@@ -597,6 +597,9 @@ export class LayerManager {
layerManager: this,
});
// 设置命令的撤销状态
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
// 执行命令
if (this.commandManager) {
this.commandManager.execute(command);

View File

@@ -1,3 +1,4 @@
import { isBoolean } from "lodash-es";
import { TransformCommand } from "../../commands/StateCommands";
import { generateId } from "../../utils/helper";
import { OperationType, OperationTypes } from "../../utils/layerHelper";
@@ -577,7 +578,7 @@ export class CanvasEventManager {
* @param {Object} options.activeLayer 当前活动图层
* @private
*/
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer }) {
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer, options }) {
// 确保有命令管理器
if (!this.layerManager || !this.layerManager.commandManager) {
console.warn("合并对象失败:没有命令管理器");
@@ -602,7 +603,7 @@ export class CanvasEventManager {
// 如果只有一个新图像且图层为空,直接添加到图层
if (hasNewImage && !hasExistingObjects) {
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id, options);
return;
}
@@ -615,6 +616,9 @@ export class CanvasEventManager {
fabricImage
);
// 设置命令的撤销状态
if (isBoolean(options.undoable)) command.undoable = options.undoable; // 是否撤销
this.layerManager?.commandManager?.execute?.(command, {
name: `合并图层 ${activeLayer.name} 中的对象为组`,
});

View File

@@ -627,10 +627,10 @@ export class LiquifyCPUManager {
// 使用增强的像素算法
switch (mode) {
case this.modes.CLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
break;
case this.modes.COUNTERCLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
break;
case this.modes.PINCH:
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);
@@ -1300,10 +1300,10 @@ export class LiquifyCPUManager {
// 使用增强的像素算法
switch (mode) {
case this.modes.CLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
break;
case this.modes.COUNTERCLOCKWISE:
this._applyEnhancedRotationDeformation(x, y, radius, strength, false);
this._applyEnhancedRotationDeformation(x, y, radius, strength, true);
break;
case this.modes.PINCH:
this._applyEnhancedPinchDeformation(x, y, radius, strength, true);

View File

@@ -81,6 +81,7 @@ export async function createImageLayer({
fabricImage,
toolManager,
layerName = null,
undoable,
} = {}) {
if (!layerManager || !fabricImage) {
console.error("图层管理器或图片对象无效");
@@ -96,6 +97,9 @@ export async function createImageLayer({
layerName,
});
// 设置命令的撤销状态
if (isBoolean(undoable)) createImageLayerCmd.undoable = undoable; // 是否撤销
// 执行复合命令
const newLayerId = await layerManager.commandManager.execute(
createImageLayerCmd
@@ -249,6 +253,7 @@ export function loadImageUrlToLayer(
layerManager,
fabricImage,
toolManager,
...options,
});
resolve(layerId);

View File

@@ -1,9 +1,10 @@
// 栅格化帮助
import { fabric } from "fabric-with-all";
/**
* 创建栅格化图像
* 使用增强版栅格化方法,不受原始画布变换影响
* @returns {Promise<fabric.Image>} 栅格化后的图像对象
* 创建栅格化图像 - 重构版本
* 采用复制原对象+裁剪路径的方式,保持原始质量和准确位置
* @returns {Promise<fabric.Image|fabric.Group|string>} 栅格化后的图像对象或DataURL
* @private
*/
export const createRasterizedImage = async ({
@@ -12,11 +13,12 @@ export const createRasterizedImage = async ({
maskObject = null, // 用于裁剪的对象 - 可选
clipPath = null, // 裁剪路径对象 - 可选优先级高于maskObject
trimWhitespace = true, // 是否裁剪空白区域
trimPadding = 1, // 裁剪边距
trimPadding = 0, // 裁剪边距
quality = 1.0, // 图像质量
format = "png", // 图像格式
scaleFactor = 1, // 高清倍数 - 默认是画布的高清倍数
isReturenDataURL = false, // 是否返回DataURL而不是fabric.Image对象
preserveOriginalQuality = true, // 是否保持原始质量(新增)
} = {}) => {
try {
console.log(`📊 开始栅格化 ${fabricObjects.length} 个对象`);
@@ -29,79 +31,448 @@ export const createRasterizedImage = async ({
// 处理裁剪对象优先使用clipPath
const clippingObject = clipPath || maskObject;
// 如果保持原始质量且有裁剪对象,使用新的裁剪方法
if (preserveOriginalQuality && clippingObject) {
return await createClippedObjects({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
});
}
// 高清倍数
const currentZoom = canvas.getZoom?.() || 1;
// 如果只是简单复制而不需要裁剪,直接克隆对象
if (!clippingObject) {
return await createSimpleClone({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
});
}
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.min(scaleFactor, 3); // 最大不能大于3
console.log(`高清倍数: ${scaleFactor}, 当前缩放: ${currentZoom}`);
// 计算绝对边界框(原始尺寸)和相对边界框(当前缩放后的尺寸)
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
console.log("📏 绝对边界框:", absoluteBounds);
console.log("📏 相对边界框:", relativeBounds);
// 使用绝对边界框创建高质量的离屏渲染
const rasterizedImage = await createOffscreenRasterization({
// 兼容原有的离屏渲染方法(作为备选方案)
return await createLegacyRasterization({
canvas,
objects: fabricObjects,
absoluteBounds,
relativeBounds,
scaleFactor,
fabricObjects,
clippingObject,
trimWhitespace,
trimPadding,
scaleFactor,
quality,
format,
currentZoom,
isReturenDataURL,
});
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
}
};
if (!rasterizedImage) {
console.warn("⚠️ 栅格化图像创建失败,返回空图像");
return null;
}
/**
* 创建带裁剪的对象 - 新方法
* 直接复制原对象并应用裁剪路径,保持原始质量
*/
const createClippedObjects = async ({
canvas,
fabricObjects,
clippingObject,
isReturenDataURL,
}) => {
try {
console.log("🎯 使用新的裁剪方法创建对象");
// 获取选区边界框
const selectionBounds = clippingObject.getBoundingRect(true);
console.log("📐 选区边界框:", selectionBounds);
// 方法1如果只需要返回DataURL使用画布裁剪方法
if (isReturenDataURL) {
console.log("✅ 栅格化图像创建成功返回DataURL");
return rasterizedImage; // 返回DataURL
return await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
}
// 设置栅格化图像的属性
if (rasterizedImage) {
rasterizedImage.set({
// 方法2如果需要返回fabric对象先生成DataURL再转换为fabric对象
const clippedDataURL = await createClippedDataURLByCanvas({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
});
// 将DataURL转换为fabric.Image对象
const fabricImage = await createFabricImageFromDataURL(clippedDataURL);
// 使用fabric原生方法恢复到选区的原始大小和位置
fabricImage.scaleToWidth(selectionBounds.width);
fabricImage.scaleToHeight(selectionBounds.height);
// 设置到选区的原始位置(中心点)
fabricImage.set({
left: selectionBounds.left + selectionBounds.width / 2,
top: selectionBounds.top + selectionBounds.height / 2,
originX: "center",
originY: "center",
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "clipped",
clippedAt: new Date().toISOString(),
hasClipping: true,
preservedQuality: true,
originalBounds: selectionBounds,
restoredToOriginalSize: true,
},
});
// 更新坐标
fabricImage.setCoords();
console.log("✅ 返回裁剪后的fabric对象已恢复到原始大小和位置");
return fabricImage;
} catch (error) {
console.error("创建裁剪对象失败:", error);
throw error;
}
};
/**
* 通过画布裁剪生成DataURL
* 裁剪掉选区以外的内容,保持和选区大小一致
*/
const createClippedDataURLByCanvas = async ({
canvas,
fabricObjects,
clippingObject,
selectionBounds,
}) => {
try {
console.log("🖼️ 使用画布裁剪方法生成DataURL");
// 创建临时画布,尺寸与选区完全一致
const tempCanvas = new fabric.StaticCanvas();
// 使用高分辨率以保证质量
const pixelRatio = window.devicePixelRatio || 1;
const qualityMultiplier = Math.max(2, pixelRatio);
const canvasWidth = Math.ceil(selectionBounds.width * qualityMultiplier);
const canvasHeight = Math.ceil(selectionBounds.height * qualityMultiplier);
tempCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
console.log(
`📏 临时画布尺寸: ${canvasWidth}x${canvasHeight} (质量倍数: ${qualityMultiplier})`
);
// 克隆并添加所有需要裁剪的对象
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
// 调整对象位置:将选区左上角作为新的原点(0,0)
// 同时应用质量倍数缩放
clonedObj.set({
left: (clonedObj.left - selectionBounds.left) * qualityMultiplier,
top: (clonedObj.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clonedObj.scaleX || 1) * qualityMultiplier,
scaleY: (clonedObj.scaleY || 1) * qualityMultiplier,
});
tempCanvas.add(clonedObj);
}
// 克隆裁剪路径并调整位置
const clipPath = await cloneObjectAsync(clippingObject);
clipPath.set({
left: (clipPath.left - selectionBounds.left) * qualityMultiplier,
top: (clipPath.top - selectionBounds.top) * qualityMultiplier,
scaleX: (clipPath.scaleX || 1) * qualityMultiplier,
scaleY: (clipPath.scaleY || 1) * qualityMultiplier,
fill: "transparent",
stroke: "",
strokeWidth: 0,
absolutePositioned: true,
});
// 为整个画布设置裁剪路径
tempCanvas.clipPath = clipPath;
// 渲染画布
tempCanvas.renderAll();
// 生成高质量DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
});
// 清理临时画布
tempCanvas.dispose();
console.log("✅ 画布裁剪完成生成DataURL");
return dataURL;
} catch (error) {
console.error("画布裁剪失败:", error);
throw error;
}
};
/**
* 创建简单克隆对象
* 当不需要裁剪时,直接克隆原对象
*/
const createSimpleClone = async ({
canvas,
fabricObjects,
isReturenDataURL,
quality,
format,
}) => {
try {
console.log("📋 创建简单克隆对象");
const clonedObjects = [];
// 克隆所有对象
for (const obj of fabricObjects) {
const clonedObj = await cloneObjectAsync(obj);
clonedObj.set({
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "rasterized",
rasterizedAt: new Date().toISOString(),
objectCount: fabricObjects.length,
absoluteBounds,
relativeBounds,
originalZoom: currentZoom,
hasClipping: !!clippingObject,
...clonedObj.custom,
type: "cloned",
clonedAt: new Date().toISOString(),
preservedQuality: true,
},
});
console.log(`✅ 栅格化图像创建完成`);
clonedObjects.push(clonedObj);
}
return rasterizedImage;
// 如果需要返回DataURL需要渲染
if (isReturenDataURL) {
return await renderObjectsToDataURL(clonedObjects, quality, format);
}
// 如果只有一个对象,直接返回
if (clonedObjects.length === 1) {
return clonedObjects[0];
}
// 创建组合
const group = new fabric.Group(clonedObjects, {
selectable: true,
evented: true,
hasControls: true,
hasBorders: true,
custom: {
type: "clonedGroup",
clonedAt: new Date().toISOString(),
objectCount: clonedObjects.length,
preservedQuality: true,
},
});
return group;
} catch (error) {
console.error("创建栅格化图像失败:", error);
throw new Error(`栅格化失败: ${error.message}`);
console.error("创建简单克隆失败:", error);
throw error;
}
};
/**
* 将对象渲染为DataURL
*/
const renderObjectsToDataURL = async (objects, quality, format) => {
try {
// 计算对象边界框
const bounds = calculateBounds(objects);
if (!bounds.absoluteBounds) {
throw new Error("无法计算对象边界框");
}
// 创建临时画布用于渲染
const tempCanvas = new fabric.StaticCanvas();
const { absoluteBounds } = bounds;
tempCanvas.setDimensions({
width: Math.ceil(absoluteBounds.width),
height: Math.ceil(absoluteBounds.height),
});
// 调整对象位置并添加到临时画布
for (const obj of objects) {
const tempObj = await cloneObjectAsync(obj);
tempObj.set({
left: tempObj.left - absoluteBounds.left,
top: tempObj.top - absoluteBounds.top,
});
tempCanvas.add(tempObj);
}
// 渲染并获取DataURL
tempCanvas.renderAll();
const dataURL = tempCanvas.toDataURL({
format,
quality,
});
// 清理临时画布
tempCanvas.dispose();
return dataURL;
} catch (error) {
console.error("渲染对象为DataURL失败:", error);
throw error;
}
};
/**
* 渲染裁剪后的对象为DataURL
* 专门处理带有裁剪路径的对象渲染(备用方法)
*/
const renderClippedObjectsToDataURL = async (clippedObjects) => {
try {
console.log("🖼️ 渲染裁剪对象为DataURL");
// 计算所有裁剪对象的总边界框
let totalBounds = null;
for (const obj of clippedObjects) {
const objBounds = obj.getBoundingRect(true, true);
if (!totalBounds) {
totalBounds = { ...objBounds };
} else {
const right = Math.max(
totalBounds.left + totalBounds.width,
objBounds.left + objBounds.width
);
const bottom = Math.max(
totalBounds.top + totalBounds.height,
objBounds.top + objBounds.height
);
totalBounds.left = Math.min(totalBounds.left, objBounds.left);
totalBounds.top = Math.min(totalBounds.top, objBounds.top);
totalBounds.width = right - totalBounds.left;
totalBounds.height = bottom - totalBounds.top;
}
}
if (!totalBounds) {
throw new Error("无法计算对象边界框");
}
// 创建临时画布,使用高分辨率
const tempCanvas = new fabric.StaticCanvas();
const pixelRatio = window.devicePixelRatio || 1;
const scaleFactor = Math.max(2, pixelRatio);
const canvasWidth = Math.ceil(totalBounds.width * scaleFactor);
const canvasHeight = Math.ceil(totalBounds.height * scaleFactor);
tempCanvas.setDimensions({
width: canvasWidth,
height: canvasHeight,
});
// 调整对象位置并添加到临时画布
for (const obj of clippedObjects) {
const tempObj = await cloneObjectAsync(obj);
// 调整位置到画布坐标系
tempObj.set({
left: (tempObj.left - totalBounds.left) * scaleFactor,
top: (tempObj.top - totalBounds.top) * scaleFactor,
scaleX: (tempObj.scaleX || 1) * scaleFactor,
scaleY: (tempObj.scaleY || 1) * scaleFactor,
});
// 如果有裁剪路径,也需要调整裁剪路径
if (tempObj.clipPath) {
tempObj.clipPath.set({
scaleX: (tempObj.clipPath.scaleX || 1) * scaleFactor,
scaleY: (tempObj.clipPath.scaleY || 1) * scaleFactor,
});
}
tempCanvas.add(tempObj);
}
// 渲染画布
tempCanvas.renderAll();
// 获取DataURL
const dataURL = tempCanvas.toDataURL({
format: "png",
quality: 1.0,
multiplier: 1, // 已经通过尺寸处理了缩放
});
// 清理临时画布
tempCanvas.dispose();
console.log("✅ 裁剪对象渲染完成");
return dataURL;
} catch (error) {
console.error("渲染裁剪对象失败:", error);
throw error;
}
};
/**
* 兼容的离屏渲染方法(原有逻辑,作为备选)
*/
const createLegacyRasterization = async ({
canvas,
fabricObjects,
clippingObject,
scaleFactor,
quality,
format,
isReturenDataURL,
}) => {
console.log("⚠️ 使用兼容的离屏渲染方法");
// 这里保留原有的离屏渲染逻辑作为备选方案
const currentZoom = canvas.getZoom?.() || 1;
scaleFactor = Math.max(
scaleFactor || canvas?.getRetinaScaling?.(),
currentZoom
);
scaleFactor = Math.min(scaleFactor, 3);
const { absoluteBounds, relativeBounds } = calculateBounds(fabricObjects);
return await createOffscreenRasterization({
canvas,
objects: fabricObjects,
absoluteBounds,
relativeBounds,
scaleFactor,
clippingObject,
trimWhitespace: true,
trimPadding: 1,
quality,
format,
currentZoom,
isReturenDataURL,
});
};
/**
* 计算对象的绝对边界框和相对边界框
* @param {Array} fabricObjects fabric对象数组
@@ -203,8 +574,8 @@ const createOffscreenRasterization = async ({
}
// 设置离屏画布尺寸,并应用高清倍数
const canvasWidth = Math.ceil(renderBounds.width * scaleFactor);
const canvasHeight = Math.ceil(renderBounds.height * scaleFactor);
const canvasWidth = Math.ceil(renderBounds.width);
const canvasHeight = Math.ceil(renderBounds.height);
offscreenCanvas.setDimensions({
width: canvasWidth,