看D3.js的时候无意中看到了一个例子,觉得很有意思,像一个会分裂的圆形马赛克。看了代码,是使用svg完成的,但是具体的实现方式导致无法在手机端播放,所以自己实现了一个canvas版本。代码很简单,canvas初学者可以当练习笔试一下,挺有意思的效果。在OnlineDemoonlinedemo中,从demo中的局部区域任意选择一张图片,然后在移动端移动鼠标或touchmove,即可实现圆形分割的效果。如果觉得有用,可以在自己的项目中安装使用这个效果。npmicircle-split-S难度说是难,其实一点都不难。刚看到的时候很好奇大圆圈和小圆圈的颜色是怎么计算的,这个面积下的平均值是怎么计算的?其实很简单。就是从绘制图片的画布上获取图片对应位置的圆心坐标的颜色值。当这样的算法圆半径较大时,覆盖图像区域的颜色表现其实并不好,但从整个分裂过程来看,这种颜色选择方案的效果还不错。技术要点Canvas绘图:CanvasRenderingContext2D.drawImage()Canvas画圆:CanvasRenderingContext2D.arc()获取canvas上指定坐标上的颜色值:CanvasRenderingContext2D.getImageData()思路是离线绘制图片(即就是,没有挂在DOM树上面的canvas上),为了得到指定位置的颜色,另外创建一个canvas画圆。两张画布的大小相同(而且都是正方形),所以不需要坐标转换就可以很方便的获取颜色。绘制第一个圆,以画布中心为圆心,填充离线画布坐标对应的颜色。维护一个圆数组,代表所有的圆,每个元素都有坐标(x,y),半径(r)和是否标记分割(readyToSplit)需要渲染循环(renderingloop),不断找出标记的圆待拆分(readyToSplit),并用于拆分Drawing事件处理:当圆上发生mousemove或touchmove时,圆上标记readyToSplit=true,然后有渲染循环处理试驾。当我自己做这种编程时,我会以测试驱动的方式开始编写代码。因此,我会先在脑海里记下我的类将如何使用,以及如何使它易于使用。我准备把这个效果封装到一个类中,使用时实例化。最后的效果肯定是要显示在DOM树上的,所以实例化的时候肯定需要指定一个挂载节点,所有的事情都在里面进行。而且,按照惯例,开放一些配置,让用户可以做一些简单的定制。但是,我们还没有弄清楚哪些内部配置更适合拿出来,所以第二个参数options可以在后面考虑。varcs=newCircleSplit('#mountNode',options);我希望能够动态切换显示的图片内容,所以想提供一个setImage方法,该方法应该可以接受图片路径或者Image元素对象。cs.setImage(图片);OK,这就是我要实例化的方式和我要提供的接口。后面在具体的实现过程中,可以继续添加或者修改。内部尝试结合上面提到的实现思路,考虑如何在CircleSplit类中定义属性和私有公共方法。从构造函数开始。个人习惯是在构造函数的最后加上init方法,在init方法中做一些准备工作,在setImage之前完成一些必要的事情。functionCircleSplit(el,options){...this._init();}CircleSplit.prototype._init=function(){this._createSourceCanvas();//创建源画布,用于画图,作为离线画布,提供坐标颜色使用this._createTargetCanvas();//创建一个目标画布来绘制你看到的大大小小的圆圈this._render();//开启渲染循环this.bindEvent();//绑定事件,touchmovemousemovethese}这样我们一下子多了几个函数,目的很明确,很容易判断需要哪些实例属性,各个函数体如何实现。这里可能需要多注意_render()。idea提到这里要画一个需要分割的圆,所以大致应该是这样的:CircleSplit.prototype._render=function(){//loopbodythis.circles.forEach(function(circle){if(circle.readyToSplit){this._splitCircle(circle);circle.readyToSplit=false;}},这个);//下一个循环requestAnimationFrame(this._render.bind(this));什么时候设置circle.readyToSplit?它在bindEvent()的事件处理函数中。这里会通过_tagCircle()遍历圆,找到一个能命中事件坐标的圆,并在readyToSplit上标记(tag)。从共享方法开始。setImage之后,相当于重置了整个CircleSplit中的state,circles数组要重置,两个canvas都要重置等等CircleSplit.prototype.setImage=function(image){this._resetCanvas(this.sourceCanvas);//清除源画布this._drawSourceImage(image);//绘制源画布this._resetCanvas(this.targetCanvas);//清除目标画布this._drawCircle(x,y,r)//绘制目标画布。画出第一个也是最大的圆。圆心为画布中心,半径为画布的一半}_drawSourceImage()调用CanvasRenderingContext2D.drawImage()绘制图像。该API函数有3种参数传递形式。我这里选择了5个参数的形式,使用了自己写的简单居中库CenterIt来解决图片居中问题:不管图片大小,都可以轻松居中填充(cover)或者居中(contain))填充。这里的_drawCircle(x,y,r)应该是可重用的,每次圆分裂时都可以调用。一开始给它3个参数,圆心坐标和半径。应该可以自己获取坐标对应的颜色值。所以简单地想象一下它的内部结构:CircleSplit.prototype._drawCircle=function(x,y,r){...context.fillStyle=this._getColor(x,y);//获取坐标颜色context.beginPath();context.arc(x,y,r,0,2*Math.PI);context.closePath();context.fill();...}画圆的时候使用CanvasRenderingContext2D.arc()API,不算简单明了,每次都需要begin和closePath。相比之下,一些画布游戏库或图形库要简单和直观得多://create.jsvarcircle=newcreatejs.Shape();circle.graphics.beginFill("DeepSkyBlue").drawCircle(0,0,50);//two.jsvarcircle=two.makeCircle(72,100,50);circle.fill='#FF8000';circle.stroke='橙色';圆.linewidth=5;对于比较复杂的绘图操作,建议找一个适合自己的canvas库,这样工作起来会轻松很多。关于_getColor()函数,这里用到了CanvasRenderingContext2D.getImageData():CircleSplit.prototype._getColor=function(x,y){...varpixelData=this.sourceCanvas.getContext('2d').getImageData(parseInt(x),parseInt(y),1,1).数据;return'rgb('+pixelData[0]+','+pixelData[1]+','+pixelData[2]+')';}如下图:假设左上角的起点为(x,y),一格为一个像素,则getImageData(x,y,1,1).data会返回[255,0,0,255],代表Red=255,Alpha=255。如果getImageData(x,y,2,2).data会返回[255,0,0,255,255,0,0,255,255,0,0,255,255,0,0,255]一个长度为16的数组,每4个一组代表一个像素的rgba值。getImageData()是一个API函数,可以帮助我们对画布进行像素级别的操作。一些基于canvas的“刮刮卡”插件也是getImageData()的应用:在图片上绝对放置一个灰色的canvas,代表刮刮卡层;修改手指触摸的像素点的alpha值,达到“划痕”的效果。当然这里的修改需要使用配套的putImageData()函数;同时统计alpha值为0的像素在整个canvas像素中所占的百分比,刮到80%就可以达到显示所有图片的效果。实现以上就是实现的大致思路,以及编码的思路。为了表达我完成一个功能时是如何从头开始,定义属性和定义API。自己的经历,希望对你有帮助。如果对这些知识不熟悉但是有兴趣,可以参考github项目代码问题,github上的代码优化和上面的思路是一致的,但是会有些区别,主要是功能实现后,我找到了需要优化的地方。渲染速度在_render()渲染循环中,我们遍历了所有的圆。但是当整张图分割的很彻底的时候,就会有上万个圆圈,这样会导致每个渲染周期中的计算时间过长,导致下一个渲染周期在理想的时间之后才执行,从而导致滞后的感觉。所以为了解决这个问题,引入了renderingCircles数组,将所有标记的圆插入到这个数组中。在渲染循环中,只关心这里的值,额外的存储空间换来更短的计算时间。模糊显示在第一种实现中,两个画布的大小是根据mountNode来确定的,canvas.widthcanvas.height设置为与mountNode相同的维度值。这会导致某些设备上出现明显的边缘混叠。这里的解决方法是将canvas的宽高设置为mountNode宽高的2倍,然后通过style设置canvas显示与mountNode一样大小。这里就是对canvas本身的宽高和canvas样式的宽高的区别的理解和应用。图片跨域问题在canvas上操作图片时,可能会遇到这样的错误信息:Unabletogetimagedatafromcanvasbecausethecanvashasbeentaintedbycross-origindata。官方对此的解释是:可以在没有跨域权限的情况下在canvas上绘制图片资源(未经CORS认可的图片),但这样做会“感染(taints)”canvas,调用toBlog()、toDataURL()、受感染(污染)画布上的getImageData()将抛出上述安全错误。CircleSplit.setImage(imageUrl)时可能会遇到这个问题。解决方法是首先要求镜像有跨域许可。这个需要在提供图片服务的服务器上配置。这里不多做介绍。加载跨域license的图片时,应该可以在控制台看到:(这里我用的是七牛的图片)其次,加载图片的时候需要设置crossOrigin属性:varimage=新图片();image.crossOrigin='匿名';image.onload=function(){};image.src=imageUrl;应用其实我个人比较喜欢最后的交互效果(有点强迫症,喜欢不停地戳泡泡),所以把这个小效果做成一个简单的H5页面,年底的时候,讲述和复习一下2016年大事件。你也可以体验一下:2016-recap原文地址:http://blog.jackyang.me/blog/...
