作者簡介 wuyue 螞蟻金服·資料體驗技術團隊
我們的征途是星辰大海
—阿里人的宣言
如何用程式碼構建阿里的星辰大海?
首先我們想象一下當前發生的一個場景, 漫天繁星閃爍,不時有流星劃破天際, 地球在自轉, 無數的人在不同的地點因為阿里提供的便捷服務在交易。
效果如下:
結構分解
如何構建上面的場景呢, 分解得到如下結構:
星空
首先要有一片天, 一個矩形,給黑色背景就好了。
我們暫定 寬 width, 高 height, 三維空間要有深度 暫定為 depth。
對應一片閃爍的繁星, 首先要在空間裡生成一堆的星星,其次要讓星星不停的閃爍, 暫定 按線性差值變化, 有變大(三維空間就是變亮) 有變小(對應變暗) 部分關鍵程式碼如下:
var starts = [];
for (let i = 0; i < 10000; i++) {
starts.push({
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * depth,
//當前進度 0 - 1, 當前大小按 (1 - t) * minSize + t * maxSize
t: Math.random(),
//變化方向
direction: Math.random(),
//變化步長 暫定為常量 後面可以根據需要每個星星都不一樣
step: 0.01,
//最小
minSize: 10,
//最大
maxSize: 20
});
}
animate();
function animate () {
requestAnimationFrame(animate);
starts.forEach((item) => {
let { t, minSize, maxSize } = item;
//計算當前尺寸
item.size = (1 - t) * minSize + t * maxSize;
//改變進度
if (t > 1) {
//如果已經最大了 則開始減少
item.direction = -1;
} else if (t < 0) {
//最小了 則開始變大
item.direction = 1;
}
//修改進度
item.t += item.step * item.direction;
});
//根據上面的值進行渲染
render();
}
複製程式碼
至此 我們就完成了繁星。 但是 漫天星光閃爍,是要有流星的, 這樣才夠美麗。流星就是在星空有快速劃過一條線的星星。所以可以如下:
//流星處理邏輯
var falls = [];
for (let i = 0; i < 10; i++) {
falls.push({
//流星的起點
start: {
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * depth
},
//流星的終點
end: {
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * depth
},
//當前進度 0 - 1, 當前位置按 二次貝塞爾曲線計算即可
t: Math.random(),
step: 0.01
});
}
function animate () {
requestAnimationFrame(animate);
starts.forEach((item) => {
let { t, start as p0, end as p2} = item;
//隨意設定一個控制點
let p1 = {
x: p0.x,
y: (p0.y + p2.y) / 2,
z: p2.z
};
//按二次貝塞爾曲線 計算當前位置
//(1 - t) * (1 - t) * p0 + 2 * t * (1 - t ) * p1 + t * t * p2
let tmp = {
x: (1 - t) * (1 - t) * p0.x + 2 * t * (1 - t ) * p1.x + t * t * p2.x,
y: (1 - t) * (1 - t) * p0.y + 2 * t * (1 - t ) * p1.y + t * t * p2.y,
z: (1 - t) * (1 - t) * p0.z + 2 * t * (1 - t ) * p1.z + t * t * p2.z
};
item.t += item.step;
item.tmp = tmp;
//改變進度
if (t > 1) {
//如果已經最大了變為0
item.t = 0;
//重置起始點
item.start = {
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * depth
};
item.end = {
x: Math.random() * width,
y: Math.random() * height,
z: Math.random() * depth
};
}
});
//根據上面的值進行渲染
render();
}
複製程式碼
當然 上面只是為了 講解方便, 把閃爍
和流星
的效果分開了, 其實這兩部分是有重合的邏輯的, 在星星狀態中加一個標誌位是閃爍還是流星,然後兩個邏輯就可以合在一起了,具體見github上程式碼實現部分這裡不再囉嗦。
地球
星空有了,下面我們要構建地球了。 地球大概分為兩部分, 首先 建立一個球,在三維空間的原點,然後給地球蒙上一層紋理皮膚,就是一張平面地圖 從 -90 ~ 90, -180 -180的平面地圖 如下:
然後就是關鍵的來了 ,地球上的交易點需要高亮, 所以也就是在相應的經緯度上 給一些高亮效果。具體定位程式碼如下:
//經緯度轉x,y平面座標
lnglatToXY ({lng, lat}, width, height) {
let x = (lng - (-180)) / 360 * width;
let y = Math.abs((lat - 90) / 180) * height;
return {
x,y
};
}
複製程式碼
然後就是生產紋理, 大背景圖片,加上熱點canvas上面按位置畫圖就可以了。
//渲染紋理
async function render () {
//全球背景圖片
let worldBg = await util.loadImg(worldSrc);
//熱點背景圖片
let hotBg = await util.loadImg(hotSrc);
//畫大背景
ctx.drawImage(worldBg, 0, 0, width, height);
//按經緯度繪製熱點
data.forEach((item) => {
let lng = item.lnglat[0];
let lat = item.lnglat[1];
let {x, y} = util.lnglatToXY({lng, lat}, width, height);
ctx.drawImage(hotBg, x - size / 2, y - size / 2, size, size);
});
//生產紋理,然後 直接對映蒙到球上就可以了
let texture = new THREE.Texture(canvas);
texture.needsUpdate = true;
return texture;
}
複製程式碼
ok,到這裡就地球就生成了。後面就讓地球每幀繞y軸旋轉既可
animate();
function animate () {
requestAnimationFrame(animate);
earth.rotation.y += 0.01;
render();
}
複製程式碼
交易線
ok 旋轉的地球也有了, 下面就是要繪製 地球上的交易線了。還是首先要有經緯度 到空間座標的一個轉換。 具體原理見下面。
//經緯度轉空間座標的具體程式碼, r 為球體半徑
lnglatToXYZ ({lng, lat}, r) {
var phi = (90 - lat) * Math.PI / 180;
var theta = -1 * lng * Math.PI / 180;
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.cos(phi),
z: r * Math.sin(phi) * Math.sin(theta)
};
}
複製程式碼
所以 就很明確了,垂直於地球的線, 就是 同樣的經緯度 ,不同的 半徑。所以可以按上面公式計算出 p1, p2確定出一條直線。然後和閃爍的星空道理一樣加入控制點和方向變數 大量線段就起伏變化了。關鍵程式碼如下:
//原始資料
let lines = [
{lng, lat },
{lng, lat },
....
];
//生成一些控制引數
lines.forEach((item) => {
//控制點 0 - 1 用來計算直線的終點 t * length 這裡隨機數是為了 不同的線初始狀態不同
item.t = Math.random();
//變化方向
item.direction = Math.random() > 0.5 ? 1 : -1;
//變化速度 暫定都一樣
item.step = 0.01;
//線段的長度 這裡可以根據實際資料 比如 value來對映長度 本出為了示意 用隨機數了
item.length = Math.random() * r;
});
animate();
function animate () {
requestAnimationFrame(animate);
lines.forEach((item) => {
let p1 = lnglatToXYZ(item.lng, item.lat, r);
//當前的終點 用 t
let p2 = lnglatToXYZ(item.lng, item.lat, r + item.length * t);
//根據 p1, p2 即可繪製一條直線
if (item.t > 1) {
item.direction = -1;
} else if (item.t < 0) {
item.direction = 1;
}
item.t += item.direction * item.step;
});
render();
}
複製程式碼
ok 以上就是 閃爍的繁星, 天邊劃過的流星, 旋轉的地球, 呼吸的交易線 按對應位置組合在一起, 就是星辰大海了。 當然 看到這裡 你可能會問, 星辰有了 但是 說好的大海呢, 其實有的 你仔細看 地球表面都是水啊,那就是海。
最後附上原始碼,供大家參考。https://github.com/liuwuyue/earth 具體效果見這裡 http://yiqihaiqilai.com, http://yiqihaiqilai.com?line (個人伺服器,可能會抽風, 如不能看,請自己下載下原始碼 執行下,不用來找我)
最最後的一個重要事情,我們組這種場景還有很多, 歡迎入夥,感興趣的同學可以關注專欄或者傳送簡歷至'wuyue.lwy####alibaba-inc.com'.replace('####', '@'),大家一起描繪阿里的星辰大海。~