摘要
贴吧社区上线了用户等级权限系统,“涂鸦”属于“等级权限”项目中的单项权限功能,有助于丰富完善等级权限体系,为高等级用户提供更强大的功能,帮助产出差异化内容。
“涂鸦”使用Actionscript3开发。本文主要介绍功能实现方式和开发过程中值得注意的地方。
TAG
Actionscript3 画图板
术语或简称
AS: Actionscript3.0;
DOOUM: AS的鼠标五个事件,MouseDown、MouseOver、MouseOut、MouseUp、MouseMove事件;
内容
1.分层设计 — 简化的MVC;
2.单例模式;
3.画笔、橡皮擦和背景的实现;
4.撤销、重做的实现;
分层设计
网络层
CheckJsReady:负责初始化时和JS互相确认初始化完毕;
SayToJs:负责ActionScript和JavaScript的交互工作;
ImageUpload: 负责ActionScript和Server的交互;
应用层
1) View & Model 因为View层的元素不会出现多例的情况,View和Model层已结合在一起。
ViewList: 保存所有主要View对象引用,单例;
Paper:涂鸦画板;
ToolPanel: 工具栏,放置涂鸦相关工具;
BackgroundPicture: 画板背景对象;
WaterMarker:涂鸦水印;
2) Control
ControlCore: 控制中心,单例;
分层模型图示(图1)
MVC模型图示(图2)
基本配置文件
BaseConf: 基本配置,包括Flash所有的默认配置;
FunctionalButtonImages: 加载所有的按钮上的Icon图片,默认编译在swf中;
MouseCursorImages: 加载所有的鼠标指针,默认编译在swf中;
MsgList: 消息文本列表,程序所有的提醒消息文本。
单例模式
单例模式可以不用在多个类中产生实例,而只是产生一个实例。单例模式可以实现在多个类中可以共享一个实例的数据。同时也可以避免在多个类中反复创建某个实例的工作,因为实际上我们并不需要也不想要每次都new一遍。
如前面所述,ControlCore和 ViewList均使用的是单例模式。
ControlCore作为控制中心,应该是以一个全局的对象存在。所以它不应该在每个类中都出现一个实例。
ViewList保存的是主要View层对象的引用,其实质是对象引用的List,也应该是一个全局的对象。
在单例模式中,使用单例模式的类应该是无法被实例化的。这样才能保证这个类的正确使用。一般将构造函数定义成私有的(private)即可达到这个目的。但是在AS中,构造函数是无法定义成私有的。所以在AS选择了另一种做法。
例: 以下是 A.as代码
-
- 01 Package {
-
- 02 public class A{
-
- 03 static private var _a:A;
-
- 04 //构造函数需要传入Class N的实例
-
- 05 public function A(n:N){}
-
- 06
-
- 07 public static function g():A{
-
- 08 if(A._a == null) {
-
- 09 A._a = new A(new N());
-
- 10 }
-
- 11 return A._a;
-
- 12 }
-
- 13
-
- 14 public function doSomething():void{}
-
- 15 }
-
- 16 }
-
- 17
-
- 18 //在package外再定义一个类N
-
- 19 Class N{}
-
画笔、橡皮擦和背景
涂鸦最主要的逻辑实现都集中在Paper上。
一是因为Paper是画纸,是用户主要操作区域;
二是因为当初设计时,没有再进行细分,一些附属的功能也加在Paper里。
画笔
其实实现画笔很简单,监听好鼠标的DOOUM五个事件即可。
在实现上,Paper只是一个容器,装载着已画好的图像和正在画图像。并提供给ControlCore一个提取图像数据的接口当做提交之用。
背景BackgroundPicture处于Paper之下。当用户选择加载本地的图片之后,显示该图片。
大致的层次关系如(图3)所示:
如图所示,在用户开始绘画的时候(触发Paper的MouseDown事件),Paper则会创建一个和自己一般大小的A。Paper通过监听用户鼠标事件获得的鼠标轨迹数据,A通过接口获得数据,并draw出。
由于一些原因,在鼠标快速移入移出Paper的时候会导致笔迹与画纸边界出现断裂的现象。通过监听Paper的MouseOver & MouseOut,在触发这两个事件的时候获取鼠标触发以上两个事件的坐标,在A中进行一些偏差容错的计算,使得笔迹连贯自然。(不过应该有更好的方式来实现)
用户画完后,触发Paper的MouseUp事件。在此时将A的数据与B的数据进行合并,同时将A移除。用户看到的就是最新画好的图像了。
橡皮擦
橡皮擦,可以看做是一种比较独特的画笔。记得以前有一种笔,笔迹是透明的,而且可以把笔迹经过的地方的其他的颜色抹去,很类似这里的橡皮擦。
其实橡皮擦只是在draw的模式上有所区别。
亦如上图。
默认情况下,A的blendMode为BlendMode.Normal,在两层数据合并时的模式也是一样。
用户选择橡皮擦之后, A的BlendMode则被定义为BlendMode.LAYER,在两层数据合并时的模式改为BlendMode. ERASE。
这样用户用橡皮擦“画笔”画过的地方就变成了透明的。
撤销、重做
在最开始设计的时候,思维形成了定式。从表面上看每次撤销和重做,都是回滚或者回退用户操作的某一笔。程序需要操作的是某一笔的信息—笔迹的坐标记录。
实际实现中发现,这样不靠谱。因为一笔可以无限长,这样就会导致撤销和重做会都抖需要遍历一个长度不可控的数组。即便是使用Vector,时间和空间的复杂度都是单位计量*N。
通过参考其他的绘画应用,使用了以下方式来实现撤销和重做,使得时间和空间都变得可控:
为撤销和重做自定义两个固定长度的“队列”。同时插入B的图像初始数据对象(BitmapData)入撤销队列。
在每次A与B的数据合并之后(参考图3),将B的数据对象的副本插入撤销队列。
当用户撤销时将队尾的数据对象弹出并插入重做队列中,再将此时的顶部数据对象显示在B中。
当用户重做时将重做队尾数据对象弹出并插入撤销队列中,并将对象显示在B中。
具体如图 4.1 – 4.3所示,
图4.1 画好的图像压入撤销栈
图4.2 撤销
4.3 重做
从图中可以看出,撤销队列中始终是需要有数据对象的,而且当前队尾的数据对象和当前显示的图像数据对象一致。当确定了撤销的步数为N的之后,那么撤销队列的最大值则为N+1,而重做队列的最大值则为N。
撤销队列满了时,将队头的数据对象弹出并销毁。
by lanbin