矩形拖动操作。"/>
【canvas】在Vue3+ts中实现 canva内的矩形拖动操作。
前言
canvas内的显示内容如何拖动?
这里提供一个 canvas内矩形移动的解决思路。
描述
如何选中canvas里的某部分矩形内容,然后进行拖动?
我的解决思路:
- **画布搭建。**用一个div将canvas元素包裹,设置宽高,div设置成相对定位(relative),canvas设置绝对定位(absolute)。
- 在往canvas内添加内容时,请保存添加内容的相关属性,长宽、位置、样式等,以此确定这部分内容的初始状态。例如:往canvas加一个矩形 ,就要先保存一下它的宽高和原点。
- 确定选中的内容。并与第一步保存的相关内容数据匹配。 由于canvas内添加的内容无法进行事件绑定,我们需要靠给canvas绑定点击事件,并根据点击位置确定哪部分内容被选中了。
- 生成可操作盒子。通过选中内容的数据,生成一个新的Dom元素盒子,并清除canvas内当前选中内容部分。给dom盒子绑定移动事件(mouse模拟拖动)。
- 拖动结束后,更新选中内容数据,在结束区域,canvas重新绘制。
实现
1.画布搭建
<div class="content" ref="canvasContent"><canvas id="canvas" ref="canvas" @click="canvasClickFn"></canvas></div>
.content {position: relative;width: 800px;height: 600px;}#canvas {position: absolute;width: 800px;height: 600px;border: 1px solid #000;background-color: #fafafa;}
这一步要保证外层盒子和canvas大小一致。
2. 初始化canvas内容
在往canvas内添加内容时,请保存添加内容的相关属性,长宽、位置、样式等,以此确定这部分内容的初始状态。
import { onMounted, reactive, ref, type Ref } from 'vue';interface DivStyle {boder?: string;backgroundColor?: string;width?: string;height?: string;}interface DiagramObj {id: string | number;path: Float32Array;origin: Array<number>;type: string;width?: number;height?: number;r?: number;style?: DivStyle;}let ctx: CanvasRenderingContext2D | null | undefined;const canvasContent: Ref<HTMLElement | null> = ref(null);const canvas: Ref<HTMLCanvasElement | null> = ref(null);const diagramObjArr: Array<DiagramObj> = reactive([]);const initCanvas = () => {if (canvas.value) {ctx = canvas.value?.getContext('2d');canvas.value.width = 800;canvas.value.height = 600;}};onMounted(() => {initCanvas();if (ctx) {let rect1 = new Float32Array([1, 1, 50, 1, 50, 30, 1, 30]);let rectObj = {id: 'rect1',path: rect1,origin: [1, 1],width: 50,height: 30,type: 'rect',style: {boder: '1px solid #000',backgroundColor: '#fff',width: '50px',height: '30px',},children: [],};drawRect(ctx, rect1);diagramObjArr.push(rectObj);}});// 绘制图形function drawRect(ctx: CanvasRenderingContext2D, array: Float32Array) {if (array.length % 2 !== 0) {console.error('drwaRect函数Float32Array参数长度需要偶数位');return;}ctx.beginPath();for (let i = 0; i < array.length; i += 2) {let x = array[i];let y = array[i + 1];if (i === 0) {ctx.moveTo(x, y);} else {ctx.lineTo(x, y);}}ctx.closePath();ctx.stroke();}
rectObj是一个原点1,1;宽50,高30的盒子,然后 根据canvas路径api绘制图形。
3. 选中内容
绑定canvas点击事件,却定点击位置和点击位置下的内容。
const canvasClickFn = (e: MouseEvent) => {let point = [e.offsetX, e.offsetY];let res = isGraphIstersection(point, diagramObjArr[0]);if (res && ctx) {console.log('在内部::', res.width);// 在图形正上方创建可操作图形createElementFn(canvasContent.value, res);// 清除该区域clearRect(ctx, [...res.origin, res.width, res.height]);}};function clearRect(ctx: CanvasRenderingContext2D, array: Array<number | undefined>) {const [x, y, width, height] = array as Array<number>;// 把1px 的边框算上ctx.clearRect(x - 1, y - 1, width + 2, height + 2);}// 圆点 和 多边形相交检测function isGraphIstersection(point: Array<number>, target: DiagramObj) {const { origin, width, height, r } = target;let apogee = [0, 0];// 求两矩形形中心点距离switch (target.type) {case 'rect':// 矩形 坐标轴法,不考虑矩形旋转if (!width || !height) return false;// 最远点apogee = [origin[0] + width, origin[1] + height];if (point[0] >= origin[0] &&point[0] <= apogee[0] &&point[1] >= origin[1] &&point[1] <= apogee[1]) {return target;}return false;case 'circle':if (!r) return false;if (Math.pow(Math.abs(point[0] - origin[0]), 2) +Math.pow(Math.abs(point[1] - origin[0]), 2) <r * r) {return target;}return false;case 'polygon':return false;}}
圆点 和 多边形相交检测 这个函数我只简单实现了矩形和圆形的检测(不考虑旋转)。如果想多检测其他的形状,需要自行实现。
4. 生成可操作盒子
根据选中的数据生成可操作盒子,盒子绑定事件,实现拖动
function createElementFn(source: HTMLElement | null, obj: DiagramObj) {const { width, height, origin, style } = obj;if (!source || !width || !height) return;const div = document.createElement('div');div.setAttribute('style',`position:absolute;top:${origin[1]}px;left:${origin[0]}px;width:${style?.width};height:${style?.height};border:${style?.boder};background-color:${style?.backgroundColor};box-shadow: 0px 0px 3px skyblue;`,);let divClickLeft = 0,divClickTop = 0; // 元素点击时本身偏移量let isStart = false;let finallyLeft = origin[0],finallyTop = origin[1]; // 最终偏移量div.onmousedown = (e: MouseEvent) => {divClickLeft = e.offsetX as number;divClickTop = e.offsetY as number;isStart = true;};div.onmousemove = (e: MouseEvent): void => {if (!isStart) return;const parentV = source.getBoundingClientRect();const [left, top] = [e.pageX - parentV.left - divClickLeft,e.pageY - parentV.top - divClickTop,];if (left < 0 ||top < 0 ||left > parentV.width - (width as number) ||top > parentV.height - (height as number))return;e.target.style.top = top + 'px';e.target.style.left = left + 'px';finallyLeft = left;finallyTop = top;};div.onmouseup = div.onmouseleave = (e: MouseEvent) => {if (!isStart) return;isStart = false;// 拖动好后在新区域重新绘画let newRectObj: DiagramObj = obj;let pw = finallyLeft + width;let ph = finallyTop + height;Object.assign(newRectObj, {path: new Float32Array([finallyLeft, finallyTop, pw, finallyTop, pw, ph, finallyLeft, ph]),origin: [finallyLeft, finallyTop],} as DiagramObj);if (ctx) {drawRect(ctx, newRectObj.path);let index = diagramObjArr.findIndex((item) => item.id === newRectObj.id);diagramObjArr.splice(index, 1, newRectObj);source.removeChild(div);}};
5.拖动完成后重新绘制图形
拖动完成后,在新的位置重新绘制图形。需要在生成盒子的鼠标抬起和鼠标移出实现。
div.onmouseup = div.onmouseleave = (e: MouseEvent) => {if (!isStart) return;isStart = false;// 拖动好后在新区域重新绘画let newRectObj: DiagramObj = obj;let pw = finallyLeft + width;let ph = finallyTop + height;Object.assign(newRectObj, {path: new Float32Array([finallyLeft, finallyTop, pw, finallyTop, pw, ph, finallyLeft, ph]),origin: [finallyLeft, finallyTop],} as DiagramObj);if (ctx) {drawRect(ctx, newRectObj.path);let index = diagramObjArr.findIndex((item) => item.id === newRectObj.id);diagramObjArr.splice(index, 1, newRectObj);source.removeChild(div);}};
效果
canvas移动
效果地址:
由于是模拟的拖动,不能拖动过快,下次想办法优化下,下次一定。
结语
结束了。 这个canvas拖动如果封装好的话,感觉是很有用的。
更多推荐
【canvas】在Vue3+ts中实现 canva内的矩形拖动操作。
发布评论