图片区域绘制
需求
- 在图片上绘制一个不规则闭合图形,形成一个区域,在此区域上做扩展。重点就是生成此区域。
思路
- 先根据用户的操作来绘制/修改区域
- 保存所有区域及坐标信息
- 将坐标信息动态的放在图片上,扩展操作
正文
- 首先先在html里面把背景图和canvas标签及相应布局写好
<div id="app">
canvas画布
<br />
<canvas id="myCanvas" width="800" height="500"></canvas>
<br />
<button id="btn">生成路径</button>
带路径的图片
<br />
<div id="imgWrap" class="img-wrap" style="width: 800px;height: 500px">
<img src="./Web前端资料/img/1南宫污水厂_看图王.jpg" style="width: 800px;height: 500px" id="bgImg" />
</div>
</div>
- js里面写相关的逻辑
- 监听click事件,判断用户是不是在已画的区域中click的,是的话return,不是的话判断用户是不是当前区域的第一个点,如果是的话,创建新区域,然后创建用户点击的第一个点,不是的话就只创建点,这个点归属于区域列表中的最后一个。最后判断这个点是不是跟这个区域的第一个点位置相同(是否区域闭合了),如果是的话则这个区域就完成了。
- 监听mousedown和mousemove事件,判断当前所有区域都闭合了。然后保存mousedown时的位置信息。如果mousedown点击的位置在某个区域内,则所有区域的坐标位置移动mousemove与mousedown的差值,实现拖动区域的效果。如果mousedown点击的位置在某个区域的某个点上,则修改这个点的位置差值,实现拖动某个点的效果
- 封装函数,因为拖动的效果,其实是canvas重绘刷新出来的,所以我们将重绘的逻辑写到一个函数里面,然后保证每次坐标更新后立刻清空重绘,就可以做出相应的动画效果了。
// 创建画布
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
// 定义状态,分为非连线状态、连线状态
let lineStatus = false;
// 创建多个区域数组,用于存放多个区域的数据
const areas = [];
// 用于判断用户是要画点连线还是要移动/修改位置
let canDraw = true;
// 用户选中了某个区域,该区域在数组中的下标
let selectAreaIndex = -1;
// 用户选中了某个区域中的某个路径点,改点在这个区域所有点中的下标
let selectPointIndex = -1;
// 添加各个监听事件
// 添加点击事件,内部判断是画点还是画点和线,同时构造区域和点对象,将area对象保存在数组中
canvas.onclick = (e) => {
if (!canDraw) return; // 判断用户选中了已画的某个区域,就不再画点了
let firstPoint = false; // 是不是这个区域的第一个点
let area = null;
if (!lineStatus) { // 第一个点
lineStatus = true;
area = new Area();
firstPoint = true;
} else if (areas.length > 0) {
area = areas[areas.length - 1];
}
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
const index = clickPointIndex(x, y, area); //判断当前点击的位置在不在已画的某个点上
const p = new Point(x, y);
if (index === -1) area.add(p); // 判断是继续画点的
// 先画之前所有的区域,在画本次未完成的点线
drawAll(ctx, areas.filter((i, j) => (j < areas.length - 1 || firstPoint)));
drawPoint(ctx, area);
ctx.beginPath();
drawLine(ctx, area);
//判断这次画的点是不是这个区域的第一个点,如果是,这个区域就闭合了
if (index === 0) {
lineStatus = false;
ctx.closePath();
ctx.fillStyle = 'red';
ctx.fill();
ctx.restore();
// ctx.addHitRegion({id: areas.length + 1});
} else if(index === -1) { // 新区域首个点,保存数据到数组中
if (firstPoint) areas.push(area);
}
}
let isMouseDown = false; // 这个要和鼠标移动配合使用
let downx, downy; //记录首次鼠标点击下来的坐标信息
canvas.onmousedown = (e) => {
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
downx = x;
downy = y;
isMouseDown = true;
if (!lineStatus) { // 这个状态表示,目前是所有区域都闭合的状态,此时在某个区域长按,可以移动该区域。长按某个区域的路径点,可以伸缩变化改点和区域的位置
let {pointIndex, areaIndex} = findPointIndex(ctx, areas, x, y); // 先找点,看看用户长按的是不是某个区域的某个点,找到了就不用再找区域了
if (areaIndex === -1) areaIndex = drawAndFindAreaIndex(ctx, areas, x, y);//找不到点,再找区域,看看用户是不是长按了某个区域
if (areaIndex >= 0) {
canDraw = false;
selectAreaIndex = areaIndex;
selectPointIndex = pointIndex;
} else { //没找到,证明用户是想新建区域画点。
canDraw = true;
}
}
}
canvas.onmouseup = (e) => { // 重置部分数据
isMouseDown = false;
drawAll(ctx, areas)
selectAreaIndex = -1;
selectPointIndex = -1;
}
canvas.onmousemove = (e) => { // 移动的时候,计算差值,然后改变区域的位置,重新绘制
if (!isMouseDown || lineStatus || selectAreaIndex === -1) return;
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
const disx = x - downx;
const disy = y - downy;
const {points} = areas[selectAreaIndex];
if (selectPointIndex >= 0) {
points[selectPointIndex].x += disx;
points[selectPointIndex].y += disy;
} else {
points.forEach(p => {
p.x += disx;
p.y += disy;
})
}
downx = x;
downy = y;
drawAll(ctx, areas, selectAreaIndex);
}
- 封装的函数
class Point { // 点对象
constructor(x, y) {
this.x = x;
this.y = y;
this.radius = 10;
this.color = "blue";
this.isSelected = false;
}
}
class Area { //区域对象
constructor() {
this.points = [];
this.isCompleted = false;
this.isSelected = false;
this.link = '';
}
add(point) {
this.points.push(point);
}
}
function drawImg(ctx, img) { // 画背景图图
ctx.drawImage(img,0,0,canvas.width, canvas.height);
}
function drawPoint(ctx, area) { // 画当前区域所有的点
const {points} = area;
ctx.save();
points.forEach(p => {
ctx.moveTo(p.x, p.y);
ctx.globalAlpha = 0.85;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI*2);
ctx.closePath();
ctx.fillStyle = p.color;
ctx.strokeStyle = "black";
ctx.fill();
})
ctx.restore();
}
function drawLine(ctx, area) { // 画当前区域的所有的线
const {points} = area;
points.forEach((p,i) => {
if (i === 0) {
ctx.moveTo(p.x, p.y)
} else {
ctx.lineTo(p.x, p.y)
}
})
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.stroke();
}
function clickPointIndex(x, y, area) { // 计算当前坐标是不是在当前区域的路径点上,并返回下标
const {points} = area;
for(let i = 0; i< points.length; i++) {
const p = points[i];
//使用勾股定理计算这个点与圆心之间的距离
const distanceFromCenter = Math.sqrt(Math.pow(p.x - x, 2)
+ Math.pow(p.y - y, 2));
if (distanceFromCenter <= p.radius) {
//停止搜索
return i;
}
}
return -1;
}
function drawAll(ctx, areas, selectAreaIndex = -1) { // 清空画布,画背景图,画所有区域
ctx.clearRect(0, 0, canvas.width, canvas.height);
const img=document.getElementById("bgImg");
drawImg(ctx, img);
areas.forEach((a,i) => {
drawPoint(ctx, a);
ctx.beginPath();
drawLine(ctx, a);
ctx.closePath();
if (i === selectAreaIndex) {
ctx.fillStyle = 'blue';
} else {
ctx.fillStyle = 'red'
}
ctx.fill();
})
}
function drawAndFindAreaIndex(ctx, areas, x, y) { // 画所有区域,并且找到当前坐标在哪个区域上,并返回下标
ctx.clearRect(0, 0, canvas.width, canvas.height);
const img=document.getElementById("bgImg");
drawImg(ctx, img);
let index = -1;
areas.forEach((a,i) => {
drawPoint(ctx, a);
ctx.beginPath();
drawLine(ctx, a);
ctx.closePath();
const inPath = ctx.isPointInPath(x, y);
if (inPath) index = i;
ctx.fillStyle = 'red';
ctx.fill();
})
return index;
}
function findPointIndex(ctx, areas, x, y) { // 找到当前坐标在哪个区域的哪个坐标点上,并将两个下标返回
let areaIndex = -1;
let pointIndex = -1;
areas.forEach((a, i) => {
if (areaIndex >= 0) return;
const index = clickPointIndex(x, y, a);
if (index >= 0) {
areaIndex = i;
pointIndex = index;
}
})
return {
areaIndex,
pointIndex
}
}
- 加入点击事件
// 开始画背景图
const img = document.getElementById("bgImg");
// 按钮监听
const btn = document.getElementById('btn');
btn.onclick = () => {
map.innerHTML = null;
console.log(areas);
areas.forEach((a, i) => {
const area = document.createElement('area');
area.shape = 'poly';
area.coords = a.points.map(p => `${p.x},${p.y}`).join(',');// .filter((i,j) => j< a.points.length - 1)
let polygon = a.points.map(item => {
return {
x: item.x,
y: item.y
}
})
var con = new Contour(polygon);
center = con.centroid();
console.log('x: ' + center.x + ' y: ' + center.y)
const odiv = document.createElement('div');
odiv.style.width = '20px'
odiv.style.height = '20px'
odiv.style.background = 'red'
odiv.style.position = 'absolute'
odiv.style.top = center.y + 'px'
odiv.style.left = center.x + 'px'
document.querySelector('#imgWrap').appendChild(odiv)
})
}
- 找区域的中心点
class Contour {
constructor(points) {
this.pts = points || [];
}
area() {
var area = 0;
var pts = this.pts;
var nPts = pts.length;
var j = nPts - 1;
var p1;
var p2;
for (var i = 0; i < nPts; j = i++) {
p1 = pts[i];
p2 = pts[j];
area += p1.x * p2.y;
area -= p1.y * p2.x;
}
area /= 2;
return area;
}
centroid() {
var pts = this.pts;
var nPts = pts.length;
var x = 0;
var y = 0;
var f;
var j = nPts - 1;
var p1;
var p2;
for (var i = 0; i < nPts; j = i++) {
p1 = pts[i];
p2 = pts[j];
f = p1.x * p2.y - p2.x * p1.y;
x += (p1.x + p2.x) * f;
y += (p1.y + p2.y) * f;
}
f = this.area() * 6;
// return new Point1(x / f, y / f);
return {
x: x / f,
y: y / f
}
}
}
- 最后加点css
<style>
.img-wrap {
position: relative;
}
.img-wrap img {
position: absolute;
top: 0;
left: 0;
}
</style>
总结
利用canvas制作完成,但是后续还有需要优化的地方,例如利用鼠标右键闭合…