feat(CanvasManager): enhance image layer management and event handling
This commit is contained in:
@@ -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 |
1
src/assets/icons/CClear.svg
Normal file
1
src/assets/icons/CClear.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 KiB |
1
src/assets/icons/CCut.svg
Normal file
1
src/assets/icons/CCut.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -385,7 +385,7 @@ function findParentLayerId() {
|
||||
<SvgIcon
|
||||
v-if="!isHidenDragHandle"
|
||||
:name="isChild ? 'CSort' : 'CSort'"
|
||||
:size="16"
|
||||
:size="32"
|
||||
></SvgIcon>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: ({
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} 中的对象为组`,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user