交互动画的一个主要目标是创建出流畅的用户体验,其中大多数的用户交互都是通过鼠标和触摸屏实现的。

在这篇博文中,我想分享一些JS对于物体移动的常见用法,包括拖拽和投掷效果。

一. 使用鼠标事件

可以将一个鼠标单击事件分解成两个事件:鼠标按下事件和按键弹起事件。通常情况下这两个事件是同时发生的。不过,有时鼠标按下后,鼠标还会移动一段时间才弹起,这种操作称为拖曳,即按下、移动、在释放。

在canvas动画中,鼠标事件只能被HTML DOM树上的canvas元素所捕获,因此,我们需要手动计算出鼠标事件在canvas上的发生位置,并判断出它是否发生在哪个绘制到canvas的物体上。需要关注的鼠标事件有:mousedown、mousemove和mouseup。具体细节可参考我的相关博文《JavaScript动画详解(一) —— 循环与事件监听》。

二. 使用触摸事件

随着触摸屏设备的流行,我们很可能需要在动画中捕捉用户的触摸事件。虽然触摸屏与鼠标是不同的设备,但幸运的是,在DOM树上捕捉触摸事件与捕捉鼠标事件的差别不大。

与鼠标事件mousedown、mousemove和mouseup相对应的触摸事件分别是touchstart、touchend与touchmove。

使用手指与鼠标的一个比较大的区别在于,鼠标始终出现在屏幕上,而手指却不是一直处于触摸状态。常见的做法是,引入自定义属性isPressed,用来告诉我们屏幕上是否有手指在触摸。具体细节可参考我的相关博文《JavaScript动画详解(一) —— 循环与事件监听》。

三. 拖拽事件

拖拽事件包含了三个子事件:鼠标按下、移动、释放。通过不断更新物体的坐标位置使其追随鼠标指针的位置,就可以实现在canvas元素上拖拽物体。另外还需要一个自定义属性isPressed来标示当前鼠标是否按下,默认为false表示鼠标为弹起状态。实现代码包含以下过程:

1 . 捕捉mousedown事件,判断当前鼠标是否在物体内。当鼠标在物体内按下时,设置isPressed = true;

2 . 捕捉mousemove事件,在处理程序内判断当isPressed = true时,通过不断更新物体的坐标位置使其追随鼠标指针的位置来模拟出鼠标拖拽效果;

3 . 捕捉mouseup事件,将isPressed设置为false;

HTML代码如下:

<canvas id="canvas" width="400" height="400"></canvas>

JavaScript代码如下:

// 创建画球函数
function Ball() {
    this.x = 0;
    this.y = 0;
    this.radius = 20;
    this.fillStyle = "#f85455";
    this.draw = function(cxt) {
        cxt.fillStyle = this.fillStyle;
        cxt.beginPath();
        cxt.arc(this.x, this.y, this.radius,  0, 2 * Math.PI, true);
        cxt.closePath();
        cxt.fill();
    }
}
// 获得当前鼠标位置
function getMouse(ev) {
    var mouse = {
        x: 0,
        y: 0
    };
    var event = ev || window.event;
    if(event.pageX || event.pageY) {
        x = event.x;
        y = event.y;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = event.clientX + scrollLeft;
        y = event.clientY + scrollTop;
    }
    mouse.x = x;
    mouse.y = y;

    return mouse;
}

var canvas = document.getElementById("canvas"),
        context = canvas.getContext("2d"),
        ball = new Ball(),
        mouse = {x: 0, y: 0},
        isPressed = false;
ball.x = 20;
ball.y = 20;

// 渲染小球
ball.draw(context);

// 小球拖拽事件
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("mouseup", mouseUp, false);

function mouseDown(ev) {
    // 判断当前鼠标是否在小球内
    mouse = getMouse(ev);
    if(Math.pow(mouse.x - ball.x, 2) + Math.pow(mouse.y - ball.y, 2) <= Math.pow(ball.radius, 2)) {
        isPressed = true;
    }
}

function mouseMove(ev) {
    if(isPressed) {
        mouse = getMouse(ev);

        ball.x = mouse.x;
        ball.y = mouse.y;
        context.clearRect(0, 0, canvas.width, canvas.height);
        ball.draw(context);
    }
}

function mouseUp(ev) {
    // 标示鼠标弹起事件
    isPressed = false;
}

来个例子:

【备注:把鼠标移上去拖动红色小球试试~】

但是,这个例子是有bug的!很快你就能发现,在拖拽的时候,小球的中心位置都在鼠标位置上,特别是当鼠标单击小球边缘时,会看见小球的中心点突然就跳动到了鼠标光标的位置上了。显然,这显得有点唐突。

我们可以稍作改良:

在鼠标按下的时候记录当前鼠标位置与小球中心点位置的偏移量;

// 记录鼠标按下时,鼠标与小球圆心的偏移量
dx = mouse.x - ball.x;
dy = mouse.y - ball.y;

在鼠标移动时,用鼠标的当前位置减去鼠标按下时记录的偏移量

ball.x = mouse.x - dx;
ball.y = mouse.y - dy;

效果如下:

【备注:把鼠标移上去拖动红色小球试试~】

四. 投掷事件

在动画中如何表现投掷呢?用鼠标选中一个物体,拖拽着它向某个方向移动,松开鼠标后,物体沿着拖拽的方向继续移动。

在投掷物体时,必须在拖拽物体的过程中计算物体的速度向量,并在释放物体时将这个速度向量赋给物体。实际上,计算拖拽时物体的速度向量的过程,恰好与对物体应用速度向量的过程相反。在对物体应用速度向量时,将速度追加到物体原来所在的位置上,从而计算出物体的新位置,这个公式可以写成:旧的位置 + 速度向量 = 新的位置,即速度向量 = 新的位置 – 旧的位置

为了实现投掷行为,需要对前面的代码做一些改动。首先,检查鼠标是否按下,如果按下,用oldX和oldY变量保存小球旧的x、y坐标位置,并更新小球的拖拽速度。

具体JavaScript代码实现如下:

// 创建画球函数
function Ball() {
    this.x = 0;
    this.y = 0;
    this.radius = 20;
    this.fillStyle = "#f85455";
    this.draw = function(cxt) {
        cxt.fillStyle = this.fillStyle;
        cxt.beginPath();
        cxt.arc(this.x, this.y, this.radius,  0, 2 * Math.PI, true);
        cxt.closePath();
        cxt.fill();
    }
}
// 获得当前鼠标位置
function getMouse(ev) {
    var mouse = {
        x: 0,
        y: 0
    };
    var event = ev || window.event;
    if(event.pageX || event.pageY) {
        x = event.x;
        y = event.y;
    }else {
        var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
        var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        x = event.clientX + scrollLeft;
        y = event.clientY + scrollTop;
    }
    mouse.x = x;
    mouse.y = y;

    return mouse;
}

var canvas = document.getElementById("canvas"),
        context = canvas.getContext("2d"),
        ball = new Ball(),
        mouse = {x: 0, y: 0},
        isPressed = false,
        oldX = 0,
        oldY = 0,
        currentX = 0,
        currentY = 0,
        vx = 0,
        vy = 0;
ball.x = 200;
ball.y = 200;

// 声明鼠标按下时,鼠标与小球圆心的距离
var dx = 0,
        dy = 0;

// 渲染小球
ball.draw(context);

// 小球拖拽事件
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("mouseup", mouseUp, false);

function mouseDown(ev) {
    // 判断当前鼠标是否在小球内
    mouse = getMouse(ev);
    if(Math.pow(mouse.x - ball.x, 2) + Math.pow(mouse.y - ball.y, 2) <= Math.pow(ball.radius, 2)) {
        isPressed = true;
        // 记录鼠标按下时,鼠标与小球圆心的距离
        dx = mouse.x - ball.x;
        dy = mouse.y - ball.y;
        // 获得小球拖拽前的位置
        mouse = getMouse(ev);
        oldX = mouse.x;
        oldY = mouse.y;
    }
}

function mouseMove(ev) {
    if(isPressed) {
        mouse = getMouse(ev);
        ball.x = mouse.x - dx;
        ball.y = mouse.y - dy;
        context.clearRect(0, 0, canvas.width, canvas.height);
        ball.draw(context);
    }
}

function mouseUp(ev) {
    // 标示鼠标弹起事件
    isPressed = false;
    // 把鼠标与圆心的距离位置恢复初始值
    dx = 0;
    dy = 0;
    // 获得小球拖拽后的位置
    mouse = getMouse(ev);
    currentX = mouse.x;
    currentY = mouse.y;

    // 更新速度向量:速度向量 = 新的位置 - 旧的位置
    vx = (currentX - oldX) * 0.05;
    vy = (currentY - oldY) * 0.05;

    drawFrame();
}

// 缓动动画
function drawFrame() {
    animRequest = window.requestAnimationFrame(drawFrame, canvas);
    context.clearRect(0, 0, canvas.width, canvas.height);
    if(ball.x >= canvas.width - 30 || ball.x <= 30 || ball.y >= canvas.height - 30 || ball.y <= 30) {
        window.cancelAnimationFrame(animRequest);
    }
    ball.x += vx;
    ball.y += vy;
    ball.draw(context);
}

效果如下:

【备注:把鼠标移上去拖动红色小球试试~】

这个Demo的边界判断还有一些bug,过些日子再修复。好累哇今天~

五. 总结

物体移动事件可以有很多总运动形式,但是都可以分解为三个单独的事件来控制:按下、移动、释放,在鼠标事件中分别对应的是mousedown、mousemove和mouseup,在触摸事件中分别对应的是touchstart、touchmove和touchend。通过不断更新物体的坐标位置使其追随鼠标指针的位置,就可以实现在canvas元素上拖拽和投掷的效果。

本文作者:子匠_Zijor,转载请注明出处:http://www.dengzhr.com/frontend/html/511