【Flutter&GLSL】用Fragment Shader来实现高性能的动画效果——如何修正翻页范围

书接上文,距离达到翻页动画本身的可用,还需要增加对翻页范围的限制,例如下图这种情况:

那么这次就来解决这个问题,首先还是奉上这次实现后的效果:

方案设计

在通过canvas来实现的方案中,实现一个对范围的处理并不是一件很困难的事,canvas生成内容基本是跟path或多或少挂钩的,因此我们可以直接修改Path的范围来处理范围限制问题。

而在Fragment Shader中,并没有线条和路径的概念,片段着色器仅仅会根据程序代码,将像素点对应的纹理信息生成出来,其操作的对象不是path,而是一个个的像素点,对于某个点来说,它不会像path一样能得知其他点的位置,更无从获知其他点是否在展示范围外。因此,对于首先要做的事,就是寻找一种全新的范围判断和修正方案。


吐槽完,回到主线任务中来,中间的心酸泪就不提了,直接说目前的方案:

假如按照翻页角范围最大来设想,那么翻页部分的页脚,其轨迹等同于下图的绿色轨迹:

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

如果在这个绿圈范围内,那么翻页范围怎么样都不会在范围外,在绿圈范围外,则就会导致翻页范围的溢出,需要处理。这样,如何判断是否需要调整位置的判断依据就有了,根据当前触摸点是否在绿圈范围内即可。

有了判断依据,第二步就是如何修正,这点我是制定了这样的方案:

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

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

为了防止发生以上图片的情况,这次就对这些步骤进行拆解和详细说明

首先主要还是对图片中的一些关键点进行标注,以上图的为例:

那么根据这些标注点,进行步骤拆解:

  1. 计算翻页对应点
glsl 复制代码
        vec2 startPoint = vec2(0.0, cornerFrom.y==0.0?0.0:1.0);
  1. 计算当前触摸点在基准线上的投影点位置
glsl 复制代码
        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. 根据投影点算出触摸点到基准线的距离
glsl 复制代码
        float base_line_distance = length(targetMouse_proj - targetMouse);
  1. 当前触摸点到弧线的距离(也就是当前触摸点到翻页对应点-弧线半径),并取两者最小值做为结果
glsl 复制代码
        float arc_distance = distance(targetMouse, startPoint) - aspect;
        float actual_distance = min(abs(base_line_distance), abs(arc_distance));
  1. 计算当前触摸点在弧线上的投影点
glsl 复制代码
        vec2 currentMouse_arc_proj = startPoint+ normalize(mouse - startPoint)*aspect;
  1. 从这个投影点的位置,根据当前触摸点相对于基准线的上下来决定方向,偏移第4步结果的弧线长度。
glsl 复制代码
        vec2 newPoint_arc_proj = pointOnCircle(startPoint, currentMouse_arc_proj, aspect, actual_distance/2, mouse.y<=tan(pi/3)*mouse.x);
  1. 根据第六步修正后的位置,修改最终触摸点位置
glsl 复制代码
        mouse = newPoint_arc_proj;
        currentMouse.xy = mouse * resolution.xy / vec2(aspect, 1.0);

之后的流程就按照原来的步骤来就是了,对于后续步骤来说,他们计算的触摸点已经是修正好的了。

shader文件代码

glsl 复制代码
#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图书的翻页动画?

相关推荐
qiyi.sky5 分钟前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~9 分钟前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常18 分钟前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n01 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。1 小时前
案例-任务清单
前端·javascript·css
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称3 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js