334 lines
9.2 KiB
Vue
334 lines
9.2 KiB
Vue
<template>
|
|
<div class="overall-canvas" ref="el"><canvas ref="canvasRef"></canvas></div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import TaskQueue from "../utils/TaskQueue.js";
|
|
import { fabric } from "fabric-with-all";
|
|
import { ref, watch, onMounted, nextTick } from "vue";
|
|
// 任务队列
|
|
const taskQueue = new TaskQueue();
|
|
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",
|
|
O_FILL_REPEAT: "object.fill_repeat",
|
|
};
|
|
const ACTIONS = {
|
|
ADD: "add",
|
|
SELECT: "select",
|
|
UPDATE: "update",
|
|
DELETE: "delete",
|
|
SORT: "sort",
|
|
};
|
|
const emit = defineEmits(["change-canvas", "init-canvas"]);
|
|
const props = defineProps({
|
|
// list: { type: Array, default: () => [] },
|
|
width: { type: Number, required: true },
|
|
height: { type: Number, required: true },
|
|
});
|
|
const list = ref([]);
|
|
const el = ref(null);
|
|
const canvasRef = ref(null);
|
|
const observer = ref(null);
|
|
var canvas = null;
|
|
onMounted(async () => {
|
|
initCanvas();
|
|
// taskQueue.addTask(async () => await setCanvasData());
|
|
emit("init-canvas", list.value);
|
|
let throttleDelay = 100;
|
|
let trailingTimeout = null;
|
|
observer.value = new ResizeObserver((entries) => {
|
|
clearTimeout(trailingTimeout);
|
|
trailingTimeout = setTimeout(() => {
|
|
console.log("OverallCanvas: resize");
|
|
taskQueue.addTask(async () => await updateCanvasSize());
|
|
}, throttleDelay);
|
|
});
|
|
observer.value.observe(el.value);
|
|
});
|
|
onBeforeUnmount(() => {
|
|
observer.value.disconnect();
|
|
unbindEvent();
|
|
});
|
|
const initCanvas = () => {
|
|
console.log("OverallCanvas: 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 () => {
|
|
console.log("OverallCanvas: updateCanvasSize");
|
|
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("OverallCanvas: onObjectModified", e);
|
|
const object = e.target;
|
|
const action = e.action;
|
|
const list = [];
|
|
const token = object.token;
|
|
if (action === "drag" || action === "rotate") {
|
|
list.push({
|
|
token,
|
|
action: ACTIONS.UPDATE,
|
|
key: KEYS.O_TOP,
|
|
value: (props.height / canvas.height) * object.top,
|
|
});
|
|
list.push({
|
|
token,
|
|
action: ACTIONS.UPDATE,
|
|
key: KEYS.O_LEFT,
|
|
value: (props.width / canvas.width) * object.left,
|
|
});
|
|
if (action === "rotate") {
|
|
list.push({
|
|
token,
|
|
action: ACTIONS.UPDATE,
|
|
key: KEYS.O_ANGLE,
|
|
value: object.angle,
|
|
});
|
|
}
|
|
} else if (action === "scale") {
|
|
list.push({
|
|
token,
|
|
action: ACTIONS.UPDATE,
|
|
key: KEYS.O_SCALE_X,
|
|
value: object.scaleX,
|
|
});
|
|
list.push({
|
|
token,
|
|
action: ACTIONS.UPDATE,
|
|
key: KEYS.O_SCALE_Y,
|
|
value: object.scaleY,
|
|
});
|
|
}
|
|
emit("change-canvas", list);
|
|
};
|
|
// 对象选中
|
|
const onObjectSelected = (e) => {
|
|
const token = e.selected[0].token;
|
|
const list = [{ token, action: ACTIONS.SELECT }];
|
|
emit("change-canvas", list);
|
|
};
|
|
// 删除对象
|
|
const onDeleteItem = (object) => {
|
|
const list = [{ token: object.token, action: ACTIONS.DELETE }];
|
|
emit("change-canvas", list);
|
|
DeleteItemByToken(object.token);
|
|
canvas.renderAll();
|
|
};
|
|
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();
|
|
console.log("OverallCanvas: setCanvasData", list.value);
|
|
for (let i = 0; i < list.value.length; i++) {
|
|
let item = list.value[i];
|
|
await addObject(item);
|
|
}
|
|
canvas.renderAll();
|
|
};
|
|
const addObject = async (item) => {
|
|
const token = item.token;
|
|
const cwidth = canvas.width;
|
|
const cheight = canvas.height;
|
|
let pattern = await setFill(item);
|
|
let rect = new fabric.Rect({
|
|
token: token,
|
|
width: cwidth,
|
|
height: cheight,
|
|
fill: pattern,
|
|
...item.object,
|
|
top: item.object.top / (props.height / canvas.height),
|
|
left: item.object.left / (props.width / canvas.width),
|
|
onDelete: (v) => onDeleteItem(v),
|
|
});
|
|
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 scaleX = ((cwidth / image.width) * item.scale[0]) / 5;
|
|
let scaleY = ((cheight / image.height) * item.scale[1]) / 5;
|
|
let scale = cwidth > cheight ? scaleX : scaleY;
|
|
let offsetX =
|
|
(item.location[0] * cwidth) / props.width + (image.width * scale) / 2;
|
|
let offsetY =
|
|
(item.location[1] * cheight) / props.height +
|
|
(image.height * scale) / 2;
|
|
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: item.object?.fill_repeat || "repeat",
|
|
patternTransform,
|
|
offsetX, // 水平偏移
|
|
offsetY, // 垂直偏移
|
|
});
|
|
return pattern;
|
|
};
|
|
const updataList = async (list_) => {
|
|
const cd = async (list_) => {
|
|
console.log("OverallCanvas: updataList", list_);
|
|
const objects = canvas.getObjects();
|
|
for (let i = 0; i < list_.length; i++) {
|
|
let item = list_[i];
|
|
let object = objects.find((o) => o.token === item.token);
|
|
if (item.action === ACTIONS.UPDATE) {
|
|
if (!object) continue;
|
|
let value = item.value;
|
|
switch (item.key) {
|
|
case KEYS.O_TOP:
|
|
object.set(
|
|
"top",
|
|
value / (props.height / canvas.height)
|
|
);
|
|
break;
|
|
case KEYS.O_LEFT:
|
|
object.set(
|
|
"left",
|
|
value / (props.width / canvas.width)
|
|
);
|
|
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:
|
|
case KEYS.O_FILL_REPEAT:
|
|
let pattern = await setFill(
|
|
list.value.find((v) => v.token === item.token)
|
|
);
|
|
object.set("fill", pattern);
|
|
break;
|
|
}
|
|
} else if (item.action === ACTIONS.SELECT) {
|
|
if (object) canvas.setActiveObject(object);
|
|
} else if (item.action === ACTIONS.SORT) {
|
|
let tokens = item.tokens;
|
|
canvas.clear();
|
|
const list_ = [];
|
|
for (let j = 0; j < tokens.length; j++) {
|
|
let item_ = list.value.find((v) => v.token === tokens[j]);
|
|
if (item_) list_.push(item_);
|
|
let object = objects.find((o) => o.token === tokens[j]);
|
|
if (object) canvas.add(object);
|
|
}
|
|
list.value = list_;
|
|
canvas.renderAll();
|
|
} else if (item.action === ACTIONS.DELETE) {
|
|
DeleteItemByToken(item.token);
|
|
} else if (item.action === ACTIONS.ADD) {
|
|
DeleteItemByToken(item.data.token);
|
|
list.value.push(item.data);
|
|
await addObject(item.data);
|
|
}
|
|
}
|
|
canvas.renderAll();
|
|
};
|
|
taskQueue.addTask(async () => await cd(list_));
|
|
};
|
|
/** 删除 */
|
|
const DeleteItemByToken = (token) => {
|
|
list.value = list.value.filter((v) => v.token !== token);
|
|
const objects = canvas.getObjects().filter((o) => o.token === token);
|
|
objects.forEach((o) => canvas.remove(o));
|
|
};
|
|
defineExpose({
|
|
updataList,
|
|
});
|
|
</script>
|
|
|
|
<style lang='less' scoped>
|
|
.overall-canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style> |