用 kokomi.js 打造飄逸的3D輪播效果

語言: CN / TW / HK

我正在參加「碼上掘金挑戰賽」詳情請看:碼上掘金挑戰賽來了!

前言

大家好,這裡是 CSS 兼 WebGL 魔法使——alphardex。本文就讓我們來用kokomi.js實現一個飄逸的3D輪播效果。

(以下demo點開全屏觀看效果最佳)

https://code.juejin.cn/pen/7141175290247512068

準備

以下是筆者寫的WebGL圖片特效模板,點開並點選右上角的“Fork”即可

https://code.juejin.cn/pen/7142438055843430400

簡單地說明一下這個模板吧:

HTML裡是canvas元素,用來渲染效果圖,同時下面還有一個img圖片元素

CSS裡讓canvas鋪滿全屏,同時隱藏了HTML中的圖片元素

JS裡分別定義了最基本的頂點著色器vertexShader和片元著色器fragmentShader,以及一個kokomi.Gallery元件,這個元件的作用是將HTML元素和WebGL世界進行同步,原理可以看我之前寫過的這篇文章,這裡封裝的Gallery元件省下了很多冗餘的搭建程式碼,有興趣的讀者可以檢視其原始碼

一張圖片感覺不怎麼夠,那就多來幾張吧,直接將以下的程式碼分別拷貝至HTML和CSS中

```html

```

```css body { margin: 0; overflow: hidden; }

sketch {

width: 100vw;
height: 100vh;
background: black;

}

body { overflow: visible; }

img { opacity: 0; }

sketch {

position: fixed;
z-index: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background: #424B59;

}

.gallery { display: flex; flex-direction: column; }

.gallery .gallery-item-img { width: 25rem; height: 14rem; cursor: pointer; } ```

1.gif

接下來,讓我們正式開始WebGL部分

開始

傾斜效果

建立一個組,將圖片的所有網格元素放入組中,再對組進行旋轉變換,即可達成整體變換的效果,這裡的旋轉度數可以自行微調,只要夠酷就行~

```js const g = new THREE.Group(); gallary.makuGroup.makus.forEach((maku) => { g.add(maku.mesh); }); this.scene.add(g);

g.rotation.y = -THREE.MathUtils.degToRad(30); g.rotation.x = -THREE.MathUtils.degToRad(18); g.rotation.z = -THREE.MathUtils.degToRad(6); ```

2.jpg

浮動效果

這裡只要是通過動態增減頂點和uv座標的y軸來模擬浮動的效果

編寫頂點著色器vertexShader

```glsl void main(){ vec3 p=position; vec2 u=uv;

// float
p.y+=sin(iTime*2.)*.02;
u.y-=sin(iTime*2.)*.02;

gl_Position=projectionMatrix*modelViewMatrix*vec4(p,1.);

vUv=u;

} ```

這裡用到了sin函式,因為它的函式圖形是上下波動的

sin.jpg

因此根據時間變化的量也能得到一個波動的值,用它來控制頂點和uv的y軸即可

3.gif

扭曲效果

在扭曲圖片前,先將js裡的gallery元件更改為以下配置

js const gallary = new kokomi.Gallery(this, { vertexShader, fragmentShader, makuConfig: { meshSizeType: "scale", }, });

這裡我們將圖片網格的建立方式改成了縮放形式,PlaneGeometry本身的長寬都變成了1,這樣我們就能隨意地扭曲頂點座標了

編寫頂點著色器vertexShader

```glsl const float PI=3.14159265359;

void main(){ vec3 p=position; vec2 u=uv;

...

// distort
p.y+=sin(PI*u.x)*.05;
p.z+=sin(PI*u.x)*.1;

...

} ```

4.jpg

這裡依舊用sin函式來扭曲它,因為sin函式本身就有一種彎曲的美~

找出C點陣圖

接下來是本效果最關鍵的一部分——找出哪張圖片處於“C位”(也就是中心)

首先,在gallery中新增一個uniform量uDistanceCenter,表示C點陣圖到畫面中心的距離

js const gallary = new kokomi.Gallery(this, { vertexShader, fragmentShader, makuConfig: { meshSizeType: "scale", }, uniforms: { uDistanceCenter: { value: 0, }, }, }); await gallary.addExisting();

然後,我們就要利用一定的數學計算來算出C位的距離和下標

```js const gap = 64;

this.update(() => { if (gallary.makuGroup) { const dists = Array(gallary.makuGroup.makus.length).fill(0);

gallary.makuGroup.makus.forEach((maku, i) => {
  const sc = gallary.scroller.scroll.current;
  const h = maku.el.clientHeight;

  const d1 = Math.min(Math.abs(sc - i * (h + gap)) / h, 1);
  const d2 = 1 - d1 ** 2;
  dists[i] = d2;

  maku.mesh.material.uniforms.uDistanceCenter.value = d2;

  const activeIndex = dists.findIndex(
    (item) => item === Math.max(...dists)
  );
  this.activeIndex = activeIndex;
});

} }); ```

其中gallary.makuGroup.makus是所有圖片物件,maku代表一個圖片,包含DOM部分el以及WebGL部分mesh

sc是當前畫面已經滾動過的距離,h是每張圖片的DOM高度

下圖是用DOM動畫描述的公式原理

c.gif

成功算出C位距離後,我們便可以利用它來實現一些C點陣圖專屬的效果

縮放效果

當圖片滾到C位時,我們可以通過放大來突顯它

編寫頂點著色器vertexShader

```glsl uniform float uDistanceCenter;

void main(){ ...

// pos scale
p*=(1.+.2*uDistanceCenter);

// uv scale
u=2.*u-1.;
u*=(1.+.2*uDistanceCenter)*.82;
u=(u+1.)*.5;

...

} ```

5.gif

這裡我們既縮放了頂點,也縮放了uv座標(注意縮放uv前先要居中下uv)

顏色濾鏡

讓C點陣圖彩色顯現,非C位的圖直接奪走它的色彩,並且還要降低它的存在感(透明度)

編寫片元著色器fragmentShader

```glsl uniform float uDistanceCenter;

vec3 blackAndWhite(vec3 color){ return vec3((color.r+color.g+color.b)/5.); }

void main(){ vec2 p=vUv;

vec4 tex=texture(uTexture,p);

float alpha=clamp(uDistanceCenter,.4,1.);

vec4 col=vec4(tex.rgb,alpha);

vec4 bwCol=vec4(blackAndWhite(tex.rgb),alpha);

vec4 finalCol=mix(col,bwCol,1.-uDistanceCenter);

gl_FragColor=finalCol;

} ```

6.gif

這個blackAndWhite可以當成是一個通用的黑白濾鏡函式,將其和原圖根據C位距離混合即可

透明度alpha也用clamp函式限制在一定範圍內

操控DOM

到這裡,我們已經基本完成了WebGL部分,DOM部分可能算是錦上添花吧,我們可以將其單獨抽成一個函式updateDOM

```js class Sketch extends kokomi.Base { ... updateDOM() { const charInfos = [ { name: "珊瑚宮心海", color: "#d27273" }, { name: "甘雨", color: "#46a4e1" }, { name: "神裡綾華", color: "#45484f" }, { name: "雷電將軍", color: "#141c4b" }, { name: "胡桃", color: "#452b2c" }, ]; const charNameEl = document.querySelector(".char-name");

this.update(() => {
  const activeIndex = this.activeIndex;
  charNameEl.textContent = charInfos[activeIndex].name;

  gsap.to("#sketch", {
    backgroundColor: charInfos[activeIndex].color,
    ease: "none",
  });,
});

} } ```

這裡主要實現了文字和背景隨C位距離變化的DOM效果,也就是文章頭圖的預覽效果

CSS與WebGL

有人看到這可能會有個疑惑:既然CSS也有3D變換,那我用CSS的3D變換不也能實現本文的這種效果?

答案是當然可以,只不過有一部分還是無法企及,那就是最微妙的扭曲效果,因為WebGL是能夠畫素級地自由操控圖片的,而CSS目前暫時還做不到這點(SVG濾鏡也能實現扭曲,但效果也可能很受限)

如果不考慮扭曲、高階濾鏡、微粒化等效果的話,光用CSS就能實現99%以上的動畫效果了吧

因此結論是如果想實現的效果夠簡單且線性,用CSS,想扭曲一切,用WebGL

最後

希望本教程能給予你創作新特效的靈感~