用Canvas實現Photoshop的鋼筆工具(貝塞爾曲線)


前兩天在用Canvas實現一個繪制路徑的小功能。做完之后發現加以完善可以“復刻”一下PS里面的鋼筆工具。

PS里的鋼筆工具對我來說是PS中最好用的工具!

所以本文主要介紹如何用Canvas來實現Photoshop中的鋼筆工具

需求分析

首先我們來分析一下需求。

1、在畫布上的點擊效果

1.1點擊可生成方形錨點

1.2錨點數量>=2時開始繪制路徑

1.3繪制完成的錨點再次點擊可進行刪除

1.4第一次點擊初始錨點可閉合路徑(當然以后再點擊就是刪除路徑啦)

2、點擊錨點同時按住鍵盤按鍵(這里的按鍵主要是Ctrl鍵和Alt鍵)

2.1點擊方形錨點並按住Ctrl鍵可對錨點進行拖動

2.2點擊方形錨點並按住Alt鍵可在對應錨點周圍生成小圓點,此時移動鼠標會繪制小圓點與方形錨點形成的一條直線路徑,長度由鼠標拖動進行控制

2.2.1拖動小圓點可變換弧形路徑

3、總體功能使用與功能撿漏

2.2.2點擊小圓點並按住Alt鍵可同時改變兩個錨點的角度與長度,此時兩個小圓點與方形錨點永遠在一條直線上

2.2.3點擊小圓點並按住Ctrl鍵可單獨改變一個錨點的角度與長度,另一個小圓點不受影響

功能實現

首先要實現一個鋼筆的工具,我們要有點擊區域,而且這個點擊區域必須是與Canvas重合的,這樣才能獲取到正確的坐標。

<canvas id="configCan" width="800" height="600"></canvas><!--canvas繪制區-->
<div id="drawLine">
<div id="clickZone" style="width: 800px;height: 600px;"></div><!--點擊區域--> <div id="point"></div><!--錨點放置區--> </div>

基礎的樣式

.mini-box{/*錨點樣式*/
    width: 10px;
    height: 10px;
    background-color: #ffffff;
    border: 1px solid #1984ec;
    position: absolute;
}
.mini-box-down{/*錨點選中樣式*/
    background-color: #1984ec;
}
.closeP{/*閉合路徑鼠標樣式*/
    cursor:pointer;
}
.delP{/*刪除錨點鼠標樣式*/
    cursor:pointer;
}
.move{
    cursor: move;
}
.mini-cir{/*圓點樣式*/
    position: absolute;
    display: inline-block;
    width: 10px;
    height: 10px;
    border: 1px solid #1984ec;
    background-color: #ffffff;
    border-radius: 5px;
}
.mini-cir-down{/*圓點選中樣式*/
    background-color: #1984ec;
}

知識點:

cursor:光標的樣式屬性

border-radius:向DIV添加圓角邊框屬性

 

點擊區域的事件

$(document).ready(function(){
    let currentX1,currentY1;
    $("#clickZone").click(function () {
    }).mousedown(function (e) {
        let length = document.getElementsByClassName("point-can").length;
        let poDiv;
        currentX1 = e.offsetX;//獲取當前鼠標位置
        currentY1 = e.offsetY;
        if(length){//判斷當前是否是第一個錨點
            let poCan = document.getElementsByClassName("point-can");
            let targetId = parseInt(poCan[(length-1)].id.substring(2));
            poDiv = $('<div></div>');
            poDiv.attr({"class": "mini-box point-can delP mini-box-down","id":"po"+(targetId+1),"title": "刪除錨點"});
            let poId = "#po"+targetId;
            $(poId).removeClass("mini-box-down");
            $(poId).after(poDiv);
        }else{
            poDiv = $('<div></div>');
            poDiv.attr({"class": "mini-box point-can closeP mini-box-down","id":"po1","title": "閉合路徑"});
            $("#point").html(poDiv);
        }
        $("#"+poDiv[0].id).css({top:currentY1,left:currentX1});//錨點位置為當前點擊位置
        drawAll();//注冊錨點的事件的方法
    });
});

實現方法:

點擊后記錄當前鼠標的坐標,判斷是否是第一個點,然后插入DIV,並為其設置坐標。

知識點:

e事件的offset屬性表示原點為觸發事件元素的左上角,例如offsetX的數值,即表示點擊時,鼠標距離被點擊元素左上角原點的x值。

具體可見:offsetX、clientX、screenX、pageX、layerX

 

錨點的注冊事件

var drawAll = function () {
    $("#point").css('visibility', 'visible');
    drawPath();
    let miniBoxs = document.getElementsByClassName("point-can");
    let cmove = false;//圓點移動的標志
    let flag = false;//錨點移動的標志
    let po1State = false;//第一個錨點的狀態
    let delState = true;//刪除狀態
    let currentX,currentY;//存儲當前坐標
    let that;//存儲錨點狀態
    $("#drawLine").css("z-index",999);
    for (let i = 0; i < miniBoxs.length; i++) {
        $("#"+miniBoxs[i].id).off('click').on('click',function (e) {//為每一個錨點注冊事件
            if(closeP){//判斷路徑是否閉合
                if(po1State === false){//這是第一次點擊第一個錨點,此時觸發的事件為閉合路徑
                    po1State =true;//修改第一個錨點的狀態為true
                    flag = false;
                    cmove = false;
                    return;
                }
                if(delState){//判斷是否刪除錨點
                    if(miniBoxs.length===2){//如果錨點數=2,就不可再刪除
                        return;
                    }
                    $("#"+e.currentTarget.id).remove();//刪除錨點
                    let target = parseInt(e.currentTarget.id.substring(2));
                    $(".cir-can"+target).remove();//刪除當前錨點下已存在的圓點
                    delState = false;
                    drawPath();//重新繪制路徑
                }
            }
            if(miniBoxs.length>1&&delState){//路徑未閉合狀態
                if (e.currentTarget.id ==="po1"){
                    return;
                }else{
                    $("#"+e.currentTarget.id).remove();
                    let target = parseInt(e.currentTarget.id.substring(2));
                    $(".cir-can"+target).remove();
                    delState = false;
                    drawPath();
                }
            }
        }).off('mousedown').on('mousedown',function (e) {
            cmove = true;
            cirChange = true;//設置圓點改變狀態為true,表示此時圓點的狀態已經改變
            $(".mini-cir").removeClass("mini-cir-down");
            that = null;
            if(window.event.ctrlKey) {//點擊錨點並按住ctrl鍵
                delState = false; //設置刪除狀態為false
                flag = true;//移動標志
                that = e;
            }else{
                delState = true;
            }
            if(window.event.altKey){//點擊錨點並按住alt鍵
                that = e;
            }
            if(that===null){
                that = e;
            }
            $("#"+e.target.id).addClass("mini-box-down");
            currentX = e.pageX - parseInt($("#"+e.currentTarget.id).css("left"));
            currentY = e.pageY - parseInt($("#"+e.currentTarget.id).css("top"));
            if(e.currentTarget.id === "po1"&&!po1State){//第一次點擊 第一個生成的錨點,閉合路徑
                closeP = true; //設置閉合路徑的狀態為true
                $("#po1").removeClass("closeP");
                $("#po1").addClass("mini-box-down delP");
                $("#po1").removeAttr("title");
                $("#po1").attr("title","刪除錨點");
                $("#po"+miniBoxs.length).removeClass("mini-box-down");//移除上一個錨點的選中狀態
                drawPath();
            }
        }).off('mouseup').on('mouseup',function (e) {
            flag = false;
            cmove = false;
            that = null;
        });
    }
    $("#drawLine").on('mousemove',function (e) {
        let targetId;
        if(that){//獲取當前點擊的錨點ID
            targetId = "#"+that.target.id;
        }
        if(window.event.ctrlKey&&flag&&that) {
            delState = false;
            if (flag) {
                var x = e.pageX - currentX;//移動時根據鼠標位置計算控件左上角的絕對位置
                var y = e.pageY - currentY;
                $(targetId).css({top: y, left: x});//控件新位置
                $(targetId).addClass("mini-box-down");//添加選中狀態
                let target = parseInt(that.target.id.substring(2));
                var cir = document.getElementsByClassName("cir-can"+target);
                if(cir.length){
                    if(cirChange){//判斷與上次相比,圓點是否發生變化
                        cir1X = cir[0].offsetLeft - x;
                        cir1Y = cir[0].offsetTop - y;
                        cir2X = cir[1].offsetLeft - x;
                        cir2Y = cir[1].offsetTop - y;
                    }
                    if(cir1X){
                        $(cir[0]).css({top:y+cir1Y,left:x+cir1X});
                        $(cir[1]).css({top:y+cir2Y,left:x+cir2X});

                    }else{
                        $(cir[0]).css({top:(y),left:(x)});
                        $(cir[1]).css({top:(y),left:(x)});
                    }
                }
                drawPath();
                cirChange = false;
            }
            return;
        }
        if(window.event.altKey&&cmove&&that){//點擊錨點並按住alt鍵
            delState = false;
            $(targetId).addClass("mini-box-down");
            let target = parseInt(that.target.id.substring(2));
            let cirCans = document.getElementsByClassName("cir-can"+target);
            if(!cirCans.length){//判斷圓點是否存在,否則創建
                let cirs = [];
                let cirDiv1 = $('<div></div>');
                cirDiv1.attr({"class": "mini-cir cir-can"+target,"id":"cir"+(2*target-1)});
                cirs.push(cirDiv1);
                let cirDiv2 = $('<div></div>');
                cirDiv2.attr({"class": "mini-cir cir-can"+target,"id":"cir"+(target*2)});
                cirs.push(cirDiv2);
                $("#"+that.target.id).after(cirs);
                drawCir();
            }
            let x = e.pageX - currentX;//移動時根據鼠標位置計算控件左上角的絕對位置
            let y = e.pageY - currentY;
            $("#cir"+(target*2-1)).css({left:x,top:y});//根據鼠標位置改變奇數圓點即此錨點的第一個圓點坐標
            $("#cir"+(target*2-1)).addClass("mini-cir-down");
            let po = document.getElementById("po"+target);
            let X = x - parseInt(po.offsetLeft);
            let Y = y - parseInt(po.offsetTop);
            $("#cir"+target*2).css({left:(po.offsetLeft-X),top:(po.offsetTop-Y)});
            drawPath();
            return;
        }
        that = null;
    });
};

實現方法:

點擊錨點(即小方塊)判斷刪除狀態delState,判斷是否刪除錨點。(點擊並按下按鍵使delState為false)

由於第一個錨點是判斷路徑是否閉合的關鍵錨點,所以設置po1State,來記錄狀態,當第一次點擊時,設置錨點狀態為true,此時路徑已閉合,此錨點也成為普通錨點可進行刪除操作。

按住ctrl鍵時可拖動錨點的位置,這個時候需要注意記錄此錨點是否存在圓點,要獲取圓點的坐標一並進行移動。

此時需要注意與圓點的移動進行關聯,當圓點位置改變時,需記錄狀態,更新圓點的坐標。

按住alt鍵可生成/移動圓點坐標,移動的是當前錨點的第一個圓點的坐標(也就是奇數圓點的坐標),另一個圓點(偶數圓點)的坐標根據第一個圓點的坐標進行設置,保證此時兩個圓點與錨點永遠在一條直線上,並且兩個圓點距離錨點的距離相同。

知識點:

獲取當前按鍵是否按下使用window.event.altKey、window.event.ctrlKey屬性來進行判斷。

 

圓點的注冊事件

var drawCir = function () {//圓點的事件注冊
    let miniCirs = document.getElementsByClassName("mini-cir");
    let ccurrentX,ccurrentY;
    let cthat = null;
    let changeId;//記錄當前圓點是否發生變化
    let targetId = 0;
    for (let i = 0; i < miniCirs.length; i++) {
        $("#"+miniCirs[i].id).off('click').on('click',function (e) {
            cFlag = false;
            $(".mini-cir").removeClass("mini-cir-down");//移除所有選中狀態
        }).off('mousedown').on('mousedown',function (e) {
            cFlag = true;//圓點移動標記
            if(cthat===null){
                cthat = e;
            }
            ccurrentX = e.pageX - parseInt($("#"+e.currentTarget.id).css("left"));
            ccurrentY = e.pageY - parseInt($("#"+e.currentTarget.id).css("top"));
            targetId = parseInt(e.target.id.substring(3));
        });
    }
    $("#drawLine").on('mousemove',function (e) {
        if (cFlag) {
            if(cthat===null){
                return;
            }
            if(window.event.altKey&&cthat){//點擊圓點並按下alt鍵
                let x = e.pageX - ccurrentX;//移動時根據鼠標位置計算控件左上角的絕對位置
                let y = e.pageY - ccurrentY;
                $("#"+cthat.target.id).css({top: y, left: x});//選中圓點的新位置
                $("#"+cthat.target.id).addClass("mini-cir-down");//添加選中狀態
                let ctarget = targetId;//獲取當前點擊的圓點ID
                let po,cX,cY;

                if(ctarget%2){//根據圓點ID獲取與其成對的另一個圓點的坐標
                    po = document.getElementById("po"+(ctarget+1)/2);
                    cX = parseInt($("#cir"+(ctarget+1)).css('left')) - parseInt($("#po"+(ctarget+1)/2).css('left'));
                    cY = parseInt($("#cir"+(ctarget+1)).css('top')) - parseInt($("#po"+(ctarget+1)/2).css('top'));
                    changeId = ctarget+1;
                }else{
                    po = document.getElementById("po"+ctarget/2);
                    cX = parseInt($("#cir"+(ctarget-1)).css('left')) - parseInt($("#po"+ctarget/2).css('left'));
                    cY = parseInt($("#cir"+(ctarget-1)).css('top')) - parseInt($("#po"+ctarget/2).css('top'));
                    changeId = ctarget-1;
                }
                let X = parseInt(cthat.target.offsetLeft) -  parseInt(po.offsetLeft);//當前點擊圓點與錨點的距離
                let Y = parseInt(cthat.target.offsetTop) - parseInt(po.offsetTop);
                let sLength = Math.sqrt(X*X + Y*Y);//計算當前圓點與錨點的長度
                if(cId === null){
                    cId = ctarget;
                }
                if(cId !== ctarget){//判斷當前的圓點ID是否發生變化來確定是否重新計算成對圓點的長度
                    cId = ctarget;
                    cIdChange = true;
                }else{
                    cIdChange = false;
                }
                if(cLength===0||cIdChange){
                    cLength = parseInt(Math.sqrt(cX*cX + cY*cY));//計算與當前點擊圓點成對的另一個圓點與錨點的長度
                }
                let mul1 = (X/sLength).toFixed(2);//省略小數以減小誤差
                let mul2 = (Y/sLength).toFixed(2);
                if(X>0){//根據當前圓點相對於錨點的位置設置與之對應的圓點的坐標,使得當前圓點與對應圓點永遠在一條直線上
                    if(mul2<0){
                        $("#cir"+changeId).css({top:(po.offsetTop-(cLength*mul2)),left:(po.offsetLeft-(cLength*mul1))});
                    }else{
                        $("#cir"+changeId).css({top:(po.offsetTop-(cLength*mul2)),left:(po.offsetLeft-(cLength*mul1))});
                    }
                }else{
                    if(mul2<0){
                        $("#cir"+changeId).css({top:(po.offsetTop-(cLength*mul2)),left:(po.offsetLeft-(cLength*mul1))});
                    }else{
                        $("#cir"+changeId).css({top:(po.offsetTop-(cLength*mul2)),left:(po.offsetLeft-(cLength*mul1))});
                    }
                }
            }
            if(window.event.ctrlKey&&cthat){//點擊圓點並按住ctrl鍵
                let target = parseInt(cthat.target.id.substring(3));
                $(".mini-cir").removeClass("mini-cir-down");
                let x = e.pageX - ccurrentX;//移動時根據鼠標位置計算控件左上角的絕對位置
                let y = e.pageY - ccurrentY;
                $("#cir"+target).css({left:x,top:y});//此時只改變當前圓點的坐標
                $("#cir"+target).addClass("mini-cir-down");
            }

            drawPath();
        }
    }).off('mouseup').on('mouseup',function (e) {
        cthat = null;
        cFlag = false;
    });
};

實現方法:

點擊圓點並按住Alt鍵可對當前圓點進行拖動,此時另一個圓點也隨之改變,永遠與當前錨點,當前圓點三點連成一條直線。

其中,當按住Alt鍵移動當前圓點時,另一個圓點與錨點的長度不變,所以需要記錄鼠標按下狀態時,另一圓點的坐標,根據坐標計算長度。

如圖所示,兩個圓點與錨點呈相似三角形,所以當鼠標松開時,根據此時圓點與錨點的角度,可以計算另一圓點的坐標。

點擊圓點並按住Ctrl鍵可對當前圓點進行拖動,此時僅改變當前點擊圓點的坐標,不對另一個圓點造成影響。

知識點:

Math.sqrt():可返回一個數的平方根。

toFixed(num):把數字四舍五入為指定小數位數num的數字。

 

繪制路徑方法

var poPositions = [];
var drawPath = function () {

    let configCan = document.getElementById("configCan");
    let ctx = configCan.getContext("2d");
    ctx.clearRect(0, 0, 800, 600);//清空畫布
    let poCan = document.getElementsByClassName("point-can");
    poPositions = [];
    for(let i = 0;i<poCan.length;i++){
        let position = {x:0,y:0};
        position.x = poCan[i].offsetLeft + 4;
        position.y = poCan[i].offsetTop + 4;
        poPositions.push(position);
    }//獲取錨點坐標
    let cirCanP = [];
    for(let i = 0;i<poCan.length;i++){
        let targetId = parseInt(poCan[i].id.substring(2));
        let cir = document.getElementsByClassName("cir-can"+targetId);
        let cirP ;
        if(cir.length){
            cirP = [];
            for(let j = 0;j<2;j++){
                let position = {x:0,y:0};
                position.x = cir[j].offsetLeft + 4;
                position.y = cir[j].offsetTop + 4;
                cirP.push(position);
            }
        }
        cirCanP[i] = cirP;
    }//獲取圓點坐標
    for(let i = 0;i<poPositions.length;i++){
        if(poPositions[i]&&cirCanP[i]){
            ctx.beginPath();
            ctx.strokeStyle = "#1984ec";
            ctx.moveTo(cirCanP[i][0].x,cirCanP[i][0].y);
            ctx.lineTo(poPositions[i].x,poPositions[i].y);
            ctx.lineTo(cirCanP[i][1].x,cirCanP[i][1].y);
            ctx.stroke();
        }
    }//繪制已存在圓點與其對應錨點的直線
    ctx.beginPath();
    ctx.strokeStyle = "#1984ec";
    ctx.moveTo(poPositions[0].x, poPositions[0].y);
    if(cirCanP[0]){//如果第一個錨點的圓點存在
        if(!cirCanP[1]){//且第二個錨點的圓點不存在,則繪制二次貝塞爾曲線
            ctx.quadraticCurveTo(cirCanP[0][1].x,cirCanP[0][1].y,poPositions[1].x,poPositions[1].y);
        }else{//且第二個錨點的圓點存在,則繪制三次貝塞爾曲線
            ctx.bezierCurveTo(cirCanP[0][1].x, cirCanP[0][1].y, cirCanP[1][0].x,cirCanP[1][0].y, poPositions[1].x, poPositions[1].y);
        }
        for(let i = 1;i<poPositions.length;i++){
            if(i===(poCan.length-1)){
                if(cirCanP[i]){//如果最后一個錨點的圓點存在
                    if(cirCanP[(i-1)]){//且倒數第二個錨點的圓點存在,則繪制三次貝塞爾曲線
                        ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[0][0].x,cirCanP[0][0].y, poPositions[0].x, poPositions[0].y);
                        ctx.stroke();
                        return;
                    }else{//且倒數第二個錨點不存在
                        ctx.quadraticCurveTo(cirCanP[i][0].x,cirCanP[i][0].y,poPositions[i].x, poPositions[i].y);//先繪制倒數第二個錨點與倒數第三個點的二次貝塞爾曲線
                        ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[0][0].x,cirCanP[0][0].y, poPositions[0].x, poPositions[0].y);//再繪制最后一個錨點與第一個錨點的三次貝塞爾曲線
                        ctx.stroke();
                        return;
                    }

                }else{//如果最后一個錨點不存在
                    if(cirCanP[(i-1)]){//且倒數第二個錨點存在,則繪制第一個錨點與最后一個錨點的二次貝塞爾曲線
                        ctx.quadraticCurveTo(cirCanP[0][0].x,cirCanP[0][0].y,poPositions[0].x, poPositions[0].y);
                        ctx.stroke();
                        return;
                    }else{//且倒數第二個錨點不存在
                        ctx.lineTo(poPositions[i-1].x, poPositions[i-1].y);//繪制第二個錨點
                        ctx.lineTo(poPositions[i].x, poPositions[i].y);//繪制最后一個錨點
                        ctx.quadraticCurveTo(cirCanP[0][0].x,cirCanP[0][0].y,poPositions[0].x, poPositions[0].y);//繪制最后一個錨點到第一個錨點的二次貝塞爾曲線
                        ctx.stroke();
                        return;
                    }
                }
            }
            if(cirCanP[i]){//如果當前錨點存在小圓點
                if(cirCanP[i-1]){//且前一個錨點小圓點存在
                    if(cirCanP[i+1]){//且后一個錨點小圓點存在
                        ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[i+1][0].x,cirCanP[i+1][0].y, poPositions[i+1].x, poPositions[i+1].y);//繪制當前錨點與下一個錨點的三次貝塞爾曲線
                    }else{//且后一個錨點小圓點不存在
                        ctx.quadraticCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, poPositions[i+1].x, poPositions[i+1].y);//繪制當前錨點與下一個錨點的二次貝塞爾曲線
                    }
                }else if(cirCanP[i+1]){//且后一個錨點小圓點存在,此時前一個錨點不存在小圓點
                    ctx.quadraticCurveTo(cirCanP[i][0].x, cirCanP[i][0].y, poPositions[i].x, poPositions[i].y);//則繪制前一個錨點與當前錨點的二次貝塞爾曲線
                    ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[i+1][0].x,cirCanP[i+1][0].y, poPositions[i+1].x, poPositions[i+1].y);//繪制當前錨點與后一個錨點的三次貝塞爾曲線
                }else{//前一個錨點與后一個錨點都不存在小圓點
                    ctx.quadraticCurveTo(cirCanP[i][0].x, cirCanP[i][0].y, poPositions[i].x, poPositions[i].y);//繪制前一個錨點的與當前錨點的二次貝塞爾曲線
                    ctx.quadraticCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, poPositions[i+1].x, poPositions[i+1].y);//繪制當前錨點與后一個錨點的二次貝塞爾曲線
                }
            }else{//如果當前錨點不存在小圓點
                ctx.lineTo(poPositions[i].x, poPositions[i].y);//則繪制當前錨點
            }
        }
    }else{//第一個錨點的小圓點不存在的情況,其余同上
        for(let i = 1;i<poPositions.length;i++){
            if(i===(poCan.length-1)){
                if(cirCanP[i]){
                    if(cirCanP[i-1]){
                        ctx.quadraticCurveTo(cirCanP[i][1].x,cirCanP[i][1].y,poPositions[0].x, poPositions[0].y);
                        ctx.stroke();
                        return;
                    }else{
                        ctx.lineTo(poPositions[i-1].x, poPositions[i-1].y);
                        ctx.quadraticCurveTo(cirCanP[i][0].x,cirCanP[i][0].y,poPositions[i].x, poPositions[i].y);
                        ctx.quadraticCurveTo(cirCanP[i][1].x,cirCanP[i][1].y,poPositions[0].x, poPositions[0].y);
                        ctx.stroke();
                        return;
                    }
                }
            }
            if(cirCanP[i]){
                if(cirCanP[i-1]){
                    if(cirCanP[i+1]){
                        ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[i+1][0].x,cirCanP[i+1][0].y, poPositions[i+1].x, poPositions[i+1].y);
                    }else{
                        ctx.quadraticCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, poPositions[i+1].x, poPositions[i+1].y);
                    }

                }else if(cirCanP[i+1]){
                    ctx.quadraticCurveTo(cirCanP[i][0].x, cirCanP[i][0].y, poPositions[i].x, poPositions[i].y);
                    ctx.bezierCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, cirCanP[i+1][0].x,cirCanP[i+1][0].y, poPositions[i+1].x, poPositions[i+1].y);
                }else{
                    ctx.quadraticCurveTo(cirCanP[i][0].x, cirCanP[i][0].y, poPositions[i].x, poPositions[i].y);
                    ctx.quadraticCurveTo(cirCanP[i][1].x, cirCanP[i][1].y, poPositions[i+1].x, poPositions[i+1].y);
                }
            }else{
                ctx.lineTo(poPositions[i].x, poPositions[i].y);
            }

        }
    }
    if(closeP){//如果閉合路徑狀態為true,則閉合路徑
        ctx.closePath();
    }
    ctx.stroke();
};

實現方法:

獲取錨點坐標,並獲取對應的圓點坐標,分別存在數組中。

繪制圓點與錨點的直線,根據圓點數量判斷錨點繪制直線、二次貝塞爾曲線、三次貝塞爾曲線。

知識點:

Canvas方法/屬性:

clearRect(x,y,width,height):清除畫布,x、y表示開始清除的坐標,width、height表示清除的區域。

beginPath():畫布中開始子路徑的一個新的集合,建議每次開始畫一個子路徑的時候都顯示地調用,不然會出行路徑粘連。

strokeStyle屬性:筆觸顏色,可選值為顏色、漸變、圖案

(與上面相對的是fillStyle屬性:填充顏色,可選值也為顏色、漸變、圖案)

moveTo(x,y):開始繪制的第一個點坐標

lineTo(x,y):創建下一個點坐標

closePath():將最后一個點與第一個點相連

注:以上三個方法並沒有繪制路徑,只是確定了坐標

stroke():繪制路徑,這個方法才真正將坐標點相連繪制了路徑

繪制曲線最重要的知識點就是就是貝塞爾曲線。

貝塞爾曲線的知識介紹參考這個鏈接:Canvas學習:貝塞爾曲線

這里我就簡單講一下我自己的理解。

大體上,PS中的曲線分為兩種,一種普通曲線,一種高級曲線。普通曲線就是由三點驅動的曲線,這種曲線就一個弧度,要么凸,要么凹。曲線的曲度由中間那個點來控制,繪制時就是貝塞爾二次曲線。

高級曲線就是四點驅動的曲線,這種曲線有波浪的效果,曲線是曲度由兩個點來控制,所以可以實現S型曲線,也就是兩個弧度的曲線,繪制時就是貝塞爾三次曲線。

繪制貝塞爾曲線時,需要使用moveTo(x,y)方法顯示指定第一個點的坐標。

quadraticCurveTo(cx,cy,x,y):繪制貝塞爾二次曲線,cx,cy就是中間那個控制點,表現為那個小圓點,x,y就是相鄰錨點的坐標。

bezierCurveTo(c1x,c1y,c2x,c2y,x,y):繪制貝塞爾三次曲線,c1x,c1y就是前一個錨點的第二個小圓點坐標,c2x,c2y就是后一個錨點的第一個小圓點坐標,x,y就是后一個錨點的坐標。

理清楚這些,整體實現起來就很快啦!

以上就是實現Photoshop鋼筆工具的全部代碼和部分講解,來看看最終實現效果。

不足:沒有實現PS中創建錨點時拖動鼠標就生成小圓點的效果,也沒有刪除圓點坐標的操作,只能無限接近錨點坐標。

方法寫得亂七八糟,JQ和原生JS混亂使用。

改進:根據我自己的使用習慣,在點擊小圓點按住Ctrl鍵改變坐標,使兩個點不在一條直線上后,再按住Alt鍵可使兩個點重新恢復到一條直線上。PS中的話,就無法恢復到一條直線了,除非重新生成圓點坐標。

后續還會繼續進行修改!

源碼地址:https://github.com/Chellyyy/Canvas_PS_PenTool


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM