OverallCanvas

This commit is contained in:
李志鹏
2026-01-09 17:31:48 +08:00
parent 7af8bc96c8
commit 7fc0e3bace
3 changed files with 348 additions and 338 deletions

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,304 @@
<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;
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>