起源
在Qt的演示样例中看到了一个有趣的demo。截图例如以下:
这个demo的名字叫Drag and Drop Robot,简单概括而言,在这个demo中,能够把机器人四周的颜色拖动到机器人的各个部位,比方说头。臂,身躯等。然后这个部位就会变成相应的颜色。相似于换装小游戏。
上图就是经过愚下的拖动颜色使其简略换装后的样子。
当然,拖动颜色使部件变色的功能并不难实现,关键在于这个机器人是动态的,我们要研究的就恰恰是这个机器人动画是怎么做出来的。
光凭两张图片我们无法知道这个动画究竟是什么样子的,大家能够參考本次用html5移植到浏览器平台的demo:
http://yuehaowang.github.io/demo/drag_and_drop_robot/
截图例如以下:
因为愚下对人体运动了解不深,所以demo里的机器人运动得不是非常和谐。各位看官能够在文末下载源码,通过本次解说,拿回去自己改改,让这个机器人动得更带感一点。
以下是实现过程。
准备工作
先来看看文件结构:
当中,lufylegend-1.9.9.simple.min.js是html5引擎lufylegend里的文件,因为该引擎带有缓动类,所以实现本次效果会easy一些。
引擎官方地址:http://lufylegend.com
中文文档地址:http://lufylegend.com/api/zh_CN/out/index.html
因为下文的代码中会多次出现一些引擎里的类和方法,所以我把这些类和方法在文档里的地址放在以下,供大家參考:
- LExtends:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LExtends
- LLoadManage:http://lufylegend.com/api/zh_CN/out/classes/LLoadManage.html
- LInit:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LInit
- LSprite:http://lufylegend.com/api/zh_CN/out/classes/LSprite.html
- LTextField:http://lufylegend.com/api/zh_CN/out/classes/LTextField.html
- LDropShadowFilter:http://lufylegend.com/api/zh_CN/out/classes/LDropShadowFilter.html
- LTweenLite:http://lufylegend.com/api/zh_CN/out/classes/LTweenLite.html
- LGraphics:http://lufylegend.com/api/zh_CN/out/classes/LGraphics.html
实现过程
Main.js
完整代码:
LInit(50, "mydemo", 800, 600, loadRes);
var stageLayer, selectedColorBox = null, partList = null;
function loadRes () {
var loadList = [
{path : "./Robot.js"},
{path : "./Part.js"},
{path : "./Body.js"},
{path : "./Head.js"},
{path : "./Limb.js"},
{path : "./ColorBox.js"}
];
var loadingTxt = new LTextField();
loadingTxt.text = "Loading...";
addChild(loadingTxt);
LLoadManage.load(loadList, null, function () {
loadingTxt.remove();
initStageLayer();
addRobot();
addColors();
});
}
function initStageLayer () {
stageLayer = new LSprite();
stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height]);
addChild(stageLayer);
stageLayer.addEventListener(LMouseEvent.MOUSE_MOVE, function () {
if (selectedColorBox) {
selectedColorBox.x = mouseX;
selectedColorBox.y = mouseY;
}
});
stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
if (selectedColorBox) {
if (partList) {
for (var i = 0, l = partList.length; i < l; i++) {
var o = partList[i], p = o.part, e = o.exec;
if (isPartHitObject(p, selectedColorBox)) {
setPartAlpha(p, 1);
e.fillColor = selectedColorBox.color;
e.draw();
}
}
}
selectedColorBox.remove();
selectedColorBox = null;
}
});
stageLayer.addEventListener(LEvent.ENTER_FRAME, loop);
}
function loop () {
if (partList && selectedColorBox) {
for (var i = 0, l = partList.length; i < l; i++) {
var p = partList[i].part;
if (isPartHitObject(p, selectedColorBox)) {
setPartAlpha(p, 0.5);
} else {
setPartAlpha(p, 1);
}
}
}
}
function isPartHitObject (list, obj) {
for (var i = 0, l = list.length; i < l; i++) {
if (list[i].hitTestObject(obj)) {
return true;
}
}
return false;
}
function setPartAlpha (list, a) {
for (var i = 0, l = list.length; i < l; i++) {
list[i].alpha = a;
}
}
function addRobot () {
var robot = new Robot();
robot.x = (LGlobal.width - robot.getWidth()) / 2;
robot.y = 220;
stageLayer.addChild(robot);
}
function addColors () {
var colorList = [
"orange",
"red",
"yellow",
"green",
"blue",
"lightblue",
"purple",
"brown",
"lightgreen",
"orangered"
];
var r = (LGlobal.height - 80) / 2;
var layer = new LSprite();
layer.x = LGlobal.width / 2;
layer.y = LGlobal.height / 2;
stageLayer.addChild(layer);
for (var i = 0, l = colorList.length; i < l; i++) {
var angle = 2 * i * Math.PI / l;
var colorBox = new ColorBox(colorList[i]);
colorBox.x = r * Math.cos(angle);
colorBox.y = r * Math.sin(angle);
layer.addChild(colorBox);
colorBox.addEventListener(LMouseEvent.MOUSE_DOWN, function (e) {
selectedColorBox = e.currentTarget.clone();
selectedColorBox.x = e.offsetX;
selectedColorBox.y = e.offsetY;
stageLayer.addChild(selectedColorBox);
});
}
}
变量介绍:
- stageLayer:舞台层
- selectedColorBox:正在拖动的颜色
- partList:机器人部件列表,下文会有具体介绍
函数介绍:
- loadRes:用于载入文件
- initStageLayer:初始化舞台层。包含舞台层增加事件,以实现拖动颜色以及拖动的颜色与机器人碰撞检測(当中出现变量partList的地方可临时忽略,读到后文,看官再回头来看。自会明确代码的意思)
- loop:循环事件监听器
- isPartHitObject:推断机器人的部件是否与某对象碰撞(推断拖动的颜色是否与机器人部件相碰撞)
- setPartAlpha:设置机器人部件的透明度(拖动的颜色碰到机器人部件上后,需改变部件透明度以提示碰撞)
- addRobot:增加机器人
- addColors:增加四周的颜色
这里主要讲一下怎样实现拖动颜色,并怎样给部件上色。
首先我们须要的是几个事件:鼠标移动,鼠标按下,鼠标松开。循环事件。鼠标按下是加在ColorBox对象上的(此类于后文解说),鼠标移动、和松开事件以及循环事件是载入舞台层stageLayer的。当我们在ColorBox对象上按下鼠标,首先要克隆该对象,并将克隆产物赋值给selectedColorBox。
这时再移动鼠标,触发鼠标移动事件监听器,并推断到了存在selectedColorBox,即鼠标在某ColorBox上按下,这时就运行ColorBox尾随鼠标操作。当鼠标松开后,首先推断克隆产物selectedColorBox是否正在与机器人部件产生碰撞,假设是则为该部件上色,随后将克隆产物销毁。这时假设再移动鼠标,则检測到克隆产物不存在,则尾随鼠标的操作不会运行。循环事件用于运行假设克隆产物碰到机器人部件则将部件变为半透明的操作。
ColorBox.js
上面的代码中有这个类的出现,这里把这个类的代码展示了:
function ColorBox (color) {
var s = this;
LExtends(s, LSprite, []);
s.color = color;
s.graphics.drawArc(0, "", [0, 0, 25, 25, 0, Math.PI * 2], true, color);
s.filters = [new LDropShadowFilter(null, null, color)];
}
代码非常easy,如有不懂之处能够先參考给出的文档地址,或者在文章下方留言。
Robot.js
前面我们看到的机器人就是通过这个类来实现的。可是正如学过生物必修一的同学都知道,动物生命层次是这种:个体->系统->器官->组织->细胞。我们的机器人就是个体,那么四肢构成运动系统,以此类推。
所以我们的这个Robot类就仅仅是个装载头部,身躯,四肢的容器。
在上面给出的文件结构中能够看到。还有Head.js和Body.js这些类。他们的实例化对象就是放在Robot这个个体里的部件了。
因此先来看Robot.js:
function Robot () {
var s = this;
LExtends(s, LSprite, []);
s.body = null;
s.head = null;
s.leftArm = null;
s.rightArm = null;
s.leftLeg = null;
s.rightLeg = null;
s.addBody();
s.addHead();
s.addArms();
s.addLegs();
partList = [
{
exec : s.body,
part : [s.body.bodyLayer]
},
{
exec : s.head,
part : [s.head.faceLayer]
},
{
exec : s.leftArm,
part : [s.leftArm.part1, s.leftArm.part2]
},
{
exec : s.rightArm,
part : [s.rightArm.part1, s.rightArm.part2]
},
{
exec : s.leftLeg,
part : [s.leftLeg.part1, s.leftLeg.part2]
},
{
exec : s.rightLeg,
part : [s.rightLeg.part1, s.rightLeg.part2]
}
];
}
Robot.prototype.addBody = function () {
var s = this;
s.body = new Body(80, 100, 15);
s.addChild(s.body);
};
Robot.prototype.addHead = function () {
var s = this;
s.head = new Head(40, 50);
s.head.x = s.body.getWidth() / 2;
s.body.addChild(s.head);
};
Robot.prototype.addArms = function () {
var s = this, l = 60, r = 7.5;
s.leftArm = new Limb(l, r, 90, 90, 60, 5);
s.leftArm.x = r + 4;
s.leftArm.y = r + 4;
s.body.addChild(s.leftArm);
s.rightArm = new Limb(l, r, -140, -140, -30, -5);
s.rightArm.x = 76 - r;
s.rightArm.y = r + 4;
s.body.addChild(s.rightArm);
};
Robot.prototype.addLegs = function () {
var s = this, l = 70, r = 7.5;
s.leftLeg = new Limb(l, r, 70, -40, 80, 0);
s.leftLeg.x = r + 3;
s.leftLeg.y = 96 -r;
s.body.addChild(s.leftLeg);
s.rightLeg = new Limb(l, r, -60, 30, 10, 60);
s.rightLeg.x = 76 - r;
s.rightLeg.y = 96 -r;
s.body.addChild(s.rightLeg);
};
属性介绍:
- body:机器人身躯对象
- head:机器人头部对象
- leftArm & rightArm:机器人手臂对象
- leftLeg & rightLeg:机器人腿部对象
函数介绍:
- 构造器:调用其它各个函数并为partList赋值
- addBody & addHead & addArms & addLegs:增加各个部件
partList数据结构介绍:
先前我们在Main.js中看到过这个变量。这个变量是个数组。里面存放了多个Object。
这些Object中有part和exec两个属性。
part相应的值是部件中參与碰撞检測的对象(LSprite对象)。比方说头部里的faceLayer,手臂中的两个部分part1和part2。
exec主要是在刷新部件时用到,毕竟改变了颜色后,机器人身上的部件要重画一遍,那么就须要调用exec相应的对象中的重画函数。
画出各种部件及其缓动动画的实现
※ 提示:以下的代码。会用到非常多LGraphics,LTweenLite,不熟悉的同学,建议先阅读上文给出的文档
Part.js
全部部件的父类——Part类:
function Part () {
var s = this;
LExtends(s, LSprite, []);
s.fillColor = "lightgray";
}
仅仅有一个属性fillColor:部件填充的颜色
Body.js
身躯部件——Body类:
function Body (w, h, r) {
var s = this;
LExtends(s, Part, []);
s.w = w;
s.h = h;
s.r = r;
s.bodyLayer = new LSprite();
s.addChild(s.bodyLayer);
s.bodyLayer.addShape(LShape.RECT, [0, 0, w, h]);
s.draw();
LTweenLite.to(s, 1, {
rotate : 5,
loop : true,
ease : Cubic.easeInOut
}).to(s, 1, {
rotate : -10,
ease : Cubic.easeInOut
});
}
Body.prototype.draw = function () {
var s = this,
w = s.w,
h = s.h,
r = s.r,
c = s.fillColor,
lx = r - 3,
rx = w - r + 3,
uy = r - 3,
dy = h - r + 3,
pi = Math.PI * 2;
s.bodyLayer.graphics.clear();
s.bodyLayer.graphics.drawRoundRect(1, "black", [0, 0, w, h, 10], true, c);
s.bodyLayer.graphics.drawArc(1, "black", [lx, uy, r, 0, pi], true, c);
s.bodyLayer.graphics.drawArc(1, "black", [rx, uy, r, 0, pi], true, c);
s.bodyLayer.graphics.drawArc(1, "black", [lx, dy, r, 0, pi], true, c);
s.bodyLayer.graphics.drawArc(1, "black", [rx, dy, r, 0, pi], true, c);
};
參数介绍:
- w:身躯的宽度
- h:身躯的高度
- r :身躯上用于装饰的圆的半径
在该类中,draw函数就是用来绘制部件的。假设反复调用draw,则可达到刷新的目的。除此之外,我们使用了LTweenLite来实现缓动动画。以下其它的类和此类原理同样。
Head.js
头部部件——Head类:
function Head (w, h) {
var s = this;
LExtends(s, Part, []);
s.w = w;
s.h = h;
s.faceLayer = new LSprite();
s.faceLayer.x = -w / 2;
s.faceLayer.y = -h * 0.9;
s.addChild(s.faceLayer);
s.faceLayer.addShape(LShape.RECT, [0, 0, w, h]);
s.draw();
LTweenLite.to(s, 0.8, {
rotate : -20,
loop : true,
ease : Sine.easeInOut
}).to(s, 0.8, {
rotate : 20,
ease : Sine.easeInOut
});
}
Head.prototype.draw = function () {
var s = this, w = s.w, h = s.h;
s.faceLayer.graphics.clear();
s.faceLayer.graphics.drawRoundRect(1, "black", [0, 0, w, h, 10], true, s.fillColor);
s.faceLayer.graphics.drawArc(1, "black", [12, 15, 6, 0, Math.PI * 2], true, "white");
s.faceLayer.graphics.drawArc(1, "black", [11, 15, 2, 0, Math.PI * 2], true, "black");
s.faceLayer.graphics.drawArc(1, "black", [w - 12, 15, 6, 0, Math.PI * 2], true, "white");
s.faceLayer.graphics.drawArc(1, "black", [w - 11, 15, 1, 0, Math.PI * 2], true, "black");
s.faceLayer.graphics.add(function () {
var c = LGlobal.canvas;
c.lineWidth = 3;
c.strokeStyle = "black";
c.lineCap = "round";
c.moveTo(10, 30);
c.quadraticCurveTo(20, 50, w - 10, 30);
c.stroke();
});
};
Limb.js
肢干部件——Limb类:
function Limb (l, r, rotate1, rotate2, rotate3, rotate4) {
var s = this;
LExtends(s, Part, []);
s.l = l;
s.r = r;
s.part1 = new LSprite();
s.addChild(s.part1);
s.part2 = new LSprite();
s.part2.y = l - r;
s.part1.addChild(s.part2);
s.draw();
s.part1.addShape(LShape.RECT, [-r, -r, r * 2, l]);
s.part2.addShape(LShape.RECT, [-r, -r * 1.5, r * 2, l]);
LTweenLite.to(s.part1, 1, {
rotate : rotate1,
loop : true
}).to(s.part1, 0.8, {
rotate : rotate2
});
LTweenLite.to(s.part2, 1, {
rotate : rotate3,
loop : true
}).to(s.part2, 0.8, {
rotate : rotate4
});
}
Limb.prototype.draw = function () {
var s = this,
l = s.l,
r = s.r,
w = r * 2,
c = s.fillColor;
s.part1.graphics.clear();
s.part2.graphics.clear();
s.part1.graphics.drawRoundRect(1, "black", [-r, -r, w, l, r], true, c);
s.part1.graphics.drawArc(1, "black", [0, 0, r * 1.4, 0, Math.PI * 2], true, c);
s.part2.graphics.drawRoundRect(1, "black", [-r, -r * 1.5, w, l, r], true, c);
s.part2.graphics.drawArc(1, "black", [0, 0, r * 1.4, 0, Math.PI * 2], true, c);
};
以上的代码应该注意的是每种部件的画法。当然艺术这种东西怎么能靠言传呢?所以我就不打算深究代码的含义了。至于各种部件的画法。欢迎各位借鉴。如有不通之处,敬请留言。
源码
Github地址:https://github.com/yuehaowang/drag_and_drop_robot
本次梦幻之旅就到此为止,喜欢该系列的看官能够来此专栏阅读该系列其它文章:
http://blog.csdn.net/column/details/dreamy-travel-in-h5.html
欢迎大家继续关注我的博客
转载请注明出处:Yorhom’s Game Box