前兩天在用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中的話,就無法恢復到一條直線了,除非重新生成圓點坐標。
后續還會繼續進行修改!