原生JS 实现一个裁剪效果

lxf2023-04-20 12:18:01

效果

原生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转换成角度

此时我们已经得到了初始化的效果

原生JS 实现一个裁剪效果

现在我们需要使用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()
}

此时我们得到了一个完整的裁剪框,但是所有的点位还无法移动

原生JS 实现一个裁剪效果

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)

    }
})

由于所有点位在拖动时都会影响其他点位,且都要重新计算点位坐标以及要旋转边点的角度,所以每个点位都要一一计算

当移动第一个点即左上角的点时,上边点和左边点也要随之移动,所以要保证上边点始终处于左上角和右上角两个点的中间位置,左边点始终处于左上角和左下角两个点的中间位置,且需要旋转一定的角度

原生JS 实现一个裁剪效果

// 移动第一个点
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变量

原生JS 实现一个裁剪效果

// 移动第二个点
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记录移动前后的点位信息

不足

代码实现的只是一个简单的拖动效果,对于细致的边界处理还不完善,裁剪部分的高亮效果没有做,在拖动时可能需要放大镜效果,需要后续在实现。