在实现图片幻灯片切换效果时,常用的效果包括左右滑动,渐隐渐显。对于熟悉 CSS Transition 的开发者,实现这些切换效果并不难。 最近阅读了 Codrop 上的两篇文章,提供了切换效果的新思路:CSS Mask 和 WebGL。
CSS Mask
CSS Mask 支持使用位图或者 SVG 来裁切背景图片。
值得注意的是浏览器支持度,实际使用时需要添加 -webkit-
前缀,下面的例子图方便就省略了。
很自然的想到,如果能让这个 mask 动起来,随着前一张图片显示区域的变化,后一张图片渐渐显露,效果拔群。
让背景图片动起来不是难事,常逛 B 站的同学一定见过,视频播放器下方的“收藏”等图标就使用了 background-image
动画。
mask
和 background-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 中许多命令式编程的繁琐过程,也更加便于记忆。 但是效果炫酷的同时也带来了性能问题,在低性能设备甚至手持设备上会出现明显卡顿。