前两天在用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重合的,这样才能获取到正确的坐标。
复制代码
复制代码
基础的样式
复制代码
.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 = $('
');
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 = $('
');
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 = $('
');
cirDiv1.attr({"class": "mini-cir cir-can"+target,"id":"cir"+(2*target-1)});
cirs.push(cirDiv1);
let cirDiv2 = $('
');
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键移动当前圆点时,另一个圆点与