原生JS 实现一个裁剪效果
效果
实现
1.画出初始结构
#canvas{
position:relative;
}
.circel{
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid red;
position: absolute;
background-color: #fff;
z-index: 2;
cursor: pointer;
}
.rect{
width: 40px;
height: 6px;
background-color: red;
border-radius: 2px;
position: absolute;
z-index: 2;
cursor: pointer;
}
<div class="box">
<!-- canvas画布 -->
<canvas id="canvas" width="400px" height="400px"></canvas>
<!-- 可移动点位 -->
<div class="circel leftTop" data-id="0"></div>
<div class="rect top" data-id="1"></div>
<div class="circel rightTop" data-id="2"></div>
<div class="rect right" data-id="3"></div>
<div class="circel rightBtm" data-id="4"></div>
<div class="rect bottom" data-id="5"></div>
<div class="circel leftBtm" data-id="6"></div>
<div class="rect left" data-id="7"></div>
</div>
canvas画布用来画出红色线,框选出裁剪部分,八个可移动点位用来拖动裁剪部分的大小和形状。其中四个角可以随意拖动,四个边只能上下或者左右拖动
2.初始化结构点位
给出八个点位的初始值,并将它们的位置用js放在正确的地方
// 初始值
const initPosi = [
{x:50,y:50},
{x:200,y:50},
{x:350,y:50},
{x:350,y:200},
{x:350,y:350},
{x:200,y:350},
{x:50,y:350},
{x:50,y:200}
]
moveDot(initPosi)
const domArr = [leftTopDom,topDom,rightTopDom,rightDom,rightBtmDom,bottomDom,leftBtmDom,leftDom]
function moveDot(posi){
// 遍历所有点位并移动到正确的位置
for(let i=0;i<domArr.length;i++){
domArr[i].style.top = posi[i].y - domArr[i].clientHeight/2 + "px"
domArr[i].style.left = posi[i].x - domArr[i].clientWidth/2 + "px"
// 如果是四个边,需要对长条形的点位进行旋转
if(i%2 == 1){
i == 7 ?
domArr[i].style.transform = `rotate(${getAngel(posi[i-1],posi[0])})`
:
domArr[i].style.transform = `rotate(${getAngel(posi[i-1],posi[i+1])})`
}
}
}
// 计算角度
function getAngel(dot1,dot2){
let ver = dot1.y - dot2.y
let col = dot1.x - dot2.x
return Math.atan2(ver,col)*180/Math.PI + "deg"
}
在每一次移动点位时都可能需要旋转边的角度,计算边需要旋转的角度值是整个功能的难点之一,需要使用Math的atan2方法求出弧度,并使用Math.atan2(ver,col)*180/Math.PI
转换成角度
此时我们已经得到了初始化的效果
现在我们需要使用canvas将所有的点位连接起来
function drawLine(posi){
// 每次重新画线都需要将上一次的画布清空
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.beginPath()
// 初始化canvas
// 设置图形的轮廓颜色
ctx.strokeStyle = "#FF453E";
// 设置线段厚度的属性(即线段的宽度)
ctx.lineWidth = 2;
for (let i = 0, len = posi.length; i < len; i++) {
let dot = posi[i]
// 起始点
if (i == 0) {
ctx.moveTo(dot.x, dot.y)
} else {
ctx.lineTo(dot.x, dot.y)
}
}
// 回到起点
ctx.lineTo(posi[0].x, posi[0].y)
ctx.stroke()
ctx.closePath()
}
此时我们得到了一个完整的裁剪框,但是所有的点位还无法移动
3.给点位注册事件
// 给可移动的点注册点击事件
box.addEventListener("mousedown",function(e){
picPosi = [].concat(changePosi)
if(e.target.classList.contains("circel") || e.target.classList.contains("rect")){
currentTargetId = Number(e.target.getAttribute("data-id"))
canMove = true
}
})
box.addEventListener("mouseup",function(e){
picPosi = [].concat(changePosi)
// console.log(picPosi)
canMove = false
})
当鼠标按下时,我们获取到当前点位的ID,并使全局变量 canMove为true,此时点位可以被拖动,当鼠标抬起,canMove为false,禁止拖动。这里设置变量 picPosi是用来记录每次开始拖动或者停止拖动时所有点位的快照信息。如果没有这个变量我们将不能使点位移动到正确的位置。
4.实现点位拖动
// 监听mouseMove
box.addEventListener("mousemove",function(e){
x = e.clientX - box.offsetLeft
y = e.clientY - box.offsetTop
// 边界处理
if(x >= (box.clientWidth - dotWidth/2)
|| y >= (box.clientHeight - dotHeight/2)
|| x <= dotWidth/2 || y <= dotHeight/2
){
canMove = false
}
if(canMove){
// 计算点位坐标...
// 使点位随之移动
moveDot(changePosi)
// 重新画线
drawLine(changePosi)
}
})
由于所有点位在拖动时都会影响其他点位,且都要重新计算点位坐标以及要旋转边点的角度,所以每个点位都要一一计算
当移动第一个点即左上角的点时,上边点和左边点也要随之移动,所以要保证上边点始终处于左上角和右上角两个点的中间位置,左边点始终处于左上角和左下角两个点的中间位置,且需要旋转一定的角度
// 移动第一个点
if(currentTargetId == 0){
changePosi[0] = {x,y}
// 第二个点也要随之移动
let colMid2 = (changePosi[0].x + changePosi[2].x) / 2
let verMid2 = (changePosi[0].y + changePosi[2].y) / 2
changePosi[1] = {x:colMid2,y:verMid2}
// 第八个点也要随之移动
let colMid8 = (changePosi[0].x + changePosi[6].x) / 2
let verMid8 = (changePosi[0].y + changePosi[6].y) / 2
changePosi[7] = {x:colMid8,y:verMid8}
}
当移动第二个点即上边点时,左上角的点、右上角的点要随之移动,左边点和右边点也要随之上下移动,且点位的 x坐标不会改变,只会改变y坐标,所以要使用到上述的 picPosi变量
// 移动第二个点
if(currentTargetId == 1){
// 移动上 边点
changePosi[currentTargetId] = {x:picPosi[currentTargetId].x,y}
// 当上 边点移动时,左上角和右上角两个角点 也要随之上下移动
changePosi[currentTargetId - 1] = {x:picPosi[currentTargetId - 1].x,y:picPosi[currentTargetId - 1].y + y-picPosi[currentTargetId].y}
changePosi[currentTargetId + 1] = {x:picPosi[currentTargetId + 1].x,y:picPosi[currentTargetId + 1].y + y-picPosi[currentTargetId].y}
// 当上 边点移动时,左 边点和 右 边点也要随之上下移动
// 且左右两个边点位始终处于其对应的上下点位的中间位置
let leftMid = (changePosi[0].y + changePosi[6].y) / 2
let rightMid = (changePosi[2].y + changePosi[4].y) / 2
// 边界处理
if(Math.abs(changePosi[0].y - changePosi[6].y) <= dotLineWidth*2 || Math.abs(changePosi[2].y - changePosi[4].y) <= dotLineWidth*2)return
changePosi[7] = {x:changePosi[7].x,y:leftMid}
changePosi[3] = {x:changePosi[3].x,y:rightMid}
}
相类似的,当移动第三个点即右上角的点时,上边点和右边点也要随之移动;当移动第四个点即右边点时,右上角和右下角的点也要随之移动,上边点和下边点也要时刻保持在对应点位的中间位置。。。以此类推可以计算每个点位移动的坐标
// 移动第三个点
if(currentTargetId == 2){
changePosi[2] = {x,y}
// 第二个点也要随之移动
let colMid2 = (changePosi[0].x + changePosi[2].x) / 2
let verMid2 = (changePosi[0].y + changePosi[2].y) / 2
changePosi[1] = {x:colMid2,y:verMid2}
// 第四个点也要随之移动
let colMid4 = (changePosi[2].x + changePosi[4].x) / 2
let verMid4 = (changePosi[2].y + changePosi[4].y) / 2
changePosi[3] = {x:colMid4,y:verMid4}
}
// 移动第四个点
if(currentTargetId == 3){
changePosi[currentTargetId] = {x,y:picPosi[currentTargetId].y}
changePosi[currentTargetId - 1] = {x:picPosi[currentTargetId - 1].x + x-picPosi[currentTargetId].x,y:picPosi[currentTargetId - 1].y}
changePosi[currentTargetId + 1] = {x:picPosi[currentTargetId + 1].x + x-picPosi[currentTargetId].x,y:picPosi[currentTargetId + 1].y}
// 处理上下两个点位,上下两个点位始终处于左右点位的中间位置
let topMid = (changePosi[0].x + changePosi[2].x) / 2
let btmMid = (changePosi[4].x + changePosi[6].x) / 2
// 边界处理
if(Math.abs(changePosi[0].x - changePosi[2].x) <= dotLineWidth*2 || Math.abs(changePosi[4].x - changePosi[6].x) <= dotLineWidth*2)return
changePosi[1] = {x:topMid,y:changePosi[1].y}
changePosi[5] = {x:btmMid,y:changePosi[5].y}
}
// 移动第五个点
if(currentTargetId == 4){
changePosi[4] = {x,y}
// 第四个点也要随之移动
let colMid4 = (changePosi[2].x + changePosi[4].x) / 2
let verMid4 = (changePosi[2].y + changePosi[4].y) / 2
changePosi[3] = {x:colMid4,y:verMid4}
// 第六个点也要随之移动
let colMid6 = (changePosi[4].x + changePosi[6].x) / 2
let verMid6 = (changePosi[4].y + changePosi[6].y) / 2
changePosi[5] = {x:colMid6,y:verMid6}
}
// 移动第六个点
if(currentTargetId == 5){
changePosi[currentTargetId] = {x:picPosi[currentTargetId].x,y}
changePosi[currentTargetId - 1] = {x:picPosi[currentTargetId - 1].x,y:picPosi[currentTargetId - 1].y + y-picPosi[currentTargetId].y}
changePosi[currentTargetId + 1] = {x:picPosi[currentTargetId + 1].x,y:picPosi[currentTargetId + 1].y + y-picPosi[currentTargetId].y}
// 处理左右两个点位,左右两个点位始终处于上下点位的中间位置
// rightTop与rightBtm的距离
let leftMid = (changePosi[0].y + changePosi[6].y) / 2
let rightMid = (changePosi[2].y + changePosi[4].y) / 2
// 边界处理
if(Math.abs(changePosi[0].y - changePosi[6].y) <= dotLineWidth*2 || Math.abs(changePosi[2].y - changePosi[4].y) <= dotLineWidth*2)return
changePosi[7] = {x:changePosi[7].x,y:leftMid}
changePosi[3] = {x:changePosi[3].x,y:rightMid}
}
// 移动第七个点
if(currentTargetId == 6){
changePosi[6] = {x,y}
// 第六个点也要随之移动
let colMid6 = (changePosi[4].x + changePosi[6].x) / 2
let verMid6 = (changePosi[4].y + changePosi[6].y) / 2
changePosi[5] = {x:colMid6,y:verMid6}
// 第八个点也要随之移动
let colMid8 = (changePosi[0].x + changePosi[6].x) / 2
let verMid8 = (changePosi[0].y + changePosi[6].y) / 2
changePosi[7] = {x:colMid8,y:verMid8}
}
// 移动第八个点
if(currentTargetId == 7){
changePosi[currentTargetId] = {x,y:picPosi[currentTargetId].y}
changePosi[currentTargetId - 1] = {x:picPosi[currentTargetId - 1].x + x -picPosi[currentTargetId].x,y:picPosi[currentTargetId - 1].y}
changePosi[0] = {x:picPosi[0].x + x - picPosi[currentTargetId].x,y:picPosi[0].y}
// 处理上下两个点位,上下两个点位始终处于左右点位的中间位置
let topMid = (changePosi[0].x + changePosi[2].x) / 2
let btmMid = (changePosi[4].x + changePosi[6].x) / 2
// 边界处理
if(Math.abs(changePosi[0].x - changePosi[2].x) <= dotLineWidth*2 || Math.abs(changePosi[4].x - changePosi[6].x) <= dotLineWidth*2)return
changePosi[1] = {x:topMid,y:changePosi[1].y}
changePosi[5] = {x:btmMid,y:changePosi[5].y}
}
如此我们的功能就大体上实现了
总结
1.当移动某个点位时,会影响其他点位,要一一计算其他点位需要移动的坐标
2.上右下左四个边点需要根据四个角的位置改变角度
3.每次更新画布需要清除上一次的画布内容
4.需要定义一个变量picPosi记录移动前后的点位信息
不足
代码实现的只是一个简单的拖动效果,对于细致的边界处理还不完善,裁剪部分的高亮效果没有做,在拖动时可能需要放大镜效果,需要后续在实现。
转载自:https://juejin.cn/post/7213654163320799293