GitHub 首页 地球仪 技术大揭秘
GitHub是世界构建软件的地方。全世界有5600多万开发者在GitHub上共同开发和工作。通过我们的新主页,我们希望展示开源开发如何超越我们生活的边界,并通过一个开发者的旅程来讲述我们的产品故事。
在2019年的Satellite上,我们的首席执行官Nat展示了30天内GitHub上开源活动的可视化。绝对的数量和全球影响力是惊人的,我们知道我们想在这个故事的基础上再接再厉。
我们在设计和发展全球化的过程中要达到的主要目标是:
一个互联的社区. 我们探索了许多不同的方案,但最终还是选择了拉取请求上。结果是一个美丽的可视化的拉动请求在世界的一个地方被打开,在另一个地方被关闭。
一个展示真实工作的窗口。我们一开始只是简单地展示了拉动请求的弧线和塔顶,但很快意识到我们需要 "生命的证明"。这些弧线很可能只是设计动画,而不是真实的工作。我们反复研究了提供更多细节的方法,发现最能引起共鸣的是清晰的悬停状态,显示拉取请求、repo、时间戳、语言和位置。Nat提出了让每一行都可以点击的想法,这确实提升了体验的层次,让它更有沉浸感。
对细节和性能的关注。对我们来说,地球仪不仅要看起来鼓舞人心、美丽动人,而且要在所有设备上表现良好,这一点非常重要。我们经历了很多很多的迭代完善,还有更多的工作要做。
用WebGL渲染地球仪
在最基本的层面上,地球仪运行在一个由three.js驱动的WebGL上下文中。我们通过一个JSON文件向它提供最近在世界各地创建和合并的拉取请求数据。场景由五个图层组成:一个光环、一个地球仪、地球的各个区域,蓝色尖峰代表开放的拉取请求,粉色弧线代表合并的拉取请求。我们没有使用任何纹理:我们将四盏灯指向一个球体,使用大约12000个五边形圆圈来渲染地球的区域,并在球体的背面用简单的自定义着色器绘制一个光环。
为了绘制地球的区域,我们首先定义所需的圆的密度(这将取决于您的机器的性能--稍后将详细介绍),然后在一个嵌套的 for 循环中沿经度和纬度循环。我们从南极开始向上,计算每个纬度的周长,沿着这条线均匀地分布圆圈,环绕球体。
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
}
}
为了确定一个圆是否应该可见(是水还是陆地?),我们加载一个包含世界地图的小PNG,通过canvas的context.getImageData()解析其图像数据,并通过visibilityForCoordinate(long, lat)方法将每个圆映射到地图上的一个像素。如果该像素的alpha值至少是90(255中的),则画出这个圆;如果不是,我们就跳到下一个。
在收集了所有我们需要的数据,通过这些小圆形可视化地球的区域之后,我们创建一个CircleBufferGeometry的实例,并使用InstancedMesh来渲染所有的几何图形。
确保你能看到自己的位置
当你进入新的GitHub主页时,我们希望确保你能在地球仪出现时看到自己的位置,这意味着我们需要计算出你在地球上的位置。我们想在不延迟IP查找后的第一次渲染的情况下实现这个效果,所以我们将地球仪的起始角度设置为格林威治上空的中心,查看设备的时区偏移量,并将这个偏移量转换为围绕地球仪自身轴线的旋转(单位:弧度)。
const date = new Date();
const timeZoneOffset = date.getTimezoneOffset() || 0;
const timeZoneMaxOffset = 60*12;
rotationOffset.y = ROTATION_OFFSET.y + Math.PI * (timeZoneOffset / timeZoneMaxOffset);
这不是一个精确的测量你的位置,但它是快速的,并做工作。
可视化拉取请求
当然,地球仪的主要行为是可视化世界各地正在打开和合并的所有拉请求。让这一切成为可能的数据工程本身就是一个不同的话题,我们将在下一篇文章中分享我们如何实现这一点。在这里,我们想给你一个概述,我们是如何可视化你所有的拉请求的。
让我们关注一下被合并的拉请求(粉色的弧线),因为它们更有趣一些。每个合并的拉请求条目都有两个位置:打开的位置和合并的位置。我们将这些位置映射到我们的地球仪上,并在这两个位置之间画一条贝塞尔曲线。
const curve = new CubicBezierCurve3(startLocation, ctrl1, ctrl2, endLocation);
我们为这些曲线设置了三个不同的轨道,两点之间的距离越长,我们就会把任何特定的弧线拉出更远的空间。然后,使用TubeBufferGeometry的实例沿着这些路径生成几何体,这样我们就可以使用setDrawRange()在线条出现和消失时对它们进行动画处理。
当每条线动画进来并到达其合并位置时,会在一个实心圆中生成和动画,该圆在线存在时保持不变,而一个环则放大并立即淡出。这些动画的渐进渐出是通过将一个速度(这里是0.06)与目标(1)和当前值(animated.dot.scale.x)之间的差值相乘,并将其加到现有的比例值上而产生的。换句话说,我们每走近一帧,就会向目标靠近6%,当我们离目标越来越近时,动画就会自然而然地慢下来。
// 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;
性能优化带来的创新约束
主页和地球仪需要在各种设备和平台上有良好的表现,这在早期给我们带来了一些创意上的限制,使我们广泛关注于创建一个优化良好的页面。虽然一些现代电脑和平板电脑在开启抗锯齿功能的情况下可以以60 FPS的速度渲染地球仪,但并不是所有设备都能做到这一点,很早就决定关闭抗锯齿功能,优化性能。这样一来,当地球仪的高亮边缘与背景的深色相接时,地球仪的左上角就会出现一条尖锐而像素化的线条。
这促使我们探索一种能够隐藏像素化边缘的光晕效果。我们通过使用自定义着色器在一个比地球仪稍大的球体背面绘制一个渐变效果,将其放置在地球仪的后面,并将其稍稍倾斜,以强调左上角的效果。
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);
这样做可以抚平尖锐的边缘,同时比开启抗锯齿更有性能。不幸的是,关闭抗锯齿也产生了相当突出的莫瑞效果,因为所有组成世界的圆圈在接近地球边缘时,彼此越来越近。我们降低了这一效果,并通过对圆圈使用碎片着色器来模拟更浓厚的大气,其中每个圆圈的alpha是其与摄像机距离的函数,随着圆圈的进一步移动,每个圆圈都会逐渐消失。
if (gl_FragCoord.z > fadeThreshold) {
gl_FragColor.a = 1.0 + (fadeThreshold - gl_FragCoord.z ) * alphaFallOff;
}
提高感知速度
我们不知道地球仪在特定设备上的加载速度有多快(或多慢),但希望确保主页上的头部构成是平衡的,而且即使在我们渲染第一帧之前有一点延迟,你也会觉得地球仪加载得很快。
我们在Figma中只使用渐变创建了一个裸版的地球仪,并将其导出为SVG。将这个SVG嵌入到HTML文档中,增加了很少的开销,但可以确保在页面加载时一些东西是立即可见的。一旦我们准备好渲染地球仪的第一帧,我们就会使用Web Animations API在SVG和画布元素之间进行交叉渐变,并放大这两个元素。使用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();
});
质量层级的优雅降级
我们的目标是在保持60 FPS的同时,尽可能地渲染出一个美丽的地球仪,但要找到这个平衡点是很困难的--有成千上万的设备,它们的性能都因运行的浏览器和心情而不同。我们不断地监控所达到的FPS,如果我们不能在最后50帧保持55.5 FPS,我们就会开始降低场景的质量。
有四个质量层级,每降低一个质量层级,我们就会减少昂贵的计算量。这包括降低像素密度、我们的射线广播频率(弄清楚你的光标在场景中悬停的内容),以及在屏幕上绘制的几何体数量--这又让我们回到了构成地球区域的圆圈。当我们沿着质量层向下移动时,我们会降低所需的圆圈密度,并重建地球的区域,这里从原来的约12000个圆圈变成了约8000个。
// 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();
广泛努力的一小部分。
这些是我们用来渲染地球仪的一些技术,但地球仪和新主页的创建是一个更长的故事的一部分,跨越多个团队、学科和部门,包括设计、品牌、工程、产品和通信。
关于本文
译者:@飘飘
作者:@Tobias Ahlin
原文:https://github.blog/2020-12-21-how-we-built-the-github-globe/
作者:@Tobias Ahlin
欢迎关注微信公众号 :前端开发爱好者
添加好友备注【进阶学习】拉你进技术交流群