『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)

时间:2022-02-02 08:58:25

起源

在Qt的示例中看到了一个有趣的demo,截图如下:

『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)

这个demo的名字叫Drag and Drop Robot,简单概括而言,在这个demo中,可以把机器人四周的颜色拖动到机器人的各个部位,比如说头,臂,身躯等,然后这个部位就会变成相应的颜色,类似于换装小游戏。

『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)

上图就是经过愚下的拖动颜色使其简略换装后的样子。

当然,拖动颜色使部件变色的功能并不难实现,关键在于这个机器人是动态的,我们要研究的就恰恰是这个机器人动画是怎么做出来的。
光凭两张图片我们无法知道这个动画到底是什么样子的,大家可以参考本次用html5移植到浏览器平台的demo:
http://yuehaowang.github.io/demo/drag_and_drop_robot/

截图如下:
『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)

由于愚下对人体运动了解不深,所以demo里的机器人运动得不是很和谐。各位看官可以在文末下载源代码,通过本次讲解,拿回去自己改改,让这个机器人动得更带感一点。

以下是实现过程。

准备工作

先来看看文件结构:
『HTML5梦幻之旅』 - 仿Qt示例Drag and Drop Robot(换装机器人)
其中,lufylegend-1.9.9.simple.min.js是html5引擎lufylegend里的文件,由于该引擎带有缓动类,所以实现本次效果会容易一些。
引擎官方地址:http://lufylegend.com
中文文档地址:http://lufylegend.com/api/zh_CN/out/index.html

由于下文的代码中会多次出现一些引擎里的类和方法,所以我把这些类和方法在文档里的地址放在下面,供大家参考:

  1. LExtends:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LExtends
  2. LLoadManage:http://lufylegend.com/api/zh_CN/out/classes/LLoadManage.html
  3. LInit:http://lufylegend.com/api/zh_CN/out/classes/%E5%85%A8%E5%B1%80%E5%87%BD%E6%95%B0.html#method_LInit
  4. LSprite:http://lufylegend.com/api/zh_CN/out/classes/LSprite.html
  5. LTextField:http://lufylegend.com/api/zh_CN/out/classes/LTextField.html
  6. LDropShadowFilter:http://lufylegend.com/api/zh_CN/out/classes/LDropShadowFilter.html
  7. LTweenLite:http://lufylegend.com/api/zh_CN/out/classes/LTweenLite.html
  8. 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)];
}

代码很简单,如有不懂之处可以先参考给出的文档地址,或者在文章下方留言。

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

http://blog.csdn.net/yorhomwang