> 本篇案例实现基于 Pixijs ,高性能的 2D 渲染引擎。

先看效果


上图炫酷的液体流动效果,是如何实现的呢,我们先从『滤镜』开始讲起。

啥是滤镜?

滤镜是在摄影的时候放在镜头上一种玻璃或者塑料的镜片,能够对不同的波段的光进行选择性过滤,使得照片呈现不同的效果。

比如这种滤光镜

滤镜的种类很多,我们单单以 Photoshop 提供的滤镜为例,就有3万种以上。这也是照片可以呈现各种不同特效的原因。那要实现水波的效果使用的是哪种滤镜呢?

置换滤镜

置换滤镜可以根据另一张图片的亮度值使现有图像的像素重新排列并产生位移。

这句话可能不是很明白,但我们先来看看置换滤镜可以实现什么效果。最后我们再来分析下它的原理。

按照丝绸的纹理扭曲的文字,按照人脸纹理扭曲的报纸等等,这些都是置换滤镜可以搞定的特效。当然这些特效对于一个设计师来说是基本的 Photoshop 操作啦,但是我们是前端啊,要写代码啊,我们要做动画的啊,能不能来点干货!

OK! 那下面我们来看看水波的案例是怎么用 Canvas 实现的吧。

动手做

准备工作

  • Pixijs ,我们的案例使用的动画渲染引擎是 Pixijs ,是一种高性能的 2D 渲染引擎。不太了解的同学可以先看看教程。当然不看也是可以的,你只要按照我的讲解也可以实现的,就把他理解为一个 Canvas 或者 WebGL 库,甚至就是一个 Javasript 库。
  • 图片。
    • 灰色图片一枚。
    • 水面洗脚图片一枚!
  • 让水面洗脚的图片按照灰色图片的纹理,来扭动起来!

码代码

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.8.6/pixi.min.js"></script>
<div id="app"></div>

引入 Pixijs。添加一个 div 容器,用于添加 Canvas 或者 WebGL 对象。

Javascript

  1. Pixi 对象

var appDiv=document.getElementById("app") // 新建一个pixi的对象,可以理解为对canvas context对象的封装。 var app = new PIXI.Application(1000, 600); appDiv.appendChild(app.view); // pixi 把动画面板定义成一个舞台,舞台上有各种元素,其中 container 可以包含其他元素。 var container = new PIXI.Container(); app.stage.addChild(container);
  1. 加载背景图片和置换图片
// 加载水面洗脚图
var flag = PIXI.Sprite.fromImage("https://img.alicdn.com/tfs/TB1KhwrIQvoK1RjSZPfXXXPKFXa-1920-1080.jpg");
container.addChild(flag);
// 调整图片的位置,替换滤镜会导致原图像素发生位移,会导致边缘黑边,所以...
flag.y=-50
flag.x=-50
flag.width=1100
flag.height=700
// 加载置换图(灰色纹理图)
var displacementSprite = PIXI.Sprite.fromImage('https://img.alicdn.com/tfs/TB1VXsyIQzoK1RjSZFlXXai4VXa-2048-2048.jpg');
// 设置置换图是平铺还是重复
displacementSprite.texture.baseTexture.wrapMode = PIXI.WRAP_MODES.REPEAT;
// 新增一个置换滤镜对象。 pixi支持多种滤镜,都在PIXI.filters对象里。
var displacementFilter = new PIXI.filters.DisplacementFilter(displacementSprite);
// 图片位置
displacementSprite.position = flag.position;
// 将置换图添加到stage上
app.stage.addChild(displacementSprite);
// 重点在这,设置背景图的滤镜模式为置换滤镜,pixi中图片的滤镜可以添加多个。
flag.filters = [displacementFilter];
// 滤镜的缩放,缩放越大,波越大!
displacementFilter.scale.x = 30;
displacementFilter.scale.y = 60;

此时图片已经发生了扭曲。

  1. 动起来
// ticker 是循环函数,可以设置每秒执行多少次,也就是帧数
app.ticker.add(function() {
    // 置换图沿着x轴位移
    displacementSprite.x++;
    if(displacementSprite.x > displacementSprite.width)
        displacementSprite.x = 0;
});

搞定!
详细代码在此:Codepen Displacement Filter

如果你想要更多的互动,比如点击可以改变水流的速度,可以引入 TweenMax 对置换图做加速动画。

效果已经实现了,你是不是觉得大功告成! NO! 本着知其然并知其所以然的好学精神,我们再来深入研究下,置换滤镜的原理!

置换滤镜的原理

理解它之前我们得先来聊一下什么是通道和灰度值?

通道

  • 我们在写 CSS 样式的时候,颜色可以使用 RGB 模式来表示,这里的 RGB 就是三个通道。
  • 显示器由像素点构成,每个像素点是什么样子的呢?我们可以理解为每个像素点都是排列好的三个灯。

灰度值

  • 我们在写 color:rgb(255,11,24) 的时候,括号里的每个值其实就是灰度值,它表现在屏幕上就是灯的亮度。
  • 红,绿,蓝 灯,不同的亮度组合,就组成了丰富的颜色啦,很神奇!

屏幕硬件提供的能力就是调整灯的亮度,其他都是软件算法来做的啦,比如模糊,抗锯齿,透明通道等。所以置换滤镜其实就是一种软件算法。

置换滤镜算法

图片滤镜算法,一般都是调整原有图片像素点的位置,亮度,或者是过滤掉某种颜色。
置换滤镜则是像素的移动。怎样移动的?是有置换图的灰度来决定的。

  • 置换图一般采用灰色图,好处是 RGB 每个通道的灰度值是一样的!

  • 大体来说,灰度值就像等高线,灰度值越高,表示该位置越亮,可以理解为是水波的涟漪,而灰度值越低,位置越暗,可以理解为水波的中心,当然反过来理解也是可以的,这取决于你的置换图是怎样的,是用白色表现波纹,还是黑色。
  • 原图像素的移动,也按照灰度值的分布,呈现一定的波形。
  • 置换算法有多种,下面是比较简单的,
    过程如下:

    1. 取得原图 [x,y] 坐标的像素点。
    2. 取得置换图 [x,y] 坐标的像素点的灰度值 i。
    3. 原图像素按照灰度值做相应的位移。即 x=x+(i-128)/A*Math.sin(45),y=y+(i-128)/A*Math.sin(45) , A 为抖动系数,我测试后的最佳值是 50,当然你也可以按照具体的实现效果来设置。

具体的实现可以参考我基于 Canvas 实现的代码, Codepen:Displacement Filter Algorithm,基本按照上面的思路实现了,但是性能很差,翻看了pixijs的源码实现, 原来是基于 OpenGL Vertex 和 Fragment ,使用矩阵向量变换来实现,性能很好,所以如果要实现上述的特效,Pixijs 是一个很好的选择。

参考