在之前的《还在人挤人赏龙年焰火?你难道不想拥有一个私人的全景天边盛宴吗?|春节创意投稿》中,我们经过fragment shader完成了一个全景播放器以及数个fragment shader的demo工程,在探索了其强壮功用之后,那么也该回归主线任务————完成翻页动画上面来了。

至于运用fragment shader的原因,其实一个字:功能!

【Flutter&GLSL】用Fragment Shader来完成高功能的动画作用——翻页动画(一)

现在回顾19年写下的第一版:《Flutter完成一个小说阅读APP(可能是现在交互最好的阅读APP)?》,能够发现其翻页动画其实仍是存在必定问题的:

  1. 仿真翻页动画帧率波动太大,鸿沟翻页的时分功能很差。
  2. 3d球功能一般。

那么是时分 make flutter_novel great again了

思路收拾

先从简单的作用开端,我了解fragment shader来自于郭神的这篇文章:Flutter 小技巧之不相同的思路完成炫酷 3D 翻页折叠动画

其中的3d翻页作用,固定方向为横向,完成上相较简单,先从这块开端上手

riveo_page_curl 的翻页作用分析

这个横向翻页作用,还真用附带的那一张图就能阐明:

【Flutter&GLSL】用Fragment Shader来完成高功能的动画作用——翻页动画(一)

要看懂这张图,首要需求对片段着色器的完成方法有必定的了解:

片段着色器处理展示作用的进程,能够说是依据输入的像素点方位,找到其在传入的图片中对应方位像素点的色彩,终究回来这个色彩值的进程。

而图片中的几个参数结合相关代码,能够推导出过程是这样的:

  1. 先模仿一个圆柱出来,圆柱的半径为r,圆柱圆心的横坐标为x,圆柱用来模仿页面弯曲作用。

  2. 界说一个浮点数d,代表圆心到当前正在处理的像素点的间隔。

  3. 假如d大于半径,那么阐明当前像素点的方位处于翻页动画的弯曲规模外,对应这部分的色值就应该是透明色

    【Flutter&GLSL】用Fragment Shader来完成高功能的动画作用——翻页动画(一)

  4. 假如d是正数,且不大于半径r,那么处于这部分:

    【Flutter&GLSL】用Fragment Shader来完成高功能的动画作用——翻页动画(一)

    这部分图中代码并未体现的一点的是,假如像图中的情况相同,像素点所在方位可能对应圆柱上两个点:p2,p1,我该怎么经过代码决策用哪个点?

    这点的解决计划其实也很简单,先不论是否存在p2点,假设必定会存在p2点的情况,核算出p1和p2两点的方位:

    p1点的核算方法便是简单的三角函数,先界说p1,圆心跟纵轴的夹角为,依据三角函数可知sin = d/r,那么的值便是d/r的反正弦。得到了的弧度值之后,对应的弧长经过半径*弧度即可得到,得到的弧长便是对应图片纹路上的x坐标。

    依据相同的道理能够试图核算出p2的x坐标,因为p2和p1是镜像关系,因此核算弧长的时分只是将传入的改为-即可。

    终究假如p2点横坐标处于纹路的规模内(比如说归一化后大于0,小于1)那么阐明存在p2,直接用p2即可,不然就用p1。

  5. 终究,假如d是负数,那阐明在剩下部分,这部分处理的逻辑也跟d是正数的情况相同,需求判断下是不是有过度滑动超出的p1和p2两点:

    因为假如不存在p2点,直接展示原有的图片纹路即可,因此只需求判断下有没有p2点即可,p2点的横向间隔等于整个弯曲部分弧长(*R)+ 圆柱圆心的横坐标(x)+ 当前判断像素点的横坐标跟圆柱圆心横坐标的绝对值(abs(xy.x-x)); 相同的,假如这个p2点终究算出来的横坐标在整个纹路规模内,那阐明存在这个点,直接运用p2点的纹路即可,不然用xy.x的纹路即可。

更详细的介绍能够看下@菠萝没有桃子了大佬翻译的文章,里面的阐明更详细,一起弥补了xy两个方向共同作用的阐明。

应用改造

在菠萝大佬翻译的文章中,原大佬作者现已将写好的着色器上传到shadertoy(其实第一次知道这个东西仍是靠issue的提示……)中,那么直接在这个基础上进行修正就好了,这个是原理阐明图:

【Flutter&GLSL】用Fragment Shader来完成高功能的动画作用——翻页动画(一)

那么罗列下假如要应用到flutter_novel 中所需求的修正内容:

  1. 让弯曲后的页脚固定在接触点,因此需求调整curl axis的生成规则。
  2. 弯曲页部分的暗影
  3. 小修正来支撑上一页翻回来的作用

curl axis的修正计划

假如想要要翻起后的页角跟随手势接触点,需求修正的内容一个是角度,另一个是弯曲轴的方位

角度的问题,能够将原理图中的click坐标改为最近的边角(右上角和右下角),这部分完全能够依据接触起点的方位来决议;

curl axis的的问题就扎手一些,毕竟控制的是弯曲轴而非页脚,所以这里需求经过核算来修正同步,终究全体是这样的:


#include <flutter/runtime_effect.glsl>
uniform vec2 resolution;
uniform vec4 iMouse;
uniform sampler2D image;
#define pi 3.14159265359
#define radius 0.05
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)
out vec4 fragColor;
void main() {
    vec2 fragCoord = FlutterFragCoord().xy;
    float aspect = resolution.x / resolution.y;
    vec2 uv = fragCoord * vec2(aspect, 1.0) / resolution.xy;
    // 归一化鼠标坐标
    vec2 mouse = iMouse.xy  * vec2(aspect, 1.0) / resolution.xy;
    vec2 cornerFrom = (iMouse.w<resolution.y/2)?vec2(resolution.x, 0.0):vec2(resolution.x, resolution.y);
    // 鼠标方向的向量
    vec2 mouseDir = normalize(abs(cornerFrom) - iMouse.xy);
    // 翻页原点的核算,能够视为转换为横轴下的x轴起点方位
    vec2 origin = clamp(mouse - mouseDir * mouse.x / mouseDir.x, 0.0, 1.0);
    // 鼠标拖动间隔
    float mouseDist = distance(mouse, origin);
    //    float mouseDist = clamp(length(mouse - origin)
    //    + (aspect - (abs(cornerFrom.x) / resolution.x) * aspect) / mouseDir.x, 0.0, aspect / mouseDir.x);
    // 假如鼠标方向向左,那么鼠标拖动间隔便是鼠标到原点的间隔
    if (mouseDir.x < 0.0)
    {
        mouseDist = distance(mouse, origin);
    }
    float proj = dot(uv - origin, mouseDir);
    float dist = proj - mouseDist;
    vec2 curlAxisLinePoint = uv - dist * mouseDir;
    if (!(distance(mouse, cornerFrom* vec2(aspect, 1.0) / resolution.xy)<pi*radius)){
        float params = (distance(mouse, cornerFrom* vec2(aspect, 1.0) / resolution.xy)-pi*radius)/2;
        curlAxisLinePoint = uv - dist * mouseDir +params*mouseDir;
        dist -=params;
    }
    if (dist > radius)
    {
        fragColor = TRANSPARENT;
        fragColor.rgb *= pow(clamp(dist - radius, 0.0, 1.0) * 1.5, 0.2);
    }
    else if (dist >= 0.0)
    {
        // map to cylinder point
        float theta = asin(dist / radius);
        vec2 p2 = curlAxisLinePoint + mouseDir * (pi - theta) * radius;
        vec2 p1 = curlAxisLinePoint + mouseDir * theta * radius;
        if (p2.x <= aspect && p2.y <= 1.0 && p2.x > 0.0 && p2.y > 0.0){
            uv = p2;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            fragColor.rgb *= pow(clamp((radius - dist) / radius, 0.0, 1.0), 0.2);
        } else {
            uv = p1;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            fragColor.rgb *= pow(clamp((radius - dist) / radius, 0.0, 1.0), 0.2);
        }
    }
    else
    {
        vec2 p = curlAxisLinePoint + mouseDir * (abs(dist) + pi * radius);
        if (p.x <= aspect && p.y <= 1.0 && p.x > 0.0 && p.y > 0.0){
            uv = p;
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            //        } else if ((p.x <= aspect+0.02 && p.y <= 1.0+0.02) && (p.x > 0.0 - 0.02 && p.y > 0.0- 0.02)){
            //
            //            vec3 col = texture(image, uv * vec2(1.0 / aspect, 1.0)).rgb;
            //            fragColor = vec4(col*smoothstep(-1.0, 1.0, 0.5), 1.0);
        } else {
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            if (distance(uv, origin) < 0.02){
                fragColor = TRANSPARENT;
            }
        }
    }
}

怎么运用

flutter 中运用fragment shader的方法有许多种,不过大体上终究都是经过Paint..shader = <FragmentShader>来完成的

在这个比如中,结合的是ShaderBuilder+AnimatedSampler来完成的,举个比如:


Stack(
  children: [
    Positioned.fill(
        child: Container(
      height: double.infinity,
      width: double.infinity,
      color: Colors.blue,
      padding: const EdgeInsets.symmetric(
        vertical: ProjectCardConstants.cornerRadius,
      ),
      child: Text(
        '许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn',
        style: TextStyle(fontSize: 15),
      ),
    )),
    Positioned.fill(
        child: GestureDetector(
      onPanDown: (details) {
        setState(() {
          downPosition = details.localPosition;
          updatePosition = details.localPosition;
          developer.log(details.toString(), name: 'down');
        });
      },
      onPanUpdate: (details) {
        setState(() {
          updatePosition = details.localPosition;
          developer.log(details.localPosition.toString(), name: 'update');
        });
      },
      onPanEnd: (details) {
        setState(() {
          downPosition = Offset.zero;
          updatePosition = Offset.zero;
          developer.log(details.velocity.toString(), name: 'end');
        });
      },
      onPanCancel: () {
        setState(() {
          downPosition = Offset.zero;
          updatePosition = Offset.zero;
          developer.log('', name: 'cancel');
        });
      },
      child: ShaderBuilder(
        (context, shader, _) {
          return AnimatedSampler(
            (image, size, canvas) {
              shader
                ..setFloat(0, size.width) // resolution
                ..setFloat(1, size.height) // resolution
                ..setFloat(2, updatePosition.dx) // mouse
                ..setFloat(3, updatePosition.dy) // mouse
                ..setFloat(4, downPosition.dx) // mouse
                ..setFloat(5, downPosition.dy) // mouse
                ..setImageSampler(0, image); // image
              ShaderHelper.drawShaderRect(shader, size, canvas);
            },
            child: Container(
              height: double.infinity,
              width: double.infinity,
              color: Colors.red,
              padding: const EdgeInsets.symmetric(
                vertical: ProjectCardConstants.cornerRadius,
              ),
              child: Text(
                '许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字许多文字nn',
                style: TextStyle(fontSize: 15),
              ),
            ),
          );
        },
        assetKey: ProjectCardConstants.shaderAsset2Key,
      ),
    )),
  ],
)

drawShaderRect方法便是直接在canvas上进行制作而已:

static void drawShaderRect(
    ui.FragmentShader shader, ui.Size size, ui.Canvas canvas) {
  canvas.drawRect(
    ui.Rect.fromCenter(
      center: ui.Offset(size.width / 2, size.height / 2),
      width: size.width,
      height: size.height,
    ),
    ui.Paint()..shader = shader,
  );
}

这段代码中,AnimatedSampler担任采样方针控件的纹路,ShaderBuilder将canvas区域暴露出来,这样就能够制作shader处理后的内容了;

需求修正制作内容的话,也只是需求将AnimatedSampler的child改变一下就能够了。

小结

现在翻页动画的雏形就现已完成了,不过间隔完美复刻,还需求处理下暗影作用,一起还需求添加翻页的规模限制。

最主要的是应用到整个小说阅读器上,让其能够体现出来全体的翻页作用,这部分就放到后面再说吧,现在轮到我去当帕鲁了