TQ的博客

欢迎来到TQ的博客~

share_img

No one is king for long.

摄影集 / 代码集 / 关于我

拖拽粘性小红球Canvas实现

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

一键下班示意图

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

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

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

粘性小球拖拽效果

开始动手

生成小球

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

监听方法

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

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

拖拽前DragStart

由于采用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

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

拖拽时方案1

这个比较简单,因为拖动夹角固定为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,也有相应的贝塞尔方法:

  • 一个控制点

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

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

这次我们只需要使用一个控制点,也就是使用quadraticCurveTo方法就可以了。
至于如何定义这次的控制点?

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

定贝塞尔曲线控制点

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

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

// 计算控制点
var c1x = (a2x + b1x) / 2;
var c1y = (a2y + b1y) / 2;

// 先移到开始点
context.moveTo(a1x,a1y);

// 根据目标点和控制点画出贝塞尔曲线
context.quadraticCurveTo(c1x,c1y,b1x,b1y);

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

方案3

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

拖动夹角示例图

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

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;
}

// 因此两圆的四个切点需要加减以下几个偏移值
dis_x1 = Math.cos(Math.abs(Bangle/2-angle))*oRadius;
dis_y1 = Math.sin(Bangle/2-angle)*oRadius;
dis_x2 = Math.cos(Math.abs(Math.PI-Bangle/2-angle))*oRadius;
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一样
}

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

结语

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

Comments