lottie动画之活灵活现《三》

320 阅读12分钟

前情摘要

         书接上回, 在之前的文章中已经对lottie动画进行了两篇文章的相关介绍,分别是 《lottie动画深入浅出一》,介绍lottie的基本使用和相关用法,重在“会用”;然后发布了第二部篇《lottie初步认识与动态修改 二》,讲述了lottie动画的基本原理、json结构等,同时如何在真实动画场景上如何动态修改json有了一定的升华,重在“好用”;今天一切都看似那么的完美方案下又充斥着那么多的不完美,身处快速发展的金融互联网行业快速发展的业务需求,不断探索创新的交互形式,追求极致用户体验的背景下,看似完美的方案中透漏着些许遗憾:目前的方案并不能将人的情感动作与lottie动画很好的结合在一起,这种呆板的、机械重复性的lottie动画是没有生命的,就如同当下许多不能处理上下文流畅理解自然语言的智能(智障)音箱一样。那么如何赋予lottie生命(可交互性)的意义就是我们今天一起探索的话题。在进入今天话题之前,先和大家分享一个富有生命(可交互性)的lottie动画应该是什么样的? 请大家观看《变色龙吃虫记》

        在上面的《变色龙吃虫记》中,我们可以将上面的lottie动画交互进行拆分来分析:第一步盯上它(虫子)、第二步伪装它(周边环境)、第三步吃掉它(虫子)。接下来让我们一起看看变色龙是如何一步步吃掉虫子的?

交互过程

盯上它(虫子)

坐标转换

既然要盯上它(虫子)就要知道虫子在哪里,这与现实中的位置类似,比如我们经常购买东西填写邮寄地址一样:北京市朝阳区xxx路xxx小区xxx室,这种地址只是更具有现实意义辅助人们记忆对应更深层次中的位置信息其实就是对应着地球的经纬度信息,类比到虚拟世界的二维平面中同样适用与之对应的就是坐标x,y。我们一般的将可视区窗口的**左上角为原点,左至右的水平方向为 ×轴,上至下的垂直方向为y轴的直角坐标,**这样鼠标在屏幕中的任意位置都可以用(x,y)坐标来有效的标识,如图所示:

         但是如果图像的每次绘制都参考一个固定点将缺少灵活性,于是在lottie中引入“当前坐标系”的概念,所谓“当前坐标系”即指图像在此时绘制的时候所参考的坐标系,它也会作为图像状态的一部分,选择的参考点不一样,鼠标所在位置就要依据参考点的位置进行坐标转换,选择的参考点不一样,鼠标位置的x、y坐标也会不一样,将视图平面坐标转换为lottie动画系统坐标就需要用到 animationAPI.toContainerPoint 。

在Lottie的API中,toContainerPoint 方法通常用于将世界空间(或屏幕)空间中的点转换为容器(或动画空间)空间中的点。这在处理交互、点击事件或定位元素时非常有用,因为动画中的坐标系统可能与实际的容器或屏幕坐标系统不同。

具体来说,toContainerPoint 方法接受一个数组作为参数,这个数组通常包含三个元素:[x, y, z]。在这里,x 和 y 是二维平面上的坐标,而 z 通常用于表示深度或层级。在某些情况下,z 可能被用作一个额外的参数来指定某种特定的转换或行为。

代码片段中:

// 值数组,存储鼠标 x y 值
var valueArr = [100, 500, 100];
// 初始化lottie动画
var animData = {
    container: animationContainer,
    renderer: "svg",
    loop: true,
    autoplay: true,
    rendererSettings: {
        preserveAspectRatio: "xMidYMid meet"
    },
    path: "https://labs.nearpod.com/bodymovin/demo/chameleon/chameleon2.json"
};

//创建实例
anim = lottie.loadAnimation(animData);
//dom加载事件
anim.addEventListener("DOMLoaded", function () {
 
    //createAnimation API 来创建和控制这些动画。
    animationAPI = lottie_api.createAnimationApi(anim);

    //监听鼠标移动
    window.addEventListener("mousemove", updateValue);
    // 监听手指移动touchmove
    window.addEventListener("touchmove", updateValue);
    
});
/**
 * 监听鼠标移动
 * @param {*} ev 
 */
function updateValue(ev) {
    //鼠标是否移动的标识
    mouse_changed = true;
    var mouseX, mouseY;
    // 兼容方式获取鼠标移动 x y 坐标
    if (ev.touches && ev.touches.length) {
        var mouseX = ev.touches[0].pageX;
        var mouseY = ev.touches[0].pageY;
    } else if (ev.pageX !== undefined) {
        mouseX = ev.pageX;
        mouseY = ev.pageY;
    }
    valueArr[0] = mouseX;
    valueArr[1] = mouseY;
}

// 坐标转换
var point2 = animationAPI.toContainerPoint(valueArr);

//[x, y, 400] 是一个坐标数组,其中 x 和 y 是动画空间中的坐标,而 400 可能是一个额外的参数,其含义取决于 toContainerPoint 方法的具体实现。这个实现可能是Lottie API的一部分,也可能是某个特定封装或插件提供的。
//toContainerPoint 方法将返回一个数组,该数组包含了转换后的坐标,这些坐标是相对于容器(如HTML元素)的。这样,你就可以在屏幕上定位这个点了。

角度换算

获             得了虫子(鼠标)的位置,怎么实现眼睛盯着虫子不断变换转动的效果呢?我们通过图示观察下虫子(鼠标)在不同位置眼睛的变化是什么样子。发现虫子(鼠标)在移动的过程中变色龙的眼睛也是在不停的旋转变化的,那么只有坐标的位置如何计算变色龙应该另眼相看(角度)虫子(鼠标)呢?如果您对数学中的三角函数与图表坐标系比较擅长有可能会想到一种方式Math.atan2Math.atan2 方法返回一个 -pi 到 pi 之间的数值,表示点 (x, y) 对应的偏移角度。这是一个逆时针角度,以弧度为单位,正X轴和点 (x, y) 与原点连线之间。 那么如果我们把变色龙的眼睛看做原点,把虫子(鼠标)的位置看着目标点是不是就是求虫子(鼠标)相对于原点(变色龙眼睛)的对应偏移角度,同样我们用图进行说明:

         在实际的项目中眼睛是由多个图层叠加在一起的,为了便于理解我们把眼睛当做了一个整体。到这里就需要用到另一个api了**animationAPI.toKeypathLayerPoint,**在 Lottie 中,animationAPI.toKeypathLayerPoint 方法用于将指定关键路径(keypath)中的点坐标从动画中的本地坐标系转换到关键路径所在图层的本地坐标系。具体来说,它的作用是将指定关键路径中的点坐标从动画的坐标系转换到关键路径所在图层的坐标系,使得你可以在特定图层的坐标系中获取该点的位置。

// left_eye  为眼睛关键图层路径
var  eyeKeyPath = animationAPI.getKeyPath('left_eye')
// 转换后的鼠标位置
var point2 = animationAPI.toContainerPoint(valueArr);

//转换为以眼睛图层为参照点的坐标系
var trasformedPoint = animationAPI.toKeypathLayerPoint(
      eyeKeyPath,
      point2
  );

// 计算旋转角度
const eye_angle = Math.atan2(0 - trasformedPoint[1], 0 - trasformedPoint[0]) / Math.PI / 180

//设置每一帧眼睛的旋转角度
 animationAPI.addValueCallback(eyeKeyPath, function(value){
     return eye_angle
 })

        到这里我们就已经实现盯上它(虫子)相关分析,接着我们往下看是如何伪装它(周边环境)的。

伪装它(周边环境)

       变色龙一个明显的特点就是善于结合周边环境进行伪装,在《变色龙吃虫记》中有四片树叶,并且每一片树叶具有不同的颜色,当虫子(鼠标)飞到不同的颜色叶子上时,盯着它的变色龙也会相应变换成该片树叶的颜色,颜色伪装它是怎么做到的呢?接下来让我们卸下变色龙的伪装,看看它的本来面目。

        首先我们先查看下四片树叶的dom结构如图所示。我们发现图中每一个片叶子的结构都有一个id标识,分别是leaf_1,leaf_2,leaf_3,leaf_4,如果你之前有看过《lottie动画初步认识与动态修改》第一篇文章应该就会大概理解这个id的来源和意义,lottie渲染的dom结构是可以有id、 calss等属性信息的,而且可以通过这个关键标识调用animationAPI.getKeyPath用来获取特定属性的关键帧路径(key path)的。在Lottie的动画系统中,关键帧路径是一个用于唯一标识动画中某个属性的字串。同时我们看下JSON文件是否可以找到相关的内容。

     代码示例

var leaveElement = document.getElementById('#leaf_4');
// 监听鼠标 mouseover 事件
var colorSelector = document.querySelector(":root");
leaveElement.addEventListener("mouseover", function () {
    changeColor(leave_name);
});


function changeColor(leave_name) {
    var leafColorKey = animationAPI.getKeyPath(
        "#leaf_4',Contents,color_group,fill_prop,Color"
    );
    // 获取到指定关键路径 color属性,一般color获取到的是数组
    var colorValue = leafColorKey.getPropertyAtIndex(0).getValue();
    var colorString = "rgba(";
    colorString += Math.round(colorValue[0]);
    colorString += ",";
    colorString += Math.round(colorValue[1]);
    colorString += ",";
    colorString += Math.round(colorValue[2]);
    colorString += ",1)";


    colorSelector.style.setProperty("--chame-color", colorString);
    colorSelector.style.setProperty("--chame-color-eyes", colorString);
    if (camouflage_timeout) {
        clearTimeout(camouflage_timeout);
    }
    camouflage_timeout = setTimeout(function () {
        colorSelector.style.setProperty("--chame-color", "rgba(240,237,231,1)");
        colorSelector.style.setProperty("--chame-color-eyes", "rgba(228,225,218,1)");
        camouflage_timeout = null;
    }, 15000);
}

通过document.querySelector(":root")设置css变量内容。

// 变色龙样式颜色设置
.chameleon_color {
    stroke: var(--chame-color);
    fill: var(--chame-color);
    transition: stroke 2s, fill 2s;
}

         到此就实现了第二步伪装它(周边环境)。其中需要注意的是在Lottie JSON文件中,颜色信息通常出现在表示图层样式的字段中。常见的字段是 "c"(color)"c" 字段表示颜色,通常以RGBA格式(红绿蓝透明度)存储。例如:

"c": {"a":0,"k":[0.987999949736,0.838999968884,0.33300000359,1]}

         这表示#leaf_4叶子的填充色,渲染到页面是 rgb(251, 213, 84) 转换为 RGBA值为 [251/255, 213/255, 84/255, 1],如JSON文件结果所示的小数。

吃掉它(虫子)

碰撞检测

     在分析完盯着它(虫子)、伪装它(周边环境),接下来就差最后一步如何吃掉它(虫子),变色龙能不能吃掉虫子的条件是什么呢?那就是能不能够得着、吃的到,简单理解就是嘴巴的位置与虫子的位置的直线距离要在舌头的长度范围内,但是基于动画的流畅度和实现成本,我们给变色龙吃虫子规定了一个范围,比如变色龙能够吃虫子的最远距离是405 ~ 415范围内,为了便于理解同样我们用图展示这个范围,如图:

        在圆环范围内的虫子(鼠标)是可以被变色龙吃掉的,这个与我们比较熟知的碰撞检测是类似的,当虫子(鼠标)飞入到圆环的范围内就触发了碰撞,边界条件可以转换为以嘴巴为原点虫子的(x,y)坐标距离的计算,直接可以使用勾股定理很容易计算出来,同样适用了我们前两端提到的api能力**animationAPI.toContainerPoint 与 animationAPI.toKeypathLayerPoint。**示例代码如下:

// 通过嘴巴 图层 名称获取特定关键路径
var keyPathMouthInner = animationAPI.getKeyPath("Mouth,ReferencePoint");
// 通过容器图层关键路径
var keyPathMouthContainerTimeRemap = animationAPI.getKeyPath(
    "Mouth,Time Remap"
);
var distanceToMouse = 0

animationAPI.addValueCallback(keyPathMouthContainerTimeRemap, function (currentValue) {
  //toContainerPoint 是一个在 Lottie 动画库中使用的方法,它用于将动画中的一个点转换为容器元素中的点。这通常用于定位或者调整 Lottie 动画的位置
  var point2 = animationAPI.toContainerPoint(valueArr);
  // 这行代码调用了toKeypathLayerPoint方法,该方法可能是将容器空间中的点point2转换为特定图层或关键帧路径上的点。keyPathMouthInner是之前通过getKeyPath方法获取的关键帧路径,它指向了名为"MouthInner"的图层或属性。
  point2 = animationAPI.toKeypathLayerPoint(keyPathMouthInner, point2);

  //这行代码计算了从原点(0, 0)到转换后的点point2之间的欧几里得距离。它首先计算了point2的x和y坐标与原点之间的距离的平方,然后将这两个平方值相加,最后取平方根得到距离。这个距离代表了point2在二维空间中的位置离原点的远近。
  distanceToMouse = Math.sqrt(
      Math.pow(0 - point2[0], 2) + Math.pow(0 - point2[1], 2)
  );
  return ...
});

// 关键路径舌头容器时间线控制器
var keyPathTongueContainerTimeRemap = animationAPI.getKeyPath(
    "Mouth,Tongue_Comp,Time Remap"
);

animationAPI.addValueCallback(keyPathTongueContainerTimeRemap, function ( currentValue) {
    // 如果鼠标在舌头范围内,且 isActive 未激活状态 则开始 tongue 动画
    if (
        distanceToMouse > minTongueRadius &&
        distanceToMouse < maxTongueRadius &&
        !isActive
    ) {
       //触发吃虫子动画
        animateTongue();
    }
    if (isActive) {
        tongueCurrentTime = 2 * (Date.now() - tongueInitialAnimationTime) / 1000;
    }
    // 舌头动画结束,重置
    if (tongueCurrentTime > 2) {
        tongueCurrentTime = 0;
        resetTongue();
    }
    return tongueCurrentTime;
});

消失的虫子(鼠标)

        当变色龙的舌头触发到鼠标的瞬间果然虫子(鼠标)消失了,这个其实原理很简单只是使用与背景颜色完全一样的图片替换成了鼠标的样式,在指定的时间内过渡渐变,完成吃掉的过程后重置了鼠标。

function animateTongue() {
    tongueInitialAnimationTime = Date.now() - 1500 / 30;
    isActive = true;
    // 给容器添加和背景一样的 鼠标样式,错觉产生鼠标被吃掉
    mouse_container.setAttribute("class", "active");
}

function resetTongue() {
    // 重置样式,恢复
    isActive = false;
    mouse_container.setAttribute("class", "");
}

对应的css样式如下:

#mouse-container.active {
    cursor: url(https://labs.nearpod.com/bodymovin/demo/chameleon/cursor_empty_32.png), url(https://labs.nearpod.com/bodymovin/demo/chameleon/cursor_empty_32.cur), auto;
}
#cursor_preloader {
    cursor: url(https://labs.nearpod.com/bodymovin/demo/chameleon/cursor_empty_32.png), url(https://labs.nearpod.com/bodymovin/demo/chameleon/cursor_empty_32.cur), auto;
    position: absolute;
}

小结

至此,我们一步步分析了Lottie可交互动画《变色龙吃虫记》的整个过程。这个案例展示了Lottie动画实现交互的典型方式。尽管Lottie动画可以实现较为复杂的交互,但并不意味着它适合所有复杂的交互场景。我个人认为,主要原因如下:

  1. 对UI和开发同学提出了较高的要求。UI在设计时需要提前规划图层的唯一标识和动画变换属性,并提供给开发同学。同时,开发同学需要深入研究和掌握Lottie JSON结构层级,这增加了设计与开发的人力成本2

  2. Lottie复杂动画需要较多的逻辑梳理和动画逻辑,需要具备较高的数学基础和坐标转换等基础能力,这构成了一定的门槛要求。

  3. 复杂交互的Lottie动画难以沉淀和抽离通用的公共能力,每种交互方式和Lottie JSON都需要从零开始编写,这增加了投入成本。

虽然Lottie动画在复杂交互上有一定的门槛要求,但这并不能掩盖它可以实现高度可定制动画效果的优势。随着技术的不断发展和社区成员的贡献,也许不久的将来会有更高效的解决方案出现。敬请期待下回解说。

参考链接

segmentfault.com/a/119000001…

blog.csdn.net/weixin_4268…

baijiahao.baidu.com/s?id=169423…

airbnb.io/lottie/#/su…