小折腾:JavaScript与元素间的抛物线轨迹运动

这篇文章发布于 2013年12月30日,星期一,20:40,归类于 JS实例。 阅读 131923 次, 今日 35 次 46 条评论

 

一、一剂预防针

好的体验应该是在用户试用之前就告知产品的一些特点、局限性等。类似裤子都脱了,才来一句:“我今天大姨妈巅峰期”的场景显然是会让兴致勃勃的用户受挫的。

SO, 有必要在这里打个预防针,本文实际上没什么技术养料,就是昨晚因实际工作需要折腾的个东西,抛物线轨迹动画。如果你急着跟女神搞昧,下面的可以直接pass~~
没养料 鑫表情

不过这里有个有养料的东西,要跟大家分享下,当当当当,就是下面这个:
公司福利-猕猴桃

正宗曲江猕猴桃。公司福利,内部价,37元一箱,对!你没看错!就是37元!货真价实37元!足足有27个猕猴桃!各个多汁甜美,诱人无比!我周围同事几乎人人搞了一箱!是不是超划算哈!是不是很心动啊!是不是很羡慕哈!我厂类似有养料的福利还有很多哦!心动不行动,把妹永被动。偷偷告诉你哦,我们这边还差一个前端,我们ISUX上海可是我厂非常优秀的团队哦,还不简历速速飞来 – zhangxinxu@zhangxinxu.com . 不要担心,你简历再low我也会耐心回复你的,绝对不会石沉大海,重复过去你的不开心经历的哈!

二、动画与视觉引导

在页面上添加元素的位移动画,除了视觉效果(这是次要的),还有个作用就是视觉引导(重点)。举个大家可能见过的例子,选择商品的时候,我们希望商品飞到边缘或角落的购物车里,作用是:一来告知放在购物车里成功了,二来让用户知道购物车在哪里。

但是,直来直去的运动你用我也用,不出彩啊,于是,就有想法,我抛物线过去,会不会有别样的风采。

于是,下班后就抽时间折腾哈~~

于是就有了今天这篇没什么养料的文章。

哈,稍等,我先回家,夫人催了,回去吃个猕猴桃补充下养料,继续哈……

三、数学函数与动画轨迹

//zxx: 翌日……

常有云:“书到用时方恨少”。其实我觉得这有一部分责任在教者身上。好比高中时候,拼死拼活学各种函数曲线。我们为什么而学?很明显,为了考试!短视而功利的教育环境让老师一族的眼界也放在了考试成绩上。这也是为何中国学习应试考试一级棒,实践与创新吊车尾的原因。

实际上,学习这些函数不是为了考试,而是为了日后的工作、解决生活实际问题。知道这些可以做什么,才能培养真正的兴趣,才能真正出类拔萃,学以致用。所以,您不必懊悔当初没好好学习什么的,是大环境限制了你当时的学习。

就拿计算机图形动画举例,实际上,任何有规律的运动都是数学函数的呈现。

试想下,如果数学老师可以把函数,与实际应用结合,比方说告诉喜欢玩游戏的同学:《剑灵》《英雄联盟》中那些绚烂的效果都是这里的抛物线啊重力啊等一干函数实现的。要是你们对游戏感兴趣,从事游戏相关的工作,这些函数是要牢牢掌握的哦!这种教育要比“你不好好学习,考个好成绩,找个好工作”有用多了。如果再能手把手辅导出什么不一样的成果——小装置,或者小游戏之类。我擦,那还不上天了!这个娃未来不是精英也是奇葩!

哈,讲正经的。

要让页面元素抛物线运行,很简单。套用抛物线函数即可。公式如下:

y = a*x*x + b*x + c

(x, y)就是点坐标,在网页上可以理解为相对于页面左上角的偏移像素大小。

然后,套用求参,即可得到当前位置的抛物线函数啦!当然,实际操作不是键盘啪啪几个文字这么简单。

四、抛物线函数求解

//zxx: 如果本段内容勾起你过去那段惨绝人寰的回忆,我这里先说声抱歉! – 搞咩纳塞~

现在,我们的任务变成了求解:a, b, c三个参数。

3个参数需要3个条件才能完全求解。

由于我们要实现元素A到元素B的抛物线运动,因此,我们可以指定两个点的坐标位置,也就是知道了2个条件,那第3个条件呢?

了解抛物线函数的同学应该知道,a可以近似理解为弧度,曲率。在我们实现效果的时候,这个是应该要可控的。——你总不可能把元素抛到天宫号上再掉下来吧。在网页上,一般都是起伏不大的抛物线(否则会出屏幕之外)。

因此,我们可以把a作为一个参数常量。

于是,我们任务就变成了,已知参数a,以及两个点的坐标,求该抛物线函数。

哈,事情就简单多了。

y = a*x*x + b*x + c
↓
y1 = a * x1*x1 + b*x1 + c;
y2 = a * x2*x2 + b*x2 + c;
↓
a已知,求b, c

为了简化我们的计算求解,以及与我们高中时候的函数坐标匹配,我们可以以移动元素的初始位置作为坐标轴的中心(网页默认的坐标系左上角是中心,x轴向右,y轴向下,与高中的坐标轴不一样)。如下链接对应demo所示。

您可以狠狠地点击这里:元素抛物线运动demo

demo页面中,圆形的小球球就是要运动的元素;椭圆的大便池就是小球球运动的目的地,其默认开始的坐标如下所示:
符合高中数学的坐标系

也就是,我们限定了抛物线经过中心点(0, 0), 代入y1 = a * x1*x1 + b*x1 + c可以得到c = 0, 于是b = (y2+ a*x2*x2) / x2, 带入大便池元素的坐标,就可以计算出b的大小,于是,抛物线函数就出来了。

注意,这里“大便池元素的坐标”是相对于小球球的相对坐标,而不是页面左上角。

有了抛物线函数,我们就有了小球球运动的轨迹了。

五、demo演示及说明

demo演示页面就是上面提前曝光的链接地址啦!

1. 点击页面的任意位置,都会触发小球球奔向大便池的抛物线运动。
2. 拖动大便池到页面的任意位置,也会触发小球球义无反顾奔向大便池的效果。

demo页面的这个抛物线运动基本上是个独立的JS方法,代码如下:

var funParabola=function(d,t,g){var i={speed:166.67,curvature:0.001,progress:function(){},complete:function(){}};var p={};g=g||{};for(var v in i){p[v]=g[v]||i[v]}var u={mark:function(){return this},position:function(){return this},move:function(){return this},init:function(){return this}};var e="margin",r=document.createElement("div");if("oninput" in r){["","ms","webkit"].forEach(function(b){var a=b+(b?"T":"t")+"ransform";if(a in r.style){e=a}})}var s=p.curvature,q=0,o=0;var k=true;if(d&&t&&d.nodeType==1&&t.nodeType==1){var n={},j={};var h={},m={};var f={},l={};u.mark=function(){if(k==false){return this}if(typeof f.x=="undefined"){this.position()}d.setAttribute("data-center",[f.x,f.y].join());t.setAttribute("data-center",[l.x,l.y].join());return this};u.position=function(){if(k==false){return this}var b=document.documentElement.scrollLeft||document.body.scrollLeft,a=document.documentElement.scrollTop||document.body.scrollTop;if(e=="margin"){d.style.marginLeft=d.style.marginTop="0px"}else{d.style[e]="translate(0, 0)"}n=d.getBoundingClientRect();j=t.getBoundingClientRect();h={x:n.left+(n.right-n.left)/2+b,y:n.top+(n.bottom-n.top)/2+a};m={x:j.left+(j.right-j.left)/2+b,y:j.top+(j.bottom-j.top)/2+a};f={x:0,y:0};l={x:-1*(h.x-m.x),y:-1*(h.y-m.y)};q=(l.y-s*l.x*l.x)/l.x;return this};u.move=function(){if(k==false){return this}var a=0,b=l.x>0?1:-1;var c=function(){var z=2*s*a+q;a=a+b*Math.sqrt(p.speed/(z*z+1));if((b==1&&a>l.x)||(b==-1&&a<l.x)){a=l.x}var w=a,A=s*w*w+q*w;d.setAttribute("data-center",[Math.round(w),Math.round(A)].join());if(e=="margin"){d.style.marginLeft=w+"px";d.style.marginTop=A+"px"}else{d.style[e]="translate("+[w+"px",A+"px"].join()+")"}if(a!==l.x){p.progress(w,A);window.requestAnimationFrame(c)}else{p.complete();k=true}};window.requestAnimationFrame(c);k=false;return this};u.init=function(){this.position().mark().move()}}return u};(function(){var b=0;var c=["webkit","moz"];for(var a=0;a<c.length&&!window.requestAnimationFrame;++a){window.requestAnimationFrame=window[c[a]+"RequestAnimationFrame"];window.cancelAnimationFrame=window[c[a]+"CancelAnimationFrame"]||window[c[a]+"CancelRequestAnimationFrame"]}if(!window.requestAnimationFrame){window.requestAnimationFrame=function(h,e){var d=new Date().getTime();var f=Math.max(0,16.7-(d-b));var g=window.setTimeout(function(){h(d+f)},f);b=d+f;return g}}if(!window.cancelAnimationFrame){window.cancelAnimationFrame=function(d){clearTimeout(d)}}}());

//zxx: 为节约文章篇幅,这里放的是压缩后的代码。如果您对实现感兴趣,可以demo页面右键的源代码是非压缩版本,含有非常多的注释。或者使用这个JS文件:parabola.js

该抛物线方法名为funParabola,您可以根据自己的喜好修改,参数以及基本使用如下:

var myParabola = funParabola(element, target, options);

关于myParabola:
直接执行funParabola方法是不会产生运动的。因为,实际上funParabola执行返回的是一个对象。包含如下四个方法:

  1. mark 在目标元素以及移动元素上通过data-center自定义属性标记当前的中心坐标,如-234, -345. 此方法主要用在demo中,方便测试与预览用的。实际可能用途不大。
  2. position 重新获取元素的位置。在元素相对位置改变的时候,此方法很有用。否则会出现计算误差的情况。例如,页面布局是自适应或者响应式的,浏览器宽度变小了,两元素之间的距离变化了,此时需要执行下position,存储新的坐标位置。
  3. move 触发抛物线运动。
  4. init 初始化方法。实际上就是连续调用position, mark, move3个方法。

demo点击页面任意位置触发抛物线运动就是这么触发的:

/* 元素 */
var element = document.getElementById("element"), 
    target = document.getElementById("target");
// 抛物线元素的的位置标记
var parabola = funParabola(element, target).mark();
// 抛物线运动的触发
document.body.onclick = function() {
    element.style.marginLeft = "0px";
    element.style.marginTop = "0px";
    parabola.init();
};

参数说明:

  • element表示移动的元素,例如demo中的小球球。原生DOM节点
  • target表示目标元素。例如demo中的椭圆形的大便池。原生DOM节点
  • options为可选参数。各个API名称以及含义如下:
    • speed 表示每帧移动的像素大小,每帧(对于大部分显示屏)大约16~17毫秒。默认大小是166.67。也就是默认10px/ms.
    • curvature 可以近似理解为抛物线的开头大小,也就是曲率。正数表示开口向下。默认大小是0.001. 数值越大,开头越小,弧度越高。因为web页面动辄大小几百像素,因此,曲率值较小。
    • progress 表示抛物线运动过程中的回调,支持两个参数,x, y,表示当前的坐标,您可以根据这些坐标值做一些特殊的处理。
    • complete 表示抛物线运动结束后的回调。

其他说明:

  1. 可选参数speed不是指x轴的位移,也不是y轴位移,而是抛物线特定坐标的切线距离。利用切线公式:y'=2ax+b就可以计算出x轴这一帧应该移动的距离。形成奔向目的地的运动效果。
  2. funParabola方法不依赖任何JS框架。您可以大胆使用。对了,funParabola使用了requestAnimationFrame方法。关于requestAnimationFrame身世、工作等私密信息可以参考我之前的“CSS3动画那么强,requestAnimationFrame还有毛线用?”一文。上面压缩的JS包含了requestAnimationFrame相关的兼容处理,因此IE6浏览器也是可以使用滴!

    如果您使用的是demo源代码中的funParabola方法。如果您想低版本IE浏览器也有效果,需要再调用后面这个JS: requestAnimationFrame.js, 很少量的JS代码,主要做兼容处理的。因此,您可以可以直接拷贝出来。

    当然,最偷懒的方法是直接使用parabola.js或者压缩版parabola-min.js. 建议您拷贝到本地使用,尽量不要直接我站点外链地址。一旦外链请求超过我的忍受程度,我会加上这么一句JS:

    ddocument.body.insertAdjacentHTML("afterBegin", '<a href="ooxxooxxooxxooxx-huluwa.mp4">老板和秘书的激情战斗720x480.mp4</a>');

    大家可以自己斟酌下~

六、实际应用示意

概念作品会让人眼前一亮,但只有实物产品才会让人怦然心动。因此,附上一个符合实际应用场景的例子,即开始提到的“商品飞到购物车”的效果。自然,借助上面折腾的funParabola方法。

您可以狠狠地点击这里:商品抛物线飞到购物车效果demo

Demo页面采用的是某猫某商品页面。原本的加入购物车效果也是很精致的——移动到购物车上(直线),然后落下(含高度变化)。//zxx: 您可以自己打开一个商品详细页感受下。

这里,则改成了抛物线效果。

点击demo两个“加入购物车”按钮,就可以看到抛物线落下的效果了。

加入购物车按钮 张鑫旭-鑫空间-鑫生活

加入购物车按钮 张鑫旭-鑫空间-鑫生活

商品抛物线进入购物车的效果截图 张鑫旭-鑫空间-鑫生活

面向实际使用,实际上需要更多的细节处理。例如,接近购物车的时候,购物车是否应该有动作。进入购物车,购物车是否需要抖一下,表示我吃进去商品了,我很得瑟!等~

七、其他函数

举一反三,触类旁通。如果你想实现其他的运动轨迹效果,例如,双曲线,或者椭圆运动。都是可以借助数学函数,按照上面抛物线实现的思路。稍微折腾折腾,效果就会出来了。

这些其实都是简单的动画效果。您可能会觉得作用有限。实际上,您所见过的很多炫得掉渣子的效果都是由这个基本动画效果演变或叠加出来的。如何个演变以及叠加,这是需要技巧的。这又属于另外一个方向的知识了。

如果您对动画相关的诸多算法或技巧很在行,您也可以成为很多企业抢手的香饽饽。

八、缓动与速度

之前有介绍过缓动的一些算法。可以让动画速度先快后慢,或者先慢后快,或者弹弹弹。虽然缓动本质上也是曲线。但是,与这里的抛物线函数曲线还是不一样的。

缓动的变量单位是时间;而这里抛物线效果的变量单位是像素。

因此,缓动改变的是速度;这里改变的则是运动轨迹。

但是,相同的是,他们绘制的曲线的模样可以是类似的。也就是,他们都用到了数学。

小时候,老师的嘴巴里经常会蹦出这么句话:“学好数理化,走遍天下都不怕!”

现在明白,真的是很有道理的哈!

实际上,更有道理的是后面半句:“除非你有一个好爸爸!”

太深刻了!

九、贝塞尔曲线与任意轨迹

对于有规律的轨迹,我们可以套用数学函数。如果是没有规律的呢?比方说,我想让汽车像喝醉了酒一样曲线运动,怎么办?

可能就需要借助贝塞尔曲线绘制了。

借助SVG或者canvas.

要说SVG和canvas……

额~~一瞬间,我的大脑像高速服务区的大便池一样堵住了,这SVG以及canvas相关知识点超多,实属超大话题为避免营养过盛,消化不良,相关内容后面一点一点和大家分享。

本文至此为止,感谢大家能够阅读至此!

一般而言,搓文后面会是大作,尽请期待!

欢迎交流!

(本篇完)

分享到:1

赞助商推荐(我也要赞助)



发表评论(目前46 条评论)

  1. caicai说道:

    一直看你的文章,非常受用

  2. caicai说道:

    脑子现在一大堆抛物线

  3. 佳佳说道:

    谢谢大神~ 用到了公司的点餐系统上~哦hohohohoho~···

  4. Rh九尾鱼说道:

    大体看懂了!可以转载吗?!

  5. 雪剑无影说道:

    如果这个效果你能录成视频就更好了

  6. jon_shen说道:

    曾经我也是会抛物线的的,大概高中时候。。。

  7. 炫海_神鹰说道:

    b = (y2+ a*x2*x2) / x2, 带入大便池元素的坐标,就可以计算出b的大小

    公式是不是写错啦?应该是 b = (y2- a*x2*x2) / x2,吧,y2- 不是y2+吧

  8. 拉拉啦说道:

    我想问一下 原地起跳的这个抛物线怎么搞

  9. 2713说道:

    x=0

  10. sky91说道:

    先恭维一下:看你的博客许久,获得不少知识,学到了很多,先感谢下。
    然后,简单谈一下,我个人认为这个插件如果加以自动化的话会略好用。
    花了一段时间简单应用了一下,如果有什么没注意的地方,原谅则个。
    几点意见/优化:
    1、超边界问题:
    当起点和终点都在屏幕偏顶部时(将demo中line:17 中bottom:0;改为top:0;),即会出现超边界问题,所以应该加个配置,抛物线的top点会超边界时,应将抛物线翻转。
    tmall的加入购物车也有这个问题,抛物线有一部分会被遮挡。
    诚然,可以通过改变curvature的值来“压扁”抛物线或者翻转抛物线,但curvature这个值的量级太小(我这改成-0.0005才有较好的弧度),或者改为曲率半径在量级上表现会更好?
    而且随着页面的上下滚动(当购物车icon为fixed,商品位置变化时),这个值肯定需要变化,不然依然会出现这种“超边界”问题。
    这个问题没想到什么解决方法,兹以为需要计算下直线距离和当前元素与屏幕边界的关系、对曲度curvature进行动态调整(包括翻转和增减)。
    2、速度的自动计算:
    与其去设置速度,不如换成完成动作的时间,因为同样的速度,在距离不同时,加入到购物车的时间不同,会导致用户等待时间不同,(个人认为)体验的效果感觉略差。
    最明显的就是当一个网页由多个商品(类似于某东的首页楼层),而购物车icon位置在右边栏,在相同速度时,最左边商品加入到购物车的时间和最右边商品加入到购物车的时间相差较大。
    当然这样也有个问题,就是最右边商品加入到购物车的速度会明显很慢。所以可以设置速度和时间的关系(比如最小时间为3s,用于最右边商品的效果,而最大时间/最小速度为5s,由于最左边商品的效果,中间采取一个中间值,懒的时候直接设置一个区间,进行random),保持一个较为均衡的效果。
    3、多动画效果并行:
    本文中第四部分的demo中,“小球球”作为移动的元素。而实例demo中,移动的元素是“#flyItem”。
    这样,使用这个插件的多数人需要先构造一个移动的元素,因为你不能把商品的原图给移没了;
    还有,包括tmall在内的商品的图片不只一个,当你选择另一个图片时,动画中“移动的元素”是需要变化的,如果是通过隐藏层元素来进行动画,需要写监听来切换“移动的元素”中的商品图片。
    更为关键的,当需要加入多个商品时,会触发“多动画”,很明显,原始的 funParabola不支持多个动画的同时触发。而且,因为“移动的元素”只有一个,所以也无法一次性并行多个动画。
    暂时想到的解决方案(应该算是正常人的逻辑?),在click事件触发时,将制定移动的元素clone,然后移动clone后的元素。当然,clone后的元素要进行一定的css修改,不然大多数商品的图片很大,移动起来的效果就比较。。。

    暂时想到这些问题,有什么说的不对的地方,欢迎斧正。再次,感谢!

    • andy说道:

      关于抛物线的切线,就是下面这段代码:
      // 切线 y’=2ax+b
      var tangent = 2 * a * startx + b; // = y / x
      // y*y + x*x = speed
      // (tangent * x)^2 + x*x = speed
      // x = Math.sqr(speed / (tangent * tangent + 1));
      startx = startx + rate * Math.sqrt(params.speed / (tangent * tangent + 1));
      能不能详细的讲解一下,因为高中数学学的不太好,自己也上网找了一些资料,但是还是看不太明白,谢谢了!

  11. 榆钱斗斗说道:

    请问你的博客编辑器用的是什么啊?Windows Live Writer还是什么?

  12. neil说道:

    大神, 我还是不了解这地方的意思

    // 切线 y’=2ax+b
    // y = a*x*x + b*x + c;
    var tangent = a * startx * 2 + b; // = y / x

    // y*y + x*x = speed
    // (tangent * x)^2 + x*x = speed
    // x = Math.sqr(speed / (tangent * tangent + 1));
    // 这里是每一步滚动的距离
    startx = startx + rate * Math.sqrt( params.speed / ( tangent * tangent + 1 ) );

    • thevenin说道:

      我对着快也是很不明白!

    • 007说道:

      这里是把路径上的速度变换在横向 x 轴上速度,并加上 step

    • siyuejiwang说道:

      其实就是勾股定理,每帧走的长度speed为直角三角形的斜边,两个直角边x,y分别对应x轴,Y轴的位移,两个直角边比例y/x就为正弦tan(角度值),而这个正弦值就等于切线的斜率,切线的斜率就是上面的y‘

  13. Running说道:

    原来抛物线还能这么用…一直觉得当年学习的东西怎么都没有用到…唉 too young too simple~
    转载了

  14. xni220说道:

    博主人才啊

  15. megasu说道:

    加入购物车例子的事件可以再完善些,点击第一个按钮时候,动画没执行完毕再点击第二个按钮,漂浮的商品缩略图会错位。

  16. acershan说道:

    大神,谷歌可以,火狐为什么不动,也不报错,JS渣渣,求赐教啊

  17. Hydra说道:

    博主你的幽默好难笑哦
    不过还是膜拜支持一下高手

  18. amibug说道:

    参考tmall例子,写了一个demo: http://codepen.io/hzxs1990225/full/ogLaVp/
    求张鑫旭大神的指点

  19. 小简说道:

    觉得很享受,对前端的一些细节描绘的那么淋漓尽致,这份热爱和执着,正是推进自己前进的动力,向牛人致敬。

  20. 明明是小明说道:

    如果移动的小球#one在一个移动的物体上。position()无法调整位置

  21. 杨六郎说道:

    你好,用的你的这个东西,我是在移动端浏览器用的,IOS和Android自带的浏览器打开http://2.yangliulang.sinaapp.com/
    当点击加号时会有加入菜单的效果,可是当滚动条往下滚动后在点击添加按钮时,就出现抛物线定位目标点问题,IOS没问题,Android里面有问题。麻烦大神能替我解决下啊,,拜托啦~~

  22. tracy说道:

    还有物理上的自由落体公式以及水平匀速运动公式的结合,模拟出来的运动似乎更像真实的运动轨迹

  23. steveyu说道:

    ieBetter.js和jQuery.js一起用的时候,ie6报“eventType” is undefined,大神怎么解决啊,我菜鸟,用你的抛物线正在做购物车,大神帮忙啊

    去掉这句话就可以了,为什么啊 global.addEventListener = function(eventType, funcHandle, useCapture) {
    oDomExtend.addEventListener.call(global, eventType, funcHandle, useCapture);
    };

  24. xuan说道:

    startx = startx + rate * Math.sqrt(params.speed / (tangent * tangent + 1));

    这一句有点不懂呀

  25. 可惜不是你说道:

    b = (y2+ a*x2*x2) / x2 ,难道我是算错了啊,我得出来的是 b = (y2—a*x2*x2) / x2;

    还有就是飞向购物车的那个元素要绝对定位,不知道固定定位可不可以;

    我在本地测试,用火狐26.0版本下报错: ReferenceError: event is not defined ;

    应该是遗漏了事件对象吧!谢谢你的文章,我今天正好在做购物车这个功能。

  26. color说道:

    赞一个~
    看了你的职位推荐
    哈哈,人不在上海了
    不然也投一个试试

  27. bean说道:

    如果我是产品经理或者是个会独立思考的前端仔,那么面对这样一个需求,我只会考虑支持css3, animation的浏览器, 什么?兼容ie678 ? 见鬼去吧,用户如果还用着上一个年代的浏览器,活该不给那么炫的玩意给他玩。

  28. jiel说道:

    给力 先回复在看

  29. 702017364说道:

    请问,做好一个好的前端要掌握什么,懂得什么。。。

  30. 八戒说道:

    难道我会说那个抛物线我看不懂吗?

  31. isaries说道:

    nice

    • 麻雀说道:

      // 移动元素的中心点坐标
      centerElement = {
      x: rectElement.left + (rectElement.right – rectElement.left) / 2 + scrollLeft,
      y: rectElement.top + (rectElement.bottom – rectElement.top) / 2 + scrollTop
      };
      这部分求得的距离不是很懂。好像不是中心距离。比如窗口大小为600,移动元素left:400,right:150,然后求得中心x是250.不对哦!