> 脚本语言 > >
使用CANVAS实现交互性圆形马赛克效果 2017-01-03 16:34 出处:清屏网 人气:
在看D3.js的时候,无意间看到了一个 例子 ,觉得很有趣,像是会分裂的圆形的马赛克。看了下代码,发现是使用svg完成的,但是具体实现方式使得在手机端无法把玩,于是就自己实现了一个canvas版本的。代码很简单,canvas初学者可以自己试试当做练笔,还是挺有趣的一个效果。
Online Demoonline demo
在demo中任意从本地选择一张图片,然后通过鼠标移动或者移动端touchmove就能实现圆形分裂的效果。
难点说是难点,其实根本不难。一开始看到的时候会好奇大大小小的圆形的颜色是怎么计算的,计算该面积下的平均值?其实很简单,就是从绘制了图片的canvas上获取圆心坐在图片对应位置上的颜色值。这样的算法在圆形半径较大的时候,对被遮盖的图片区域的颜色代表性其实不好,但是从整个分裂过程来看,这个取色的方案效果还不错。
关键技术点canvas绘图: CanvasRenderingContext2D.drawImage()
canvas绘制圆形: CanvasRenderingContext2D.arc()
canvas上取指定坐标上的颜色值: CanvasRenderingContext2D.getImageData()
将图片绘制在一个offline(即不用挂在DOM树上)的canvas上,为了在指定位置获取颜色用
创建另一个canvas,用来绘制圆。两个canvas尺寸保持一致(而且都是方形),方便通过坐标获取颜色
绘制第一个圆形,以canvas中心为原点,使用对应offline canvas坐标上的颜色填充
维持一个 circles 数组,代表所有的圆,每个元素有坐标(x, y),半径(r)和是否标记分裂(readyToSplit)
需要一个渲染循环(rendering loop),不断的找出被标记出来需要分裂(readyToSplit)的圆,拿去做分裂绘制
事件处理:当mousemove或者touchmove发生在圆上时,该圆被标记 readyToSplit = true ,后面的则有渲染循环去处理
在我自己做这样的编程时,会以测试驱动的方式开始代码。因此会脑子里先写下自己写的类将被如何使用,怎么样能够简单易用。
我打算把这个效果封装成一个类,它将在使用时被实例化。最终的效果肯定是要在DOM树上显示的,所以这里在实例化时肯定需要指定一个mount节点,所有的事情在其内部进行。而且,按照通常的习惯,开放一些配置,使得使用者可以做一些简单的定制化。但是目前还没有想好哪些内部的配置拿出来比较合适,所以第二个参数 options 可以后面再考虑。
var cs = new CircleSplit('#mountNode', options);我希望能够动态的切换显示的图片内容,所以想提供一个 setImage 的方法,它应该能接受图片路径,或者 Image 元素对象。
cs.setImage(image);OK,这就是目前我希望的实例化方式,和想要提供的接口。后面再具体实现过程中,可以再继续添加或者修改。
试想内部结合前面谈到的实现思路,考虑 CircleSplit 类里面该如果定义属性和私有共有方法。
从构造函数入手。个人习惯在构造函数最后加上init方法,init方法里做一些准备工作,完成 setImage 前的一些必要的事情。
function CircleSplit (el, options) { ... this._init(); } CircleSplit.prototype._init = function () { this._createSourceCanvas(); // 创建源canvas,用来绘制图片,作为offline canvas,提供坐标颜色使用 this._createTargetCanvas(); // 创建目标canvas,用来绘制看到的大大小小的圆 this._render(); // 开启渲染循环 this.bindEvent(); // 绑定事件,touchmove mousemove这些 }这样我们一下子多了好几个函数,而且目的都很明确,因此可以很容易的判断需要那些实例属性和该如何实现各自函数体。这里可能需要多注意一下 _render() ,思路中谈到在这里应该去绘制需要分裂的圆,那么大致应该像下面这样:
CircleSplit.prototype._render = function () { // 循环体 this.circles.forEach(function (circle) { if (circle.readyToSplit) { this._splitCircle(circle); circle.readyToSplit = false; } }, this); // 下一个循环 requestAnimationFrame(this._render.bind(this)); }而什么时候设置 circle.readyToSplit 呢?就是在 bindEvent() 的事件处理函数里面。这里会通过 _tagCircle() 遍历circles,找到能hitd到事件坐标的一个圆,将其标记(tag)上readToSplit。
从共有方法入手。 setImage 之后,相当于将整个CircleSplit中的状态都重置了下, circles 数组得重置,两个canvas得重置等。
CircleSplit.prototype.setImage = function (image) { this._resetCanvas(this.sourceCanvas); // clear source canvas this._drawSourceImage(image); // draw source canvas this._resetCanvas(this.targetCanvas); // clear target canvas this._drawCircle(x, y, r) // draw target canvas。绘制第一个,也是最大的一个圆形。圆心为canvas中心,半径为canvas的一半 }_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和close Path。相比而下,一些canvas的游戏库或者图形库,则简单直观的多:
// create.js var circle = new createjs.Shape(); circle.graphics.beginFill("DeepSkyBlue").drawCircle(0, 0, 50); // two.js var circle = two.makeCircle(72, 100, 50); circle.fill = '#FF8000'; circle.stroke = 'orangered'; circle.linewidth = 5;因此,如果要做比较复杂的绘制操作,推荐找一个适合自己的canvas库,会使得工作变得容易的多。