平时我们见过很多的图片悬浮和滚动特效,大部分是用 CSS 和 SVG 实现的,但是有一种特效它们绝对实现不了——扭曲特效。为何?CSS 擅长直线型变换,而 SVG 擅长曲线型变换。扭曲特效则两者都不是,它是像素级变换,能做到像素级变换的只有 canvas,而 webgl 的片元着色器其实特别擅长这一点,我们可以利用它来实现各种酷炫的扭曲特效。以下是最终实现的效果图
笔者的three.js 模板[1]:点击右下角的 fork 即可复制一份
实现本文效果最重要的一点是:将 HTML 世界与 webgl 世界同步起来!
一旦两个世界同步了,各种炫酷的效果都可以应用在原生 HTML 元素上,且不影响基本的交互!
第一步:将你想展示的所有图片搭成一个简单的 HTML 页面:
<main class="overflow-hidden">
<div data-scroll>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/16/cqyJiYlRwnTeHmj.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/Ujf6n75o8TtIsWX.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/buDT4YS6zUMfHst.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/10/18/uXF1Kx7lzELB6wf.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/03/RtVq2wxQYySDb8L.jpg" alt="" crossorigin="anonymous" />
</div>
<div class="relative w-screen h-screen flex-center">
<img class="w-240 h-120" src="https://i.loli.net/2019/11/16/FLnzi5Kq4tkRZSm.jpg" alt="" crossorigin="anonymous" />
</div>
</div>
</main>
<div class="twisted-gallery fixed -z-1 inset-0 w-screen h-screen"></div>
.w-240 {
width: 60rem;
}
.h-120 {
height: 30rem;
}
看到图片整齐的排列后,可以把它们隐藏起来了:
img {
opacity: 0;
}
在我们的主类中,先创建一个总函数,里面未实现的函数可以先注释掉,下文会慢慢地实现它们。
class TwistedGallery extends Base {
...
async init() {
this.createScene();
this.createPerspectiveCamera();
this.createRenderer();
this.createPlane();
// await preloadImages();
// this.createDistortImageMaterial();
// this.createImageDOMMeshObjs();
// this.setImagesPosition();
// this.listenScroll();
// this.createPostprocessingEffect()
this.createLight();
this.createOrbitControls();
this.addListeners();
this.setLoop();
}
...
}
const start = () => {
const twistedGallery = new TwistedGallery(".twisted-gallery", true);
twistedGallery.init();
};
为了在 webgl 中渲染出 HTML 里的所有 img,我们需要把 HTML 里的 img 的像素信息同步给 webgl。
问题来了,如何使得 webgl 里的 1 单位长度===HTML 里元素的 1px?
这时我们就要搬上那张经典的透视示意图了,我们需要根据公式来计算出视场角 fov:
fov
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean) {
...
this.cameraPosition = new THREE.Vector3(0, 0, 600);
const fov = this.getScreenFov();
this.perspectiveCameraParams = {
fov,
near: 100,
far: 2000,
};
}
getScreenFov() {
return ky.rad2deg(
2 * Math.atan(window.innerHeight / 2 / this.cameraPosition.z)
);
}
}
为了验证,我们将 createPlane 函数里的 PlaneBufferGeometry 的宽高都调为 100。
1画面中的平面宽高皆为 100px,和 HTML 完全一致!
createPlane
函数完成了它的测试使命,可以删掉了
在显示图片之前,我们要确保图片全部加载完毕,故用imagesLoaded[2]库来进行判断
import imagesLoaded from "https://cdn.skypack.dev/imagesloaded@4.1.4";
const preloadImages = (sel = "img") => {
return new Promise((resolve) => {
imagesLoaded(sel, { background: true }, resolve);
});
};
我们创建一个 DOMMeshObject 类,该类是一座桥梁,负责将 DOM 里的信息同步至 webgl 世界首先获取 DOM 元素的长宽和位置,再根据这些数据计算出它们在 webgl 中对应的位置和大小。
class DOMMeshObject {
constructor(
el: Element,
scene: THREE.Scene,
material: THREE.Material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
) {
this.el = el;
const rect = el.getBoundingClientRect();
this.rect = rect;
const { width, height } = rect;
const geometry = new THREE.PlaneBufferGeometry(width, height, 10, 10);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
this.mesh = mesh;
}
setPosition() {
const { mesh, rect } = this;
const { top, left, width, height } = rect;
const x = left + width / 2 - window.innerWidth / 2;
const y = -(top + height / 2 - window.innerHeight / 2) + window.scrollY;
mesh.position.set(x, y, 0);
}
}
在 webgl 世界中创建出图片材质,并将 HTML 里的 img 元素作为贴图传给着色器,再同步好位置即可。
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean) {
...
this.images = [...document.querySelectorAll("img")];
this.imageDOMMeshObjs = [];
}
// 创建材质
createDistortImageMaterial() {
const distortImageMaterial = new THREE.ShaderMaterial({
vertexShader: twistedGalleryMainVertexShader,
fragmentShader: twistedGalleryMainFragmentShader,
side: THREE.DoubleSide,
uniforms: {
uTexture: {
value: 0
}
}
});
this.distortImageMaterial = distortImageMaterial;
}
// 创建图片DOM物体
createImageDOMMeshObjs() {
const { images, scene, distortImageMaterial } = this;
const imageDOMMeshObjs = images.map((image) => {
const texture = new THREE.Texture(image);
texture.needsUpdate = true;
const material = distortImageMaterial.clone();
material.uniforms.uTexture.value = texture;
const imageDOMMeshObj = new DOMMeshObject(image, scene, material);
return imageDOMMeshObj;
});
this.imageDOMMeshObjs = imageDOMMeshObjs;
}
// 设置图片位置
setImagesPosition() {
const { imageDOMMeshObjs } = this;
imageDOMMeshObjs.forEach((obj) => {
obj.setPosition();
});
}
}
顶点着色器twistedGalleryMainVertexShader
跟模板一致。
varying vec2 vUv;
void main(){
vec4 modelPosition=modelMatrix*vec4(position,1.);
vec4 viewPosition=viewMatrix*modelPosition;
vec4 projectedPosition=projectionMatrix*viewPosition;
gl_Position=projectedPosition;
vUv=uv;
}
片元着色器twistedGalleryMainFragmentShader
将贴图作为颜色显示了出来:
uniform sampler2D uTexture;
varying vec2 vUv;
void main(){
vec2 newUv=vUv;
vec4 texture=texture2D(uTexture,newUv);
vec3 color=texture.rgb;
gl_FragColor=vec4(color,1.);
}
经过一番努力,图片总算显示在画面上了,接下来我们就要开始让它跟随画面滚动起来了。
这里的滚动监听原生的scroll
事件即可实现,但本文并不会这么做,为什么呢?因为原生的scroll
事件只能获取滚动的位置,而无法获取滚动的速度。如果用户滚得快,我们也要在我们的特效上体现出来。因此,我们将采用locomotive-scroll[3]这个库,它能捕获到用户滚动的位置和速度。
import LocomotiveScroll from "https://cdn.skypack.dev/locomotive-scroll@4.1.0";
class TwistedGallery extends Base {
// 监听滚动
listenScroll() {
const scroll = new LocomotiveScroll({
getSpeed: true
});
scroll.on("scroll", () => {
this.setImagesPosition();
});
this.scroll = scroll;
}
}
图片终于在 WEBGL 的世界里滚动起来了,两个世界同步完成~接下来开始我们的重头戏——扭曲特效!
开头的那个特效可以看出是全屏幕上对图片进行扭曲的,因此我们将采用后期处理postprocessing
来实现,它提供了屏幕tDiffuse
。
import { RenderPass } from "https://cdn.skypack.dev/three@0.124.0/examples/jsm/postprocessing/RenderPass.js";
import { ShaderPass } from "https://cdn.skypack.dev/three@0.124.0/examples/jsm/postprocessing/ShaderPass.js";
import gsap from "https://cdn.skypack.dev/gsap@3.6.0";
class TwistedGallery extends Base {
constructor(sel: string, debug: boolean) {
...
this.scrollSpeed = 0;
}
// 创建后期处理特效
createPostprocessingEffect() {
const composer = new EffectComposer(this.renderer);
const renderPass = new RenderPass(this.scene, this.camera);
composer.addPass(renderPass);
const customPass = new ShaderPass({
vertexShader: twistedGalleryPostprocessingVertexShader,
fragmentShader: twistedGalleryPostprocessingFragmentShader,
uniforms: {
tDiffuse: {
value: null
},
uRadius: {
value: 0.75
},
uPower: {
value: 0
}
}
});
customPass.renderToScreen = true;
composer.addPass(customPass);
this.composer = composer;
this.customPass = customPass;
}
// 设置滚动速度
setScrollSpeed() {
const scrollSpeed = this.scroll.scroll.instance.speed || 0;
gsap.to(this, {
scrollSpeed: Math.min(Math.abs(scrollSpeed) * 1.25, 2),
duration: 1
});
}
// 动画
update() {
if (this.customPass) {
this.setScrollSpeed();
this.customPass.uniforms.uPower.value = this.scrollSpeed;
}
}
}
可以看出我们将滚动速度作为uniform
传入了着色器,并且用gsap[4]
来实现缓动效果,接下来就是着色器部分了顶点着色器twistedGalleryPostprocessingVertexShader
跟上文所说的模板一致,故略过。
片元着色器twistedGalleryPostprocessingFragmentShader
负责动态计算 uv 坐标,实现起来稍有难度,关键是多调,里面的值用gl_FragColor
显示出来,这样就容易理解值的变化。
uniform sampler2D tDiffuse;
uniform float uRadius;
uniform float uPower;
varying vec2 vUv;
void main(){
vec2 pivot=vec2(.5);
vec2 d=vUv-pivot;
float rDist=length(d);
float gr=pow(rDist/uRadius,uPower);
float mag=2.-cos(gr-1.);
vec2 uvR=pivot+d*mag;
vec4 color=texture2D(tDiffuse,uvR);
gl_FragColor=color;
}
Twisted Gallery[5]
本文所实现的特效仅仅是众多特效中的一种,但真正重要的是如何将 HTML 与 webgl 世界所同步起来的这个过程,一旦掌握了这一方法,想做出一个非常酷炫的网站将不再是一个难事。
[1] three.js模板: https://codepen.io/alphardex/pen/yLaQdOq
[2] imagesLoaded: https://github.com/desandro/imagesloaded
[3] locomotive-scroll: https://github.com/locomotivemtl/locomotive-scroll
[4] gsap: https://github.com/greensock/GSAP
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/3uDeTVCUiqDyLANsgtHaFA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。