开发项目:http://game.flyh5.cn/resources/game/wechat/zjh/fangtuo/index.html
案例1:http://cpic18ny1.energytrust.com.cn/
案例2:https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxc8a2ce35fa3799ea&redirect_uri=https%3A%2F%2Fapi-wx.51h5.com%2Fweb%2Foauth%2Fcallback%2Fid%2Fzhihuipingmu.html%3Fredirect%3Dhttp%253A%252F%252Fevent.ews.m.jaeapp.com%252Foneleaf%252Fwx%252F&response_type=code&scope=snsapi_base&connect_redirect=1&state=af817cf4f82372fbb17733029aebd16e#wechat_redirect
案例3:http://wap.i-h5.cn/hzx_game/dell/index.html?from=groupmessage&isappinstalled=0
我所做的项目是一个展示类的h5营销项目,没有太多的交互逻辑,主要在于那个360场景的编写,其实我也是赶鸭子上架,boos突然丢了一个案例给我研究一下,说会有一个项目进来要做成360旋转效果,我&*¥@%*@¥%@@—¥#¥。
好吧,研究的坎坷历程就略过了吧,我也是在别人的项目基础上做的文章,而且之前也没有写过3d动画,所以只拿一些我研究出来的东西说道记录一下,具体原理我也一知半解,不过使用他写个简单项目应该还是够了。
项目中主要使用的3d动画库为:css3d-engine.js。github为:https://github.com/shrekshrek/css3d-engine
还使用了一个名为jstween的动画库,可以配合css3d-engine.js使用。代码中以JT代替。github为:https://github.com/shrekshrek/jstween
项目过程中使用了PxLoader进行360场景图片等资源的预加载。github为:https://github.com/thinkpixellab/PxLoader
话不多说,让我们一步一步来构建一个简单的360场景动画吧。
三维场景,需要首先创建,其他所有显示内容都通过addchild方法放入场景即可。
const anta = new C3D.Stage({
el: $("#anta")[0]
});
我们选用一个id为anta的一个div作为场景的承载元素。
C3D.Sprite是一个三维显示元素基类,一般作为容器使用,用它来创建一个显示在页面上的元素的容器
const spMain = new C3D.Sprite();
spMain.position(0, 0, -750).update();
anta.addChild(spMain);
创建一个C3D.Sprite容器作为承载所有显示内容的主容器,后续所有的显示内容都添加到这个容器中。并添加这个主容器到场景中。
C3D.Plane表示为一个平面,就是一个二维平面,一般用它来创建一个显示元素,也就是场景中所有可以看到的平面都是用它来创建,比如场景中交互的按钮这些,通过创建一个C3D.Plane,并把它添加到C3D.Sprite容器中即可。
ps:刷新相应的dom内容,位置,角度,尺寸,材质等信息只有在执行了update方法后才会被作用到dom节点,具体可以参考github
场景的背景也是通过一个C3D.Sprite容器来承载,并且显示的背景元素会通过C3D.Plane来创建,这里会配置一些参数,具体看代码:
// 一些参数定义
// 定义背景图片的个数
var bg_num = 20;
// 背景的宽高信息,也就是那张背景大图的宽高 bgInfo
var o = {
w: 3868,
h: 2474
},
// 每一个背景元素的宽度。其实就是背景大图的宽度除以你的切好的背景图片个数
M = o.w / 20,
// 此处需要根据不同的场景宽度(大图的总宽度)进行调整,否则背景图片元素之间可能有间隔
h = 605,
//
bgImage = [
// 这里是场景的背景图片url链接,比如:
'bg-image1.jpg',
'bg-image2.jpg',
...
];
// 创建背景容器
var panoBg = new C3D.Sprite();
// No need for attention
var d = {
lat: 0,
lon: 0
},
f = {
lon: 0,
lat: 0
};
// c.lon设置场景的水平初始的位置,也就是场景生成后,默认停留到哪一个位置,这里是角度
// 你可以根据项目自行设置场景最后停留的位置。而且在场景初始动画中,他的动画结束位置也需要
// 和此处保持一致
var c = {
lon: 205,
lat: 0,
};
var p = true;
// 设置背景容器初始信息,并添加至spMain这个主容器中。
panoBg.name("panoBg").position(0, 0, 0).update();
spMain.addChild(panoBg);
for (var R = 0; R < bg_num; R++) {
// No need for attention
var F = new C3D.Plane,
H = -360 / bg_num * R,
J = H / 180 * Math.PI,
U = h;
// 设置场景的基本显示信息,这里默认是不可见的,alpha为0
// size:设置大小 position:设置位置 rotation:设置选择角度 visibility:设置可见性
// material 设置材质,这里的话,直接用来简单的设置背景图片
F.size(M, o.h).position(Math.sin(J) * U, 150, Math.cos(J) * U).rotation(0, H + 180, 0).visibility({
alpha: 0
}).material({
image: bgImage.url,
bothsides: !1
}).update();
panoBg.addChild(F);
}
// 重力感应,通过监测重力感应,做到场景随手机的摇摆进行改动。
// 通过此重力感应更新d,f,c这几个对象的信息,这几个对象具体有什么用,
// 我也不太清楚。但是有些不支持这个Orienter的可能就不行。
var o2 = new Orienter();
o2.handler = function (t) {
d.lon = -t.lon;
d.lat = t.lat;
if (p) {
f.lat = -d.lat;
f.lon = -d.lon;
}
};
o2.init();
场景的背景是以一张左右可以闭合的大图,垂直等分为一定数量的等宽小图片,比如一张大图垂直等分为20张小图,每张小图都创建一个C3D.Plane元素。
ps: 代码中基本上我知道的都注释了,不过代码中注释了 No need for attention 字样的,表示我也不是太清楚具体的作用,但这些不用了解也可以基本满足功能。就这样就可以了。
至此,360场景中的背景的实现基本上就完成了。不过现在还是不可见的状态,不过先不急,稍后会讲解。我们先来往场景中添加一些可交互的按钮吧。
360场景中的交互,一般就是可点击的按钮,这是最基础的交互方式,这里也是通过创建一个C3D.Sprite主容器用来承载所有的交互区域,并且一个交互区域也是通过一个C3D.Plane来创建的,具体看代码:
var btnDots = [
// 通过这个配置对象,生成场景中的交互元素。ps:这一类的交互一定是类似的
// 如果不是,可以根据这个自己重新编写其他的交互元素
// name: 这个交互元素的名称,不要重名了 x,y:此元素在场景中的位置
// dot为所需要的背景图片地址,w,h为此元素的宽高。
{
name: "dialog1",
x: 667,
y: 1068,
dot: 'images/click.png',
w: 186,
h: 196,
},
{
name: "dialog2",
x: 1960,
y: 1400,
dot: 'images/click.png',
w: 186,
h: 196,
},
]
var panoDots = new C3D.Sprite;
panoDots.name("panoDots").visibility({
alpha: 0
}).position(0, 0, 0).update();
$.each(btnDots, function (A, B) {
// No need for attention
var g = B,
Q = -360 * (g.x - 80) / o.w,
G = 90 * (g.y - o.h / 2) / o.h,
M = Q / 180 * Math.PI,
Y = h - 80,
// C3D.create 通过配置来创建指定的元素
i = C3D.create({
type: "sprite",
name: g.name,
scale: [1],
children: [{
type: "plane",
name: "dot",
size: [g.w, g.h],
position: [0, 2, 2],
rotation: [G, 0, 0],
material: [{
image: g.dot,
size: 'cover',
}],
bothsides: !1
},
{
type: "plane",
name: "label",
size: [0, g.h],
rotation: [G, 0, 0],
origin: [-18, 33],
material: [{
image: g.label
}],
bothsides: !1
}]
});
i.position(Math.sin(M) * Y, .9 * (g.y - o.h / 2), Math.cos(M) * Y).rotation(0, Q + 180 - 5, 0).updateT(),
// 为此元素绑定触摸事件,此处可以用于点击时,执行一些操作。
i.on("touchend", function () {
// TODO
}),
// No need for attention
i.r0 = Q,
i.w0 = g.w,
i.dot.alpha = .5,
i.dot.updateV(),
panoDots.addChild(i)
});
spMain.addChild(panoDots);
以上就是向场景中添加一些交互元素,目前只是实现了简单的点击交互,而且,现在场景中还无法移动,仅仅只能通过Orienter进行移动,所以我们需要给anta这个场景添加触摸事件。来支持场景随手指移动进行移动。
// No need for attention
var originTouchPos = {
x: 0,
y: 0
},
oldTouchPos = {
x: 0,
y: 0
},
newTouchPos = {
x: 0,
y: 0
},
firstDir = "",
originTime = 0,
oldTime = 0,
newTime = 0,
dx = 0,
dy = 0,
ax = 0,
ay = 0,
time = 0;
// touchstart事件处理函数
var onTouchStart = function (t) {
firstDir = "",
t = t.changedTouches[0];
originTouchPos.x = oldTouchPos.x = newTouchPos.x = t.clientX;
originTouchPos.y = oldTouchPos.y = newTouchPos.y = t.clientY;
originTime = oldTime = newTime = Date.now();
dx = dy = ax = ay = 0,
anta.on("touchmove", onTouchMove),
anta.on("touchend", onTouchEnd)
};
anta.on("touchstart", onTouchStart);
var onTouchMove = function (t) {
anta.off("touchend", onTouchEnd);
return t = t.changedTouches[0],
newTouchPos.x = t.clientX,
newTouchPos.y = t.clientY,
newTime = Date.now(),
checkGesture(),
oldTouchPos.x = newTouchPos.x,
oldTouchPos.y = newTouchPos.y,
oldTime = newTime, !1
};
var onTouchEnd = function (e) {
newTime = Date.now();
var t = (newTime - oldTime) / 1e3;
// 这里可以通过e.target获取到触发了此touchend事件的dom对象,
// 这样就可以根据此对象来判断是点击了哪个场景的元素。当然,可以直接为元素绑定点击事件,在此处绑定
// TODO
anta.off("touchmove", onTouchMove),
anta.off("touchend", onTouchEnd);
}
// No need for attention
function checkGesture() {
dx = fixed2(newTouchPos.x - originTouchPos.x),
dy = fixed2(newTouchPos.y - originTouchPos.y),
ax = fixed2(newTouchPos.x - oldTouchPos.x),
ay = fixed2(newTouchPos.y - oldTouchPos.y),
time = (newTime - oldTime) / 1e3,
"" == firstDir && (Math.abs(ax) > Math.abs(ay) ? firstDir = "x" : Math.abs(ax) < Math.abs(ay) && (firstDir = "y"));
// 此处可以调整45和-45的数值,他们用于场景的上下视角控制
if (!p) {
c.lon = (c.lon - .2 * ax) % 360,
c.lat = Math.max(-45, Math.min(45, c.lat + .2 * ay))
}
}
// 功能函数
function fixed2(t) {
return Math.floor(100 * t) / 100
}
上述为场景增加的触摸事件,他并没有更新场景位置的功能,也就是他在手指滑动时,只是更新了一些位置信息数据,而触发场景位置更新的另有其他的函数执行。
而且touchmove事件时为了场景的移动而触发,而touchend是作为场景的点击的功能触发,所以他们的执行
都会移除后续相应的touch事件。以下是持续触发的场景位置更新操作,他是一直在执行的:
// 执行动画,有兴趣的可以了解一下requestAnimationFrame
requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame ||
function (callback) {
setTimeout(callback, 1000 / 60);
};
// 执行场景的位置更新操作,他根据触摸事件更新的数据来进行场景的位置更新。
function actiondh() {
var t = (d.lon + f.lon + c.lon) % 360,
i = .35 * (d.lat + f.lat + c.lat);
t - panoBg.rotationY > 180 && (panoBg.rotationY += 360),
t - panoBg.rotationY < -180 && (panoBg.rotationY -= 360);
var n = t - panoBg.rotationY,
a = i - panoBg.rotationX;
Math.abs(n) < .1 ? panoBg.rotationY = t : panoBg.rotationY += .3 * n,
Math.abs(a) < .1 ? panoBg.rotationX = i : panoBg.rotationX += .15 * a,
panoBg.updateT();
// 每次更新场景位置时,都需要重新更新你添加的交互元素的位置,你添加了哪些容器
// 每次就需要更新那些容器的位置。
panoDots.rotationY = panoBg.rotationY,
panoDots.rotationX = panoBg.rotationX,
panoDots.updateT(),
t - panoItems.rotationY > 180 && (panoItems.rotationY += 360),
t - panoItems.rotationY < -180 && (panoItems.rotationY -= 360);
var o = t - panoItems.rotationY,
r = i - panoItems.rotationX;
Math.abs(o) < .1 ? panoItems.rotationY = t : panoItems.rotationY += .25 * o, Math.abs(r) < .1 ? panoItems.rotationX = i : panoItems.rotationX += .15 * r, panoItems.updateT();
var s12 = -150 - 20 * Math.abs(n);
spMain.z += .1 * (s12 - spMain.z),
spMain.updateT(),
A = requestAnimationFrame(actiondh);
}
这样,每次通过重力感应对象Orienter或者通过触摸事件时都会更新场景的位置信息。基本至此,一个360场景h5项目的简单点击交互功能都可以实现了,还有一些有点复杂的交互判断,我自己也不太搞得清楚所以就不拿出来说了。
基本上我们看这种类型的h5项目都有一个类似卷轴般的展开动画效果,其时机在于场景背景资源加载完毕时,就可以通过JT动画库来实现一个卷轴效果,具体代码如下:
// loc是和上面创建背景容器时设置的c变量中的lon是一致的。
var loc = 205;
const stageInit = function stageInit() {
// spMain设置主容器的缩放,z为缩放,此处是从小变大的缩放效果,持续四秒,并且每次动画执行都需要更新场景的位置信息。
JT.fromTo(spMain, 4, {
z: -2200
}, {
z: -150,
ease: JT.Quad.Out,
onUpdate: function () {
this.target.updateT().updateV()
}, onEnd: function () {
p = false
// 开始执行场景位置更新
actiondh();
}
});
// 设置背景容器的卷轴滚动效果。
JT.fromTo(panoBg, 4, {
rotationY: -720,
}, {
rotationY: loc,
ease: JT.Quad.Out,
onUpdate: function () {
this.target.updateT().updateV()
}, onEnd: function () {
this.target.updateT().updateV()
}
});
// 设置背景容器中的所有背景元素的滚动动画(主要效果)
for (var A = 0, B = panoBg.children.length; B > A; A++) {
JT.from(panoBg.children[A], 0.5, {
x: 0,
z: 0,
scaleX: 0,
scaleY: 0,
delay: .05 * A,
ease: JT.Quad.Out,
onUpdate: function () {
this.target.updateT()
},
onStart: function () {
this.target.visibility({
alpha: 1
}).updateV()
}
});
}
// 等到卷轴滚动动画执行完后,显示交互元素
JT.fromTo(panoDots, 0.1, {
rotationY: -360,
alpha: 0
}, {
rotationY: loc,
alpha: 1,
delay: 4,
ease: JT.Quad.Out,
onUpdate: function () {
this.target.updateT().updateV()
},
onStart: function () {
this.target.visibility({
alpha: 1
}).updateV()
}
})
}
以上代码就是场景的类似卷轴滚动的交互动画,而此动画的执行时机可以根据项目需要进行调用。比如通常项目会有loading页,可以等待loading页加载完后在执行此动画函数。
至此一个简单的360场景类h5交互项目就可以实现了,不过在此还是有一些需要注意的地方:
1.场景的背景图最好不要太大,因为图像越大,在执行场景动画时效果就卡,ios还好,安卓就不太理想了。
2.场景图最好提前使用PxLoader进行预加载,他的使用也很简单,看一下官方文档即可,你也可以等场景图预加载完成后在执行场景动画函数。PxLoader的github:https://github.com/thinkpixellab/PxLoader
3.注意场景背景创建中的h变量的设置,不同的总图片宽度不同,h变量就不同,需要自行测试设置,图片宽度越大
则h需要调整的越大。
总结:
最开始boos丢个案列给我时,让我以这个案例为基础改出一个新项目时,其实我是一脸懵b的,不过老板交代的,没办法,所幸也不是很复杂的项目,交互也仅仅限于场景中的点击交互,经过我一段段代码的测试,一个个参数的修改,查明他是什么意思,费了比较大的一番功夫,而且有些东西不是全都可以明白的。让我自己从头开始重新写,那可就不是看看别人写的这么简单的了,不过也不是没有收获吧,编写这种360效果的h5项目,让我多了解了一个新领域,而且,相对于功能单一,交互简单的这种,我对一个叫做krpano的软件编写出来的全景效果更感兴趣。而且这也可以作为一个技术方向,了解和学习。
ps:以上代码是在某个项目中截取出来,以作为学习交流之用,如有侵权,请联系删除。