Merge branch 'dev_vite' of http://18.167.251.121:10003/aidlab/aida_front into dev_vite
This commit is contained in:
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 |
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
<!-- 遍历固定层 -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 图层项样式
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "",
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
346
src/component/Canvas/OverallCanvas/demo.vue
Normal file
346
src/component/Canvas/OverallCanvas/demo.vue
Normal 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>
|
||||
305
src/component/Canvas/OverallCanvas/index.vue
Normal file
305
src/component/Canvas/OverallCanvas/index.vue
Normal 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>
|
||||
@@ -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,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user