GitHub 首頁 地球儀 技術(shù)大揭秘
GitHub是世界構(gòu)建軟件的地方。全世界有5600多萬開發(fā)者在GitHub上共同開發(fā)和工作。通過我們的新主頁,我們希望展示開源開發(fā)如何超越我們生活的邊界,并通過一個(gè)開發(fā)者的旅程來講述我們的產(chǎn)品故事。
在2019年的Satellite上,我們的首席執(zhí)行官Nat展示了30天內(nèi)GitHub上開源活動的可視化。絕對的數(shù)量和全球影響力是驚人的,我們知道我們想在這個(gè)故事的基礎(chǔ)上再接再厲。
我們在設(shè)計(jì)和發(fā)展全球化的過程中要達(dá)到的主要目標(biāo)是:
一個(gè)互聯(lián)的社區(qū). 我們探索了許多不同的方案,但最終還是選擇了拉取請求上。結(jié)果是一個(gè)美麗的可視化的拉動請求在世界的一個(gè)地方被打開,在另一個(gè)地方被關(guān)閉。
一個(gè)展示真實(shí)工作的窗口。我們一開始只是簡單地展示了拉動請求的弧線和塔頂,但很快意識到我們需要 "生命的證明"。這些弧線很可能只是設(shè)計(jì)動畫,而不是真實(shí)的工作。我們反復(fù)研究了提供更多細(xì)節(jié)的方法,發(fā)現(xiàn)最能引起共鳴的是清晰的懸停狀態(tài),顯示拉取請求、repo、時(shí)間戳、語言和位置。Nat提出了讓每一行都可以點(diǎn)擊的想法,這確實(shí)提升了體驗(yàn)的層次,讓它更有沉浸感。
對細(xì)節(jié)和性能的關(guān)注。對我們來說,地球儀不僅要看起來鼓舞人心、美麗動人,而且要在所有設(shè)備上表現(xiàn)良好,這一點(diǎn)非常重要。我們經(jīng)歷了很多很多的迭代完善,還有更多的工作要做。
用WebGL渲染地球儀
在最基本的層面上,地球儀運(yùn)行在一個(gè)由three.js驅(qū)動的WebGL上下文中。我們通過一個(gè)JSON文件向它提供最近在世界各地創(chuàng)建和合并的拉取請求數(shù)據(jù)。場景由五個(gè)圖層組成:一個(gè)光環(huán)、一個(gè)地球儀、地球的各個(gè)區(qū)域,藍(lán)色尖峰代表開放的拉取請求,粉色弧線代表合并的拉取請求。我們沒有使用任何紋理:我們將四盞燈指向一個(gè)球體,使用大約12000個(gè)五邊形圓圈來渲染地球的區(qū)域,并在球體的背面用簡單的自定義著色器繪制一個(gè)光環(huán)。
為了繪制地球的區(qū)域,我們首先定義所需的圓的密度(這將取決于您的機(jī)器的性能--稍后將詳細(xì)介紹),然后在一個(gè)嵌套的 for 循環(huán)中沿經(jīng)度和緯度循環(huán)。我們從南極開始向上,計(jì)算每個(gè)緯度的周長,沿著這條線均勻地分布圓圈,環(huán)繞球體。
for (let lat = -90; lat <= 90; lat += 180/rows) {
const radius = Math.cos(Math.abs(lat) * DEG2RAD) * GLOBE_RADIUS;
const circumference = radius * Math.PI * 2;
const dotsForLat = circumference * dotDensity;
for (let x = 0; x < dotsForLat; x++) {
const long = -180 + x*360/dotsForLat;
if (!this.visibilityForCoordinate(long, lat)) continue;
// Setup and save circle matrix data
}
}
為了確定一個(gè)圓是否應(yīng)該可見(是水還是陸地?),我們加載一個(gè)包含世界地圖的小PNG,通過canvas的context.getImageData()解析其圖像數(shù)據(jù),并通過visibilityForCoordinate(long, lat)方法將每個(gè)圓映射到地圖上的一個(gè)像素。如果該像素的alpha值至少是90(255中的),則畫出這個(gè)圓;如果不是,我們就跳到下一個(gè)。
在收集了所有我們需要的數(shù)據(jù),通過這些小圓形可視化地球的區(qū)域之后,我們創(chuàng)建一個(gè)CircleBufferGeometry的實(shí)例,并使用InstancedMesh來渲染所有的幾何圖形。
確保你能看到自己的位置
當(dāng)你進(jìn)入新的GitHub主頁時(shí),我們希望確保你能在地球儀出現(xiàn)時(shí)看到自己的位置,這意味著我們需要計(jì)算出你在地球上的位置。我們想在不延遲IP查找后的第一次渲染的情況下實(shí)現(xiàn)這個(gè)效果,所以我們將地球儀的起始角度設(shè)置為格林威治上空的中心,查看設(shè)備的時(shí)區(qū)偏移量,并將這個(gè)偏移量轉(zhuǎn)換為圍繞地球儀自身軸線的旋轉(zhuǎn)(單位:弧度)。
const date = new Date();
const timeZoneOffset = date.getTimezoneOffset() || 0;
const timeZoneMaxOffset = 60*12;
rotationOffset.y = ROTATION_OFFSET.y + Math.PI * (timeZoneOffset / timeZoneMaxOffset);
這不是一個(gè)精確的測量你的位置,但它是快速的,并做工作。
可視化拉取請求
當(dāng)然,地球儀的主要行為是可視化世界各地正在打開和合并的所有拉請求。讓這一切成為可能的數(shù)據(jù)工程本身就是一個(gè)不同的話題,我們將在下一篇文章中分享我們?nèi)绾螌?shí)現(xiàn)這一點(diǎn)。在這里,我們想給你一個(gè)概述,我們是如何可視化你所有的拉請求的。
讓我們關(guān)注一下被合并的拉請求(粉色的弧線),因?yàn)樗鼈兏腥ひ恍?。每個(gè)合并的拉請求條目都有兩個(gè)位置:打開的位置和合并的位置。我們將這些位置映射到我們的地球儀上,并在這兩個(gè)位置之間畫一條貝塞爾曲線。
const curve = new CubicBezierCurve3(startLocation, ctrl1, ctrl2, endLocation);
我們?yōu)檫@些曲線設(shè)置了三個(gè)不同的軌道,兩點(diǎn)之間的距離越長,我們就會把任何特定的弧線拉出更遠(yuǎn)的空間。然后,使用TubeBufferGeometry的實(shí)例沿著這些路徑生成幾何體,這樣我們就可以使用setDrawRange()在線條出現(xiàn)和消失時(shí)對它們進(jìn)行動畫處理。
當(dāng)每條線動畫進(jìn)來并到達(dá)其合并位置時(shí),會在一個(gè)實(shí)心圓中生成和動畫,該圓在線存在時(shí)保持不變,而一個(gè)環(huán)則放大并立即淡出。這些動畫的漸進(jìn)漸出是通過將一個(gè)速度(這里是0.06)與目標(biāo)(1)和當(dāng)前值(animated.dot.scale.x)之間的差值相乘,并將其加到現(xiàn)有的比例值上而產(chǎn)生的。換句話說,我們每走近一幀,就會向目標(biāo)靠近6%,當(dāng)我們離目標(biāo)越來越近時(shí),動畫就會自然而然地慢下來。
// The solid circle
const scale = animated.dot.scale.x + (1 - animated.dot.scale.x) * 0.06;
animated.dot.scale.set(scale, scale, 1);
// The landing effect that fades out
const scaleUpFade = animated.dotFade.scale.x + (1 - animated.dotFade.scale.x) * 0.06;
animated.dotFade.scale.set(scaleUpFade, scaleUpFade, 1);
animated.dotFade.material.opacity = 1 - scaleUpFade;
性能優(yōu)化帶來的創(chuàng)新約束
主頁和地球儀需要在各種設(shè)備和平臺上有良好的表現(xiàn),這在早期給我們帶來了一些創(chuàng)意上的限制,使我們廣泛關(guān)注于創(chuàng)建一個(gè)優(yōu)化良好的頁面。雖然一些現(xiàn)代電腦和平板電腦在開啟抗鋸齒功能的情況下可以以60 FPS的速度渲染地球儀,但并不是所有設(shè)備都能做到這一點(diǎn),很早就決定關(guān)閉抗鋸齒功能,優(yōu)化性能。這樣一來,當(dāng)?shù)厍騼x的高亮邊緣與背景的深色相接時(shí),地球儀的左上角就會出現(xiàn)一條尖銳而像素化的線條。
這促使我們探索一種能夠隱藏像素化邊緣的光暈效果。我們通過使用自定義著色器在一個(gè)比地球儀稍大的球體背面繪制一個(gè)漸變效果,將其放置在地球儀的后面,并將其稍稍傾斜,以強(qiáng)調(diào)左上角的效果。
const halo = new Mesh(haloGeometry, haloMaterial);
halo.scale.multiplyScalar(1.15);
halo.rotateX(Math.PI*0.03);
halo.rotateY(Math.PI*0.03);
this.haloContainer.add(halo);
這樣做可以撫平尖銳的邊緣,同時(shí)比開啟抗鋸齒更有性能。不幸的是,關(guān)閉抗鋸齒也產(chǎn)生了相當(dāng)突出的莫瑞效果,因?yàn)樗薪M成世界的圓圈在接近地球邊緣時(shí),彼此越來越近。我們降低了這一效果,并通過對圓圈使用碎片著色器來模擬更濃厚的大氣,其中每個(gè)圓圈的alpha是其與攝像機(jī)距離的函數(shù),隨著圓圈的進(jìn)一步移動,每個(gè)圓圈都會逐漸消失。
if (gl_FragCoord.z > fadeThreshold) {
gl_FragColor.a = 1.0 + (fadeThreshold - gl_FragCoord.z ) * alphaFallOff;
}
提高感知速度
我們不知道地球儀在特定設(shè)備上的加載速度有多快(或多慢),但希望確保主頁上的頭部構(gòu)成是平衡的,而且即使在我們渲染第一幀之前有一點(diǎn)延遲,你也會覺得地球儀加載得很快。
我們在Figma中只使用漸變創(chuàng)建了一個(gè)裸版的地球儀,并將其導(dǎo)出為SVG。將這個(gè)SVG嵌入到HTML文檔中,增加了很少的開銷,但可以確保在頁面加載時(shí)一些東西是立即可見的。一旦我們準(zhǔn)備好渲染地球儀的第一幀,我們就會使用Web Animations API在SVG和畫布元素之間進(jìn)行交叉漸變,并放大這兩個(gè)元素。使用Web Animations API可以讓我們在過渡過程中完全不接觸DOM,確保它盡可能的無停頓。
const keyframesIn = [
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' }
];
const keyframesOut = [
{ opacity: 1, transform: 'scale(0.8)' },
{ opacity: 0, transform: 'scale(1)' }
];
const options = { fill: 'both', duration: 600, easing: 'ease' };
this.renderer.domElement.animate(keyframesIn, options);
const placeHolderAnim = placeholder.animate(keyframesOut, options);
placeHolderAnim.addEventListener('finish', () => {
placeholder.remove();
});
質(zhì)量層級的優(yōu)雅降級
我們的目標(biāo)是在保持60 FPS的同時(shí),盡可能地渲染出一個(gè)美麗的地球儀,但要找到這個(gè)平衡點(diǎn)是很困難的--有成千上萬的設(shè)備,它們的性能都因運(yùn)行的瀏覽器和心情而不同。我們不斷地監(jiān)控所達(dá)到的FPS,如果我們不能在最后50幀保持55.5 FPS,我們就會開始降低場景的質(zhì)量。
有四個(gè)質(zhì)量層級,每降低一個(gè)質(zhì)量層級,我們就會減少昂貴的計(jì)算量。這包括降低像素密度、我們的射線廣播頻率(弄清楚你的光標(biāo)在場景中懸停的內(nèi)容),以及在屏幕上繪制的幾何體數(shù)量--這又讓我們回到了構(gòu)成地球區(qū)域的圓圈。當(dāng)我們沿著質(zhì)量層向下移動時(shí),我們會降低所需的圓圈密度,并重建地球的區(qū)域,這里從原來的約12000個(gè)圓圈變成了約8000個(gè)。
// Reduce pixel density to 1.5 (down from 2.0)
this.renderer.setPixelRatio(Math.min(AppProps.pixelRatio, 1.5));
// Reduce the amount of PRs visualized at any given time
this.indexIncrementSpeed = VISIBLE_INCREMENT_SPEED / 3 * 2;
// Raycast less often (wait for 4 additional frames)
this.raycastTrigger = RAYCAST_TRIGGER + 4;
// Draw less geometry for the Earth’s regions
this.worldDotDensity = WORLD_DOT_DENSITY * 0.65;
// Remove the world
this.resetWorldMap();
// Generate world anew from new settings
this.buildWorldGeometry();
廣泛努力的一小部分。
這些是我們用來渲染地球儀的一些技術(shù),但地球儀和新主頁的創(chuàng)建是一個(gè)更長的故事的一部分,跨越多個(gè)團(tuán)隊(duì)、學(xué)科和部門,包括設(shè)計(jì)、品牌、工程、產(chǎn)品和通信。
關(guān)于本文
譯者:@飄飄
作者:@Tobias Ahlin
原文:https://github.blog/2020-12-21-how-we-built-the-github-globe/
作者:@Tobias Ahlin
歡迎關(guān)注微信公眾號 :前端開發(fā)愛好者
添加好友備注【進(jìn)階學(xué)習(xí)】拉你進(jìn)技術(shù)交流群