• 移动UI系列


    前言

    有一个问题, 给定一个物体的运动轨迹, 包含时间和坐标的数组, 如何使用这个数据来预测物体未来的运动走势??

    本文提供了一个很简单的方式去实现这个算法. 效果够用, 又简单, 有一定的准确程度. 

    缘由

    以前做过的一些手机应用, 没有做动画的 , 也没做手势. 这个做起来挺麻烦的. 

    最近开始了新的手机项目 (微信项目模板)  ,  基于 Blazor server side , 其组件化的方式可以很方便地把各种常用的通用的东西封装成 组件/控件 

    于是, 就打算让这段时间辛苦一些, 一次过把动画的部分补上,  让以后的项目更加牛逼, 意思是让客户更加乐意地掏钱. 

    问题

    手势的一个问题是力度/速度判断, 划得快, 划得慢, 先快后慢, 先慢后快, 都得有一个合适的算法来得到一个最后速度. 

    这个算法一定要满足基本的需求后, 要尽量简单,  要目测, 理论上没有bug . 

    这个时候, 半衰期算法就派上用场了.   (不知道有没有半衰期算法这玩意,  应该说是使用半衰期的原理)

    原理

    这是一个很简单的权重计算,  有很多个不同时间点的坐标, 每个相邻时间的两个数据, 有距离差, 有时间差

    关键是解决先慢后快, 先快后慢的数据的处理问题,  那么我们只需要

    把新的数据, 乘以更大的权重,  把老的数据, 乘以更小的权重 , 最后得到这个距离权重的合成值, 就OK了. 

    预览效果:

    注意, 因为gif的帧数不够, 要更好效果还是复制代码运行一遍. 

    测试代码:

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <title>Half Life</title>
        <style>
            html, body, canvas {width: 100%;height: 100%;margin: 0;padding: 0;box-sizing: border-box;overflow: hidden;}
        </style>
    </head>
    <body>
    
        <canvas></canvas>
    
        <script type="text/javascript">
    
            var CONST_HALF_LIFE_SPAN = 50;    //半衰期 , 设置得越小 , 就越看淡以前的速度
            var CONST_MAX_SAMPLES = 15;        //保留最后15个点的数据 减少无意义的运算量, 主要看浏览器触发 onmousemove 的频率来调整.
    
            var pts = [];
            document.onmousemove = function (e) {
    
                //储存数据
                pts.push({ time: Date.now(), x: e.clientX, y: e.clientY });
                if (pts.length > CONST_MAX_SAMPLES) pts.shift();
    
                //计算
    
                var xs = 0, ys = 0;    //走过的路的长度.
                var previtem = pts[0];
                for (var index = 1; index < pts.length; index++) {
                    var newitem = pts[index];
                    var t = newitem.time - previtem.time;
    
                    //让这个数据衰减一次, 衰减程度由时间差决定.
                    var halflifefactor = Math.pow(0.5, t / CONST_HALF_LIFE_SPAN);
    
                    //注意, 这里没有计算速度, 而是直接用距离来预测将来要走过的距离.
    
                    // 走过的距离每一次都要衰减, 每一段的路程都多次乘以各时间差的factor,
                    // 原理是, 0.5 ^ (t1 + t2 + t3...) 等于 0.5 ^ t1 * 0.5 ^ t2 * 0.5 ^ t3 * ...
                    xs = xs * halflifefactor + newitem.x - previtem.x;
                    ys = ys * halflifefactor + newitem.y - previtem.y;
    
                    previtem = newitem;
                }
    
                //画图
    
                var CONST_EFFECT_FACTOR = 2;    //乘以一个因素来画图用, 这里数值可以充当'摩擦系数'大小的效果.
                xs = Math.floor(xs * CONST_EFFECT_FACTOR);
                ys = Math.floor(ys * CONST_EFFECT_FACTOR);
    
                var x0 = e.clientX, y0 = e.clientY;
    
                var canvas = document.querySelector("canvas");
                var ctx = canvas.getContext("2d");
                var w = canvas.width = canvas.offsetWidth;
                var h = canvas.height = canvas.offsetHeight;
                ctx.clearRect(0, 0, w, h);
                ctx.lineWidth = 5;
                ctx.strokeStyle = "blue";
                ctx.beginPath();
                ctx.moveTo(x0, y0);
                ctx.lineTo(x0 + xs, y0 + ys);
                ctx.closePath();
                ctx.stroke();
    
                console.log(xs, ys)
            }
        </script>
    
    </body>
    
    </html>

    简单讲解

    可以看出 , 代码量非常少.  与其说这是一种"算法" , 不然说是一种"思路" 

    代码先是记录每一点的数据,  然后放进  pts  数组 

    在鼠标移动后, 使用 pts 数组, 计算出每一点的 x , y 的移动量,  让每一段的移动量都使用 半衰期 的方式进行调整. 

    最后得出的 xs , ys  ,  是理论上 "最近移动的距离."

    使用这个数值, 作为 "参考值" ,  就可以用于更多的判断. 

    例如我最近做的一个简单的  swipe 组件 , 

    手指滑动了 1/4 个屏幕,  然后再加上理论的参考值,  一共超过了 1/2 个屏幕,  就切换到下一个panel 

    这个参考值很重要.  如果手指只是很慢地滑动, 那么参考值就很小, 就不应该切换.  如果手指很快地滑动, 那么就应该切换panel

    可改良的方案

    上面方案, 是计算 '移动距离' 的,  它有一个弊端, 无论划得多快, 移动的总数是有上限的. 

    这段代码完全可以 除以 t , 得到一个 速度值,

    这更加合理,  但是注意速度的合成方式不能普通地累加. 

    这就留给有兴趣的网友自己尝试了. 也不难.  毕竟本文传播的是 半衰期 的思路. 不宜说太细. 

    扩展思路

    其实这种算法 , 一直都有人用.  很奇怪的就是, 没有看到什么人专门写文章介绍? 

    半衰期除了计算运动轨迹, 还可以很好地去统计热度. 

    例如一篇文章 , 一个视频, 有不同时段的点击数量.  每一天都不一样. 

    怎样使用最少的储存方式, 去储存一个合理的热度参考值? 

    可行的方法是 , 储存两个数值 (就够了) : 

    articleRateValue  用于储存热度值

    articleRateTime 用于储存热度时间

    每一次点击, 都使用这个公式储存 : 

    articleRateValue = newclickcount + articleRateValue * POW(0.5, (NOW - articleRateTime) / HALF_LIFE_SPAN )

    articleRateTime = NOW 

    排序的时候麻烦点, 要实时的计算  articleRateValue * POW(0.5, (NOW - articleRateTime) / HALF_LIFE_SPAN ) 来得到每个文章的热度值. 

    (对于排序的问题, 还有两个变通的做法, 如果以后有时间写一篇文章细说这个热度方案, 再详细解说)

    (最简单的变通方法是, 每晚找服务器空闲的时候, 把表里的数值都重新计算一次, 平时排序的时候不计算) 

    注意这里有一个  HALF_LIFE_SPAN  的概念.  如果 HALF_LIFE_SPAN  是一天,  那么昨天的热度就占 1/2 权重,  前天的就占  1/4 权重 , 大前天的占 1/8 

    这样按天算也有个弊端 , 我想按周, 按月算那怎么办 ? 

    再存两组数值 :

    weeklyRateValue

    weeklyRateTime

    monthlyRateValue

    monthlyRateTime 

    如此类推. 

    这样做的最大好处是 ,  无需详细地纪录每一次的点击数据.  非常节省空间.  

    缺点是 , 每一组数据, 只适合一个半衰期时段的数值,  要储存多个参考值得准备多组数据. 

    最后

    时间过得真快. 这段时间挖的坑太多, 在业余时间内根本没有时间填坑..

    5月初的时候实现了BlazorCefApp , 到现在开了几个有意义的坑,  Blazor微信项目模板 算是一个. 

    但是由于没有时间写博客, 有时只是偶尔把测试的视频放到 B站 :  https://space.bilibili.com/540073960 

    有兴趣用 Blazor 来做微信项目或手机网页项目的,  可以去了解一下.  当项目成熟后, 会发布到github上. 

    ----

  • 相关阅读:
    LeetCode 297. Serialize and Deserialize Binary Tree 二叉树的序列化与反序列化(C++/Java)
    LeetCode 381. Insert Delete GetRandom O(1)
    LeetCode 380. Insert Delete GetRandom O(1) 常数时间插入、删除和获取随机元素(C++/Java)
    LeetCode 673. Number of Longest Increasing Subsequence 最长递增子序列的个数 (C++/Java)
    LeetCode 675. Cut Off Trees for Golf Event 为高尔夫比赛砍树 (C++/Java)
    LeetCode 460. LFU Cache LFU缓存 (C++/Java)
    LeetCode 451. Sort Characters By Frequency 根据字符出现频率排序 (C++/Java)
    LeetCode 332. Reconstruct Itinerary重新安排行程 (C++/Java)
    LeetCode 295. Find Median from Data Stream数据流的中位数 (C++/Java)
    Codeforces Round #318 (Div. 2) A Bear and Elections (优先队列模拟,水题)
  • 原文地址:https://www.cnblogs.com/zhgangxuan/p/half_life_algor_01.html
Copyright © 2020-2023  润新知