Everest.agency WebGL无限滚动器解构

我将解构Everest.agency主页上的Infinite Scroller。 它于2019年1月发布,由Aristide Benoist( @ AriBenoist)与@builtbyeverest合作开发,以其精美的设计和代码赢得了Awwwards和FWA的多个奖项。

如果您还没有看到该网站,建议您在阅读本文之前先进行操作。 我对Infinite Scroller的解构并不能使网站公正。

顺便说一句,让我们看看无限滚动的样子:

主页的滚动条

我将仅解释该效果背后的魔力。 因此,如果您想了解如何完成整个项目,请选中此沙盒或Github存储库。

效果分解

  • 无限滚动:无论滚动多少,它都不会结束!
  • Z位移:点击图像似乎越来越近
  • 图像上保留的长宽比

无限滚动

通过查看无限滚动,可能会想到不同的方法。

它是否继续提前创建新飞机并删除视线之外的飞机? 是否一直在重复使用视野外的平面? 这是什么黑魔法?

实际上,它比黑魔法既简单又聪明:

当滚动超过阈值时,滚动将移回到看起来完全相同的位置。

同样,图像偏移以抵消向后滚动。 一点点更多

由于一切都在向后移动,所以我们永远都不会走到尽头,所以它似乎是无限的。

这是远处效果的外观:

注意:好像是不断添加和删除平面,但事实并非如此。 这些都是同一架飞机被推一遍又一遍

现在, planeHeight + planeMarginY效果不可见,滚动的阈值和向后移动的距离必须为planeHeight + planeMarginY 。 如果将其移回此值,则近距离观看时结果在视觉上将相同。

  //滚动 
const spaceY =高度+ marginY;
如果(Math.abs(scroll)> spaceY){
//每当滚动超过阈值时。 向相反方向将其移回。
scroll =滚动-spaceY * scrollDirection;
}

简单明了,不是吗?

如果有足够的平面覆盖屏幕上的一点点,效果是不明显的。

但是图像呢? 如果您总是将飞机向后移动,那么我们应该一遍又一遍地看到相同的图像。

好扣! 就是这样:

要解决此问题,我们将在滚动返回时同时更改滚动方向上的所有图像。 再次使跳转变得不可察觉。

  const spaceY =高度+ marginY; 

如果(Math.abs(scroll)> spaceY){
//每当滚动超过阈值时。 向相反方向将其移回。
scroll =滚动-spaceY * scrollDirection;
planes = planes.map(plane => {
返回Object.assign({},plane,{imgNo:plane.imgNo + scrollDirection})
})
}

这就是无限滚动在引擎盖下的工作方式!

无限滚动本身很棒,但是使体验闪耀的是细节。 因此,让我们来看其中的几个!

Z位移

简短而甜美。 顶点着色器视每个顶点到中心的距离而在z轴上向前移动。

如果您对学习“着色器”的含义感到好奇。 我建议您在“着色书”中随意摆弄。 它令人兴奋,有趣,您可以在那里学习很多基础知识。

首先,我们要计算到中心的距离。

并初始化将保存实际位移的zChange

  void main(){ 
//顶点到中心的距离
浮动距离=长度(position.xy);
float zChange = 0。
//
vec3 pos = position.xyz;
pos.z + = zChange;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.0);
}

现在,我们将添加均匀的u_maxDistance来限制顶点可以达到的距离,直到效果不生效为止。 而且我们将使用摄像机视图的大小,因此,如果顶点在视图之外,则不会移位。

然后,我们将距离除以u_maxDistance来归一化。 并将zChange设置为normalizedDistance

 统一浮动u_maxDistance; 
void main(){
//顶点到中心的距离
浮动距离=长度(position.xy);
float zChange = 0。

if(distance <u_maxDistance){
float normalizedDistance =距离/ u_maxDistance;
zChange = normalizedDistance;
}


vec3 pos = position.xyz;
pos.z + = zChange;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.0);
}

我们的飞机离屏幕越近,离中心的距离也越远。 我们想要相反的情况,所以我们将反转normalizedDistance

我们要控制飞机向前移动的距离。 因此,我们将添加u_magnitude作为新的统一并将其与zChange相乘。

最后,添加统一的u_progress ,它是从1到0的数字,以控制效果的进度。

 统一浮动u_maxDistance; 
统一浮动u_progress;
均匀浮动u_magnitude;

void main(){
//顶点到中心的距离
浮动距离=长度(position.xy);
float zChange = 0。

if(distance <u_maxDistance){
float normalizedDistance =距离/ u_maxDistance;
zChange = normalizedDistance;
zChange = normalizedDistance * u_magnitude * u_progress;
}


vec3 pos = position.xyz;
pos.z + = zChange;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.0);
}

最重要的是,添加onMouseDown事件侦听器,该侦听器在500ms内将u_progress统一从0更新为1。

在那里,您可以得到无限的Scroller补充的惊人效果!

保留的宽高比

保持宽高比是一个有趣的问题。 根据我在网络上的经验,通常浏览器会处理这些问题。

想象一下这个问题出现时我的困惑:

在片段着色器中采样纹理时,我们需要使用某种纹理坐标。

我明显的猜测是使用飞机的UV。

让我们看看它是什么样的:

 精密中型浮子; 
统一采样器2D u_texture;
变化的vec2 vUv;
void main(){
vec2 texCoords = vec2(vUv.x,1.-vUv.y);
vec3 color = texture2D(u_texture,vUv).xyz;
gl_FragColor = vec4(color,1。);
}

一些图像看起来被压缩而另一些图像被拉伸了!

为什么会这样呢?

平面的UV范围从0到1,纹理坐标也从0到1。

通过使用平面的UV作为纹理坐标,我们试图将整个图像拟合到平面内。 相反,我们要使平面适合图像内:

[图像显示紫外线乘以倍数。 显示2列,一列有因数,一列没有因数,并按比较显示数字变化]

使用Javascript,我们需要找到一个将UV转换为正确的纹理坐标的因子。

为此,我们需要找到平面的比例和屏幕的比例。 根据哪个比例更大,我们将计算紫外线系数。

  const planeRatio = plane.width / plane.height; 
const imageRatio = image.width / image.height;
if(planeRatio> imageRatio){

}其他{

}

为了使图像不被拉伸或压缩,我们需要将一侧匹配,并用planeRatio计算另一侧。

取决于哪个比例更大,匹配一侧或另一侧将产生不同的结果。

让我们看看比率的可能结果是什么:

如果图像的比例大于平面的比例,

  • 给定相同的宽度,图像将在平面外出血。 它将覆盖整个平原。
  • 给定相同的高度,图像将在平面内部留下间隙。 结果:它不会覆盖整个飞机。

但是如果图像的比例小于平面的比例,

  • 给定相同的宽度,图像将在平面内部留下间隙。 结果:它不会覆盖整个飞机。
  • 给定相同的高度,图像将在平面外出血。 它将覆盖整个平原。

注意:如果比率相同,则图像将完全覆盖整个平面。 因此,我们不对此进行说明。

这是每个结果的样子: