A few days ago, the pre-heated page of Tmall Double 11 — “Invitation across the Universe”, which is almost the most cool one in the operation page of mobile terminal large-scale activities so far, has been swept a wave of friends circle. If you haven’t already, check out this link on your mobile device. Here is a detailed analysis of it from a technical point of view.
To conclude, this page is not too complicated to implement, either technically or in terms of design. Technically, it hasn’t been around much, but it’s still a lot easier than you might think. In terms of design, this idea is new to most people, but it’s not without precedent (but it’s a lot of work). What is rare is that the design scheme and the technical scheme are very well combined, and accurately grasp the current special time node.
I’ll start by explaining two terms: WebGL and ThreeJS, which are the key technologies used on this page. WebGL is a set of interfaces provided by browsers for rendering high-performance graphics, which can be basically understood as the Web version of OpenGL; ThreeJS is currently the most popular webGL-based open source 3D graphics library (star on Github is close to 3W, only slightly less than vue.js). To learn more, check out my previous article, Three.js Quick Start.
To use WebGL in a browser, you need two levels of support: hardware or system-level support and browser support. Hardware support has always been good, while browser support has been less so. On iOS, webGL is already supported in iOS8, which is fine; But Android native browsers didn’t partially support WebGL until 5.0, and obviously, that’s not a noticeable percentage.
However, Webview in wechat is special. Most of Tencent’s applications are connected to the X5 kernel, which is developed based on the WebKit kernel and supports WebGL from the beginning. This year’s 2.x release has greatly improved the stability of WebGL. Therefore, wechat page support for WebGL is relatively good.
Support for WebGL is definitely on the rise, but the numbers are unclear; In addition, for large operations, how high approval ratings are needed to adopt such a technical solution is also a matter of opinion. (It is possible to downgrade to Canvas rendering without WebGL support, but performance is poor.)
All the code is packaged into three files: index.html, main.js, and wdata.js. HTML files are not our focus; Wdata. js is a resource file, mainly base64 pictures, as well as the position of each picture in the 3D scene, skipped for the moment; The main logic of the page is in the main.js file, which is what we will focus on.
We’ll focus on the final 3D-related section. There are only two functions, one for Canvas rendering and one for WebGL rendering. It can be seen that there has been a downgrade to canvas rendering for models that do not support WebGL. Better than nothing, canvas’s performance is still difficult to support such an effect, which is an explanation for these models.
ThreeJS ‘Canvas rendering and WebGL rendering interfaces are basically the same, and the code for the two functions here is only slightly different, so we’ll just focus on webGL rendering. If you don’t know the basic concepts of scenes, cameras, etc., read ThreeJS quick Start. This function is about 500 lines long. The basic idea is to load an image resource into the scene at the preset position, and then move the camera and scene based on user input.
A main scene is adopted here, and a main Group is hung in the main scene. There are also 18 scenes that act as a kind of cache, and these 18 scenes represent different areas of the final rendering. When the camera enters an area, it will hang the Group in the scene to be loaded under the main Group and delete it when leaving, so that the desired part can be seen in the rendering of the main scene. The main benefit of this is performance. After all, the scene is very large and there are many images. If you render everything whether you can see it or not, the performance cost will increase a lot.
As mentioned earlier, this page technical solution works well with the design solution. Why do you say so? Because in the case of better effect, the implementation difficulty of technology, performance, compatibility have better results. The implementation difficulty can be seen in the above section, and it is indeed not difficult. In terms of performance, although the result is a cool 3D effect, the actual materials used are ALL 2D. Even with over 200 Sprites, there are very few faces, let alone partial renderings. The entire page performs better than simply displaying a 3D model. In terms of compatibility, just because many models support WebGL doesn’t mean they do. And ThreeJS, despite its popularity and high frequency of updates, still has a lot of holes to fill. The scheme here, however, just renders a bunch of 2D planes with textures in a 3D scene, from self-written code, to ThreeJS, to the lower level, the content involved is relatively simple. This greatly reduces the chance of trampling pits.
That’s about it from a technical point of view. Recently, IN order to meet the business needs, I also went to research ThreeJS, so I want to take a deep look at the page that uses this technology, but their level is not high, the writing is not good, welcome to correct any questions.
function(A, g, t) {
(function(g, e) {
var C = t(3)
, I = t(4)
, i = g.extend({}, g.Events, {
stage: null ,
camera: null ,
scene: null ,
renderer: null ,
effect: null ,
root: null ,
preload: null ,
stats: null ,
raycaster: null ,
mouse: null ,
fix: {
x: 0,
y: 0,
z: 0
},
aim: {
x: 0,
y: 0,
z: 0
},
data: null ,
init: function(A) {
I.ga(I.EVENT, "Ver", "Webgl");
var g = this;
this.stage = A,
this.camera = new THREE.PerspectiveCamera(60,window.innerWidth / window.innerHeight,1,1e6),
this.camera.position.z = 500,
this.scene = new THREE.Scene,
this.root = new THREE.Group,
this.root.position.y = -300,
this.root.position.z = 5e3,
this.scene.add(this.root),
this.raycaster = new THREE.Raycaster,
this.mouse = new THREE.Vector2;
var e = [{
url: t(11),
w: 960,
h: 960,
x: 0,
y: 0,
z: 0,
s: .4
}, {
url: t(12),
w: 227,
h: 960,
x: 0,
y: 0,
z: 100,
s: .8,
r: 30
}, {
url: t(18),
w: 16,
h: 256,
x: 0,
y: 0,
z: 0,
s: 4,
n: 20,
t: .5
}, {
url: t(19),
w: 431,
h: 532,
x: 0,
y: 0,
z: 0,
s: 4,
n: 6,
t: 4
}];
this.preload = this.createPreload(e),
this.preload.position.set(0, 300, -5e3),
this.root.add(this.preload),
this.preloadOn(),
this.renderer = new THREE.WebGLRenderer,
this.renderer.setClearColor(1245234),
this.renderer.setPixelRatio(window.devicePixelRatio),
this.renderer.setSize(window.innerWidth, window.innerHeight),
this.stage.prepend(this.renderer.domElement),
this.effect = new THREE.StereoEffect(this.renderer),
this.effect.eyeSeparation = 100,
this.effect.setSize(window.innerWidth, window.innerHeight),
t.e(0, function() {
g.data = t(14),
g.complete()
})
},
complete: function() {
var A = this.createSprite(this.data.bg);
this.addScene({
scene: A,
max: 2400,
min: -29650
});
var g = this.createScene(this.data.data1);
g.position.set(0, 0, 0),
this.addScene({
scene: g,
max: 2400,
min: -12e3
});
var t = this.createScene(this.data.data2);
t.position.set(0, 0, 1500),
this.addScene({
scene: t,
max: 2400,
min: -12e3
});
var e = this.createScene(this.data.data3);
e.position.set(0, 0, 4500),
this.addScene({
scene: e,
max: 2400,
min: -12e3
});
var C = this.createSprite(this.data.bg2);
this.addScene({
scene: C,
max: -12e3,
min: -29650
});
var I = this.createScene(this.data.data4);
I.position.set(0, 0, 7200),
this.addScene({
scene: I,
max: -4900,
min: -29650
});
var i = this.createScene(this.data.data5);
i.position.set(0, 0, 8500),
this.addScene({
scene: i,
max: -7e3,
min: -29650
});
var n = this.createScene(this.data.data6);
n.position.set(0, 0, 11e3),
this.addScene({
scene: n,
max: -11e3,
min: -29650
});
var o = this.createScene(this.data.data7);
o.position.set(0, 0, 18e3),
this.addScene({
scene: o,
max: -15500,
min: -29650
});
var a = this.createSprite(this.data.bg3);
this.addScene({
scene: a,
max: -29650,
min: -43600
});
var r = this.createSprite(this.data.vr);
this.addScene({
scene: r,
max: -29300,
min: -43600
});
var s = this.createScene(this.data.data8);
s.position.set(0, 0, 3e4),
this.addScene({
scene: s,
max: -29650,
min: -43600
});
var c = this.createScene(this.data.data9);
c.position.set(0, 0, 31e3),
this.addScene({
scene: c,
max: -36e3,
min: -6e4
});
var h = this.createScene(this.data.data10);
h.position.set(0, 0, 46e3);
var l = this.createParticles(this.data.red, 80, 50, 1e3, .2, {
x: 1,
y: 1,
z: 5
});
l.position.set(0, 300, 5e3),
h.add(l),
this.addScene({
scene: h,
max: -42e3,
min: -6e4
});
var u = this.createScene(this.data.data11);
u.position.set(0, 0, 56e3),
this.addScene({
scene: u,
max: -42e3,
min: -5e5
});
var d = this.createScene(this.data.data12);
d.position.set(0, 0, 61e3),
this.addScene({
scene: d,
max: -29650,
min: -5e5
});
var p = this.createScene(this.data.data13);
p.position.set(0, 0, 73e3),
this.addScene({
scene: p,
max: -71e3,
min: -5e5
}),
this.dots = this.createScene(this.data.data14),
this.dots.position.set(0, 0, 0),
this.addScene({
scene: this.dots,
max: 0,
min: -5e5
})
},
tap: function(A) {
this.mouse.x = A.x / window.innerWidth * 2 - 1,
this.mouse.y = 2 * -(A.y / window.innerHeight) + 1,
this.raycaster.setFromCamera(this.mouse, this.camera);
var g = this.raycaster.intersectObjects(this.dots.children);
g.length > 0 && (g[0].object.name < 11 ? this.trigger("dot", g[0].object.name) : 11 == g[0].object.name && this.trigger("share"))
},
preloadData: {
max: 0,
cur: 0
},
checkPreload: function() {
this.trigger("preloadProgress", Math.floor(this.preloadData.cur / this.preloadData.max * 100)),
this.preloadData.cur >= this.preloadData.max && this.preloadOff()
},
preloadOn: function() {
function A(A) {
if (A.n0 > 10) {
var t = g(700, 900);
A.position.set(t.x, t.y, 0),
A.material.rotation = t.r + Math.PI / 2;
var e = .4 * Math.random() + .2;
A.scale.set(A.w0 * e, A.h0 * e, 1)
} else {
var t = g(200, 500);
A.position.set(t.x, t.y, 0),
A.material.rotation = Math.random() * Math.PI;
var e = 1 * Math.random() + .5;
A.scale.set(A.w0 * e, A.h0 * e, 1)
}
}
function g(A, g) {
var t = C.random(A, g)
, e = Math.random() * Math.PI * 2
, I = Math.sin(e) * t
, i = Math.cos(e) * t;
return {
x: i,
y: I,
r: e
}
}
I.ga(I.PAGE, "loading", "loading_scene"),
C.isPreloaded = !1,
$.each(this.preload.children, function(g, t) {
0 == g && e.fromTo(t.scale, .01, {
x: 1 * t.w0,
y: 1 * t.h0
}, {
x: 1.1 * t.w0,
y: 1.1 * t.h0,
yoyo: !0,
repeat: -1,
onUpdate: function() {
t.material.rotation = .1 * Math.random() - .2
}
}),
g 1 || (A(t),
e.fromTo(t.position, t.t0, {
z: 500
}, {
z: -2e4,
delay: t.i0 * (t.t0 / t.n0),
repeat: -1,
onRepeat: function() {
A(t)
}
}))
}),
this.trigger("preloadOn")
},
preloadOff: function() {
var A = this;
C.isPreloaded = !0,
e.to(this.root.position, 4, {
z: -11500,
ease: e.Quad.InOut,
onEnd: function() {
$.each(A.preload.children, function(A, g) {
e.kill(g)
}),
A.root.remove(A.preload),
e.to(this.target, 3, {
z: 400,
ease: e.Quad.In,
onEnd: function() {
C.isReady = !0,
A.trigger("ready")
}
})
}
}),
this.trigger("preloadOff")
},
createPreload: function(A) {
for (var g = new THREE.Group, t = 0, e = A.length; t < e; t++)
for (var C = A[t].n || 1, I = 0; I < C; I++) {
var i = (new THREE.TextureLoader).load(A[t].url)
, n = new THREE.SpriteMaterial({
map: i
})
, o = (A[t].turn ? -1 : 1) * A[t].w * (A[t].s || 1)
, a = A[t].h * (A[t].s || 1)
, r = new THREE.Sprite(n);
r.position.set(A[t].x, A[t].y, A[t].z),
r.scale.set(o, a, 1),
r.w0 = o,
r.h0 = a,
r.t0 = A[t].t,
r.n0 = A[t].n,
r.i0 = I,
A[t].r && (r.material.rotation = A[t].r / 180 * Math.PI),
A[t].name && (r.name = A[t].name),
g.add(r)
}
return g
},
createScene: function(A) {
var g = this
, t = new THREE.Group;
t.childs = [];
for (var e = 0, C = A.length; e < C; e++) {
this.preloadData.max++;
var I = (new THREE.TextureLoader).load(A[e].url, function() {
g.preloadData.cur++,
g.checkPreload()
})
, i = new THREE.SpriteMaterial({
map: I
})
, n = new THREE.Sprite(i);
if (n.position.set(A[e].x, A[e].y, A[e].z),
n.scale.set((A[e].turn ? -1 : 1) * A[e].w * (A[e].s || 1), A[e].h * (A[e].s || 1), 1),
A[e].r && (n.material.rotation = A[e].r / 180 * Math.PI),
A[e].name && (n.name = A[e].name),
t.add(n),
n.isIn = !0,
t.childs.push(n),
!A[e].single) {
var o = new THREE.Sprite(i);
o.position.set(-A[e].x, A[e].y, A[e].z),
o.scale.set(-A[e].w * (A[e].s || 1), A[e].h * (A[e].s || 1), 1),
t.add(o),
o.isIn = !0,
t.childs.push(o)
}
A[e].walk && (A[e].walk.target = n,
this.addWalk(A[e].walk))
}
return t
},
createSprite: function(A) {
var g = new THREE.SpriteMaterial({
map: (new THREE.TextureLoader).load(A.url)
})
, t = new THREE.Sprite(g);
return t.position.set(A.x, A.y, A.z),
t.scale.set(A.w * (A.s || 1), A.h * (A.s || 1), 1),
t
},
createParticles: function(A, g, t, e, C, I) {
for (var i = new THREE.Group, n = [], o = 0, a = A.length; o < a; o++)
n[o] = new THREE.SpriteMaterial({
map: (new THREE.TextureLoader).load(A[o].url)
});
for (var r = 0; r < g; r++) {
var s = r % a
, c = new THREE.Sprite(n[s])
, h = Math.random() * (e - t) + t
, l = Math.random() * Math.PI * 2
, u = Math.random() * Math.PI * 2
, d = Math.sin(u) * h
, p = Math.cos(u) * h
, f = Math.cos(l) * p
, E = Math.sin(l) * p;
c.position.set(f * I.x, d * I.y, E * I.z);
var w = 2 * Math.random() + 1;
c.scale.set(A[s].w * w * C, A[s].h * w * C, 1),
i.add(c)
}
return i
},
resize: function() {
C.checkLandscape() ? (this.camera.aspect = window.innerWidth / window.innerHeight,
this.camera.updateProjectionMatrix(),
this.effect.setSize(window.innerWidth, window.innerHeight),
this.renderer.setSize(window.innerWidth, window.innerHeight)) : C.isVR ? (this.camera.aspect = window.innerHeight / window.innerWidth,
this.camera.updateProjectionMatrix(),
this.effect.setSize(window.innerHeight, window.innerWidth),
this.renderer.setSize(window.innerHeight, window.innerWidth)) : (this.camera.aspect = window.innerWidth / window.innerHeight,
this.camera.updateProjectionMatrix(),
this.effect.setSize(window.innerWidth, window.innerHeight),
this.renderer.setSize(window.innerWidth, window.innerHeight))
},
render: function() {
if (C.isActive) {
var A;
A = this.root.position.z < -44e3 ? this.aim.z * Math.max(1, (-this.root.position.z - 4e4) / 1e4) : this.aim.z,
C.isReady && (this.root.position.z > 200 && A > 0 && (A *= 1 - (this.root.position.z - 200) / 200),
this.root.position.z < -168e3 && A < 0 && (A *= 1 - (-this.root.position.z - 168e3) / 8e3),
this.root.position.z += A),
this.root.position.z < -20200 && (C.isFirst || (C.isFirst = !0,
e.to(this.dots.childs[11].material, .5, {
opacity: 0,
delay: 5
}))),
this.updateWalk(this.root.position.z),
this.updateScene(this.root.position.z),
this.camera.rotation.x += .3 * (this.fix.y - this.camera.rotation.x),
this.camera.rotation.y += .3 * (this.fix.x - this.camera.rotation.y),
C.checkLandscape() || C.isVR ? this.effect.render(this.scene, this.camera) : this.renderer.render(this.scene, this.camera)
}
},
walks: [],
addWalk: function(A) {
this.walks.push(A)
},
updateWalk: function(A) {
$.each(this.walks, function(g, t) {
var C = Math.max(t.from.a, t.to.a)
, I = Math.min(t.from.a, t.to.a);
if (A > I && A < C)
for (var g in t.from) {
switch (g) {
case "x":
case "y":
case "z":
case "w":
case "h":
case "r":
var i = (A - t.from.a) / (t.to.a - t.from.a)
, n = t.to.ease || e.Linear.None
, o = n(i)
, a = t.from[g] + (t.to[g] - t.from[g]) * o
}
switch (g) {
case "x":
case "y":
case "z":
t.target.position[g] = a;
break;
case "w":
t.target.scale.x = a;
case "h":
t.target.scale.y = a;
break;
case "r":
t.target.material.rotation = a / 180 * Math.PI
}
}
})
},
scenes: [],
addScene: function(A) {
this.scenes.push(A),
this.root.add(A.scene),
A.isIn = !0
},
updateScene: function(A) {
var g = this;
$.each(this.scenes, function(t, e) {
var C = Math.max(e.max, e.min)
, I = Math.min(e.max, e.min);
A > I && A < C ? e.isIn || (e.isIn = !0,
g.root.add(e.scene)) : e.isIn && (e.isIn = !1,
g.root.remove(e.scene))
})
},
animate: function() {
i.render(),
requestAnimationFrame(i.animate)
},
toDot: function(A) {
this.aim.z = 0;
var g = this.dots.childs[A - 1]
, t = -g.position.z - 300
, C = Math.min(3, Math.abs(this.root.position.z - t) / 1e4);
e.kill(this.root),
e.to(this.root.position, C, {
z: t,
ease: e.Quad.InOut
})
},
checkVR: function() {
this.resize()
}
});
A.exports = i
}
).call(g, t(1), t(2))
}
Copy the code