首先这个画布是超出浏览器画布的限制的最大范围的;
需求:在一个大画布上标注 画矩形;还是使用的fablicjs库;可以查看我的另一个文章 详细介绍了使用
fablicjs画矩形和多边形,这篇主要是讲述我完成大画布功能的过程;
准备工作
首先我需要知道各大浏览器对canvas的限制
那么重点来了,考虑怎么完成大画布呢?我采用的方法是分割画布
正式开始
- 怎么分割
因为我的画布是纵向的,所以我只考虑高度分割,宽度撑满容器的宽度即可;
我们先假设画布90000,每个画布高1000;也就是说我们分割9个画布,先完成canvas的渲染;并且给每一个canvas绑定事件
// 在HTML中创建一个包含分割画布的容器元素,例如:
<div id="canvas-container" />
const canvasWidth = 1000; // 画布宽度
const canvasHeight = 90000; // 画布高度
const blockSize = 1000; // 小块宽度和高度
// 计算需要分割的小块数
const numBlocks = Math.ceil(canvasHeight / blockSize);
// 循环创建和显示小块画布
private mounted () {
this.init();
}
private init () {
// 先清空之前的canvas 和数组
const canvasContainer = document.getElementById('canvas-container') as any;
canvasContainer.innerHTML = '';
this.canvases = [];
// 循环创建和显示小块画布
for (let i = 0; i < this.numBlocks; i++) {
const canvasElement = document.createElement('canvas');
canvasElement.id = `canvas-${i}`;
canvasElement.width = this.width;
// 假设不是整数的话 看最后还剩下多少高度
canvasElement.height = i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1);
const canvasContainer = document.getElementById('canvas-container') as any;
// 添加每个部分的画布对象到画布容器中
canvasContainer.appendChild(canvasElement);
const canvas = new fabric.Canvas(`canvas-${i}`, {
width: this.width,
height: i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1)
});
// 给canvas绑定事件
canvas.on('mouse:down', (e) => { this.mouseDown(e, i); });
canvas.on('mouse:move', (e) => { this.mouseMove(e, i); });
canvas.on('mouse:up', this.mouseUp);
canvas.on('selection:updated', e => {
this.edit(e.target);
});
canvas.on('selection:created', e => {
this.edit(e.target);
});
canvas.on('object:modified', this.onChange);
canvas.on('object:scaling', e => {
this.onScaling(e.target, i);
});
canvas.on('object:moving', e => {
this.mouseMoving(e.target, i);
});
// canvases存放所有的画布
this.canvases.push(canvas);
}
}
// 注意需要在代码中引入 fabric.js 库,并确保正确加载。
`
- 我们接下来就可以完成跨画布画矩形了
在这个步骤中,我们面临的情况比较多
原本不跨画布->跨画布
原本不跨画布->跨画布->不跨画布
从各个方向的绘画
我的解决办法:跨的画布的图形 他们的的高度都是一致的。只是他们的top不一致罢了 同时给他们同一个uuid,用于之后给后端传数据的时候 同一个uuid的数据只需要一个即可;
private mouseDownRect (position: MousePosition, index: number) {
if (this.drawingObject) {
// 点击第二个点的时候
this.drawEnd(this.drawingObject);
return;
}
// 点击第一个点的时候 记录下来点击的第几个画布
this.activeCanvas = this.canvases[index];
const rect = this.drawRect([position.x, position.y, 0, 0]);
this.drawingObject = rect;
this.activeCanvas.add(rect);
}
// 鼠标移动
private mouseMove (e, index) {
if (!this.drawType || !this.drawingObject) {
return;
}
this[`mouseMove${this.drawType}`](this.transformMouse(e), index);
}
private mouseMoveRect (position: MousePosition, mouseMoveCanvasIndex: any) {
if (this.drawingObject) {
// 从第几个画布开始绘画的
const startIndex = Number(this.activeCanvas.lowerCanvasEl.id.split('-')[1]);
// 检查是否跨越相邻画布并绘制矩形 提到上面 因为取消渲染的时候也需要判断是从上到下还是从下到上
// getCanvasFromCoordinates 是返回当前结束的坐标在第几个画布
const adjacentCanvas = this.getCanvasFromCoordinates(position.x, position.y + mouseMoveCanvasIndex * this.blockSize);
// adjacentCanvas 返回null 则不执行下面
if (!adjacentCanvas) {
return;
}
const adjacentCanvasIndex = Number(adjacentCanvas.lowerCanvasEl.id.split('-')[1]);
// 如果一直在跨了画布。看是从上到下 还是从下到上
const fromTopToBottom = startIndex < adjacentCanvasIndex;
// 最后一个画布的index不是最开始的index
if (this.adjacentCanvasIndex > -1 && this.adjacentCanvasIndex !== startIndex) {
// 从上往下 清除下面的框。从下往上 清楚上面的框【不然每次都会在所跨画布新增多个矩形】
const clearBottom = startIndex < this.adjacentCanvasIndex;
// for (let i = startIndex + 1; i < this.adjacentCanvasIndex + 1; i++) {
for (let i = clearBottom ? (startIndex + 1) : (startIndex - 1);
clearBottom ? (i < this.adjacentCanvasIndex + 1) : (i > this.adjacentCanvasIndex - 1);
clearBottom ? i++ : i--) {
const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
this.canvases[i].remove(draweredItem);
}
}
if (adjacentCanvas && adjacentCanvas !== this.activeCanvas) {
// for (let i = startIndex + 1; i < adjacentCanvasIndex + 1; i++) {
for (let i = fromTopToBottom ? startIndex + 1 : startIndex - 1;
fromTopToBottom ? i < adjacentCanvasIndex + 1 : i > adjacentCanvasIndex - 1;
fromTopToBottom ? i++ : i--) {
// 重新绘画第一个画布的矩形 修改高度为图形的总高度
const width = position.x - this.drawingObject.get('left');
const height = position.y + (mouseMoveCanvasIndex - startIndex) * this.blockSize - this.drawingObject.get('top');
this.drawingObject.set({
width: width,
height: height,
adjCanvasNumber: adjacentCanvasIndex - startIndex
});
this.activeCanvas.renderAll();
// 已经渲染过的
// const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
// this.canvases[i].remove(draweredItem);
// 绘制跨画布的矩形 同一个uuid 同时记录他们跨了几个画布adjCanvasNumber;
const adjObject = this.drawRect([
this.drawingObject.get('left'),
-((this.blockSize * (i - startIndex)) - this.drawingObject.get('top')),
position.x - this.drawingObject.get('left'),
height],
{
uuid: this.drawingObject.uuid,
adjCanvasNumber: adjacentCanvasIndex - startIndex
});
this.canvases[i].add(adjObject);
// 最后的画布记录下来 如果用户跨画布 但是又取消跨画布 需要清楚所跨的画布的图形
if (i === adjacentCanvasIndex) {
this.adjacentCanvasIndex = i;
}
}
} else {
// 没有跨画布 则正常处理
const width = position.x - this.drawingObject.get('left');
const height = position.y - this.drawingObject.get('top');
this.drawingObject.set({
width: width,
height: height,
adjCanvasNumber: 0
// startCanvansIndex: startIndex
});
this.activeCanvas.renderAll();
}
}
}
// 获取包含指定坐标的画布
private getCanvasFromCoordinates (x, y) {
for (let i = 0; i < this.canvases.length; i++) {
const canvas = this.canvases[i];
const localPoint = canvas.getPointer({ x: x, y: y });
const canvasElement = canvas.getElement();
const canvasRect = canvasElement.parentElement.getBoundingClientRect();
if (
localPoint.x >= 0 &&
localPoint.x <= canvasRect.width &&
localPoint.y >= 0 &&
localPoint.y <= canvasRect.height
) {
return canvas;
}
}
return null;
}
// 画四边形 originTop是用于缩放和移动的时候新增的字段
private drawRect (points: Array<number>, others?: OthersConfigModel) {
const rect = new fabric.Rect({
type: DrawType.Rect,
uuid: createUuid(), // todo
left: points[0],
top: points[1],
width: points[2] || 0,
height: points[3] || 0,
objectCaching: false,
transparentCorners: false,
selectionColor: 'rgba(0,0,0,0)',
lockRotation: true,
strokeUniform: true,
// 移动时候需要 记录他移动了多少 来让同一个矩形uuid也移动
originLeft: points[0],
originTop: points[1],
...defaultRectStyle,
...this.rectStyle,
...others
});
// eslint-disable-next-line spellcheck/spell-checker
rect.setControlsVisibility({ mtr: false }); // 隐藏旋转点
return rect;
}
- 处理缩放+移动
原本一个画布->缩放跨画布
原本一个画布->缩放跨画布->取消跨画布
原本跨画布->缩放为一个画布
跨多个画布的缩放
不同方向的缩放
在处理缩放的时候 一开始面临了一个问题 就是假设是从上到下绘画的话 移动上面的矩形就会出现bug
private onScaling (object, index: number) {
// 得到所有同一个uuid的图形
const sameUuidObject = this.getObjectByUuid(object.uuid);
sameUuidObject.map(item => {
item.shape.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
left: object.left// 万一用户拉的左侧的,
});
this.canvases[item.canvasIndex].renderAll();
});
// 跨canvas的时候 防止他缩放的是上面 带来的bug
this.mouseMoving(object, index);
}
移动的情况
不能移动出超出范围外;左右不能移动出去,第一个和最后一个画布不能移动到上面/下面
private mouseMoving (object, index) {
const padding = -1.5; // 内容距离画布的空白宽度,主动设置
// 限制左右不能移出外面
object.setCoords();
const objBoundingBox = object.getBoundingRect();
if (objBoundingBox.left < padding) {
object.left = Math.max(object.left, object.left - objBoundingBox.left + padding);
}
if (objBoundingBox.left + objBoundingBox.width > object.canvas.width - padding) {
object.left = Math.min(object.left, object.canvas.width - objBoundingBox.width + object.left - objBoundingBox.left - padding);
}
// 如果是第一块 限制不能移到上面
if (!index && objBoundingBox.top < 0) {
object.top = Math.max(object.top, object.top - objBoundingBox.top);
} else if (index === this.numBlocks - 1 && objBoundingBox.top + objBoundingBox.height > object.canvas.height) {
// 如果是最后一块 不能移动到下面
object.top = Math.min(object.top, object.canvas.height - objBoundingBox.height + object.top - objBoundingBox.top);
}
// 跨框的情况下
if (object.adjCanvasNumber) {
const sameUuidObject = this.getObjectByUuid(object.uuid);
sameUuidObject.map(item => {
item.shape.left = object.left;
item.shape.top = item.shape.originTop - (object.originTop - object.top);
// 防止框一闪一闪的
item.shape.visible = true;
this.canvases[item.canvasIndex].renderAll();
this.canvases[item.canvasIndex].setActiveObject(item.shape);
});
// // 原本跨多个canvas 移动到下个画布 会消失
// if (object.adjCanvasNumber > 1) {
const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
if (index + adjCanvasNumber > object.adjCanvasNumber + sameUuidObject[0].canvasIndex) {
const lastRect = sameUuidObject[object.adjCanvasNumber].shape;
// 如果在同一个画布停顿两次 第一次就会留下
const draweredItem = this.canvases[index + adjCanvasNumber].getObjects().filter(x => x.uuid === object.uuid)[0];
if (draweredItem) {
return;
}
lastRect.set({
originTop: -(this.blockSize - lastRect.originTop)
});
const adjObject = this.drawRect([
object.left,
-(this.blockSize - lastRect.originTop),
object.width,
object.height],
{
uuid: object.uuid,
adjCanvasNumber: object.adjCanvasNumber
});
// 设置为同样的缩放比例
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY
});
this.canvases[index + adjCanvasNumber].add(adjObject);
this.canvases[index + adjCanvasNumber].setActiveObject(adjObject);
// 移动/缩放 导致跨的画布+1了
sameUuidObject.map(item => {
item.shape.adjCanvasNumber = object.adjCanvasNumber + 1;
});
}
// 多个画布的向上移动
if (object.top < 0) {
const draweredItem = this.canvases[index - 1].getObjects().filter(x => x.uuid === object.uuid)[0];
if (draweredItem) {
return;
}
const adjObject = this.drawRect([
object.left,
this.blockSize + object.originTop,
object.width,
object.height],
{
uuid: object.uuid,
adjCanvasNumber: object.adjCanvasNumber
});
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
// 不然就会出现闪一下
visible: false
});
this.canvases[index - 1].add(adjObject);
}
return;
}
// 原本不跨canvas 变量adjCanvasNumber变更
const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
if (adjCanvasNumber) {
object.adjCanvasNumber = adjCanvasNumber;
for (let i = index + 1; i < adjCanvasNumber + index + 1; i++) {
const adjObject = this.drawRect([
object.left,
-(this.blockSize - object.originTop),
object.width,
object.height],
// i !== adjCanvasNumber ? this.blockSize + 1 : object.height * object.scaleY],
{
uuid: object.uuid,
adjCanvasNumber: adjCanvasNumber
});
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
visible: false
});
this.canvases[i].add(adjObject);
// this.canvases[i].setActiveObject(adjObject);
}
}
// 不跨canvas 向上移动 缩放或移动导致跨 其他块向上移动或缩放 成了1;第一块向上移动还是0
// 用于之前不跨 移动或缩放导致的跨画布处理
if (object.top < 0) {
object.adjCanvasNumber = index ? 1 : 0;
}
}
- 删除多余图形
鼠标抬起的时候删除多余图形
private mouseUp (e) {
this.setCursor();
if (this[`mouseUp${this.drawType}`] && e.pointer) {
return;
}
if (this.selectObject()) {
// 删除不在范围内的图形 并且重置他的adjCanvasNumber
this.deleteExceedShape();
}
}
private deleteExceedShape () {
this.canvases.map(item => {
const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
if (draweredItem) {
// 向下超出的被删除
// 向上超出也要被删除 || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0
if (draweredItem.top > this.blockSize || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0) {
// 修改adjCanvasNumber
const sameUuidObject = this.getObjectByUuid(draweredItem.uuid);
sameUuidObject.map(sameUuiditem => {
sameUuiditem.shape.adjCanvasNumber = sameUuiditem.shape.adjCanvasNumber - 1;
});
// 删除
item.remove(draweredItem);
item.renderAll();
}
}
});
}
- 绘画过程中 鼠标为+字
在鼠标抬起和鼠标移动过程中设置。canvas提供方法setCursor
private mouseMove (e, index) {
// 画的时候使用十字
this.setCursor();
....
}
private mouseUp (e) {
this.setCursor();
....
}
private setCursor () {
if (this.drawType) {
this.canvases.map(item => {
item.setCursor('crosshair');
});
}
}
- 鼠标点击处存在多个矩形,处理点击哪个矩形
(这个问题其实是之前处理 fabric的遗留问题了,这里顺便记录一下吧!)
因为矩形也是有面积的,在fabric中他会按照渲染顺序来处理,可以理解为就是和z-index相关,也就意味着有些小的矩形如果在下面,我们鼠标点击的时候是获取不到的;
之前因为矩形不多,所以之前的处理方法是右键点击可以置于顶层或底层,方便他选中自己想要的;
新的解决方法:按照矩形的面积来判断;
在这个过程中 出现了一个问题 就是因为存在多个画布 所以必须保证点击的最小面积是在当前这个画布内 所以新增了一个canvasIndex字段
// 鼠标按下
private mouseDown (e, index) {
// 如果type不为‘’,则认为正在编辑图形,鼠标点击事件不触发画新图形
if (!this.drawType) {
// 设置点击的图形为活跃 从面积来判断
// 需要多增一个canvasIndex字段 点击的其他位置 在其他画布也有可能有更小面积的图形 所以需要保证画布统一
const allObjects: any = [];
this.canvases.map((canvas, canvasIndex) => {
if (canvas.getObjects().length) {
canvas.getObjects().map(object => {
allObjects.push({
shape: object,
canvasIndex: canvasIndex
});
});
}
});
let smallestRect: any = null;
const pointer = this.transformMouse(e);
// 遍历画布上的对象
allObjects.forEach(function (object) {
const shape = object.shape;
const shapeArea = Math.abs(shape.width) * shape.scaleX * shape.scaleY * Math.abs(shape.height);
const smallestArea = smallestRect ? Math.abs(smallestRect.width) * smallestRect.scaleX * smallestRect.scaleY * Math.abs(smallestRect.height) : 0;
if (shape.containsPoint(pointer) && object.canvasIndex === index && (smallestRect === null || shapeArea < smallestArea)) {
smallestRect = shape;
}
});
if (smallestRect !== null) {
this.setActiveByUuid(smallestRect.uuid);
}
this.operateAttribute.map(item => {
if (this.selectObject() && this.selectObject()[item]) {
this.selectObject()[item] = !this.rectStyle ? this.selectObject()[item] : this.rectStyle[item];
}
});
// 点击的哪个图形
this.$emit('clickShape', this.selectObject());
return;
}
// // 防止在画1的里面画2的时候 影响1
// if (this.selectObject() && !this.drawingObject) {
// this.operateAttribute.map(item => {
// this.selectObject()[item] = true;
// });
// }
this[`mouseDown${this.drawType}`](this.transformMouse(e), index);
}
最后附上全部代码
<template>
<div
class="drawer"
:style="`width: ${width}px; height: ${height}px`"
>
<div id="canvas-container" />
<div class="container">
<slot />
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { Component, Prop, Watch } from 'vue-property-decorator';
import { fabric } from 'fabric';
import { createUuid } from '@/utils/uuid';
export const enum DrawType {
Pointer = '',
Polygon = 'polygon',
Rect = 'rectangle',
Line = 'Line',
}
export interface MousePosition {
x: number;
y: number;
}
export interface Shape {
type: DrawType | string;
points: Array<MousePosition | number> | number[][];
content?: number|string;
others?: OthersConfigModel;
}
export interface RectModel {
left: number;
top: number;
width: number;
height: number;
scaleX?: number;
scaleY?: number;
}
export interface GetShapeByUuidModel {
shape: any;
canvasIndex: number;
}
const defaultRectStyle: OthersConfigModel = {
stroke: 'rgb(0, 232, 8)',
strokeWidth: 1,
fill: '',
opacity: 0.8,
cornerColor: 'rgb(0, 232, 8)',
cornerSize: 4,
selectionLineWidth: 0,
hasBorders: false
};
export interface OthersModel{
stroke: string;
opacity: number;
uuid: string | null;
strokeWidth: number;
fill: string;
cornerColor: string;
cornerStyle: string;
cornerSize: number;
radius: number;
selectionLineWidth: number;
clickIndex: number;
hasBorders: boolean;
hasControls: boolean; // 不显示边框点
selectable: boolean; // 禁止选中当前元素
lockMovementX: boolean; // 禁止元素移动
lockMovementY: boolean;
lockScalingX: boolean; // 禁止元素缩
lockScalingY: boolean;
visible: boolean; // 设置元素不可见
topToRect: number; // 文字到矩形顶部的距离
leftToRect: number; // 文字到矩形左边的距离
originX: string; // 旋转x轴 设置文字时候使用
originY: string; // 旋转y轴 设置文字时候使用
adjCanvasNumber: number; // 这个图形跨canvas的数字
}
export type OthersConfigModel = Partial<OthersModel>
@Component
export default class ImageMarkDrawer extends Vue {
@Prop({
type: Object,
required: false,
default: () => {}
})
private rectStyle!: {};
@Prop({
type: Number,
required: false,
default: 780
})
private width!: number; // 画布宽度
@Prop({
type: Number,
required: false,
default: 580
})
private height!: number; // 画布高度
private blockSize = 5000; // 小块宽度和高度
// 计算需要分割的小块数
private numBlocks = Math.ceil(this.height / this.blockSize);
private canvases: any=[]; // canvas数组
private activeCanvas: any = null;
// private adjacentCanvas: any = null; // 跨canvas
private adjacentCanvasIndex = -1; // 跨canvas的最后一个index
private drawType = ''; // 绘画类型
private drawingObject: any = null;
private adjObject: any = null; // 跨canvas的对象
// private drawingShape: any[] = [];
private operateAttribute = ['lockMovementX', 'lockMovementY', 'lockScalingX', 'lockScalingY']; // 画框的时候不能放大缩小移动;
// private canvasObjects: number[] | null = null; // 用户点击置于顶层/底层 导致顺序变化 ids变化 index也变化
private mounted () {
this.init();
}
private init () {
// 先清空之前的canvas 和数组
const canvasContainer = document.getElementById('canvas-container') as any;
canvasContainer.innerHTML = '';
this.canvases = [];
// 循环创建和显示小块画布
for (let i = 0; i < this.numBlocks; i++) {
const canvasElement = document.createElement('canvas');
canvasElement.id = `canvas-${i}`;
canvasElement.width = this.width;
canvasElement.height = i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1);
const canvasContainer = document.getElementById('canvas-container') as any;
// 添加每个部分的画布对象到画布容器中
canvasContainer.appendChild(canvasElement);
const canvas = new fabric.Canvas(`canvas-${i}`, {
width: this.width,
height: i < this.numBlocks - 1 ? this.blockSize : this.height - this.blockSize * (this.numBlocks - 1)
});
canvas.on('mouse:down', (e) => { this.mouseDown(e, i); });
canvas.on('mouse:move', (e) => { this.mouseMove(e, i); });
canvas.on('mouse:up', this.mouseUp);
canvas.on('selection:updated', e => {
this.edit(e.target);
});
canvas.on('selection:created', e => {
this.edit(e.target);
});
canvas.on('object:modified', this.onChange);
canvas.on('object:scaling', e => {
this.onScaling(e.target, i);
});
canvas.on('object:moving', e => {
this.mouseMoving(e.target, i);
});
this.canvases.push(canvas);
}
}
// 鼠标按下
private mouseDown (e, index) {
// 如果type不为line,则认为正在编辑图形,鼠标点击事件不触发画新图形
if (!this.drawType) {
// 设置点击的图形为活跃 从面积来判断
// 需要多增一个canvasIndex字段 点击的其他位置 在其他画布也有可能有更小面积的图形 所以需要保证画布统一
const allObjects: any = [];
this.canvases.map((canvas, canvasIndex) => {
if (canvas.getObjects().length) {
canvas.getObjects().map(object => {
allObjects.push({
shape: object,
canvasIndex: canvasIndex
});
});
}
});
let smallestRect: any = null;
const pointer = this.transformMouse(e);
// 遍历画布上的对象
allObjects.forEach(function (object) {
const shape = object.shape;
const shapeArea = Math.abs(shape.width) * shape.scaleX * shape.scaleY * Math.abs(shape.height);
const smallestArea = smallestRect ? Math.abs(smallestRect.width) * smallestRect.scaleX * smallestRect.scaleY * Math.abs(smallestRect.height) : 0;
if (shape.containsPoint(pointer) && object.canvasIndex === index && (smallestRect === null || shapeArea < smallestArea)) {
smallestRect = shape;
}
});
if (smallestRect !== null) {
this.setActiveByUuid(smallestRect.uuid);
}
this.operateAttribute.map(item => {
if (this.selectObject() && this.selectObject()[item]) {
this.selectObject()[item] = !this.rectStyle ? this.selectObject()[item] : this.rectStyle[item];
}
});
// 点击的哪个图形
this.$emit('clickShape', this.selectObject());
return;
}
// // 防止在画1的里面画2的时候 影响1
// if (this.selectObject() && !this.drawingObject) {
// this.operateAttribute.map(item => {
// this.selectObject()[item] = true;
// });
// }
this[`mouseDown${this.drawType}`](this.transformMouse(e), index);
}
private mouseDownRect (position: MousePosition, index: number) {
if (this.drawingObject) {
this.drawEnd(this.drawingObject);
return;
}
this.activeCanvas = this.canvases[index];
const rect = this.drawRect([position.x, position.y, 0, 0]);
this.drawingObject = rect;
this.activeCanvas.add(rect);
}
// 鼠标移动
private mouseMove (e, index) {
// 画的时候使用十字
this.setCursor();
if (!this.drawType || !this.drawingObject) {
return;
}
this[`mouseMove${this.drawType}`](this.transformMouse(e), index);
}
private mouseMoveRect (position: MousePosition, mouseMoveCanvasIndex: any) {
if (this.drawingObject) {
const startIndex = Number(this.activeCanvas.lowerCanvasEl.id.split('-')[1]);
// 检查是否跨越相邻画布并绘制矩形 提到上面 因为取消渲染的时候也需要判断是从上到下还是从下到上
const adjacentCanvas = this.getCanvasFromCoordinates(position.x, position.y + mouseMoveCanvasIndex * this.blockSize);
if (!adjacentCanvas) {
return;
}
const adjacentCanvasIndex = Number(adjacentCanvas.lowerCanvasEl.id.split('-')[1]);
const fromTopToBottom = startIndex < adjacentCanvasIndex;
// 最后一个画布的index不是最开始的index
if (this.adjacentCanvasIndex > -1 && this.adjacentCanvasIndex !== startIndex) {
// 从上往下 清除下面的框
const clearBottom = startIndex < this.adjacentCanvasIndex;
// for (let i = startIndex + 1; i < this.adjacentCanvasIndex + 1; i++) {
for (let i = clearBottom ? (startIndex + 1) : (startIndex - 1);
clearBottom ? (i < this.adjacentCanvasIndex + 1) : (i > this.adjacentCanvasIndex - 1);
clearBottom ? i++ : i--) {
const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
this.canvases[i].remove(draweredItem);
}
}
if (adjacentCanvas && adjacentCanvas !== this.activeCanvas) {
// for (let i = startIndex + 1; i < adjacentCanvasIndex + 1; i++) {
for (let i = fromTopToBottom ? startIndex + 1 : startIndex - 1;
fromTopToBottom ? i < adjacentCanvasIndex + 1 : i > adjacentCanvasIndex - 1;
fromTopToBottom ? i++ : i--) {
// 重新绘画开始的矩形 具体是下面这种这样方式 还是说高度+1 将它隐藏 待定
const width = position.x - this.drawingObject.get('left');
const height = position.y + (mouseMoveCanvasIndex - startIndex) * this.blockSize - this.drawingObject.get('top');
this.drawingObject.set({
width: width,
height: height,
adjCanvasNumber: adjacentCanvasIndex - startIndex
});
this.activeCanvas.renderAll();
// 已经渲染过的
const draweredItem = this.canvases[i].getObjects().filter(x => x.uuid === this.drawingObject.uuid)[0];
this.canvases[i].remove(draweredItem);
const adjObject = this.drawRect([
this.drawingObject.get('left'),
-((this.blockSize * (i - startIndex)) - this.drawingObject.get('top')),
position.x - this.drawingObject.get('left'),
height],
{
uuid: this.drawingObject.uuid,
adjCanvasNumber: adjacentCanvasIndex - startIndex
});
this.canvases[i].add(adjObject);
// 最后的画布记录下来 如果用户跨画布 但是又取消跨画布 需要清楚所跨的画布的图形
if (i === adjacentCanvasIndex) {
this.adjacentCanvasIndex = i;
}
}
} else {
const width = position.x - this.drawingObject.get('left');
const height = position.y - this.drawingObject.get('top');
this.drawingObject.set({
width: width,
height: height,
adjCanvasNumber: 0
// startCanvansIndex: startIndex
});
this.activeCanvas.renderAll();
}
}
}
private mouseUp (e) {
this.setCursor();
if (this[`mouseUp${this.drawType}`] && e.pointer) {
return;
}
if (this.selectObject()) {
// 删除不在范围内的图形 并且重置他的adjCanvasNumber
this.deleteExceedShape();
}
}
private setCursor () {
if (this.drawType) {
this.canvases.map(item => {
item.setCursor('crosshair');
});
}
}
private deleteExceedShape () {
this.canvases.map(item => {
const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
if (draweredItem) {
// 向下超出的被删除
// 向上超出也要被删除 || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0
if (draweredItem.top > this.blockSize || draweredItem.top + draweredItem.height * draweredItem.scaleY < 0) {
// 修改adjCanvasNumber
const sameUuidObject = this.getObjectByUuid(draweredItem.uuid);
sameUuidObject.map(sameUuiditem => {
sameUuiditem.shape.adjCanvasNumber = sameUuiditem.shape.adjCanvasNumber - 1;
});
// 删除
item.remove(draweredItem);
item.renderAll();
}
}
});
}
private async drawEnd (object) {
// 设置为当前活跃
this.setActiveByUuid(object.uuid);
// 如果高度< 0 则重新整一下他的高度
if (object.height < 0) {
const sameUuidObject = this.getObjectByUuid(object.uuid);
sameUuidObject.map(item => {
item.shape.set({
top: item.shape.height + item.shape.top,
originTop: item.shape.height + item.shape.top,
height: Math.abs(object.height)
});
this.canvases[item.canvasIndex].renderAll();
});
}
this.drawingObject = null;
this.adjObject = null;
this.adjacentCanvasIndex = -1;
// this.adjacentCanvas = null;
// this.drawingShape = [];
// if (this.canvasObjects) {
// this.canvasObjects.push(object); // 置于顶层/底层之后
// }
this.edit(object);
await this.$nextTick();
this.onChange();
this.$emit('drawEnd');
}
public setActiveByUuid (uuid: string) {
// 设置为当前活跃
this.canvases.map(item => {
const draweredItem = item.getObjects().filter(x => x.uuid === uuid)[0];
// discardActiveObject 抛弃当前处于活动状态的Object。
draweredItem ? item.setActiveObject(draweredItem) : item.discardActiveObject();
item.renderAll();
});
}
// 改变时候触发
private async onChange () {
let allObjects: any = [];
this.canvases.map((canvas, index) => {
if (canvas.getObjects().length) {
// 更改他的top
const getObjects = canvas.getObjects().map(shape => {
return {
...shape,
top: shape.top + index * this.blockSize
};
});
allObjects.push(...getObjects);
}
});
const hash = {};
const newData = [...allObjects];
allObjects = newData.reduce((item, next) => {
if (!hash[next.uuid]) {
hash[next.uuid] = true;
item.push(next);
}
return item;
}, []);
this.$emit('onChange', allObjects);
}
// points 数组 存放left、top、width、height 从后端渲染框到前端
public narrowRect (points: number[], others?: OthersConfigModel) {
if (!this.canvases.length) {
return;
}
const belowXPoint = points[1] + points[3];
const startCanvasIndex = Math.floor(points[1] / this.blockSize);
const adjCanvasNumber = Math.floor(belowXPoint / this.blockSize);
// 因为points传入的top是全局图片的 需要把它转化为某个canvans的相对top
const drawPoint = [
points[0],
points[1] - startCanvasIndex * this.blockSize,
points[2],
points[3]
];
// 跨框
if (startCanvasIndex !== adjCanvasNumber) {
// const uuid = createUuid();
for (let i = startCanvasIndex; i < adjCanvasNumber + 1; i++) {
const adjObject = this.drawRect([
points[0],
// i===startCanvasIndex?points[1]:i !== adjCanvasNumber?this.blockSize + 1 :
i === startCanvasIndex ? drawPoint[1] : -((this.blockSize * (i - startCanvasIndex)) - drawPoint[1]),
points[2],
points[3]],
{
...others,
// uuid: uuid,
adjCanvasNumber: adjCanvasNumber - startCanvasIndex
});
this.canvases[i].add(adjObject);
}
} else {
// 不跨框
this.canvases[startCanvasIndex].add(this.drawRect(drawPoint, {
...others,
adjCanvasNumber: 0
}));
}
}
public narrowLine (points: number[], others?: OthersConfigModel) {
const startCanvasIndex = Math.floor(points[1] / this.blockSize);
const drawPoint = [
points[0],
points[1] - startCanvasIndex * this.blockSize,
points[2],
points[3] - startCanvasIndex * this.blockSize
];
const adjObject = this.drawLine(drawPoint, others);
this.canvases[startCanvasIndex].add(adjObject);
}
private onScaling (object, index: number) {
// 跨了之后取消跨画布 sameUuidObject 依然还是2个 后续如果清楚另一个框的话 return的判断就需要换一下了
const sameUuidObject = this.getObjectByUuid(object.uuid);
sameUuidObject.map(item => {
item.shape.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
left: object.left// 万一用户拉的左侧的,
});
this.canvases[item.canvasIndex].renderAll();
});
// 跨canvas的时候 防止他缩放的是上面 带来的bug
this.mouseMoving(object, index);
}
private mouseMoving (object, index) {
const padding = -1.5; // 内容距离画布的空白宽度,主动设置
// 限制左右不能移出外面
object.setCoords();
const objBoundingBox = object.getBoundingRect();
if (objBoundingBox.left < padding) {
object.left = Math.max(object.left, object.left - objBoundingBox.left + padding);
}
if (objBoundingBox.left + objBoundingBox.width > object.canvas.width - padding) {
object.left = Math.min(object.left, object.canvas.width - objBoundingBox.width + object.left - objBoundingBox.left - padding);
}
// 如果是第一块 限制不能移到上面
if (!index && objBoundingBox.top < 0) {
object.top = Math.max(object.top, object.top - objBoundingBox.top);
} else if (index === this.numBlocks - 1 && objBoundingBox.top + objBoundingBox.height > object.canvas.height) {
// 如果是最后一块 不能移动到下面
object.top = Math.min(object.top, object.canvas.height - objBoundingBox.height + object.top - objBoundingBox.top);
}
// 跨框的情况下
if (object.adjCanvasNumber) {
const sameUuidObject = this.getObjectByUuid(object.uuid);
sameUuidObject.map(item => {
item.shape.left = object.left;
item.shape.top = item.shape.originTop - (object.originTop - object.top);
item.shape.visible = true;
// 不在这里做判断 因为跨框之后 删除就会操成停顿 解决办法:除了mouseUp写移除 在就是 设置新增的那个为移动状态 ???
this.canvases[item.canvasIndex].renderAll();
this.canvases[item.canvasIndex].setActiveObject(item.shape);
});
// // 原本跨多个canvas 移动到下个画布 会消失
// if (object.adjCanvasNumber > 1) {
const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
if (index + adjCanvasNumber > object.adjCanvasNumber + sameUuidObject[0].canvasIndex) {
const lastRect = sameUuidObject[object.adjCanvasNumber].shape;
// 如果在同一个画布停顿两次 第一次就会留下
const draweredItem = this.canvases[index + adjCanvasNumber].getObjects().filter(x => x.uuid === object.uuid)[0];
if (draweredItem) {
return;
}
lastRect.set({
originTop: -(this.blockSize - lastRect.originTop)
});
const adjObject = this.drawRect([
object.left,
-(this.blockSize - lastRect.originTop),
object.width,
object.height],
{
uuid: object.uuid,
adjCanvasNumber: object.adjCanvasNumber
});
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY
});
this.canvases[index + adjCanvasNumber].add(adjObject);
this.canvases[index + adjCanvasNumber].setActiveObject(adjObject);
sameUuidObject.map(item => {
item.shape.adjCanvasNumber = object.adjCanvasNumber + 1;
});
}
// 多个画布的向上移动
if (object.top < 0) {
const draweredItem = this.canvases[index - 1].getObjects().filter(x => x.uuid === object.uuid)[0];
if (draweredItem) {
return;
}
const adjObject = this.drawRect([
object.left,
this.blockSize + object.originTop,
object.width,
object.height],
{
uuid: object.uuid,
adjCanvasNumber: object.adjCanvasNumber
});
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
// 不然就会出现闪一下
visible: false
});
this.canvases[index - 1].add(adjObject);
}
return;
}
// 原本不跨canvas 变量adjCanvasNumber变更
const adjCanvasNumber = Math.floor((object.top + object.height * object.scaleY) / this.blockSize);
if (adjCanvasNumber) {
object.adjCanvasNumber = adjCanvasNumber;
for (let i = index + 1; i < adjCanvasNumber + index + 1; i++) {
const adjObject = this.drawRect([
object.left,
-(this.blockSize - object.originTop),
object.width,
object.height],
// i !== adjCanvasNumber ? this.blockSize + 1 : object.height * object.scaleY],
{
uuid: object.uuid,
adjCanvasNumber: adjCanvasNumber
});
adjObject.set({
scaleX: object.scaleX,
scaleY: object.scaleY,
visible: false
});
this.canvases[i].add(adjObject);
// this.canvases[i].setActiveObject(adjObject);
}
}
// 向上移动 原本不跨canvas 缩放或移动导致跨 第一块向上移动还是0
if (object.top < 0) {
object.adjCanvasNumber = index ? 1 : 0;
}
}
// 获取包含指定坐标的画布
private getCanvasFromCoordinates (x, y) {
for (let i = 0; i < this.canvases.length; i++) {
const canvas = this.canvases[i];
const localPoint = canvas.getPointer({ x: x, y: y });
const canvasElement = canvas.getElement();
const canvasRect = canvasElement.parentElement.getBoundingClientRect();
if (
localPoint.x >= 0 &&
localPoint.x <= canvasRect.width &&
localPoint.y >= 0 &&
localPoint.y <= canvasRect.height
) {
return canvas;
}
}
return null;
}
private getObjectByUuid (uuid: number) {
const objects: GetShapeByUuidModel[] = [];
this.canvases.map((canvas, index) => {
const object = canvas.getObjects().filter(x => x.uuid === uuid)[0];
object && objects.push({
shape: object,
canvasIndex: index
});
});
return objects;
}
private edit (object) {
if (object.type === DrawType.Polygon) {
const lastControl = object.points.length - 1;
object.controls = object.points.reduce((a, point, index) => {
a['p' + index] = new fabric.Control({
positionHandler: (dim, finalMatrix, fabricObject) => {
const x = fabricObject.points[index].x - fabricObject.pathOffset.x;
const y = fabricObject.points[index].y - fabricObject.pathOffset.y;
return fabric.util.transformPoint(
{ x: x, y: y },
fabric.util.multiplyTransformMatrices(
fabricObject.canvas.viewportTransform,
fabricObject.calcTransformMatrix()
)
);
},
actionHandler: this.anchorWrapper(index > 0 ? index - 1 : lastControl, this.actionHandler),
actionName: 'modifyPolygon',
pointIndex: index
});
return a;
}, {});
} else {
object.cornerStyle = 'circle';
object.controls = fabric.Object.prototype.controls;
}
// 点击的时候 已经传给后端的框 防止画框执行下面的
// !this.drawingObject 在已有的框中结束画 会触发emit事件 导致被选中的图形不是绘画的而是点击的
if (typeof (object.clickIndex) !== 'undefined' && !this.drawingObject) {
this.$emit('editIndex', object.clickIndex);
}
// const ids = (this.canvasObjects ?? this.activeCanvas.getObjects()).map(item => item.uuid).filter(x => x);
// this.activeCanvas.requestRenderAll();
// this.$emit('editIndex', object.clickIndex ?? ids.indexOf(object.uuid));
}
// 画四边形
private drawRect (points: Array<number>, others?: OthersConfigModel) {
const rect = new fabric.Rect({
type: DrawType.Rect,
uuid: createUuid(), // todo
left: points[0],
top: points[1],
width: points[2] || 0,
height: points[3] || 0,
objectCaching: false,
transparentCorners: false,
selectionColor: 'rgba(0,0,0,0)',
lockRotation: true,
strokeUniform: true,
// 移动时候需要 记录他移动了多少 来让同一个矩形uuid也移动
originLeft: points[0],
originTop: points[1],
...defaultRectStyle,
...this.rectStyle,
...others
});
// eslint-disable-next-line spellcheck/spell-checker
rect.setControlsVisibility({ mtr: false }); // 隐藏旋转点
return rect;
}
// 起点坐标 xy // 终点坐标 xy others目前专门设置颜色
private drawLine (position: number[], others?: OthersConfigModel) {
return new fabric.Line(position, {
type: DrawType.Line,
stroke: 'blue',
strokeWidth: 2,
objectCaching: false,
hasBorders: false,
selectable: false,
transparentCorners: true,
lockRotation: true,
lockMovementX: true,
lockMovementY: true,
lockScalingX: true,
lockScalingY: true,
hasControls: false, // 隐藏控制点
...others
});
}
public selectObject () {
const selectedObjects = this.canvases.map(canvas => canvas.getActiveObject()).filter(x => x);
return selectedObjects.length ? selectedObjects[0] : undefined;
}
public setDrawType (type) {
// 外部调用此方法,切换画图模式
this.drawType = type;
}
public removeAll () {
this.canvases.map(item => {
item.clear();
});
}
public removeLine () {
this.canvases.map(item => {
const draweredItem = item.getObjects().filter(x => x.type === DrawType.Line)[0];
if (draweredItem) {
// 删除
item.remove(draweredItem);
item.renderAll();
}
});
}
public removeSelectedObject () {
this.canvases.map(item => {
if (this.selectObject()) {
const draweredItem = item.getObjects().filter(x => x.uuid === this.selectObject().uuid)[0];
if (draweredItem) {
// 删除
item.remove(draweredItem);
item.renderAll();
}
}
});
}
public setObjectVisible (uuid: string, visible: boolean) {
// 设置为当前活跃
this.canvases.map(item => {
const draweredItem = item.getObjects().filter(x => x.uuid === uuid)[0];
if (draweredItem) {
draweredItem.visible = visible;
draweredItem.setControlsVisibility({
mt: visible,
mb: visible,
ml: visible,
mr: visible,
bl: visible,
br: visible,
tl: visible,
tr: visible
});
}
item.renderAll();
});
}
private transformMouse (e): MousePosition {
return e.pointer;
}
private anchorWrapper (anchorIndex, fn) {
return function (eventData, transform, x, y) {
const fabricObject = transform.target;
const absolutePoint = fabric.util.transformPoint(
{
x: fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x,
y: fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y
},
fabricObject.calcTransformMatrix()
);
const actionPerformed = fn(eventData, transform, x, y);
const polygonBaseSize = fabricObject._getNonTransformedDimensions();
const newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x;
const newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y;
fabricObject.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5);
return actionPerformed;
};
}
private actionHandler (eventData, transform, x, y) {
const polygon = transform.target;
const currentControl = polygon.controls[polygon.__corner];
const mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center');
const polygonBaseSize = polygon._getNonTransformedDimensions();
const size = polygon._getTransformedDimensions(0, 0);
const finalPointPosition = {
x: (mouseLocalPosition.x * polygonBaseSize.x) / size.x + polygon.pathOffset.x,
y: (mouseLocalPosition.y * polygonBaseSize.y) / size.y + polygon.pathOffset.y
};
polygon.points[currentControl.pointIndex] = finalPointPosition;
return true;
}
@Watch('width')
@Watch('height')
private styleChaneg () {
this.numBlocks = Math.ceil(this.height / this.blockSize);
this.init();
}
}
</script>
<style lang="scss" scoped>
.drawer {
position: relative;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
width: 100%;
height: calc(100% - 70px);
#canvas-container,
.container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#canvas-container {
z-index: 2;
}
}
</style>
标签:canvas,浏览器,uuid,object,画布,item,操作,const
From: https://blog.csdn.net/weixin_43957384/article/details/137147356