椭圆轨迹及标签云轨迹插件的实现

椭圆轨迹

标签云一般都会设计成绕着某个中心点旋转。其中圆形轨迹相对来说最好实现了,但个人感觉不太耐看,椭圆形的轨迹更加符合我的审美。那么,如何实现若干条标签的椭圆轨迹呢?

不妨先把问题简化成“如何实现一条椭圆轨迹”。

可以把所有的标签都放置于一个容器之中,在这个容器中进行绝对定位。这个容器的左上角,设置为坐标原点,x轴沿着水平方向从左向右递增,y轴沿着垂直方向,从上到下递增;z轴从屏幕“里面”往“外面”递增。

有了坐标系,就可以借助椭圆曲线的方程进行轨迹描述。假设这个容器的高度和宽度分别是h和w,在坐标原点为容器左上角时,容器的中心点坐标就是(w/2, h/2)。这个矩形容器的内接椭圆形的方程就可以表示为

(x-w/2)2/a2 + (y - h/2)2/b2 = 1

其中 a=w/2,b=h/2

将标准形式改写成坐标形式,就可以得到

x = a cost + w/2
y = b sint + h/2

其中t是与坐标原点形成的夹角,在[0, 4Pi)内变化。这样,当t在其定义域内连续变化时,就能得到一条椭圆轨迹。

一条椭圆轨迹有了,不同的标签最好有自己的轨迹,而不是共享同一个椭圆,比较好的做法是围绕容器中心点进行旋转,以创建不同的椭圆。比如特别的,如果交换x轴和y轴,就可以得到与前一个椭圆轨迹“垂直”的另一个椭圆轨迹。一般情况下,可以将标准椭圆轨迹旋转某个角度。那么如何简洁高效的表示这些旋转过的椭圆轨迹呢?

可以把旋转椭圆,想像成在坐标原点旋转坐标轴(再进行平移),记旋转的角度为θ,平面坐标系变换方程如下。(实际上会使用逆变换方程)

x' = x * cosθ + y * sinθ
y' = y * cosθ - x * sinθ

坐标系变换后,新的坐标系中,椭圆轨迹仍然是其标准形式,随后进行逆变换,得到未旋转时的轨迹方程,这就是绕原点θ旋转的椭圆轨迹在初始坐标系下的轨迹方程,逆变换的方程如下:

x = x' * cos(-θ) + y * sin(-θ);
y = y' * cos(-θ) - x * sin(-θ);

逆变换做完,就得到了在坐标原点旋转θ度形成的新椭圆轨迹,处理还没有结束,还要把这个轨迹平移到矩形区域的中心点。至此,一个带角度的椭圆轨迹就生成好了。

将这个过程连贯起来,其代码实现如下


   
   this.rotate =  Math.PI / 2 * (1 + idx) / tags.length; // 轨迹分区,分配角度
   
   var a = tc.getContainer().offsetWidth / 2; // 计算半轴
   var b = tc.getContainer().offsetHeight / 4; // 计算半轴

   var x =  a * Math.cos(t);
   var y =  b * Math.sin(t);

   // 坐标系变换
   var _x = x * Math.cos(-this.rotate) + y * Math.sin(-this.rotate);
   var _y = y * Math.cos(-this.rotate) - x * Math.sin(-this.rotate);

   // 显示效果更新
   this.style.left = _x + a + "px";
   this.style.top = _y + a + "px";
   this.style.opacity = Math.abs(Math.sin(t));       
   this.style.fontSize = Math.abs(Math.sin(t)) * 2 +'em';                 
   this.t = t + this.velocity;
 

轨迹插件

为了方便轨迹插件开发,tagcloud进行了一定程度的封装,以椭圆轨迹为例,要开发一个轨迹插件,只需要给TagCloud.prototype.plugins添加一个新成员,新成员是一个JSON对象,包含4个方法,init, move, mouseover和mouseout,其中,mouseover和mouseout是可选方法,默认mouseover会暂停当前触发事件的标签,mouseout会使其重新移动;而init和move是必须包含的。

init方法会在一个标签被创建时调用,其参数依次为index和tagCloud实例。
move方法则会在每一次的轨迹更新时被调用。

用法非常简单,还以椭圆轨迹插件为例,其完整的代码是:


  TagCloud.prototype.plugins.ellpise = {
            init:function(idx, tc){
                var tags = tc.tagNodes;
                this.rotate =  Math.PI / 2 * (1 + idx) / tags.length;
                this.velocity = 0.01;
                this.pos = function(t){
                    t = t || this.t;
                    var a = tc.getContainer().offsetWidth / 2;
                    var b = tc.getContainer().offsetHeight / 4;

                    var x =  a * Math.cos(t);
                    var y =  b * Math.sin(t);

                    var _x = x * Math.cos(-this.rotate) + y * Math.sin(-this.rotate);
                    var _y = y * Math.cos(-this.rotate) - x * Math.sin(-this.rotate);

                    this.style.left = _x + a + "px";
                    this.style.top = _y + a + "px";
                    this.style.opacity = Math.abs(Math.sin(t));       
                    this.style.fontSize = Math.abs(Math.sin(t)) * 2 +'em';                 
                    this.t = t + this.velocity;
                };
                this.pos( (idx+1) / tags.length * 4 * Math.PI);
            }
            ,
            move:function(){
                this.pos();
            },
            mouseover:function(tc){
                return function(){
                    this.velocity = 0.001;
                };
            },
            mouseout:function(tc){
                return function(){
                    this.velocity = 0.01;
                }
            }
        }
 
Show Comments

Get the latest posts delivered right to your inbox.