html5 canvas+js实现ps钢笔抠图
1. 项目要求需要用js实现photoshop中钢笔抠图功能,就用了近三四天的时间去解决它,最终还是基本上把他实现了。
做的过程中走了不少弯路,最终一同事找到了canvans以比较核心的属性globalCompositeOperation = "destination-out",
属性可以实现通过由多个点构成的闭合区间设置成透明色穿透画布背景色或是背景图片,这样省了许多事。
2.实现效果:
鼠标点完之后会将所有的点连成闭合区间,并可*拖拉任一点,当形成闭合区间后,可在任意两点之间添加新点进行拖拉。
3.实现思路:
设置两层div,底层设置图片,顶层设置canvas画布(如果将图片渲染到画布上,抠图时会闪烁,所以至于底层),在画布上监视
鼠标事件反复渲染点及之间连线,形成闭合区间后将整体画布渲染小块背景图片,并将闭合区间渲染透明色。并把点的相对画布
坐标记录或更新到数组中去。截完图后,将点的坐标集合传回后台,由后台代码实现根据坐标点及图片宽度高度实现截图,并设
至背景色为透明色(canvas也可以实现截图,但需要处理像素点实现背景透明,暂时还没实现,计划用C#后台代码实现)。
4.js(写的不规范比较乱,大家就当参考吧)
1 <script type="text/javascript"> 2 $(function () { 3 var a = new tailorImg(); 4 a.iniData(); 5 }); 6 // 7 var tailorImg=function() 8 { 9 this.iniData = function () { 10 //画布 11 this.can.id = "canvas"; 12 this.can.w = 400; 13 this.can.h = 400; 14 this.can.roundr = 7; 15 this.can.roundrr = 3; 16 this.can.curPointIndex = 0; 17 this.can.imgBack.src = "gzf.png"; 18 this.can.canvas = document.getElementById(this.can.id).getContext("2d"); 19 //图片 20 this.img.w = 400; 21 this.img.h = 400; 22 this.img.image.src = "flower.jpg"; 23 //加载事件: 24 //初始化事件: 25 var a = this; 26 var p = a.can.pointList; 27 $("#" + a.can.id).mousemove(function (e) { 28 if (a.can.paint) {//是不是按下了鼠标 29 if (p.length > 0) { 30 a.equalStartPoint(p[p.length - 1].pointx, p[p.length - 1].pointy); 31 } 32 a.roundIn(e.offsetX, e.offsetY); 33 } 34 //判断是否在直线上 35 //光标移动到线的附近如果是闭合的需要重新划线,并画上新添加的点 36 a.AddNewNode(e.offsetX, e.offsetY); 37 }); 38 $("#" + a.can.id).mousedown(function (e) { 39 a.can.paint = true; 40 //点击判断是否需要在线上插入新的节点: 41 if (a.can.tempPointList.length > 0) { 42 a.can.pointList.splice(a.can.tempPointList[1].pointx, 0, new a.point(a.can.tempPointList[0].pointx, a.can.tempPointList[0].pointy)); 43 //清空临时数组 44 a.can.tempPointList.length = 0; 45 } 46 }); 47 $("#" + a.can.id).mouseup(function (e) { 48 //拖动结束 49 a.can.paint = false; 50 //拖动结束; 51 if (a.can.juPull) { 52 a.can.juPull = false; 53 a.can.curPointIndex = 0; 54 //验证抠图是否闭合:闭合,让结束点=开始点;添加标记 55 a.equalStartPoint(p[p.length - 1].pointx, p[p.length - 1].pointy); 56 //判断是否闭合: 57 if (a.can.IsClose) { 58 59 } 60 } 61 else { 62 //如果闭合:禁止添加新的点; 63 if (!a.can.IsClose) {//没有闭合 64 p.push(new a.point(e.offsetX, e.offsetY)); 65 //验证抠图是否闭合:闭合,让结束点=开始点;添加标记 66 a.equalStartPoint(p[p.length - 1].pointx, p[p.length - 1].pointy); 67 //判断是否闭合: 68 //重新画; 69 if (p.length > 1) { 70 a.drawLine(p[p.length - 2].pointx, p[p.length - 2].pointy, p[p.length - 1].pointx, p[p.length - 1].pointy); 71 a.drawArc(p[p.length - 1].pointx, p[p.length - 1].pointy); 72 } else { 73 a.drawArc(p[p.length - 1].pointx, p[p.length - 1].pointy); 74 } 75 } 76 else { 77 //闭合 78 } 79 } 80 //验证是否填充背景: 81 if (a.can.IsClose) { 82 a.fillBackColor(); 83 a.drawAllLine(); 84 } 85 }); 86 $("#" + a.can.id).mouseleave(function (e) { 87 a.can.paint = false; 88 }); 89 //鼠标点击事件: 90 $("#" + a.can.id).click(function (e) { 91 //空 92 }); 93 } 94 this.point = function (x, y) { 95 this.pointx = x; 96 this.pointy = y; 97 }; 98 //图片 99 this.img = { 100 image:new Image(), 101 id: "", 102 w:0, 103 h:0 104 }; 105 //画布; 106 this.can = { 107 canvas:new Object(), 108 id: "", 109 w: 0, 110 h: 0, 111 //坐标点集合 112 pointList: new Array(), 113 //临时存储坐标点 114 tempPointList: new Array(), 115 //圆点的触发半径: 116 roundr: 7, 117 //圆点的显示半径: 118 roundrr: 7, 119 //当前拖动点的索引值; 120 curPointIndex : 0, 121 //判断是否点击拖动 122 paint : false, 123 //判断是否点圆点拖动,并瞬间离开,是否拖动点; 124 juPull : false, 125 //判断是否闭合 126 IsClose: false, 127 imgBack: new Image() 128 129 }; 130 //函数: 131 //更新画线 132 this.drawAllLine=function () { 133 for (var i = 0; i < this.can.pointList.length - 1; i++) { 134 //画线 135 var p = this.can.pointList; 136 this.drawLine(p[i].pointx, p[i].pointy, p[i + 1].pointx, p[i + 1].pointy); 137 //画圈 138 this.drawArc(p[i].pointx, p[i].pointy); 139 if (i == this.can.pointList.length - 2) { 140 this.drawArc(p[i+1].pointx, p[i+1].pointy); 141 } 142 } 143 } 144 //画线 145 this.drawLine = function (startX, startY, endX, endY) { 146 //var grd = this.can.canvas.createLinearGradient(0, 0,2,0); //坐标,长宽 147 //grd.addColorStop(0, "black"); //起点颜色 148 //grd.addColorStop(1, "white"); 149 //this.can.canvas.strokeStyle = grd; 150 this.can.canvas.strokeStyle = "blue" 151 this.can.canvas.lineWidth =1; 152 this.can.canvas.moveTo(startX, startY); 153 this.can.canvas.lineTo(endX, endY); 154 this.can.canvas.stroke(); 155 } 156 //画圈: 157 this.drawArc=function(x, y) { 158 this.can.canvas.fillStyle = "blue"; 159 this.can.canvas.beginPath(); 160 this.can.canvas.arc(x, y,this.can.roundrr, 360, Math.PI * 2, true); 161 this.can.canvas.closePath(); 162 this.can.canvas.fill(); 163 } 164 //光标移到线上画大圈: 165 this.drawArcBig = function (x, y) { 166 this.can.canvas.fillStyle = "blue"; 167 this.can.canvas.beginPath(); 168 this.can.canvas.arc(x, y, this.can.roundr+2, 360, Math.PI * 2, true); 169 this.can.canvas.closePath(); 170 this.can.canvas.fill(); 171 } 172 //渲染图片往画布上 173 this.showImg=function() { 174 this.img.image.onload = function () { 175 this.can.canvas.drawImage(this.img.image, 0, 0, this.img.w,this.img.h); 176 }; 177 } 178 //填充背景色 179 this.fillBackColor = function () { 180 for (var i = 0; i <this.img.w; i += 96) { 181 for (var j = 0; j <= this.img.h; j += 96) { 182 this.can.canvas.drawImage(this.can.imgBack, i, j, 96, 96); 183 } 184 } 185 this.can.canvas.globalCompositeOperation = "destination-out"; 186 this.can.canvas.beginPath(); 187 for (var i = 0; i <this.can.pointList.length; i++) { 188 this.can.canvas.lineTo(this.can.pointList[i].pointx,this.can.pointList[i].pointy); 189 } 190 this.can.canvas.closePath(); 191 this.can.canvas.fill(); 192 this.can.canvas.globalCompositeOperation = "destination-over"; 193 this.drawAllLine(); 194 } 195 //去掉pointlist最后一个坐标点: 196 this.clearLastPoint=function () { 197 this.can.pointList.pop(); 198 //重画: 199 this.clearCan(); 200 this.drawAllLine(); 201 } 202 //判断结束点是否与起始点重合; 203 this.equalStartPoint = function (x,y) { 204 var p = this.can.pointList; 205 if (p.length > 1 && Math.abs((x - p[0].pointx) * (x - p[0].pointx)) + Math.abs((y - p[0].pointy) * (y - p[0].pointy)) <= this.can.roundr * this.can.roundr) { 206 //如果闭合 207 this.can.IsClose = true; 208 p[p.length - 1].pointx = p[0].pointx; 209 p[p.length - 1].pointy = p[0].pointy; 210 } 211 else { 212 this.can.IsClose = false; 213 } 214 } 215 //清空画布 216 this.clearCan=function (){ 217 this.can.canvas.clearRect(0, 0, this.can.w, this.can.h); 218 } 219 //剪切区域 220 this.CreateClipArea=function () { 221 this.showImg(); 222 this.can.canvas.beginPath(); 223 for (var i = 0; i <this.can.pointList.length; i++) { 224 this.can.canvas.lineTo(this.can.pointList[i].pointx,this.can.pointList[i].pointy); 225 } 226 this.can.canvas.closePath(); 227 this.can.canvas.clip(); 228 } 229 // 230 this.CreateClipImg=function() 231 { 232 233 } 234 //判断鼠标点是不是在圆的内部: 235 this.roundIn = function (x, y) { 236 //刚开始拖动 237 var p = this.can.pointList; 238 if (!this.can.juPull) { 239 for (var i = 0; i < p.length; i++) { 240 241 if (Math.abs((x - p[i].pointx) * (x - p[i].pointx)) + Math.abs((y - p[i].pointy) * (y - p[i].pointy)) <= this.can.roundr * this.can.roundr) { 242 //说明点击圆点拖动了; 243 this.can.juPull = true;//拖动 244 // 245 this.can.curPointIndex = i; 246 p[i].pointx = x; 247 p[i].pointy = y; 248 //重画: 249 this.clearCan(); 250 //showImg(); 251 if (this.can.IsClose) { 252 this.fillBackColor(); 253 } 254 this.drawAllLine(); 255 return; 256 } 257 } 258 } 259 else {//拖动中 260 p[this.can.curPointIndex].pointx = x; 261 p[this.can.curPointIndex].pointy = y; 262 //重画: 263 this.clearCan(); 264 if (this.can.IsClose) { 265 this.fillBackColor(); 266 } 267 this.drawAllLine(); 268 } 269 }; 270 271 //光标移到线上,临时数组添加新的节点: 272 this.AddNewNode=function(newx, newy) { 273 //如果闭合 274 var ii=0; 275 if (this.can.IsClose) { 276 //判断光标点是否在线上: 277 var p = this.can.pointList; 278 for (var i = 0; i < p.length - 1; i++) { 279 //计算a点和b点的斜率 280 var k = (p[i + 1].pointy - p[i].pointy) / (p[i + 1].pointx - p[i].pointx); 281 var b = p[i].pointy - k * p[i].pointx; 282 //if (parseInt((p[i + 1].pointy - p[i].pointy) / (p[i + 1].pointx - p[i].pointx)) ==parseInt((p[i + 1].pointy - newy) / (p[i + 1].pointx - newx)) && newx*2-p[i+1].pointx-p[i].pointx<0 && newy*2-p[i+1].pointy-p[i].pointy<0) { 283 // //如果在直线上 284 // alert("在直线上"); 285 //} 286 $("#txtone").val(parseInt(k * newx + b)); 287 $("#txttwo").val(parseInt(newy)); 288 if (parseInt(k * newx + b) == parseInt(newy) && (newx - p[i + 1].pointx) * (newx - p[i].pointx) <= 2 && (newy - p[i + 1].pointy) * (newy - p[i].pointy) <= 2) { 289 // 290 //parseInt(k * newx + b) == parseInt(newy) 291 //添加临时点: 292 this.can.tempPointList[0] = new this.point(newx, newy);//新的坐标点 293 this.can.tempPointList[1] = new this.point(i+1, i+1);//需要往pointlist中插入新点的索引; 294 i++; 295 //alert(); 296 //光标移动到线的附近如果是闭合的需要重新划线,并画上新添加的点; 297 if (this.can.tempPointList.length > 0) { 298 //重画: 299 this.clearCan(); 300 //showImg(); 301 if (this.can.IsClose) { 302 this.fillBackColor(); 303 } 304 this.drawAllLine(); 305 this.drawArcBig(this.can.tempPointList[0].pointx, this.can.tempPointList[0].pointy); 306 return; 307 } 308 return; 309 } 310 else { 311 // $("#Text1").val(""); 312 } 313 } 314 if (ii == 0) { 315 if (this.can.tempPointList.length > 0) { 316 //清空临时数组; 317 this.can.tempPointList.length = 0; 318 //重画: 319 this.clearCan(); 320 //showImg(); 321 if (this.can.IsClose) { 322 this.fillBackColor(); 323 } 324 this.drawAllLine(); 325 //this.drawArc(this.can.tempPointList[0].pointx, this.can.tempPointList[0].pointy); 326 } 327 } 328 } 329 else { 330 //防止计算误差引起的添加点,当闭合后,瞬间移动起始点,可能会插入一个点到临时数组,当再次执行时, 331 //就会在非闭合情况下插入该点,所以,时刻监视: 332 if (this.can.tempPointList.length > 0) { 333 this.can.tempPointList.length = 0; 334 } 335 } 336 } 337 338 }; 339 340 </script>
1 <style type="text/css"> 2 .canvasDiv { 3 position: relative; 4 border: 1px solid red; 5 height: 400px; 6 width: 400px; 7 top: 50px; 8 left: 100px; 9 z-index: 0; 10 } 11 12 img { 13 width: 400px; 14 height: 400px; 15 z-index: 1; 16 position: absolute; 17 } 18 19 #canvas { 20 position: absolute; 21 border: 1px solid green; 22 z-index: 2; 23 } 24 .btnCollection { 25 margin-left: 100px; 26 } 27 </style>
1 <div class="canvasDiv"> 2 <img src="flower.jpg" /> 3 <canvas id="canvas" width="400" height="400" style="border: 1px solid green;"></canvas> 4 </div>
5.总结:
不足:当光标移动到线上时,判断一点是否在两点连成的直线上计算方法不正确,应该计算为一点是否在两点圆两条外切线所围成的矩形
内;钢笔点应为替换为小的div方格比较合理,像下面的矩形抠图;(思路:将存取的点坐标集合和动态添加的小div方格建立对应关系
当拖动小方格时,触发事件更新坐标点集合,并重新渲染)。
6.这只是js钢笔抠图的一种解决方案,项目中现在这块还在改进,如果大家有好的方法或是资料的话,希望能分享一下。谢谢