前言
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拖动如果封装好的话,感觉是很有用的。