书接上文,距离达到翻页动画本身的可用,还需要增加对翻页范围的限制,例如下图这种情况:
那么这次就来解决这个问题,首先还是奉上这次实现后的效果:
方案设计
在通过canvas来实现的方案中,实现一个对范围的处理并不是一件很困难的事,canvas生成内容基本是跟path或多或少挂钩的,因此我们可以直接修改Path的范围来处理范围限制问题。
而在Fragment Shader中,并没有线条和路径的概念,片段着色器仅仅会根据程序代码,将像素点对应的纹理信息生成出来,其操作的对象不是path,而是一个个的像素点,对于某个点来说,它不会像path一样能得知其他点的位置,更无从获知其他点是否在展示范围外。因此,对于首先要做的事,就是寻找一种全新的范围判断和修正方案。
吐槽完,回到主线任务中来,中间的心酸泪就不提了,直接说目前的方案:
假如按照翻页角范围最大来设想,那么翻页部分的页脚,其轨迹等同于下图的绿色轨迹:
其实就是围绕左上角,以书页横向宽度为半径的一个圆而已;
如果在这个绿圈范围内,那么翻页范围怎么样都不会在范围外,在绿圈范围外,则就会导致翻页范围的溢出,需要处理。这样,如何判断是否需要调整位置的判断依据就有了,根据当前触摸点是否在绿圈范围内即可。
有了判断依据,第二步就是如何修正,这点我是制定了这样的方案:
- 设定翻页角对应x=0的点偏移60度的直线为基准线,如果触摸点位置位于这个基准线上,那么将触摸点改为半径aspect的那个弧线跟60度直线的交点,这个倒直接这么处理即可;
- 如果不在上述60度的直线上,那么取当前触摸点,跟基准线的距离,与半径aspect的弧度的距离,这两者取较小值
- 获取到这个数值后,以触摸点对应在弧线上的映射点为基准,以获取到的数值为弧长进行弧度偏移,方向取触摸点位于基准线的上下来决定。
嗯,我知道这玩意一般是看不懂的......即使是我,年前写下的注释,当年后在写这篇文章的时,回顾这个注释也愣是没太看懂
为了防止发生以上图片的情况,这次就对这些步骤进行拆解和详细说明
首先主要还是对图片中的一些关键点进行标注,以上图的为例:
那么根据这些标注点,进行步骤拆解:
- 计算翻页对应点
glsl
vec2 startPoint = vec2(0.0, cornerFrom.y==0.0?0.0:1.0);
- 计算当前触摸点在基准线上的投影点位置
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; ///起点+向量*长度就是具体位置点
- 根据投影点算出触摸点到基准线的距离
glsl
float base_line_distance = length(targetMouse_proj - targetMouse);
- 当前触摸点到弧线的距离(也就是当前触摸点到翻页对应点-弧线半径),并取两者最小值做为结果
glsl
float arc_distance = distance(targetMouse, startPoint) - aspect;
float actual_distance = min(abs(base_line_distance), abs(arc_distance));
- 计算当前触摸点在弧线上的投影点
glsl
vec2 currentMouse_arc_proj = startPoint+ normalize(mouse - startPoint)*aspect;
- 从这个投影点的位置,根据当前触摸点相对于基准线的上下来决定方向,偏移第4步结果的弧线长度。
glsl
vec2 newPoint_arc_proj = pointOnCircle(startPoint, currentMouse_arc_proj, aspect, actual_distance/2, mouse.y<=tan(pi/3)*mouse.x);
- 根据第六步修正后的位置,修改最终触摸点位置
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图书的翻页动画?