在实现图片幻灯片切换效果时,常用的效果包括左右滑动,渐隐渐显。对于熟悉 CSS Transition 的开发者,实现这些切换效果并不难。 最近阅读了 Codrop 上的两篇文章,提供了切换效果的新思路:CSS Mask 和 WebGL。

CSS Mask

CSS Mask 支持使用位图或者 SVG 来裁切背景图片。 值得注意的是浏览器支持度,实际使用时需要添加 -webkit- 前缀,下面的例子图方便就省略了。

很自然的想到,如果能让这个 mask 动起来,随着前一张图片显示区域的变化,后一张图片渐渐显露,效果拔群。

让背景图片动起来不是难事,常逛 B 站的同学一定见过,视频播放器下方的“收藏”等图标就使用了 background-image 动画。 maskbackground-image 其实是一个道理,我们同样需要准备一张 Sprite 图:

然后需要定义一个针对 mask-position 的动画,使用 steps(n) 将总长度分成 n 格(也就是 Sprite 图的数目)。 这样 mask-position 每次移动一格,mask 图片也就切换到 Sprite 中的下一部分。是不是和 background-position 一模一样呢?

mask: url(../img/mask-sprite.png);
mask-size: 7100% 100%;
animation: mask-play 1.4s steps(70) forwards;

@keyframes mask-play {
  from {
	mask-position: 0% 0;
  }
  to {
	mask-position: 100% 0;
  }
}

切换时效果如下:

当然为了避免浏览器在切换时才下载 mask 图片,我们可以提前给元素应用上,让浏览器尽早发现并下载。

WebGL

在之前学习了一些「Shader 基础知识」后, 我们了解了给纹理对象设置参数能够指导 WebGL 在贴图时使用指定的像素计算策略。使用 Three.js 能方便的创建纹理对象。

var loader = new THREE.TextureLoader();
loader.crossOrigin = "";
// 创建两张图片对应的 texture
var texture1 = loader.load(image1);
var texture2 = loader.load(image2);
// 创建
var disp = loader.load(dispImage);
// 设置上下左右超出部分的像素填充策略
disp.wrapS = disp.wrapT = THREE.RepeatWrapping;
// 设置出现缩小放大时像素的计算策略
texture1.magFilter = texture2.magFilter = THREE.LinearFilter;
texture1.minFilter = texture2.minFilter = THREE.LinearFilter;

进入 vertex shader 会有一个疑问,uv position 这些变量是哪里传进来的?

var vertex = `
    varying vec2 vUv;
    void main() {
        vUv = uv;
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
`;

原来 Three.js 提供了很多内置的变量。 由于在更高层次上做了抽象,开发者不必手动向 shader 传递变量,可以使用 Camera Geometry Model 所决定的变换矩阵和变量:

// = object.matrixWorld
uniform mat4 modelMatrix;

// = camera.matrixWorldInverse * object.matrixWorld
uniform mat4 modelViewMatrix;

// = camera.projectionMatrix
uniform mat4 projectionMatrix;

// = camera.matrixWorldInverse
uniform mat4 viewMatrix;

// = inverse transpose of modelViewMatrix
uniform mat3 normalMatrix;

// = camera position in world space
uniform vec3 cameraPosition;

// = geometry
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;

除了使用内置变量,我们还可以向 shader 中传递自定义变量。 值得注意的是 varying 不需要在 ShaderMaterial 中声明。 所以两个 shader 中传递的 vUv 不会出现在这里:

var mat = new THREE.ShaderMaterial({
    uniforms: {
        effectFactor: { type: "f", value: intensity },
        dispFactor: { type: "f", value: 0.0 },
        texture: { type: "t", value: texture1 },
        texture2: { type: "t", value: texture2 },
        disp: { type: "t", value: disp }
    },
    vertexShader: vertex,
    fragmentShader: fragment,
    transparent: true,
    opacity: 1.0
});

剩下的秘密就在 fragment shader 中了。dispFactor 是一个取值范围 0-1 的变量

void main() {
    vec2 uv = vUv;
    vec4 disp = texture2D(disp, uv);

    vec2 distortedPosition = vec2(uv.x + dispFactor * (disp.r*effectFactor), uv.y);
    vec2 distortedPosition2 = vec2(uv.x - (1.0 - dispFactor) * (disp.r*effectFactor), uv.y);

    vec4 _texture = texture2D(texture, distortedPosition);
    vec4 _texture2 = texture2D(texture2, distortedPosition2);

    vec4 finalTexture = mix(_texture, _texture2, dispFactor);

    gl_FragColor = finalTexture;
}

最后,我们只要对 dispFactor 进行实时修改,在 rAF 中绘制当前帧就能实现动画效果。这里使用了 TweenMax:

TweenMax.to(mat.uniforms.dispFactor, speedIn, {
    value: 1,
    ease: easing
});

var animate = function() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
};
animate();

最终效果如下

总结

体验了一下 Three.js,确实简化了 WebGL 中许多命令式编程的繁琐过程,也更加便于记忆。 但是效果炫酷的同时也带来了性能问题,在低性能设备甚至手持设备上会出现明显卡顿。

参考资料