WordPress 样式标签云探究 Case Study: WordPress Flash Tag Cloud

前阵子看到WordPress出了个Flash的Tag Cloud的插件。感觉挺独具匠心的,3D圆球。 wordpress

我的“山寨”心,继上次模仿Buzzword Context Menu后,又再次蠢蠢欲动。

这次的模仿,涉及到3D算法。我的数学丢下好久不用。

所以作者投机取巧了,在网上参考了ahab's math tutorial,一个3D数学教程(作者还在进一步的学习中)。

并且利用了反编译。特此声明和提倡,反编译要用于学习,而非商业和非法用途。随后的源代码,将隐去一些敏感的部分。

先送上原型和模仿后的成品的截图,以作比较。

图一:原图,可去插件原作者网站查看。

screenshot

图二:模仿后成品图

done

这次模仿,练习了利用sprite 和 movieclip制作自定义部件。sprite是一种特殊的movie clip, 和movie clip的区别就是,sprite没有帧的概念,而movie clip有。相应的,有关时间轴的一些方法,在movie clip的类中才能找到。

我们分析这个Tag Cloud插件后,会得出结论,这个插件是由tag和cloud两层来构成的(-_-||不用看图,单从字面上都可以看出来)。tag就是sprite,不用是什么动画,简单的一个 text field就可以了。动画的效果,在cloud层来实现。那么cloud就是movie clip, 一帧一帧的,是动画。

那么,我们的实现步骤就一目了然了。由简单到复杂。

第一步:建立tag sprite

package com.chestnut
{
	import flash.display.Sprite;
	import flash.events.MouseEvent;
	import flash.text.TextField;
	import flash.text.TextFieldAutoSize;
	import flash.text.TextFormat;
	//扩展sprite类
	public class Tag extends Sprite
	{
		//这里使用嵌入式字体,在字体变换大小时,能让flash变得流畅些
		[Embed(source="C:/WINDOWS/Fonts/ARIALBD.TTF", fontFamily="Arial", fontWeight="Bold")]
		private var _arial_str:String;
		//3D空间坐标
		private var _tagX:Number = 0;
		private var _tagY:Number = 0;
		private var _tagZ:Number = 0;
		//tag文字部分
		private var _textField:TextField;
		private var _active:Boolean;
		private var _color:Number;
		private var _highlightColor:Number;
		//tag背景部分
		private var _back:Sprite;
		private var _text:String;
		
		public function Tag(text:String, color:Number, highlightColor:Number)
		{
			_text = text;
			_color = color;
			_highlightColor = highlightColor;
			_active = false;
			//create a new text field,建立一个新的text field
			_textField = new TextField();
			_textField.autoSize = TextFieldAutoSize.LEFT;
			_textField.selectable = false;
			//apply a text format to the text field,设置文字的样式
			var textFormat:TextFormat = new TextFormat();
			textFormat.font = "Arial";
			textFormat.bold = true;
			textFormat.color = color;
			textFormat.size = 16;
			_textField.defaultTextFormat = textFormat;
			//如果不用嵌入式字体,这里一定要用false, 否则不出字
			_textField.embedFonts = true;
			_textField.text = text;
			addChild(_textField);
			//set text field position
			_textField.x = (-this.width) / 2;
			_textField.y = (-this.height) / 2;
			//set background,设置标签背景,这里是画了一个方框
			_back = new Sprite();
			_back.graphics.beginFill(_highlightColor, 0);
			_back.graphics.lineStyle(0, _highlightColor);
			_back.graphics.drawRect(0, 0, _textField.textWidth + 20, _textField.textHeight + 5);
			_back.graphics.endFill();
			addChildAt(_back, 0);
			_back.x = -_textField.textWidth / 2 - 10;
			_back.y = -_textField.textHeight / 2 - 2;
			_back.visible = false;
			this.buttonMode = true;
			//定义相应的鼠标事件
			addEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);
			addEventListener(MouseEvent.MOUSE_OVER, mouseOverHandler);
			addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
			return;
		}
		
		private function mouseOutHandler(event:MouseEvent):void
		{
			_back.visible = false;
			_textField.textColor = _color;
			_active = false;
			return;
		}
		
		private function mouseOverHandler(event:MouseEvent):void
		{
			_back.visible = true;
			_textField.textColor = _highlightColor;
			_active = true;
			return;
		}
		/**
		 * 原作者的代码是当点击某个标签后,将去含有标签内容的blog,这次是练习,故此省略。
		 */
		private function mouseUpHandler(event:MouseEvent):void
		{
			return;
		}
		
		private function getNumberFromString(text:String):Number
		{
			return Number(text.match(/(\d|\.|\,)/g).join("").split(",").join("."));
		}
		//get set functions
		public function set tagX(value:Number):void
		{
			_tagX = value;
		}
		
		public function get tagX():Number
		{
			return _tagX;
		}
		
		public function set tagY(value:Number):void
		{
			_tagY = value;
		}
		
		public function get tagY():Number
		{
			return _tagY;
		}
		
		public function set tagZ(value:Number):void
		{
			_tagZ = value;
		}
		
		public function get tagZ():Number
		{
			return _tagZ;
		}
		
		public function set active(value:Boolean):void
		{
			_active = value;
		}
		
		public function get active():Boolean
		{
			return _active;
		}

	}
}

第二步:建立tag cloud movie clip

tag cloud调用tag sprite的实例,在时间轴上,让许多tag按照球形的轨迹一帧一帧的变化。当然,tag的变化有位置上的,有颜色上的,还有大小。

那个movie clip类的重点就是Event.ENTER_FRAME事件的监听方程。

WordPress插件的工作原理是,tag cloud会读取WordPress生成的一个tag的xml文件,tagcloud.xml。在xml文件中,可以写入字体的大小,颜色等。当tag cloud被调用时,flash会读取xml文件,对tag的样式进行相应的设置。我们看到的原图,字体的大小,和颜色比模仿出来的要多变些。练习中,文字的大小和颜色都写死了。

下面的代码,就省略了读取xml文件的部分。

 

package com.chestnut
{
	import flash.display.MovieClip;
	import flash.display.Stage;
	import flash.display.StageAlign;
	import flash.display.StageScaleMode;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.text.TextField;

	public class TagCloud extends MovieClip
	{
		private var active:Boolean;
		private var highlightColor:Number;
		
		private var mcList:Array;
		public var tagText:TextField;
		private var d:Number;
		//3D位移的变量
		private var sa:Number;
		private var sc:Number;
		private var cb:Number;
		private var ca:Number;
		private var sb:Number;
		private var cc:Number;
		//旋转速度
		private var tspeed:Number;
		private var tcolor:Number;
		private var tcolor2:Number;
		//球半径
		private var radius:Number;
		private var lasta:Number;
		private var lastb:Number;
		//tag的载体,新的一层,方便管理
		private var holder:MovieClip;
		private var distr:Boolean;
		//translating from degrees to radians
		private var dtr:Number;
		
		public function TagCloud()
		{
			var array:Array;	
			tcolor = 0x71DCDE;
			tcolor2 = 10048768;
			highlightColor = 1;
			tspeed = 2;
			distr = true;
		}
		/**
		 * 如果标签少,不适用此方法也可以。此方法,可以放置标签过多时,相互的叠压,而看不到想看的标签。
		 */
		private function depthSort():void
		{
			var indexTmp:Number = 0;
			var index:Number = 0;
			mcList.sortOn("tagZ", Array.DESCENDING | Array.NUMERIC);
			while(index < mcList.length)
			{
				holder.setChildIndex(mcList[index], index);
				if(mcList[index].active)
				{
					indexTmp = index;
				}
				index++;
			}
			holder.setChildIndex(mcList[indexTmp], (mcList.length - 1));
			return;
		}
		/**
		 * 标签云的初始化,把关键字随意的放置成球形。
		 */
		public function init():void
		{
			var swfStage:Stage = this.stage;
			swfStage.scaleMode = StageScaleMode.NO_SCALE;
			swfStage.align = StageAlign.TOP_LEFT;
			
			radius = 120;
			dtr = Math.PI / 180;
			d = 500;
			//设置3D位移的系数
			sineCosine(0, 0, 0);
			//初始化tag array
			mcList = [];
			active = false;
			//写死关键字
			var tagArray:Array = ["Chestnut@riashanghai", "marked", "return", "HCC", "here", "Netherlands", 
					  "Ever", "dawn", "computer", "enthusiasts", "gather", "Utrecht", 
					  "year", "admire", "models", "stuff", "discount", 
					  "age", "internet", "prices", "being", "web", "shops",
					  "blogs", "detailing", "gadget", "long", "before", "shops", 
					  "formula", "lost", "appeal", "found", "interesting", "about",
					  "event", "however", "browse", "through", "refurbished", "stuff"];
			var tag:Tag;
			lasta = 1;
			lastb = 1;
			holder = new MovieClip();
			addChild(holder);
			resizeHolder();
			//每个关键字都转化成tag
			for each(var str:String in tagArray)
			{
				tag = new Tag(str, getColorFromGradient(1), getColorFromGradient(highlightColor));
				holder.addChild(tag);
				mcList.push(tag);
			}
			//把tag放到初始化的位置
			positionAll();
			//加入帧的监听和鼠标事件监听
			addEventListener(Event.ENTER_FRAME, updateTags);
			stage.addEventListener(Event.MOUSE_LEAVE, mouseExitHandler);
			stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
			stage.addEventListener(Event.RESIZE, resizeHandler);
		}
		/**
		 * 计算颜色的算法 */
		private function getColorFromGradient(value:Number):Number
		{
			...
		}
		/**
		 * 每一帧要执行的方程,关键所在。根据鼠标的位置所在,进行相应的旋转。
		 */
		private function updateTags(event:Event):void
		{
			var rx:Number = 0;
			var ry:Number = 0;
			var rz:Number = 0;
			var index:Number = 0;
			var ans6:Number = 0;
			var ans7:Number = 0;
			var ans8:Number = 0;
			var ans9:Number = 0;
			var ans10:Number = 0;
			var newX:Number = 0;
			var newY:Number = 0;
			var newZ:Number = 0;
			var newScale:Number = 0;
			//根据鼠标位置,来计算要移动的角度
			if(active)
			{
				...
			}else
			{
				...
			}
			lasta = rx;
			lastb = ry;
			if(Math.abs(rx) > 0.01 || Math.abs(ry) > 0.01)
			{
				rz = 0;
				sineCosine(rx, ry, rz);
				index = 0;
				while(index < mcList.length)
				{
					//计算要移动到的新位置
					ans6 = mcList[index].tagX;
					...
					//设置标签到新的位置
					mcList[index].tagX = newX;
					...
					//设置透视效果
					newScale = d / (d + newZ);
					...
					//设置透明度
					mcList[index].alpha = newScale * 0.5;
					index++;
				}
				depthSort();
			}
			return;
		}
		/**
		 * 调整holder层的大小
		 */
		private function resizeHolder():void
		{
			var _scaleX:Number;
			holder.x = stage.stageWidth * 0.5;
			holder.y = stage.stageHeight * 0.5;
			if(stage.stageWidth > stage.stageHeight)
			{
				_scaleX = stage.stageHeight * 0.002;
			}
			else
			{
				_scaleX = stage.stageWidth * 0.002;
			}
			holder.scaleY = _scaleX;
			holder.scaleY = _scaleX;
			return;
		}
		
		private function mouseMoveHandler(event:MouseEvent):void
		{
			active = true;
			return;
		}
		
		/**
		 * 把tag随机的放置成球形的形状
		 */
		private function positionAll():void
		{
			var phi:Number = 0;
			var theta:Number = 0;
			var max:Number = 0;
			var i:Number = 0;
			max = mcList.length;
			mcList.sort(function(){
				return Math.random() < 0.5 ? (1) : (-1);
			});
			//设置每个tag的位置
			while (i < max)
			{
				...
			}
			return;
		}
		/**
		 * Flash likes radians, but it is much easier to work with degrees.
		 * If you want to rotate a point by one degree you simply increment a variable by one, 
		 * easily done with the "++" operator. 
		 * However if you wanted to rotate a point by the equivalent of one degree in radians,
		 * you would need to increment an angle by 0.0174532925199433.
		 * The variables a, b, and c are the angle increments at which the points are going to be rotating. 
		 * I set them to be zero initially.
		 * 我从ahab's math tutorial找到的解释。就是说3D空间的点的移动,用角度操作要比弧度容易些。
		 * a b c 就是一个点要在x y z上要移动的角度。这里得到角度的cos sin值,做位移矩阵乘法时待用。
		 */
		private function sineCosine(a:Number, b:Number, c:Number):void
		{
			sa = Math.sin(a * dtr);
			ca = Math.cos(a * dtr);
			sb = Math.sin(b * dtr);
			cb = Math.cos(b * dtr);
			sc = Math.sin(c * dtr);
			cc = Math.cos(c * dtr);
			return;
		}
		
		private function mouseExitHandler(event:Event):void
		{
			active = false;
			return;
		}
		
		private function resizeHandler(event:Event) : void
        {
            resizeHolder();
            return;
        }
		
		
	}
}

第三步:在主程序中,调用tag cloud类的实例就可以了。

备注:没有在本社区发表过文章的用户,请不要找作者所要源代码。汲取信息的同时,也需要您的付出。