canvas内的显示内容如何拖动?
这里提供一个 canvas内矩形移动的解决思路。
如何选中canvas里的某部分矩形内容,然后进行拖动?
我的解决思路:
<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大小一致。
在往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绘制图形。
绑定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;
}
}
圆点 和 多边形相交检测 这个函数我只简单实现了矩形和圆形的检测(不考虑旋转)。如果想多检测其他的形状,需要自行实现。
根据选中的数据生成可操作盒子,盒子绑定事件,实现拖动
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);
}
};
拖动完成后,在新的位置重新绘制图形。需要在生成盒子的鼠标抬起和鼠标移出实现。
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拖动如果封装好的话,感觉是很有用的。