书接上文,间隔达到翻页动画自身的可用,还需求增加对翻页规模的约束,例如下图这种状况:

【Flutter&GLSL】用Fragment Shader来完成高性能的动画作用——怎么批改翻页规模

那么这次就来解决这个问题,首先还是奉上这次完成后的作用:

【Flutter&GLSL】用Fragment Shader来完成高性能的动画作用——怎么批改翻页规模

计划设计

在通过canvas来完成的计划中,完成一个对规模的处理并不是一件很困难的事,canvas生成内容基本是跟path或多或少挂钩的,因而我们可以直接修正Path的规模来处理规模约束问题。

而在Fragment Shader中,并没有线条和路径的概念,片段着色器仅仅会依据程序代码,将像素点对应的纹路信息生成出来,其操作的目标不是path,而是一个个的像素点,关于某个点来说,它不会像path一样能得知其他点的方位,更无从获悉其他点是否在展示规模外。因而,关于首先要做的事,便是寻找一种全新的规模判别和批改计划。


吐槽完,回到主线使命中来,中心的心酸泪就不提了,直接说现在的计划:

假如依照翻页角规模最大来设想,那么翻页部分的页脚,其轨道等同于下图的绿色轨道:

【Flutter&GLSL】用Fragment Shader来完成高性能的动画作用——怎么批改翻页规模

其实便是环绕左上角,以书页横向宽度为半径的一个圆而已;

如果在这个绿圈规模内,那么翻页规模怎么样都不会在规模外,在绿圈规模外,则就会导致翻页规模的溢出,需求处理。这样,怎么判别是否需求调整方位的判别依据就有了,依据当时接触点是否在绿圈规模内即可。

有了判别依据,第二步便是怎么批改,这点我是拟定了这样的计划:

  1. 设定翻页角对应x=0的点偏移60度的直线为基准线,如果接触点方位坐落这个基准线上,那么将接触点改为半径aspect的那个弧线跟60度直线的交点,这个倒直接这么处理即可;
  2. 如果不在上述60度的直线上,那么取当时接触点,跟基准线的间隔,与半径aspect的弧度的间隔,这两者取较小值
  3. 获取到这个数值后,以接触点对应在弧线上的映射点为基准,以获取到的数值为弧长进行弧度偏移,方向取接触点坐落基准线的上下来决定。

嗯,我知道这玩意一般是看不懂的……即使是我,年前写下的注释,当年后在写这篇文章的时,回忆这个注释也愣是没太看懂

【Flutter&GLSL】用Fragment Shader来完成高性能的动画作用——怎么批改翻页规模

为了避免产生以上图片的状况,这次就对这些过程进行拆解和详细阐明

首先首要还是对图片中的一些要害点进行标示,以上图的为例:

【Flutter&GLSL】用Fragment Shader来完成高性能的动画作用——怎么批改翻页规模

那么依据这些标示点,进行过程拆解:

  1. 核算翻页对应点
        vec2 startPoint = vec2(0.0, cornerFrom.y==0.0?0.0:1.0);
  1. 核算当时接触点在基准线上的投影点方位
        vec2 vector = normalize(vec2(0.5, 0.5*tan(pi/3)));/// 归一化基准线向量
        vec2 targetMouse = mouse.xy;/// 接触点
        vec2 v = targetMouse - startPoint; /// 接触点跟翻页对应点的向量
        float proj_length = dot(v, vector); /// 核算投影长度
        vec2 targetMouse_proj = startPoint + proj_length*vector; ///起点+向量*长度便是具体方位点
  1. 依据投影点算出接触点到基准线的间隔
        float base_line_distance = length(targetMouse_proj - targetMouse);
  1. 当时接触点到弧线的间隔(也便是当时接触点到翻页对应点-弧线半径),并取两者最小值做为成果
        float arc_distance = distance(targetMouse, startPoint) - aspect;
        float actual_distance = min(abs(base_line_distance), abs(arc_distance));
  1. 核算当时接触点在弧线上的投影点
        vec2 currentMouse_arc_proj = startPoint+ normalize(mouse - startPoint)*aspect;
  1. 从这个投影点的方位,依据当时接触点相关于基准线的上下来决定方向,偏移第4步成果的弧线长度。
        vec2 newPoint_arc_proj = pointOnCircle(startPoint, currentMouse_arc_proj, aspect, actual_distance/2, mouse.y<=tan(pi/3)*mouse.x);
  1. 依据第六步批改后的方位,修正最终接触点方位
        mouse = newPoint_arc_proj;
        currentMouse.xy = mouse * resolution.xy / vec2(aspect, 1.0);

之后的流程就依照本来的过程来便是了,关于后续过程来说,他们核算的接触点已经是批改好的了。

shader文件代码

#include <flutter/runtime_effect.glsl>
uniform vec2 resolution;
uniform vec4 iMouse;
uniform sampler2D image;
#define pi 3.14159265359
#define radius 0.05
#define shadowWidth 0.02
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)
out vec4 fragColor;
float calShadow(vec2 targetPoint, float aspect){
    if (targetPoint.y>=1.0){
        return max(pow(clamp((targetPoint.y-1.0)/shadowWidth, 0.0, 0.9), 0.2), pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2));
    } else {
        return max(pow(clamp((0.0-targetPoint.y)/shadowWidth, 0.0, 0.9), 0.2), pow(clamp((targetPoint.x-aspect)/shadowWidth, 0.0, 0.9), 0.2));
    }
}
vec2 rotate(vec2 v, float a) {
    float s = sin(a);
    float c = cos(a);
    return vec2(c * v.x - s * v.y, s * v.x + c * v.y);
}
vec2 pointOnCircle(vec2 center, vec2 startPoint, float currentRadius, float arcLength, bool clockwise) {
    float theta = arcLength / currentRadius;
    vec2 startVec = startPoint - center;
    startVec = normalize(startVec);
    float rotationAngle = clockwise ? -theta : theta;
    vec2 rotatedVec = rotate(startVec, rotationAngle);
    vec2 endPoint = center + rotatedVec * currentRadius;
    return endPoint;
}
void main() {
    vec2 fragCoord = FlutterFragCoord().xy;
    float aspect = resolution.x / resolution.y;
    vec2 uv = fragCoord * vec2(aspect, 1.0) / resolution.xy;
    vec4 currentMouse = iMouse;
    vec2 cornerFrom = (currentMouse.w<resolution.y/2)?vec2(aspect, 0.0):vec2(aspect, 1.0);
    // 归一化鼠标坐标
    vec2 mouse = currentMouse.xy  * vec2(aspect, 1.0) / resolution.xy;
    vec2 testPoint = vec2(0.25, 0.5);
    // 鼠标方位跟左上角的间隔大于aspect,才会产生翻页规模大于屏幕
    if (distance(mouse.xy, vec2(0.0, cornerFrom.y))>(aspect)){
        // 修复规则,结合两个部分:
        // 1. 如果接触点方位坐落左上角向右下角60度的直线上,那么将接触点改为半径aspect的那个弧线跟60度直线的交点,这个倒直接这么处理即可;
        // 2. 如果不在上述60度的直线上,那么取当时接触点,跟60度直线的间隔,与半径aspect的弧度的间隔,这两者取较小值
        // 3. 获取到第二步的数值后,以当时接触点跟左上角的直线,与半径aspect的弧线的交点为基准,依据获取到的值进行一定的偏移,偏移按弧线的方向进行,弧线长度等于偏移值?
        vec2 startPoint = vec2(0.0, cornerFrom.y==0.0?0.0:1.0);
        vec2 vector = normalize(vec2(0.5, 0.5*tan(pi/3)));
        vec2 targetMouse = mouse.xy;
        vec2 v = targetMouse - startPoint;
        float proj_length = dot(v, vector);
        vec2 targetMouse_proj = startPoint + proj_length*vector;
        /// 间隔基准直线的间隔
        float base_line_distance = length(targetMouse_proj - targetMouse);
        /// 当时接触点间隔弧线间隔
        float arc_distance = distance(targetMouse, startPoint) - aspect;
        // 取小值
        float actual_distance = min(abs(base_line_distance), abs(arc_distance));
        // 当时接触点对应在弧线上的映射点
        vec2 currentMouse_arc_proj = startPoint+ normalize(mouse - startPoint)*aspect;
        vec2 newPoint_arc_proj = pointOnCircle(startPoint, currentMouse_arc_proj, aspect, actual_distance/2, mouse.y<=tan(pi/3)*mouse.x);
        // 依据最新核算成果,批改鼠标参数
        mouse = newPoint_arc_proj;
        currentMouse.xy = mouse * resolution.xy / vec2(aspect, 1.0);
    }
    // 鼠标方向的向量
    vec2 mouseDir = normalize(abs(cornerFrom* resolution.xy / vec2(aspect, 1.0)) - currentMouse.xy);
    // 翻页辅助核算点起点
    vec2 origin = clamp(mouse - mouseDir * mouse.x / mouseDir.x, 0.0, 1.0);
    // 鼠标辅助核算间隔
    float mouseDist = distance(mouse, origin);
    if (mouseDir.x < 0.0) {
        mouseDist = distance(mouse, origin);
    }
    float proj = dot(uv - origin, mouseDir);
    float dist = proj - (mouse.x<0?-mouseDist:mouseDist);
    vec2 curlAxisLinePoint = uv - dist * mouseDir;
    // 让翻页页脚能跟随接触点
    float actualDist = distance(mouse, cornerFrom);
    if (actualDist>=pi*radius) {
        float params = (actualDist-pi*radius)/2;
        curlAxisLinePoint += params * mouseDir;
        dist -=params;
    }
    if (dist > radius) {
        fragColor = vec4(0.0, 0.0, 0.0, (1.0 - pow(clamp((dist - radius)*pi, 0.0, 1.0), 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 =mix(fragColor.rgb, vec3(1.0), 0.25);
            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));
            if (p2.x <= aspect+shadowWidth && p2.y <= 1.0+shadowWidth&& p2.x > 0.0-shadowWidth && p2.y > 0.0-shadowWidth){
                float shadow = calShadow(p2, aspect);
                fragColor = vec4(fragColor.r*shadow, fragColor.g*shadow, fragColor.b*shadow, fragColor.a);
            }
        }
    } 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));
            fragColor.rgb =mix(fragColor.rgb, vec3(1.0), 0.25);
        } else {
            fragColor = texture(image, uv * vec2(1.0 / aspect, 1.0));
            if (p.x <= aspect+shadowWidth && p.y <= 1.0+shadowWidth&& p.x > 0.0-shadowWidth && p.y > 0.0-shadowWidth){
                float shadow = calShadow(p, aspect);
                fragColor = vec4(fragColor.r*shadow, fragColor.g*shadow, fragColor.b*shadow, fragColor.a);
            }
        }
    }
    if (distance(uv, vec2(0.0)) >(aspect-0.001)&&distance(uv, vec2(0.0))<(aspect+0.001)){
        fragColor = TRANSPARENT;
    }
}

小结

现在算是解决了翻页动画的几个大问题,接下来便是正式应用部分。不过在此之前,zoharSoul同学提出了一个很好的建议,ios上的图书App的翻页作用看上去比较人性化,或许在下一步应用之前,测验完成一下这个ios图书的翻页动画?