转载

拖拽粘性小红球Canvas实现

相信大家都见过之前手机QQ中神奇的“ 一键下班 ”功能,如图:

拖拽粘性小红球Canvas实现

它通过一种优雅的方式,去掉QQ上的所有消息通知红点:用户可以拖拽粘性的小球来删除红点,动画符合用户心理预期。详情请看ISUX文章介绍,本文借鉴于此。

这个创意十分棒,但是它是基于客户端实现的。对于这个带粘性的弹性小球其实可以用在很多方面,例如loading动画、下拉刷新提示、横屏提示、按钮反馈等等,因此有必要实现一个前端版本。

没错,这是一个教程贴,客官按需往下看。

那么先看看效果: 点击demo ,打开后试试拖拉小球儿,也可以点击左上角的辅助线看看小球的轨迹~

(请用手机查看)

拖拽粘性小红球Canvas实现

开始动手

生成小球

这是最简单的一步,只需要用到 Canvas.arc(x,y,radius,start_angle,end_angle) 这一个方法,绘制一个小圆点位于画布中心,作为今天的主角。

监听方法

拖拽的过程需要用到移动端的touch事件,分别需要监听拖拽前、中、后三个过程。

canvas.addEventListener('touchstart',dragStart); canvas.addEventListener('touchmove',draging); canvas.addEventListener('touchstart',dragEnd); 

拖拽前DragStart

由于采用Canvas实现,监听都是绑定在整个画布上,因此需要实时判断开始拖拽的时候手指是否按在小球上。

这里需要用到两点间距离公式:

拖拽粘性小红球Canvas实现

获取手指触点的x和y,然后根据公式判断与小球圆点的距离是否大于小圆半径Radius,则可判断是否在小球上。注意这里需要加上一个边缘容差值move_rest,确保手指在边缘也能拖动小球。

//边缘容差 var move_rest = 20;  //触点坐标 var x = e.touches[0].clientX; var y = e.touches[0].clientY;  // 计算是否在圆内 var distance = Math.sqrt(Math.pow(x-oX,2)+Math.pow(y-oY,2));  // 标志位,判断是否可以拖动 is_canMove = (distance-move_rest) > oRadius ? false : true; 

拖拽时Draging

拖拽时需要小球伴随着手指的运动而运动,因此需要在手指触点位置绘制一个一摸一样的小球,这跟绘制中心小球一样简单。

然而,如何建立小球和中心小球的联系呢?

方案1

先建立两者连接,以两圆间相切点连成线,如下图所示:

拖拽粘性小红球Canvas实现

这个比较简单,因为拖动夹角固定为90度,获取水平夹角和四个切点的距离再连线就行了。

注意这里需要判断拖动的小球相对中心小球处于第几象限,从而确定切点。

// 计算拖拽水平夹角 var angle = Math.atan(Math.abs(y - toY)/Math.abs(x - toX));  // 计算四个切点坐标,拖动圆圆心(toX,toY),中心圆圆心(x,y),a为拖动圆两个切点,b为中心圆两个切点  <!-- 第四象限,拖动圆在中心圆右下角,其他三个象限类推... --> var a1x = toX + Math.cos(Math.abs(Math.PI/2))*oRadius; var a1y = toY - Math.cos(Math.abs(Math.PI/2))*oRadius; var a2x = toX - Math.cos(Math.abs(Math.PI/2))*oRadius; var a2y = toY + Math.cos(Math.abs(Math.PI/2))*oRadius; var b1x = x + Math.cos(Math.abs(Math.PI/2))*oRadius; var b1y = y - Math.cos(Math.abs(Math.PI/2))*oRadius; var b2x = x - Math.cos(Math.abs(Math.PI/2))*oRadius; var b2y = y + Math.cos(Math.abs(Math.PI/2))*oRadius; 

然而,这样无论如何拉伸小圆都是直直的,没有粘性的效果,怎么办呢?

方案2

想要增加粘性效果,需要把相连的直线改为 贝塞尔曲线 。

写过CSS动画的人应该都对它不陌生,因为 animation-timing-function 可以通过定义贝塞尔值来实现动画过程中速率的自由变化。

如图,一条贝塞尔曲线最少由三个点组成,分别是起点P0、终点P3和控制点P1,P2,其中控制点可以有一个或两个。

拖拽粘性小红球Canvas实现

对应于Canvas,也有相应的贝塞尔方法:

  • 一个控制点

    context.quadraticCurveTo(cpx,cpy,x,y); 
  • 两个控制点

    context.bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y); 

这次我们只需要使用一个控制点,也就是使用 quadraticCurveTo 方法就可以了。

至于如何定义这次的控制点?

如图,我们如果要定目标圆(a1x,a1y)和中心圆(b1x,b1y)的控制点,就以目标圆另一个点(a2x,a2y)与中心圆(b1x,b1y)连线的中心作为控制点,另一条线同理。

拖拽粘性小红球Canvas实现

注意,可以看到最后呈现出来的曲线并不是如图白线的三点交线,那是因为贝塞尔曲线并不是简单的三点连线,实则上不会经过控制点,这三点最后确定的点就是如图橙色的曲边。

确定了三个点,于是就可以画线了:

// 计算控制点 var c1x = (a2x + b1x) / 2; var c1y = (a2y + b1y) / 2;  // 先移到开始点 context.moveTo(a1x,a1y);  // 根据目标点和控制点画出贝塞尔曲线 context.quadraticCurveTo(c1x,c1y,b1x,b1y); 

看起来差不多了,然而在两圆中间曲线变化的时候发现速率还是不自然,是缺了点什么?

方案3

我们开始假设拖动夹角固定为90度,但这是不符合拖动力度变化的,因此我们可以做一个优化,随着拖动距离越远,拖动夹角应该越小。注意,不能无限期地变小,需要有一个最低夹角。

拖拽粘性小红球Canvas实现

于是在基础上加一点代码优化拖动:

var base_angle = Math.PI;//基准弧度 var distance_angle = Math.PI / 800;//减角力度,每移动1px距离减少的弧度值 var distance_angle_limit = Math.PI * 3/4;//最小基准弧度  var Bangle = base_angle - distance*distance_angle; if(Bangle < distance_angle_limit){         Bangle = distance_angle_limit; }  // 因此两圆的四个切点需要加减以下几个偏移值 var     dis_x1 = Math.cos(Math.abs(Bangle/2-angle))*oRadius; var     dis_y1 = Math.sin(Bangle/2-angle)*oRadius; var     dis_x2 = Math.cos(Math.abs(Math.PI-Bangle/2-angle))*oRadius; var     dis_y2 = Math.sin(Math.PI-Bangle/2-angle)*oRadius; 

拖拽后DragEnd

拖拽后就需要回弹小球,只需要记录这时候拖动球的圆心坐标,然后执行回弹函数就行了。

回弹动画

回弹动画使用了 Tween.js ,使用方法简单,只需要定义好开始的位置(拖动球圆心)以及目标位置(中心圆圆心),选择合适的动画缓动函数,再更新动画函数 bounceUpdate 即可。

// 选择缓动函数 var bounce_animate_type = TWEEN.Easing.Elastic.Out;  // 调用Tween.js,声明开始和结束位置。 coords = { x: last_x, y: last_y }; tween = new TWEEN.Tween(coords)     .to({ x: oX, y: oY }, bounce_duration_time)     .easing(bounce_animate_type)     .onUpdate(bounceUpdate)     .start();  bounceAnimate();  // RAF确保动画流畅运行 function bounceAnimate(time) {         requestAnimationFrame(bounceAnimate);         TWEEN.update(time); } 

这里最关键是 bounceUpdate 函数,它的实现方法其实也很简单,与拖动球时Draging时候一样,但是x和y不再是手指触摸点,而是Tween.js计算的数值coords.x和coords.y。

// 更新动画函数 function bounceUpdate(){         // 该时刻弹行位置         x = coords.x;         y = coords.y;          //之后就跟Draging一样 } 

至此,整个拖拽返弹效果就实现了。

结语

偶尔写写教程贴也是一种消化过程呀,有问题欢迎留言。

原文  http://tqtan.com/2016/03/27/drag-a-ball/
正文到此结束
Loading...