Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite

This commit is contained in:
2026-01-12 17:23:29 +08:00
27 changed files with 1217 additions and 592 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -52,83 +52,112 @@ export class FillRepeatCommand extends Command {
console.warn("当前对象不能平铺", object.type);
return false;
}
console.log("===========", object.toObject(["id", "layerId", "layerName"]))
this.oldObjects = object;
const img = await new Promise((resolve, reject) => {
if (object.type === "rect") {
let source = object.fill.source;
resolve(source);
} else if (object.type === "image") {
// resolve(object.getElement());
// fabric.Image.fromURL(
// object.src,
// v => resolve(v),
// { crossOrigin: "anonymous" }
// );
const imgElement = object.getElement();
// 创建透明 Canvas
const tcanvas = document.createElement('canvas');
tcanvas.width = imgElement.width;
tcanvas.height = imgElement.height;
const ctx = tcanvas.getContext('2d');
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(imgElement, 0, 0);
resolve(tcanvas);
}
});
const fill_ = {
source: FillSourceToBase64(img),
gapX: 0,
gapY: 0,
width: img.width,
height: img.height,
};
const bgObject = this.canvasManager.getBackgroundLayerObject();
const pattern = new fabric.Pattern({
source: img,
repeat: this.fillRepeat,
patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
});
const rect = new fabric.Rect({
id: object.id,
layerId: object.layerId,
layerName: object.layerName,
fill_,
});
layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
this.oldLocked = layer.locked;
// this.oldIsDisableUnlock = layer.isDisableUnlock;
// layer.isDisableUnlock = true;
if (this.oldObjects.type === "rect") {
rect.set({
width: object.width,
height: object.height,
top: object.top,
left: object.left,
originX: object.originX,
originY: object.originY,
angle: object.angle,
scaleX: object.scaleX,
scaleY: object.scaleY,
flipX: object.flipX,
flipY: object.flipY,
if (this.fillRepeat === "no-repeat") {
const fill_ = object.fill_;
const image = await new Promise((resolve, reject) => {
fabric.Image.fromURL(
fill_.source,
v => resolve(v),
{ crossOrigin: "anonymous" }
);
});
image.set({
id: object.id,
layerId: object.layerId,
layerName: object.layerName,
...(fill_.originalInfo || {
top: object.top,
left: object.left,
})
});
layer.fabricObjects = [image.toObject(["id", "layerId", "layerName"])];
this.oldLocked = layer.locked;
layer.locked = false;
this.canvas.add(image);
this.canvas.remove(object);
} else {
rect.set({
width: bgObject.width,
height: bgObject.height,
top: bgObject.top,
left: bgObject.left,
originX: bgObject.originX,
originY: bgObject.originY,
const img = await new Promise((resolve, reject) => {
if (object.type === "rect") {
let source = object.fill.source;
resolve(source);
} else if (object.type === "image") {
const imgElement = object.getElement();
// 创建透明 Canvas
const tcanvas = document.createElement('canvas');
tcanvas.width = imgElement.width;
tcanvas.height = imgElement.height;
const ctx = tcanvas.getContext('2d');
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(imgElement, 0, 0);
resolve(tcanvas);
}
});
layer.locked = true;
const fill_ = object.fill_ || {
source: FillSourceToBase64(img),
gapX: 0,
gapY: 0,
width: img.width,
height: img.height,
originalInfo: {
top: object.top,
left: object.left,
scaleX: object.scaleX,
scaleY: object.scaleY,
width: object.width,
height: object.height,
}
};
const bgObject = this.canvasManager.getBackgroundLayerObject();
const pattern = new fabric.Pattern({
source: img,
repeat: this.fillRepeat,
patternTransform: object.fill?.hasOwnProperty("patternTransform") ? object.fill.patternTransform : createPatternTransform(scale, 0),
offsetX: object.fill?.hasOwnProperty("offsetX") ? object.fill.offsetX : bgObject.width / 2, // 水平偏移
offsetY: object.fill?.hasOwnProperty("offsetY") ? object.fill.offsetY : bgObject.height / 2, // 垂直偏移
});
const rect = new fabric.Rect({
id: object.id,
layerId: object.layerId,
layerName: object.layerName,
fill_,
});
layer.fabricObjects = [rect.toObject(["id", "layerId", "layerName"])];
this.oldLocked = layer.locked;
// this.oldIsDisableUnlock = layer.isDisableUnlock;
// layer.isDisableUnlock = true;
if (this.oldObjects.type === "rect") {
rect.set({
width: object.width,
height: object.height,
top: object.top,
left: object.left,
originX: object.originX,
originY: object.originY,
angle: object.angle,
scaleX: object.scaleX,
scaleY: object.scaleY,
flipX: object.flipX,
flipY: object.flipY,
});
} else {
let scaleX = bgObject.scaleX || 1;
let scaleY = bgObject.scaleY || 1;
rect.set({
width: bgObject.width,
height: bgObject.height,
top: bgObject.top - bgObject.height * scaleY / 2,
left: bgObject.left - bgObject.width * scaleX / 2,
scaleX,
scaleY,
});
layer.locked = true;
}
rect.set("fill", pattern);
this.canvas.add(rect);
this.canvas.remove(object);
}
rect.set("fill", pattern);
this.canvas.add(rect);
this.canvas.remove(object);
await this.layerManager?.updateLayersObjectsInteractivity();
await this.layerManager?.sortLayersWithTool?.();
await this.canvasManager.thumbnailManager?.generateLayerThumbnail(

View File

@@ -144,7 +144,7 @@ export class AddLayerCommand extends Command {
// 先在一级图层中查找
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (layer.isPrintTrimsGroup) continue;
if (layer.id === layerId) {
return {
layer: layer,

View File

@@ -493,7 +493,7 @@ export class CreateTextCommand extends Command {
// 先在一级图层中查找
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
if (layer.isPrintTrimsGroup) continue;
if (layer.id === layerId) {
return {
layer: layer,

View File

@@ -287,7 +287,7 @@ const canDeleteComputed = computed(() => {
:is-child="isChild"
:is-active="layer.id === activeLayerId"
:is-selected="isLayerSelected(layer.id)"
:is-multi-select-mode="isMultiSelectMode"
:is-multi-select-mode="isMultiSelectMode && !layer.specialType"
:is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName"
:can-delete="
@@ -296,7 +296,7 @@ const canDeleteComputed = computed(() => {
:expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@context-menu="(...args) => !layer.specialType && forwardEvent('context-menu', ...args)"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@@ -337,7 +337,7 @@ const canDeleteComputed = computed(() => {
:expanded-group-ids="expandedGroupIds"
:isChild="true"
:parentLayerId="layer.id"
:group-name="groupName"
:group-name="layer.specialType || groupName"
@layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)"
@@ -385,17 +385,10 @@ const canDeleteComputed = computed(() => {
<style scoped lang="less">
// 从父组件的样式文件中继承相关样式
.layers-list {
flex: 1;
overflow-y: auto;
.sortable-layers {
min-height: 20px;
}
// .layer-group {
// // margin-bottom: 1px;
// }
.child-layers {
position: relative;
padding-left: 20px;

View File

@@ -576,7 +576,7 @@ function handleLayerClick(layer, event) {
if (event.ctrlKey || event.metaKey || event.shiftKey || isMultiSelectMode.value) {
toggleLayerSelection(layer, event);
} else {
if(!layer.isFixedClipMask) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
if(!layer.isPrintTrimsGroup) lastSelectLayerId.value = layer.id; // 更新最后选中的图层ID
// 普通点击:进入单选模式
// selectedLayerIds.value = [layer.id];
// isMultiSelectMode.value = false;
@@ -596,7 +596,7 @@ function handleLayerClick(layer, event) {
layerManager?.updateLayersObjectsInteractivity();
}
}
if(!layer.isFixedClipMask) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
if(!layer.isPrintTrimsGroup) lastSelectedIndex.value = sortableRootLayers.value.findIndex((l) => l.id === layer.id);
}
}
@@ -1240,6 +1240,12 @@ async function handleCrossLevelMove(moveData) {
}
try {
const layer = findLayerRecursively(layers.value, layerId).layer;
const toLayer = findLayerRecursively(layers.value, toParentId).layer;
if(layer?.specialType || toLayer?.specialType) {
console.warn("当前图层不可移动到外部");
return;
}
// 如果有命令管理器,使用命令模式
if (commandManager) {
console.log("📝 使用命令模式执行跨层级移动");
@@ -1593,46 +1599,48 @@ async function moveGroupToGroup(draggedLayer, fromParentId, toParentId, newIndex
<small>{{ $t('Canvas.Hint') }}</small>
</div>
<div class="layers-list-container">
<!-- 图层列表组件 -->
<LayersList
:layers="layers"
:active-layer-id="activeLayerId"
:sortable-root-layers="sortableRootLayers"
:fixed-layers="fixedLayers"
:selected-layer-ids="selectedLayerIds"
:is-multi-select-mode="isMultiSelectMode"
:editing-layer-id="editingLayerId"
:editing-layer-name="editingLayerName"
:thumbnail-manager="thumbnailManager"
:expanded-group-ids="expandedGroupIds"
:isChild="false"
group-name="layers-root"
@layer-click="handleLayerClick"
@layer-double-click="handleLayerDoubleClick"
@context-menu="showContextMenu"
@checkbox-change="handleCheckboxClick"
@toggle-visibility="toggleLayerVisibility"
@toggle-lock="toggleSelectedLayersLockByLayer"
@delete="removeLayer"
@edit-confirm="confirmEdit"
@edit-cancel="cancelEdit"
@edit-keydown="handleEditKeydown"
@touch-start="handleTouchStart"
@touch-move="handleTouchMove"
@touch-end="handleTouchEnd"
@update:editing-name="editingLayerName = $event"
@root-layers-sort="handleRootLayersSort"
@child-layers-sort="handleChildLayersSort"
@cross-level-move="handleCrossLevelMove"
@select-child-layer="selectChildLayer"
@start-child-layer-edit="startChildLayerEdit"
@child-context-menu="showChildLayerContextMenu"
@toggle-group-expanded="toggleGroupExpanded"
@toggle-child-visibility="toggleChildLayerVisibility"
@toggle-child-lock="toggleChildLayerLock"
@delete-child="deleteChildLayer"
@rename-child="renameChildLayer"
/>
<LayersList
:layers="layers"
:active-layer-id="activeLayerId"
:sortable-root-layers="sortableRootLayers"
:fixed-layers="fixedLayers"
:selected-layer-ids="selectedLayerIds"
:is-multi-select-mode="isMultiSelectMode"
:editing-layer-id="editingLayerId"
:editing-layer-name="editingLayerName"
:thumbnail-manager="thumbnailManager"
:expanded-group-ids="expandedGroupIds"
:isChild="false"
group-name="layers-root"
@layer-click="handleLayerClick"
@layer-double-click="handleLayerDoubleClick"
@context-menu="showContextMenu"
@checkbox-change="handleCheckboxClick"
@toggle-visibility="toggleLayerVisibility"
@toggle-lock="toggleSelectedLayersLockByLayer"
@delete="removeLayer"
@edit-confirm="confirmEdit"
@edit-cancel="cancelEdit"
@edit-keydown="handleEditKeydown"
@touch-start="handleTouchStart"
@touch-move="handleTouchMove"
@touch-end="handleTouchEnd"
@update:editing-name="editingLayerName = $event"
@root-layers-sort="handleRootLayersSort"
@child-layers-sort="handleChildLayersSort"
@cross-level-move="handleCrossLevelMove"
@select-child-layer="selectChildLayer"
@start-child-layer-edit="startChildLayerEdit"
@child-context-menu="showChildLayerContextMenu"
@toggle-group-expanded="toggleGroupExpanded"
@toggle-child-visibility="toggleChildLayerVisibility"
@toggle-child-lock="toggleChildLayerLock"
@delete-child="deleteChildLayer"
@rename-child="renameChildLayer"
/>
</div>
<!-- 固定层背景层和固定层 -->
<div v-if="fixedLayers.length > 0" class="fixed-layers">
<!-- 遍历固定层 -->

View File

@@ -11,9 +11,9 @@
flex-direction: column;
user-select: none;
z-index: 6;
overflow-y: auto;
width: 100%;
// max-height: 70vh;
overflow: hidden;
-webkit-overflow-scrolling: touch;
}
@@ -161,12 +161,12 @@
font-size: 1.1rem;
}
}
.layers-list-container{
overflow-y: auto;
}
// 图层列表
.layers-list {
position: relative;
flex: 1;
overflow-y: auto;
}
// 图层项样式

View File

@@ -179,7 +179,11 @@
import { ref, onMounted, watch, onUnmounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
import { OperationType, SpecialLayerId } from "../../utils/layerHelper";
import {
OperationType,
SpecialLayerId,
SpecialType,
} from "../../utils/layerHelper";
import { loadImageUrlToLayer } from "../../utils/imageHelper";
import {
calculateRotatedTopLeftDeg,
@@ -280,6 +284,9 @@
const getActiveObject = (e) => {
console.log("==========切换激活对象", e, activeObjects);
activeObjects.value = [...e.selected];
// .filter((v) =>
// v.specialType ? v.specialType === SpecialType.REPEAT_O : true
// );// 过滤出印花对象
activeObjects.value.forEach((v) => {
v.layer = props.layerManager.getLayerById(v.layerId);
});

View File

@@ -8,8 +8,8 @@
flex-direction: column;
user-select: none;
z-index: 6;
overflow-y: auto;
width: 100%;
overflow: hidden;
-webkit-overflow-scrolling: touch;
}
.layers-header {
@@ -132,10 +132,11 @@
color: #666;
font-size: 1.1rem;
}
.layers-list-container {
overflow-y: auto;
}
.layers-list {
position: relative;
flex: 1;
overflow-y: auto;
}
.layer-item {
position: relative;

View File

@@ -1,5 +1,5 @@
<template>
<div class="angle-tool">
<div class="angle-tool" :disabled="disabled">
<div
ref="dishRef"
class="dish"
@@ -11,7 +11,13 @@
</div>
</div>
<div class="input">
<input type="number" v-model="angle" @input="onInput" @change="onChange" />
<input
type="number"
v-model="angle"
@input="onInput"
@change="onChange"
:disabled="disabled"
/>
</div>
</div>
</template>
@@ -25,14 +31,22 @@
type: Number,
default: 0,
},
disabled: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["change", "input"]);
const angle = ref(props.angle);
watch(() => props.angle, (value) => {
angle.value = value;
});
watch(
() => props.angle,
(value) => {
angle.value = value;
}
);
const dishRef = ref<HTMLDivElement>();
const mousedown = (e: MouseEvent | TouchEvent) => {
if (props.disabled) return;
const mousemove = (e: MouseEvent | TouchEvent) => {
if (!dishRef.value) return;
const { left, top, width, height } =
@@ -56,9 +70,10 @@
document.addEventListener("mouseup", mouseup);
document.addEventListener("touchend", mouseup);
};
const onInput = () => emit("input", angle.value);
const onInput = () => !props.disabled && emit("input", angle.value);
var changeTime: any = null;
const onChange = () => {
if (props.disabled) return;
clearTimeout(changeTime);
changeTime = setTimeout(() => emit("change", angle.value), 500);
};
@@ -79,10 +94,17 @@
display: flex;
align-items: center;
width: 100%;
--color: #000;
&[disabled="true"] {
--color: #b2b2b2;
> .dish {
cursor: not-allowed;
}
}
> .dish {
width: 24px;
height: 24px;
border: 1px solid #000;
border: 1px solid var(--color);
border-radius: 50%;
cursor: pointer;
> .pointer {
@@ -98,7 +120,7 @@
transform: translate(-50%, 0);
width: 35%;
height: 35%;
background-color: #000;
background-color: var(--color);
border-radius: 50%;
}
}
@@ -106,7 +128,7 @@
> .input {
margin-left: 5px;
font-size: 14px;
color: #000;
color: var(--color);
flex: 1;
// min-width: 45px;
// max-width: 80px;

View File

@@ -5,6 +5,7 @@
@change="change"
:defaultValue="defaultValue"
@dropdownVisibleChange="dropdownVisibleChange"
:disabled="disabled"
>
<a-select-option
v-for="v in list"
@@ -21,6 +22,10 @@
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch } from "vue";
const props = defineProps({
disabled: {
type: Boolean,
default: false,
},
defaultValue: {
default: "",
},

View File

@@ -1,5 +1,5 @@
<template>
<div class="slider">
<div class="slider" :disabled="disabled">
<div class="input-range">
<span
class="tip"
@@ -16,6 +16,7 @@
:step="props.step"
@input="onInput"
@change="onChange"
:disabled="disabled"
/>
</div>
<div class="input" v-show="isInput">
@@ -27,6 +28,7 @@
:step="props.step"
@input="onInput"
@change="onChange"
:disabled="disabled"
/>
</div>
</div>
@@ -35,6 +37,10 @@
<script setup lang="ts">
import { ref, defineProps, defineEmits, watch } from "vue";
const props = defineProps({
disabled: {
type: Boolean,
default: false,
},
value: {
type: Number,
default: 0,
@@ -66,9 +72,10 @@
() => props.value,
(v) => (value.value = v)
);
const onInput = () => emit("input", Number(value.value));
const onInput = () => !props.disabled && emit("input", Number(value.value));
var changeTime: any = null;
const onChange = () => {
if (props.disabled) return;
clearTimeout(changeTime);
changeTime = setTimeout(() => emit("change", Number(value.value)), 500);
};

View File

@@ -46,6 +46,7 @@ import {
loadImageUrlToLayer,
loadImage,
} from "./utils/imageHelper.js";
import { optimizeCanvasRendering } from "./utils/helper";
// import MinimapPanel from "./components/MinimapPanel.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
@@ -57,6 +58,7 @@ const emit = defineEmits([
"trigger-red-green-mouseup", // 红绿图模式鼠标抬起事件
"changeCanvas", // 画布变更事件
"canvasInit", // 画布初始化事件
"canvas-load-json-success", // 画布加载JSON成功事件
"trigger-library", // 触发打开Library选择图片事件
"before-unmount-export-extra-info", // 组件卸载前导出额外信息事件
]);
@@ -261,6 +263,7 @@ onMounted(async () => {
enabledRedGreenMode: props.enabledRedGreenMode,
isFixedErasable: props.isFixedErasable,
props,
emit,
});
canvasManager.canvas.activeLayerId = activeLayerId;
canvasManager.activeLayerId = activeLayerId;
@@ -482,38 +485,13 @@ onMounted(async () => {
}, 700);
});
let throttleTimeout = null;
let lastRunTime = 0;
let trailingTimeout = null;
let throttleDelay = 100;
observer = new ResizeObserver((entries) => {
const now = Date.now();
const throttleDelay = 100;
if (!throttleTimeout) {
// 立即执行一次
handleWindowResize();
layerManager?.updateLayersObjectsInteractivity?.();
setTimeout(() => {
layerManager?.updateLayersObjectsInteractivity?.();
});
lastRunTime = now;
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
}, throttleDelay);
} else {
// 如果在节流期间有新的变化,则重置尾触发
clearTimeout(trailingTimeout);
trailingTimeout = setTimeout(() => {
handleWindowResize();
layerManager?.updateLayersObjectsInteractivity?.();
setTimeout(() => {
layerManager?.updateLayersObjectsInteractivity?.();
});
lastRunTime = Date.now();
}, throttleDelay);
}
clearTimeout(trailingTimeout);
trailingTimeout = setTimeout(() => {
optimizeCanvasRendering(canvasManager.canvas, ()=> handleWindowResize());
}, throttleDelay);
});
observer.observe(canvasContainerRef.value);
// 使用window的resize事件代替ResizeObserver
@@ -546,13 +524,9 @@ watchEffect(() => {
});
onBeforeUnmount(async () => {
// if (import.meta.hot) {
// // 热更新
// console.log("onBeforeUnmount 开发环境热更新不卸载组件...");
// return; // 开发环境下不卸载组件
// }
const extraInfo = await canvasManager.exportExtraInfo();
emit("before-unmount-export-extra-info", extraInfo);
observer.unobserve(canvasContainerRef.value);
// const extraInfo = await canvasManager.exportExtraInfo();
// emit("before-unmount-export-extra-info", extraInfo);
console.log("onBeforeUnmount 组件卸载,清理资源...");
canvasManager?.dispose?.();
@@ -576,20 +550,19 @@ onBeforeUnmount(async () => {
// 移除window resize事件监听
// window.removeEventListener("resize", handleWindowResize);
observer.unobserve(canvasContainerRef.value);
});
// 窗口大小变化处理函数
function handleWindowResize() {
console.log(132);
async function handleWindowResize() {
console.log("==========画布窗口大小变化==========");
// 使用requestAnimationFrame来防止频繁更新
setTimeout(() => {
// 更新画布大小并自动居中所有元素
updateCanvasSize();
// 确保显示的缩放信息是最新的
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
});
await new Promise(requestAnimationFrame);
if(!canvasManager) return;
updateCanvasSize();
// 确保显示的缩放信息是最新的
currentZoom.value = Math.round(canvasManager.canvas.getZoom() * 100);
await new Promise(requestAnimationFrame);
await layerManager?.updateLayersObjectsInteractivity?.();
}
function resetZoom() {
@@ -981,6 +954,18 @@ defineExpose({
...opts,
});
},
updateOtherLayers: async (otherData) => {
await new Promise((resolve) => optimizeCanvasRendering(canvasManager.canvas, resolve));
await canvasManager?.createOtherLayers?.(otherData, true);
layerManager.activeLayerId.value = ""
layerManager?.sortLayers();
await layerManager?.updateLayersObjectsInteractivity?.(true);
canvasManager?.canvas?.renderAll();
setTimeout(() => {
canvasManager?.updateAllThumbnails();
}, 500);
return true;
},
//图片url或者base64
addImageToLayer: async (
url,

View File

@@ -13,6 +13,7 @@ import {
createLayer,
LayerType,
SpecialLayerId,
SpecialType,
BlendMode,
} from "../utils/layerHelper";
import { ObjectMoveCommand } from "../commands/ObjectCommands";
@@ -69,6 +70,7 @@ export class CanvasManager {
this.eraserStateManager = null; // 橡皮擦状态管理器引用
this.handleCanvasInit = null; // 画布初始化回调函数
this.props = options.props || {};
this.emit = options.emit || (() => {});
// 初始化画布
this.initializeCanvas();
}
@@ -175,6 +177,7 @@ export class CanvasManager {
// 返回false表示使用默认行为直接添加到画布
return false;
};
this.eraserStateManager = new EraserStateManager(
this.canvas,
@@ -568,10 +571,10 @@ export class CanvasManager {
}
// 更新颜色层信息
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
if(colorObject){
await this.setObjecCliptInfo(colorObject);
}
// const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
// if(colorObject){
// await this.setObjecCliptInfo(colorObject);
// }
const groupLayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(groupLayer){
const groupRect = new fabric.Rect({});
@@ -945,7 +948,7 @@ export class CanvasManager {
options.restoreOpacityInRedGreen !== undefined
? options.restoreOpacityInRedGreen
: false, // 默认在红绿图模式下恢复透明度
excludedLayers: [SpecialLayerId.SPECIAL_GROUP],
// excludedLayers: [SpecialLayerId.SPECIAL_GROUP], // 导出时排除的图层ID数组
};
// 如果在红绿图模式下且没有指定具体的图层,自动包含所有普通图层
@@ -1040,10 +1043,10 @@ export class CanvasManager {
* 导出印花和元素图层
*/
async exportPrintTrimsLayers() {
const object = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(!object) return Promise.reject("印花和元素图层组不存在");
const ids = object.children.map((v) => v.id);
const objects = this.getObjectsByIds(ids).filter((v) => !!v.sourceData);
const glayer = this.layerManager.getLayerById(SpecialLayerId.SPECIAL_GROUP);
if(!glayer) return Promise.reject("印花和元素图层组不存在");
const ids = glayer.children.map((v) => v.id);
const objects = this.getObjectsByIds(ids);
const fixedLayerObj = this.getFixedLayerObject();
if(!fixedLayerObj) return Promise.reject("固定图层不存在");
const flWidth = fixedLayerObj.width
@@ -1055,33 +1058,55 @@ export class CanvasManager {
const prints = [];
const trims = [];
objects.forEach((v) => {
const sourceData = glayer.children.find((v_) => v_.id === v.id)?.metadata?.sourceData;
if(!sourceData) return;
const obj = {
ifSingle: v.sourceData.ifSingle,
level2Type: v.sourceData.level2Type,
designType: v.sourceData.designType,
path: v.sourceData.path,
minIOPath: v.sourceData.minIOPath,
ifSingle: typeof v.fill === "string",
level2Type: sourceData.level2Type,
designType: sourceData.designType,
path: sourceData.path,
minIOPath: sourceData.minIOPath,
location: [0, 0],
scale: [0, 0],
angle: v.angle,
name: v.sourceData.name,
priority: v.sourceData.priority,
gap: [0, 0],
name: sourceData.name,
priority: sourceData.priority,
object:{
top: 0,
left: 0,
scaleX: 0,//对象的缩放比例
scaleY: 0,//对象的缩放比例
opacity: v.opacity,
angle: v.angle,
flipX: v.flipX,//是否水平翻转
flipY: v.flipY,//是否垂直翻转
blendMode: v.globalCompositeOperation,// 混合模式
gapX: 0,// 平铺模式下的间距
gapY: 0,// 平铺模式下的间距
}
}
let left = (v.left - (flLeft - flWidth * flScaleX / 2));
let top = (v.top - (flTop - flHeight * flScaleY / 2));
let width = (v.width * v.scaleX);
let height = (v.height * v.scaleY);
let {x:cx, y:cy} = calculateCenterPoint(width, height, left, top, v.angle);
let oX = (cx-width/2) / flScaleX;
let oY = (cy-height/2) / flScaleY;
let oScaleX = (v.width * v.scaleX) / (flWidth * flScaleX);
let oScaleY = (v.height * v.scaleY) / (flHeight * flScaleY);
// obj.object.width = width;
// obj.object.height = height;
obj.object.top = oY;
obj.object.left = oX;
obj.object.scaleX = oScaleX;
obj.object.scaleY = oScaleY;
if(obj.ifSingle){
let left = (v.left - (flLeft - flWidth * flScaleX / 2));
let top = (v.top - (flTop - flHeight * flScaleY / 2));
let width = (v.width * v.scaleX);
let height = (v.height * v.scaleY);
let {x:cx, y:cy} = calculateCenterPoint(width, height, left, top, v.angle);
let x = (cx-width/2) / flScaleX;
let y = (cy-height/2) / flScaleY;
obj.location = [x, y];
obj.scale = [(v.width * v.scaleX) / (flWidth * flScaleX), (v.height * v.scaleY) / (flHeight * flScaleY)];
obj.location = [oX, oY];
obj.scale = [oScaleX, oScaleY];
}else{
let fill = v.fill;
let fill_ = v.fill_;
if(!fill || !fill_) return;
if(!fill || !fill_) return console.warn("印花元素不存在fill或fill_属性");
let {scale, angle} = getTransformScaleAngle(fill.patternTransform);
let scaleX = scale * 5 * v.fill_.width / flWidth;
let scaleY = scale * 5 * v.fill_.height / flHeight;
@@ -1138,68 +1163,6 @@ export class CanvasManager {
}
getJSON() {
// // 简化图层数据在loadJSON时要根据id恢复引用
// const simplifyLayers = (layers) => {
// return layers.map((layer) => {
// if (layer?.children?.length) {
// layer.children = layer.children.map((child) => {
// return {
// id: child.id,
// type: child.type,
// layerId: child.layerId,
// layerName: child.layerName,
// isBackground: child.isBackground,
// isLocked: child.isLocked,
// isVisible: child.isVisible,
// isFixed: child.isFixed,
// parentId: child.parentId,
// fabricObject: child.fabricObject
// ? {
// id: child.fabricObject.id,
// type: child.fabricObject.type,
// layerId: child.fabricObject.layerId,
// layerName: child.fabricObject.layerName,
// }
// : {},
// fabricObjects:
// child.fabricObjects?.map((obj) => ({
// id: obj.id,
// type: obj.type,
// layerId: obj.layerId,
// layerName: obj.layerName,
// })) || [],
// };
// });
// }
// return {
// id: layer.id,
// type: layer.type,
// layerId: layer.layerId,
// layerName: layer.layerName,
// isBackground: layer.isBackground,
// isLocked: layer.isLocked,
// isVisible: layer.isVisible,
// isFixed: layer.isFixed,
// parentId: layer.parentId,
// fabricObject: child.fabricObject
// ? {
// id: child.fabricObject.id,
// type: child.fabricObject.type,
// layerId: child.fabricObject.layerId,
// layerName: child.fabricObject.layerName,
// }
// : {},
// fabricObjects:
// child.fabricObjects?.map((obj) => ({
// id: obj.id,
// type: obj.type,
// layerId: obj.layerId,
// layerName: obj.layerName,
// })) || [],
// children: layer.children,
// };
// });
// };
try {
// 清除画布中选中状态
// this.canvas.discardActiveObject();
@@ -1339,7 +1302,6 @@ export class CanvasManager {
await this.setCanvasSize(this.canvas.width, this.canvas.height);
await this.centerBackgroundLayer(this.canvas.width, this.canvas.height);
await this.createOtherLayers(this.props.otherData);
// 重新构建对象关系
// restoreObjectLayerAssociations(this.layers.value, this.canvas.getObjects());
// 验证图层关联关系 - 稳定后可以注释
@@ -1364,12 +1326,12 @@ export class CanvasManager {
// }
// 重载代码后支持回调中操作一些内容
await calllBack?.();
// 确保所有对象的交互性正确设置
await this.layerManager?.updateLayersObjectsInteractivity?.();
console.log(this.layerManager.layers.value);
await calllBack?.();
this.emit("canvas-load-json-success");
// 更新所有缩略图
setTimeout(() => {
this.updateAllThumbnails();
@@ -1396,13 +1358,22 @@ export class CanvasManager {
* 创建其他图层:印花、颜色、元素...
* @param {Object} otherData - 其他图层数据
*/
async createOtherLayers(otherData) {
async createOtherLayers(otherData, isUpdate = false) {
if (!otherData) return console.warn("otherData 为空不需要添加");
const otherData_ = JSON.parse(JSON.stringify(otherData));
console.log("==========创建其他图层", otherData_);
const updateColor = !!otherData_.color;
const updateSpecialGroup = !!otherData_.printObject || !!otherData_.trims;
// 删除颜色图层和特殊组图层
const ids = [SpecialLayerId.COLOR, SpecialLayerId.SPECIAL_GROUP];
const ids = [];
if(isUpdate){
updateColor && ids.push(SpecialLayerId.COLOR)
updateSpecialGroup && ids.push(SpecialLayerId.SPECIAL_GROUP)
}else{
ids.push(SpecialLayerId.COLOR)
ids.push(SpecialLayerId.SPECIAL_GROUP)
}
this.layers.value = this.layers.value.filter((layer) => {
if(ids.includes(layer.id)){
ids.push(...layer.children?.map((child) => child.id));
@@ -1414,11 +1385,11 @@ export class CanvasManager {
// 创建颜色图层
await this.createColorLayer(otherData_.color);
otherData_.color && await this.createColorLayer(otherData_.color);
const printTrimsLayers = [];// 印花和元素图层
const singleLayers = [];// 平铺图层
otherData_?.printObject?.prints?.forEach((print, index) => {
otherData_.printObject?.prints?.forEach((print, index) => {
print.name = t("Canvas.Print") + (index + 1);
if(print.ifSingle){
printTrimsLayers.unshift({...print});
@@ -1426,12 +1397,13 @@ export class CanvasManager {
singleLayers.unshift({...print});
}
})
otherData_?.trims?.prints?.forEach((trims, index) => {
otherData_.trims?.prints?.forEach((trims, index) => {
trims.name = t("Canvas.Elements") + (index + 1);
printTrimsLayers.unshift({...trims});
})
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
if(isUpdate ? updateSpecialGroup : true){
await this.createPrintTrimsLayers(printTrimsLayers, singleLayers);
}
await this.changeCanvas();
}
@@ -1471,8 +1443,8 @@ export class CanvasManager {
});
tagObject.set('clipPath', transparentMask);
}
async createColorLayer(color){
if(!color) return console.warn("颜色为空不需要添加");
async createColorLayer(color_){
const color = color_ || {r:0,g:0,b:0,a:0};
// if(findLayer(this.layers.value, SpecialLayerId.COLOR)) {
// return console.warn("画布中已存在颜色图层");
// }
@@ -1490,7 +1462,7 @@ export class CanvasManager {
globalCompositeOperation: BlendMode.MULTIPLY,
originColor: color,
});
await this.setObjecCliptInfo(colorRect);
// await this.setObjecCliptInfo(colorRect);
const gradientObj = palletToFill(color);
const gradient = new fabric.Gradient({
type: 'linear',
@@ -1561,7 +1533,8 @@ export class CanvasManager {
selectable: true,
hasControls: true,
hasBorders: true,
sourceData: item,
specialType: SpecialType.PRINT_TRIMS_O,
globalCompositeOperation: BlendMode.MULTIPLY,
});
resolve(fabricImage);
}, { crossOrigin: "anonymous" });
@@ -1574,7 +1547,10 @@ export class CanvasManager {
visible: true,
locked: false,
opacity: 1.0,
specialType: SpecialType.PRINT_TRIMS_L,
blendMode: BlendMode.MULTIPLY,
fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
metadata: {sourceData: item},
})
children.push(layer);
};
@@ -1606,13 +1582,11 @@ export class CanvasManager {
layerName: name,
width: fixedLayerObj.width,
height: fixedLayerObj.height,
top: fixedLayerObj.top,
left: fixedLayerObj.left,
top: fixedLayerObj.top - fixedLayerObj.height * fixedLayerObj.scaleY / 2,
left: fixedLayerObj.left - fixedLayerObj.width * fixedLayerObj.scaleX / 2,
scaleX: fixedLayerObj.scaleX,
scaleY: fixedLayerObj.scaleY,
originX: fixedLayerObj.originX,
originY: fixedLayerObj.originY,
sourceData: item,
globalCompositeOperation: BlendMode.MULTIPLY,
fill: new fabric.Pattern({
source: image,
repeat: "repeat",
@@ -1626,7 +1600,8 @@ export class CanvasManager {
gapY: 0,
width: image.width,
height: image.height,
}
},
specialType: SpecialType.REPEAT_O,
});
this.canvas.add(rect);
let layer = createLayer({
@@ -1636,22 +1611,26 @@ export class CanvasManager {
visible: true,
locked: true,
opacity: 1,
specialType: SpecialType.REPEAT_L,
blendMode: BlendMode.MULTIPLY,
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
metadata: {sourceData: item},
})
children.push(layer);
};
if(children.length === 0){
let layer = createLayer({
id: generateId("layer_image_"),
name: t("Canvas.EmptyLayer"),
type: LayerType.BITMAP,
visible: true,
locked: false,
opacity: 1.0,
fabricObjects: [],
})
children.push(layer);
}
// if(children.length === 0){
// let layer = createLayer({
// id: generateId("layer_image_"),
// name: t("Canvas.EmptyLayer"),
// type: LayerType.BITMAP,
// visible: true,
// locked: false,
// opacity: 1.0,
// fabricObjects: [],
// })
// children.push(layer);
// }
// if(children.length === 0) return;
const groupRect = new fabric.Rect({});
await this.setObjecCliptInfo(groupRect);
// 插入组图层
@@ -1666,38 +1645,41 @@ export class CanvasManager {
fabricObjects: [],
children: children,
clippingMask: groupRect.toObject(),
isFixedClipMask: true,
isPrintTrimsGroup: true,
specialType: SpecialType.PRINT_TRIMS_G,
});
this.layers.value.splice(groupIndex, 0, groupLayer);
console.log("==========layers", [...this.layers.value]);
}
/**
* 画布事件变更后
*/
async changeCanvas(){
// const fixedLayerObj = this.getFixedLayerObject();
// if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj)
// const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
// if(colorObject){
// const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP);
// if(ids.length === 0){
// ids.unshift(SpecialLayerId.SPECIAL_GROUP);
// await this.setObjecCliptInfo(colorObject);
// this.canvas.renderAll();
// return;
// }
// const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true});
// if(!base64) return console.warn("导出图片失败", base64)
// const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true);
// const ctx = canvas.getContext('2d');
// const width = fixedLayerObj.width;
// const height = fixedLayerObj.height;
// const x = (canvas.width - width) / 2;
// const y = (canvas.height - height) / 2;
// const data = ctx.getImageData(x, y, width, height);
// await this.setObjecCliptInfo(colorObject, data);
// this.canvas.renderAll();
// }
const fixedLayerObj = this.getFixedLayerObject();
if(!fixedLayerObj) return console.warn("固定图层对象不存在", fixedLayerObj)
const colorObject = this.getLayerObjectById(SpecialLayerId.COLOR);
if(colorObject){
const ids = this.layerManager.getBlendModeLayerIds(SpecialLayerId.SPECIAL_GROUP);
if(ids.length === 0){
ids.unshift(SpecialLayerId.SPECIAL_GROUP);
await this.setObjecCliptInfo(colorObject);
this.canvas.renderAll();
return;
}
const base64 = await this.exportManager.exportImage({layerIdArray2: ids, isEnhanceImg: true});
if(!base64) return console.warn("导出图片失败", base64)
const canvas = await base64ToCanvas(base64, fixedLayerObj.scaleX * 2, true);
const ctx = canvas.getContext('2d');
const width = fixedLayerObj.width;
const height = fixedLayerObj.height;
const x = (canvas.width - width) / 2;
const y = (canvas.height - height) / 2;
const data = ctx.getImageData(x, y, width, height);
await this.setObjecCliptInfo(colorObject, data);
this.canvas.renderAll();
}
}
/**

View File

@@ -45,12 +45,6 @@ export class ExportManager {
excludedLayers = [], // 排除的图层ID数组
} = options;
try {
// 查找颜色图层并隐藏
// const colorLayer = this.layerManager.getLayerById(SpecialLayerId.COLOR);
// if (colorLayer && colorLayer.visible) {
// colorLayer.visible = false;
// await this.layerManager?.updateLayersObjectsInteractivity();
// }
// 检查是否为红绿图模式
const isRedGreenMode = this.layerManager?.isInRedGreenMode?.() || false;

View File

@@ -199,9 +199,12 @@ export class LayerManager {
if (!this.canvas) return;
if (isUseOptimize) {
// 优化渲染 - 统一批处理 支持异步回调
await optimizeCanvasRendering(this.canvas, async () => {
// 应用图层交互规则
await this._applyInteractionRules({ isMoveing });
await new Promise((resolve) => {
optimizeCanvasRendering(this.canvas, async () => {
// 应用图层交互规则
await this._applyInteractionRules({ isMoveing });
resolve();
});
});
} else {
// 直接应用图层交互规则
@@ -333,7 +336,6 @@ export class LayerManager {
const objects = this.canvas.getObjects();
const editorMode = this.editorMode || CanvasConfig.defaultTool;
const layers = this.layers?.value || [];
// 创建缓存以避免重复查找
const layerMap = {};
layers.forEach((layer) => {
@@ -3269,7 +3271,7 @@ export class LayerManager {
* @private
*/
_setupGroupMaskMovementSync(activeSelection, layer) {
if (!activeSelection || !layer || !layer.clippingMask || layer.isFixedClipMask) {
if (!activeSelection || !layer || !layer.clippingMask || layer.isPrintTrimsGroup) {
return;
}

View File

@@ -376,8 +376,8 @@ export class ToolManager {
// 设置工具特定的状态
const tool = this.tools[toolId];
if (tool && typeof tool.setup === "function") {
console.log(`画布切换工具:${tool.name}(${toolId})`)
this.canvas.toolId = toolId;
console.log(`画布切换工具:${tool.name}(${toolId})`)
this.canvas.toolId = toolId;
tool.setup();
}
@@ -458,11 +458,31 @@ export class ToolManager {
}
/**
* 检查当前工具是否禁止操作当前选中的对象
* @param {Boolean} isBrushTool 是否为画笔工具
* @returns {Boolean} 是否可以切换
*/
checkToolCanOperateSelectedObject(isBrushTool = false) {
const layer = this.layerManager?.getActiveLayer();
const isSpecialLayer = !!layer?.specialType;
if (isSpecialLayer) {
if(isBrushTool){
this._disableBrushIndicator();
}
this.canvas.defaultCursor = "not-allowed";
}
console.log("===========",isSpecialLayer, this.canvas.defaultCursor);
return isSpecialLayer;
}
/**
* 设置画笔工具
*/
setupBrushTool() {
if (!this.canvas) return;
if (this.checkToolCanOperateSelectedObject(true)) return;
this.canvas.isDrawingMode = true;
this.canvas.selection = false;
@@ -506,6 +526,8 @@ export class ToolManager {
*/
setupEraserTool() {
if (!this.canvas) return;
if (this.checkToolCanOperateSelectedObject(true)) return;
this.canvas.isDrawingMode = true;
this.canvas.selection = false;
@@ -654,6 +676,7 @@ export class ToolManager {
*/
setupLiquifyTool() {
if (!this.canvas || !this.layerManager) return;
if (this.checkToolCanOperateSelectedObject(true)) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;

View File

@@ -767,16 +767,13 @@ export function getLayerObjectsZIndex(canvas, layerId) {
* @param {number} y1 第一个点的y坐标
* @param {number} x2 第二个点的x坐标
* @param {number} y2 第二个点的y坐标
* @returns {number} 角度值(-90 - 270度
* @returns {number} 角度值(-180 - 180度
*/
export function calculateAngle(x1, y1, x2, y2, int = false) {
// 计算两点之间的差值
const deltaX = x2 - x1;
const deltaY = y2 - y1;
// 使用Math.atan2计算弧度然后转换为角
let angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90;
const deltaY = y1 - y2;
let angle = Math.atan2(deltaX, deltaY) * (180 / Math.PI);
// if(angle < 0) angle += 360;// 0 - 360
return int ? Math.round(angle) : angle;
}

View File

@@ -25,6 +25,17 @@ export const SpecialLayerId = {
SPECIAL_GROUP: "group_special", // 特殊组
COLOR: "special_color", // 颜色图层
}
/**
* 特殊类型
*/
export const SpecialType = {
PRINT_TRIMS_G: "print_trims_group", // 印花和元素图层组
PRINT_TRIMS_L: "print_trims_layer", // 印花和元素图层
PRINT_TRIMS_O: "print_trims_object", // 印花和元素图层对象
REPEAT_L: "repeat_layer",// 平铺图层
REPEAT_O: "repeat_object",// 平铺图层对象
}
@@ -180,6 +191,7 @@ export function createLayer(options = {}) {
generateId("layer_") ||
`layer_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
return {
...options,
id: id,
// 图层基本属性
name: options.name || `图层 ${id.substring(id.lastIndexOf("_") + 1)}`,
@@ -191,7 +203,7 @@ export function createLayer(options = {}) {
isHidenDragHandle: options.isHidenDragHandle || false,
isDisableUnlock: options.isDisableUnlock || false,
isFixedOther: options.isFixedOther || false,
isFixedClipMask: options.isFixedClipMask || false,
isPrintTrimsGroup: options.isPrintTrimsGroup || false,
// 确保不是背景图层
isBackground: false,

View File

@@ -177,7 +177,7 @@ export function simplifyLayers(layers, excludedLayers = []) {
isBackground: layer.isBackground || false,
isFixed: layer.isFixed || false,
isFixedOther: layer.isFixedOther || false,
isFixedClipMask: layer.isFixedClipMask || false,
isPrintTrimsGroup: layer.isPrintTrimsGroup || false,
isHidenDragHandle: layer.isHidenDragHandle || false,
isDisableUnlock: layer.isDisableUnlock || false,
clippingMask:

View File

@@ -63,9 +63,10 @@ export async function restoreFabricObject(serializedObject, canvas) {
* 获取对象黑白通道画布
* @param {fabric.Object} object - 要处理的 fabric 对象
* @param {ImageData} revData - 相反的ImageData白通道的相同位置是否为透明revData为白色为透明黑色为不透明
* @param {number} diff - 差值,默认 25
* @returns {HTMLCanvasElement|null} 包含黑白通道的画布,或 null 如果失败
*/
export function getObjectAlphaToCanvas(object, revData) {
export function getObjectAlphaToCanvas(object, revData, diff = 30) {
const image = object.getElement();
const { width, height } = image;
if (!width || !height) {
@@ -88,7 +89,7 @@ export function getObjectAlphaToCanvas(object, revData) {
const revB = revData?.data[i + 2] || 0;
const revA = revData?.data[i + 3] || 0;
if (r || g || b || a) {
if (revR || revG || revB || revA) {
if (revR > diff || revG > diff || revB > diff || revA > diff) {
data.data[i + 0] = 0;
data.data[i + 1] = 0;
data.data[i + 2] = 0;

View File

@@ -0,0 +1,346 @@
<template>
<div class="demo">
<div
class="control"
:class="{ active: item.id === activeId }"
v-for="(item, index) in list"
:key="item.id"
@click="onSelect(item.id)"
>
<div>
<b>{{ item.name }}</b>
<button
v-if="index !== 0"
@click="onMove(item.id, list[index - 1].id)"
>
</button>
<button
v-if="index !== list.length - 1"
@click="onMove(item.id, list[index + 1].id)"
>
</button>
<button @click.stop="onDelete(item.id)">删除</button>
</div>
<div>
<span>偏移X</span>
<input type="number" v-model="item.location[0]" />
</div>
<div>
<span>偏移Y</span>
<input type="number" v-model="item.location[1]" />
</div>
<div>
<span>角度</span>
<input type="number" v-model="item.angle" />
</div>
<div>
<span>缩放</span>
<input type="number" v-model="item.scale[0]" />
</div>
<div>
<span>水平间距</span>
<input type="number" v-model="item.object.gapX" />
</div>
<div>
<span>垂直间距</span>
<input type="number" v-model="item.object.gapY" />
</div>
<hr />
<hr />
<div>
<span>缩放X</span>
<input type="number" v-model="item.object.scaleX" step="0.1" />
</div>
<div>
<span>缩放Y</span>
<input type="number" v-model="item.object.scaleY" step="0.1" />
</div>
<div>
<span>X</span>
<input type="number" v-model="item.object.left" />
</div>
<div>
<span>Y</span>
<input type="number" v-model="item.object.top" />
</div>
<div>
<span>角度</span>
<input type="number" v-model="item.object.angle" />
</div>
<div>
<span>透明度</span>
<input
type="range"
v-model="item.object.opacity"
step="0.1"
min="0"
max="1"
/>
</div>
</div>
<button @click="onAdd">添加</button>
<div class="box">
<pingpu
:list="list"
ref="pingpuRef"
@change-canvas="updateCanvas"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from "vue";
import pingpu from "./index.vue";
const ACTIONS = {
ADD: "add",
UPDATE: "update",
DELETE: "delete",
SELECT: "select",
SORT: "sort",
};
const convertDotNotationToBracket = (str) =>
str.replace(/(?:^|\.)(\d+)(?=\.|$)/g, "[#$1]").replace(/\[#/g, "[");
const activeId = ref("1");
const list = ref([
{
id: "1",
ifSingle: false,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [0, 0],
scale: [1, 1],
angle: 0,
name: "Print1",
priority: 1,
object: {
top: 0,
left: 0,
scaleX: 1,
scaleY: 1,
opacity: 1,
angle: 0,
flipX: false,
flipY: false,
blendMode: "multiply",
gapX: 10,
gapY: 20,
},
},
{
id: "2",
ifSingle: false,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [0, 0],
scale: [2, 2],
angle: -45,
name: "Print2",
priority: 1,
object: {
top: 150,
left: 250,
scaleX: 0.5,
scaleY: 0.5,
opacity: 1,
angle: 45,
flipX: false,
flipY: false,
blendMode: "multiply",
gapX: 0,
gapY: 0,
},
},
]);
// 深拷贝
const deepCopy = (obj) => JSON.parse(JSON.stringify(obj));
const oldList = ref(deepCopy(list.value));
const pingpuRef = ref(null);
const updateCanvas = (arr) => {
oldList.value = deepCopy(list.value);
arr.forEach((item) => {
list.value.forEach((v) => {
if (item.action === ACTIONS.UPDATE) {
if (v.id === item.id) {
if (item.action === ACTIONS.UPDATE) {
try {
const key = item.key;
const str = /^\[/.test(item.key)
? "v" + key
: "v." + key;
eval(`${str} = item.value`);
} catch (error) {
console.error(error);
}
}
}
} else if (item.action === ACTIONS.SELECT) {
activeId.value = item.id;
}
});
});
};
const onSelect = (id) => {
activeId.value = id;
pingpuRef.value.updataList([
{
id: id,
action: ACTIONS.SELECT,
},
]);
};
const onMove = (id, id2) => {
const obj1 = list.value.find((v) => v.id === id);
const obj2 = list.value.find((v) => v.id === id2);
const index1 = list.value.indexOf(obj1);
const index2 = list.value.indexOf(obj2);
if (index1 < index2) {
list.value.splice(index2, 0, list.value.splice(index1, 1)[0]);
} else {
list.value.splice(index1, 0, list.value.splice(index2, 1)[0]);
}
const ids = list.value.map((v) => v.id);
pingpuRef.value.updataList([
{
action: ACTIONS.SORT,
ids,
},
]);
};
const onDelete = (id) => {
list.value = list.value.filter((v) => v.id !== id);
pingpuRef.value.updataList([
{
id: id,
action: ACTIONS.DELETE,
},
]);
};
const onAdd = () => {
const obj = {
id: Date.now().toString(),
ifSingle: false,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [0, 0],
scale: [1, 1],
angle: 0,
name: "Print" + (list.value.length + 1),
priority: 1,
object: {
top: 0,
left: 0,
scaleX: 1,
scaleY: 1,
opacity: 1,
angle: 0,
flipX: false,
flipY: false,
blendMode: "multiply",
gapX: 10,
gapY: 20,
},
};
list.value.push(obj);
pingpuRef.value.updataList([
{
action: ACTIONS.ADD,
data: obj,
},
]);
};
watch(
() => list.value,
() => updateList(),
{ deep: true }
);
// 监听列表变化属性变更
const updateList = () => {
const changeList = [];
oldList.value.forEach((oldItem) => {
const newItem = list.value.find((v) => v.id === oldItem.id);
if (newItem) {
const arr = findDiffProps(oldItem, newItem);
arr.forEach((item) => {
changeList.push({
...item,
id: oldItem.id,
action: ACTIONS.UPDATE,
key: convertDotNotationToBracket(item.key),
});
});
} else {
changeList.push({
id: oldItem.id,
action: ACTIONS.DELETE,
});
}
});
oldList.value = deepCopy(list.value);
if (changeList.length) {
pingpuRef.value.updataList(changeList);
}
};
// 递归查找不同的属性
const findDiffProps = (obj1, obj2, diffProps = [], path = "") => {
for (const key in obj1) {
if (typeof obj1[key] === "object") {
findDiffProps(obj1[key], obj2[key], diffProps, `${path}${key}.`);
} else if (obj1[key] !== obj2[key]) {
diffProps.push({
key: `${path}${key}`,
value: obj2[key],
});
}
}
return diffProps;
};
</script>
<style lang='less' scoped>
.demo {
> .control {
display: inline-block;
border: 1px solid #000;
padding: 10px;
margin: 10px;
border-radius: 10px;
&.active {
border-color: rgb(17, 68, 223);
box-shadow: 0 0 5px rgb(17, 68, 223);
}
> div {
display: flex;
align-items: center;
margin-bottom: 5px;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
> span {
width: 80px;
}
> input:not([type="range"]) {
padding-left: 10px;
border-radius: 5px;
}
}
}
> .box {
width: 400px;
height: 500px;
overflow: hidden;
border: 1px solid #000;
}
}
</style>

View File

@@ -0,0 +1,305 @@
<template>
<div class="pingpu" ref="el"><canvas ref="canvasRef"></canvas></div>
</template>
<script setup>
import { fabric } from "fabric-with-all";
import { ref, watch, onMounted } from "vue";
const KEYS = {
FILL_X: "location[0]",
FILL_Y: "location[1]",
FILL_ANGLE: "angle",
FILL_SCALEX: "scale[0]",
FILL_SCALEY: "scale[1]",
FILL_GAPX: "object.gapX",
FILL_GAPY: "object.gapY",
O_TOP: "object.top",
O_LEFT: "object.left",
O_SCALE_X: "object.scaleX",
O_SCALE_Y: "object.scaleY",
O_OPACITY: "object.opacity",
O_ANGLE: "object.angle",
O_FLIPX: "object.flipX",
O_FLIPY: "object.flipY",
O_BLENDMODE: "object.blendMode",
};
const ACTIONS = {
ADD: "add",
SELECT: "select",
UPDATE: "update",
DELETE: "delete",
SORT: "sort",
};
const emit = defineEmits(["change-canvas"]);
const props = defineProps({
list: { type: Array, default: () => [] },
});
const el = ref(null);
const canvasRef = ref(null);
const observer = ref(null);
var canvas = null;
onMounted(async () => {
initCanvas();
await setCanvasData();
let throttleTimeout = null;
let lastRunTime = 0;
let trailingTimeout = null;
observer.value = new ResizeObserver((entries) => {
const now = Date.now();
const throttleDelay = 100;
if (!throttleTimeout) {
updateCanvasSize();
lastRunTime = now;
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
}, throttleDelay);
} else {
clearTimeout(trailingTimeout);
trailingTimeout = setTimeout(() => {
updateCanvasSize();
lastRunTime = Date.now();
}, throttleDelay);
}
});
observer.value.observe(el.value);
});
onBeforeUnmount(() => {
observer.value.disconnect();
unbindEvent();
});
const initCanvas = () => {
canvas = new fabric.Canvas(canvasRef.value, {
selection: false,
preserveObjectStacking: true,
});
canvas.setWidth(el.value.offsetWidth);
canvas.setHeight(el.value.offsetHeight);
bindEvent();
};
const updateCanvasSize = async () => {
canvas.setWidth(el.value.offsetWidth);
canvas.setHeight(el.value.offsetHeight);
await setCanvasData();
};
// 绑定事件
const bindEvent = () => {
canvas.on("object:modified", onObjectModified);
canvas.on("selection:created", onObjectSelected);
canvas.on("selection:updated", onObjectSelected);
};
// 解绑事件
const unbindEvent = () => {
canvas.off("object:modified", onObjectModified);
canvas.off("selection:created", onObjectSelected);
canvas.off("selection:updated", onObjectSelected);
};
// 处理对象修改事件
const onObjectModified = (e) => {
console.log(e);
const object = e.target;
const action = e.action;
const list = [];
const id = object.id;
if (action === "drag" || action === "rotate") {
list.push({
id: id,
action: ACTIONS.UPDATE,
key: KEYS.O_TOP,
value: object.top,
});
list.push({
id: id,
action: ACTIONS.UPDATE,
key: KEYS.O_LEFT,
value: object.left,
});
if (action === "rotate") {
list.push({
id: id,
action: ACTIONS.UPDATE,
key: KEYS.O_ANGLE,
value: object.angle,
});
}
} else if (action === "scale") {
list.push({
id: id,
action: ACTIONS.UPDATE,
key: KEYS.O_SCALE_X,
value: object.scaleX,
});
list.push({
id: id,
action: ACTIONS.UPDATE,
key: KEYS.O_SCALE_Y,
value: object.scaleY,
});
}
emit("change-canvas", list);
};
// 对象选中
const onObjectSelected = (e) => {
const id = e.selected[0].id;
const list = [
{
id: id,
action: ACTIONS.SELECT,
},
];
emit("change-canvas", list);
};
const urlToCanvas = (url) => {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
url,
(object) => {
const imgElement = object.getElement();
// 创建透明 Canvas
const tcanvas = document.createElement("canvas");
tcanvas.width = imgElement.width;
tcanvas.height = imgElement.height;
const ctx = tcanvas.getContext("2d");
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(imgElement, 0, 0);
resolve(tcanvas);
},
{ crossOrigin: "anonymous" }
);
});
};
const setCanvasData = async () => {
canvas.clear();
for (let i = 0; i < props.list.length; i++) {
let item = props.list[i];
await addObject(item);
}
canvas.renderAll();
};
const addObject = async (item) => {
const cwidth = canvas.width;
const cheight = canvas.height;
let pattern = await setFill(item);
let rect = new fabric.Rect({
id: item.id,
width: cwidth,
height: cheight,
fill: pattern,
...item.object,
});
canvas.add(rect);
};
const setFill = async (item) => {
if (!item) return null;
console.log(item.scale);
const cwidth = canvas.width;
const cheight = canvas.height;
let image = await urlToCanvas(item.path);
let offsetX = item.location[0];
let offsetY = item.location[1];
let scaleX = ((cwidth / image.width) * item.scale[0]) / 5;
let scaleY = ((cheight / image.height) * item.scale[1]) / 5;
let scale = cwidth > cheight ? scaleX : scaleY;
let angle = item.angle;
let gapX = item.object.gapX;
let gapY = item.object.gapY;
let patternTransform = fabric.util.composeMatrix({
scaleX: scale,
scaleY: scale,
angle: angle,
});
// 创建透明 Canvas
let tcanvas = document.createElement("canvas");
tcanvas.width = image.width + gapX;
tcanvas.height = image.height + gapY;
let ctx = tcanvas.getContext("2d");
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(image, 0, 0);
let pattern = new fabric.Pattern({
source: tcanvas,
repeat: "repeat",
patternTransform,
offsetX, // 水平偏移
offsetY, // 垂直偏移
});
return pattern;
};
const updataList = async (list) => {
const objects = canvas.getObjects();
// list.forEach((item) => {
for (let i = 0; i < list.length; i++) {
let item = list[i];
let object = objects.find((o) => o.id === item.id);
if (item.action === ACTIONS.UPDATE) {
let value = item.value;
switch (item.key) {
case KEYS.O_TOP:
object.set("top", value);
break;
case KEYS.O_LEFT:
object.set("left", value);
break;
case KEYS.O_OPACITY:
object.set("opacity", value);
break;
case KEYS.O_SCALE_X:
object.set("scaleX", value);
break;
case KEYS.O_SCALE_Y:
object.set("scaleY", value);
break;
case KEYS.O_ANGLE:
object.set("angle", value);
break;
case KEYS.O_FLIPX:
object.set("flipX", value);
break;
case KEYS.O_FLIPY:
object.set("flipY", value);
break;
case KEYS.O_BLENDMODE:
object.set("blendMode", value);
break;
case KEYS.FILL_X:
case KEYS.FILL_Y:
case KEYS.FILL_ANGLE:
case KEYS.FILL_SCALEX:
case KEYS.FILL_SCALEY:
case KEYS.FILL_GAPX:
case KEYS.FILL_GAPY:
let pattern = await setFill(
props.list.find((v) => v.id === item.id)
);
object.set("fill", pattern);
break;
}
} else if (item.action === ACTIONS.SELECT) {
canvas.setActiveObject(object);
} else if (item.action === ACTIONS.SORT) {
let ids = item.ids;
canvas.clear();
for (let j = 0; j < ids.length; j++) {
let id = ids[j];
let object = objects.find((o) => o.id === id);
canvas.add(object);
}
canvas.renderAll();
} else if (item.action === ACTIONS.DELETE) {
canvas.remove(object);
} else if (item.action === ACTIONS.ADD) {
await addObject(item.data);
}
}
canvas.renderAll();
};
defineExpose({
updataList,
});
</script>
<style lang='less' scoped>
.pingpu {
width: 100%;
height: 100%;
}
</style>

View File

@@ -71,8 +71,8 @@ const editorConfig = {
const exportImage = async () => {
if (canvasEditor.value) {
const base64 = await canvasEditor.value.exportImage({
isContainFixed: false, // 是否导出底图
isContainFixedOther: false, // 是否导出其他固定图层
isContainFixed: true, // 是否导出底图
isContainFixedOther: true, // 是否导出其他固定图层
isContainBg: false, // 是否导出背景
isEnhanceImg: false, // 是否导出增强图片
});
@@ -109,7 +109,33 @@ const exportExtraInfo = async () => {
}
};
// 更新其他图层颜色
const updateOtherLayersColor = async () => {
const obj = {
color: {rgba: {r:255,g:255,b:0,a:1}},
}
await canvasEditor?.value?.updateOtherLayers?.(obj);
};
// 更新其他图层印花
const updateOtherLayersPrint = async () => {
// document.querySelector(".app-container").style.width = "50vw"
const obj = {
printObject: {
prints: [
{
ifSingle: true,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780],
scale: [0.3, 0.4],
angle: 0,
},
]
},
}
await canvasEditor?.value?.updateOtherLayers?.(obj);
};
const changeCanvas = (command) => {
console.log(command);
@@ -219,6 +245,20 @@ const customToolsList = ref([
label: "导E",
class: "export-btn",
},
{
id: "updateExtraInfo_color",
title: "更新颜色",
action: updateOtherLayersColor,
label: "更C",
class: "export-btn",
},
{
id: "updateExtraInfo_print",
title: "更新印花",
action: updateOtherLayersPrint,
label: "更P",
class: "export-btn",
},
{
id: "exportPNG",
title: "导出PNG", //导出画布图片
@@ -295,22 +335,22 @@ const otherData = {
color: {rgba: {r:255,g:0,b:0,a:1}},
printObject: {
prints: [
{
ifSingle: false,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780],
scale: [0.5 * 0.7, 0.272541 * 0.7],
angle: 0,
},
// {
// ifSingle: false,
// level2Type: "Pattern",
// designType: "Library",
// path: "/src/assets/images/canvas/yinhua1.jpg",
// location: [250, 780],
// scale: [0.3, 0.4],
// angle: 0,
// },
{
ifSingle: true,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780],
scale: [0.5 * 0.7, 0.272541 * 0.7],
location: [550, 650],
scale: [0.15, 0.2],
angle: 0,
},
{
@@ -318,8 +358,8 @@ const otherData = {
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [300, 500],
scale: [0.5 * 0.4, 0.272541 * 0.4],
location: [700, 400],
scale: [0.1, 0.133],
angle: 0,
}
]

View File

@@ -1,156 +0,0 @@
<template>
<div class="pingpu" ref="el"><canvas ref="canvasRef"></canvas></div>
</template>
<script setup>
import { fabric } from "fabric-with-all";
import { ref, watch, onMounted } from "vue";
const props = defineProps({
url: { type: String, required: true },
offsetX: { type: Number, default: 0 }, // px
offsetY: { type: Number, default: 0 }, // px
angle: { type: Number, default: 0 }, // 角度
scale: { type: Number, default: 100 }, // %
gapX: { type: Number, default: 0 }, // px
gapY: { type: Number, default: 0 }, // px
});
watch(
() => props.url,
() => getOriginalImage()
);
watch(
() => [
props.offsetX,
props.offsetY,
props.angle,
props.scale,
props.gapX,
props.gapY,
],
() => setCanvasData()
);
const el = ref(null);
const canvasRef = ref(null);
const canvas = ref(null);
const observer = ref(null);
const id = "asfs123121sfe";
onMounted(async () => {
initCanvas();
await getOriginalImage();
setCanvasData();
let throttleTimeout = null;
let lastRunTime = 0;
let trailingTimeout = null;
observer.value = new ResizeObserver((entries) => {
const now = Date.now();
const throttleDelay = 100;
if (!throttleTimeout) {
updateCanvasSize();
lastRunTime = now;
throttleTimeout = setTimeout(() => {
throttleTimeout = null;
}, throttleDelay);
} else {
clearTimeout(trailingTimeout);
trailingTimeout = setTimeout(() => {
updateCanvasSize();
lastRunTime = Date.now();
}, throttleDelay);
}
});
observer.value.observe(el.value);
});
onBeforeUnmount(() => {
observer.value.disconnect();
});
const initCanvas = () => {
canvas.value = new fabric.Canvas(canvasRef.value, {
selection: false,
evented: false,
});
canvas.value.setWidth(el.value.offsetWidth);
canvas.value.setHeight(el.value.offsetHeight);
};
const updateCanvasSize = () => {
canvas.value.setWidth(el.value.offsetWidth);
canvas.value.setHeight(el.value.offsetHeight);
setCanvasData();
};
const originalImage = ref(null);
const getOriginalImage = () => {
return new Promise((resolve, reject) => {
fabric.Image.fromURL(
props.url,
(object) => {
const imgElement = object.getElement();
// 创建透明 Canvas
const tcanvas = document.createElement("canvas");
tcanvas.width = imgElement.width;
tcanvas.height = imgElement.height;
const ctx = tcanvas.getContext("2d");
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(imgElement, 0, 0);
originalImage.value = tcanvas;
resolve(tcanvas);
},
{ crossOrigin: "anonymous" }
);
});
};
const setCanvasData = () => {
canvas.value.getObjects().forEach((obj) => {
if (obj.id === id) canvas.value.remove(obj);
});
const image = originalImage.value;
const cwidth = canvas.value.width;
const cheight = canvas.value.height;
const offsetX = props.offsetX;
const offsetY = props.offsetY;
const scaleX = ((cwidth / image.width) * (props.scale / 100)) / 5;
const scaleY = ((cheight / image.height) * (props.scale / 100)) / 5;
const scale = cwidth > cheight ? scaleX : scaleY;
const angle = props.angle;
const gapX = props.gapX;
const gapY = props.gapY;
const patternTransform = fabric.util.composeMatrix({
scaleX: scale,
scaleY: scale,
angle: angle,
});
// 创建透明 Canvas
const tcanvas = document.createElement("canvas");
tcanvas.width = image.width + gapX;
tcanvas.height = image.height + gapY;
const ctx = tcanvas.getContext("2d");
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
ctx.drawImage(image, 0, 0);
const pattern = new fabric.Pattern({
source: tcanvas,
repeat: "repeat",
patternTransform,
offsetX, // 水平偏移
offsetY, // 垂直偏移
});
const rect = new fabric.Rect({
id,
width: cwidth,
height: cheight,
top: 0,
left: 0,
// scaleX: 1,
// scaleY: 1,
fill: pattern,
evented: false,
selectable: false,
});
canvas.value.add(rect);
canvas.value.renderAll();
};
</script>
<style lang='less' scoped>
.pingpu {
width: 100%;
height: 100%;
}
</style>

View File

@@ -1,73 +1,95 @@
<template>
<div class="test">
<div class="control">
<div>
<span>偏移X</span>
<input type="number" v-model="data.offsetX" />
</div>
<div>
<span>偏移Y</span>
<input type="number" v-model="data.offsetY" />
</div>
<div>
<span>角度</span>
<input type="number" v-model="data.angle" />
</div>
<div>
<span>缩放</span>
<input type="number" v-model="data.scale" />
</div>
<div>
<span>水平间距</span>
<input type="number" v-model="data.gapX" />
</div>
<div>
<span>垂直间距</span>
<input type="number" v-model="data.gapY" />
</div>
</div>
<div class="box">
<pingpu v-bind="data" />
</div>
<div class="test" ref="testRef">
<!-- <div class="canvas-container">
<canvas id="canvas"></canvas>
</div> -->
</div>
</template>
<script lang="ts" setup>
import pingpu from "./pingpu.vue";
const data = ref({
url: "/src/assets/images/canvas/yinhua1.jpg",
offsetX: 0, // px
offsetY: 0, // px
angle: 0, // 角度
scale: 100,// %
gapX: 0, // px
gapY: 0, // px
import { fabric } from "fabric-with-all";
import { ref, watch, onMounted } from "vue";
const imageUrl = "/src/assets/images/canvas/xiangaofenge.png";
const testRef = ref(null);
var canvas = null;
onMounted(() => {
canvas = new fabric.Canvas("canvas");
canvas.setWidth(800);
canvas.setHeight(600);
// fabric.Image.fromURL(imageUrl, (img) => {
// console.log(img.getElement());
// img.set({
// scaleX: 0.5,
// scaleY: 0.5,
// });
// canvas.add(img);
// });
const image = new Image();
image.src = imageUrl;
image.onload = () => {
const canvas1 = document.createElement("canvas");
const width = image.width / 2;
const height = image.height / 2;
canvas1.width = width;
canvas1.height = height;
const ctx1 = canvas1.getContext("2d");
ctx1.drawImage(image, 0, 0, width, height);
const data = ctx1.getImageData(0, 0, width, height);
testRef.value.appendChild(canvas1);
const testData = test(data);
const canvas2 = document.createElement("canvas");
canvas2.width = width;
canvas2.height = height;
const ctx2 = canvas2.getContext("2d");
ctx2.putImageData(testData, 0, 0);
testRef.value.appendChild(canvas2);
};
});
window.data = data;
// 获取图片轮廓点位
function test(data) {
// 找过的点位
const visited = [];
// 轮廓点位
const contours = [];
const { width, height } = data;
function cd(x, y) {
const arr = [
[x, y], // 当前
[x, y - 1], // 上
[x + 1, y], // 右
[x, y + 1], // 下
[x - 1, y], // 左
];
for (let i = 0; i < arr.length; i++) {
let [x1, y1] = arr[i];
if (x1 < 0 || x1 >= width || y1 < 0 || y1 >= height) continue;
let key = `${x1},${y1}`;
if (visited.includes(key)) continue;
visited.push(key);
let index = (y1 * width + x1) * 4;
let r = data.data[index];
let g = data.data[index + 1];
let b = data.data[index + 2];
let a = data.data[index + 3];
if ((r || g || b) && a) {
contours.push({ x: x1, y: y1 });
} else {
if (i > 0) cd(x1, y1);
}
}
}
cd(0, 0);
console.log(contours);
return data;
}
</script>
<style lang='less' scoped>
.test {
> .control {
margin-left: 10px;
margin-top: 10px;
> div {
display: flex;
align-items: center;
margin-bottom: 5px;
> span {
width: 80px;
}
>input{
padding-left: 10px;
border-radius: 5px;
}
}
}
> .box {
width: 100%;
max-width: 400px;
height: 500px;
.canvas-container {
display: inline-block;
border: 1px solid #000;
}
}
</style>